fix: stop iOS chat keyboard collapse by removing destructive resync

The mobile chat keyboard would not reliably stay up on iOS. The cause was
the visibilitychange/pageshow 'resync' effect in ChatView: it only ever
ran while the composer was already focused (its activeElement guard), and
on iOS a programmatic focus() fired from setTimeout has no user-gesture
context, so it could not re-raise the keyboard after the blur(). Every
spurious iOS visibilitychange (Control Center, notification banners, app
switches) therefore dismissed the keyboard mid-session and never restored
it.

Remove the blur()+focus() resync. The visualViewport half-state it
targeted is already handled by useMobileKeyboard, which re-snapshots vv
metrics on visibilitychange/pageshow via its settle tail + rAF stability
poll without ever touching textarea focus.
This commit is contained in:
gsxdsm
2026-06-12 08:03:33 -07:00
parent df2a0a1b5d
commit ab040885fb

View File

@@ -1675,43 +1675,23 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
};
}, [isMobile, keyboardOpen]);
// On page restore, if iOS thinks the keyboard is up but the textarea
// isn't actually focused (or vice versa), the visualViewport metrics get
// stuck in a half-state — composer pushed up or covered by a blank pane.
// Force a blur+refocus on the textarea to make iOS resync.
// NOTE: a previous iOS-only "resync" effect here force-blurred and
// re-focused the active textarea on visibilitychange/pageshow to nudge
// iOS out of a stuck visualViewport half-state (composer pushed up /
// blank pane). It was removed because it was the cause of the iOS
// "keyboard won't stay up" bug: the effect only ever ran while the
// composer was already focused (its `document.activeElement !== ta`
// guard), and on iOS a programmatic focus() fired from setTimeout has
// no user-gesture context, so it cannot re-raise the keyboard after the
// blur(). In practice it never resynced the keyboard up — it only
// dismissed it whenever iOS emitted a visibilitychange (Control Center,
// notification banners, app switches, etc.) mid-session.
//
// This is an iOS-only quirk fix and must stay iOS-only: on Android a
// programmatic focus() after blur() does NOT re-raise the soft keyboard
// (Android only opens the keyboard from a real user gesture), so running
// this there permanently collapses the keyboard whenever a spurious
// visibilitychange fires — including the ones Android browsers emit
// mid-keyboard-transition while the user is typing. We also only resync
// when the document is actually becoming visible; visibilitychange also
// fires on hide, where a blur+refocus is pointless and disruptive. Only
// runs when ChatView holds the active session (avoids stealing focus
// from other views).
useEffect(() => {
if (!isMobile || !activeSession || !isIOS()) return;
const resync = () => {
const ta = inputRef.current;
if (!ta) return;
if (document.activeElement !== ta) return; // only if it was focused
ta.blur();
window.setTimeout(() => {
ta.focus({ preventScroll: true });
}, 0);
};
const resyncOnVisible = () => {
if (document.visibilityState !== "visible") return;
resync();
};
document.addEventListener("visibilitychange", resyncOnVisible);
window.addEventListener("pageshow", resync);
return () => {
document.removeEventListener("visibilitychange", resyncOnVisible);
window.removeEventListener("pageshow", resync);
};
}, [isMobile, activeSession]);
// The visualViewport half-state it targeted is now owned by
// useMobileKeyboard, which re-snapshots vv metrics on
// visibilitychange/pageshow via its settle tail + rAF stability poll —
// without ever touching textarea focus. Do not reintroduce a
// blur()+focus() resync here.
useEffect(() => {
const previousScope = previousChatScopeRef.current;