fix(mobile): pin the chat composer above the keyboard on Android browsers

The chat screen relied on the browser's focused-field reveal, which holds
on iOS Safari but not on Android, where interactive-widget is also widely
ignored — the composer just stayed behind the keyboard. The draft screen's
visual-viewport pin now covers the chat screen on Android; iOS chat keeps
the native reveal.
This commit is contained in:
Bohdan Triapitsyn
2026-08-26 23:10:37 +03:00
parent 25be1985ef
commit 11dd8c4eb0
@@ -17,6 +17,15 @@ import React from 'react';
import { isCapacitorApp } from '@/lib/platform';
import type { ComposerEditorHandle } from '../editor/ComposerEditor';
// Android mobile browsers are the pan-mode holdouts this pin exists for on
// the CHAT screen too: interactive-widget=resizes-content is ignored by a
// fair share of Android WebView/Chrome builds, and unlike iOS Safari they do
// not reliably reveal the focused field either — the composer just stays
// behind the keyboard. iOS keeps its browser-native reveal on the chat
// screen, so this stays Android-only there.
// Callers are browser-only React effects, so navigator always exists here.
const isAndroidBrowser = (): boolean => /Android/i.test(navigator.userAgent);
export interface MobileViewportPinOptions {
isMobile: boolean;
/** Composer expanded to fullscreen on mobile. */
@@ -96,12 +105,14 @@ export function useMobileViewportPin(options: MobileViewportPinOptions): void {
};
}, [editorRef, formRef, isFullscreen, isMobile]);
// Draft screen with the keyboard up: anchor the normal-height composer to
// the visible bottom. The chat screen does not need this — its own
// focused-field reveal works there.
// Keyboard up: anchor the normal-height composer to the visible bottom.
// Draft screen on every mobile browser; chat screen only on Android,
// where neither viewport resizing nor the focused-field reveal can be
// relied on (iOS chat keeps the browser's own reveal).
React.useLayoutEffect(() => {
if (!isMobile || isCapacitorApp()) return;
if (!isDraftScreen || isFullscreen || !isFocused) return;
if (isFullscreen || !isFocused) return;
if (!isDraftScreen && !isAndroidBrowser()) return;
const vv = window.visualViewport;
const form = formRef.current;
if (!vv || !form) return;