diff --git a/web/hooks/use-hide-bottom-nav-on-keyboard.ts b/web/hooks/use-hide-bottom-nav-on-keyboard.ts
index 2c898829..8a7f78f2 100644
--- a/web/hooks/use-hide-bottom-nav-on-keyboard.ts
+++ b/web/hooks/use-hide-bottom-nav-on-keyboard.ts
@@ -9,7 +9,8 @@ import {useEffect} from 'react'
* for navigation nobody is using mid-message.
*
* The keyboard state itself is tracked in `_app.tsx`, which toggles `keyboard-open` on `
` from
- * the Capacitor keyboard events; the rules live in `globals.css` so showing/hiding costs no re-render.
+ * the Capacitor keyboard events in the app, and from the visual viewport shrinking in a mobile
+ * browser; the rules live in `globals.css` so showing/hiding costs no re-render.
* Zeroing `--bnv` there also reclaims the bar's height for the page below it.
*/
export const useHideBottomNavOnKeyboard = () => {
diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx
index 0fed0b0d..60cfe6ab 100644
--- a/web/pages/_app.tsx
+++ b/web/pages/_app.tsx
@@ -140,15 +140,40 @@ function MyApp(props: AppProps) {
useEffect(() => {
debug('isAndroidWebView app:', isAndroidApp())
- if (!Capacitor.isNativePlatform()) return
const onShow = () => document.body.classList.add('keyboard-open')
const onHide = () => document.body.classList.remove('keyboard-open')
- Keyboard.addListener('keyboardWillShow', onShow)
- Keyboard.addListener('keyboardWillHide', onHide)
+ if (Capacitor.isNativePlatform()) {
+ Keyboard.addListener('keyboardWillShow', onShow)
+ Keyboard.addListener('keyboardWillHide', onHide)
+
+ return () => {
+ Keyboard.removeAllListeners()
+ }
+ }
+
+ // Mobile browsers have no keyboard events, so infer the keyboard from the visual viewport: it
+ // shrinks by the keyboard's height while the layout viewport (window.innerHeight) stays put.
+ // Without this, `keyboard-open` never fires outside the app and the page keeps reserving the
+ // bottom nav's height, leaving a gap between the composer and the keyboard.
+ const vv = window.visualViewport
+ if (!vv) return
+ // Comfortably above the ~60px the collapsing URL bar accounts for, well below any keyboard.
+ const KEYBOARD_MIN_PX = 150
+ const update = () => {
+ const hidden = window.innerHeight - (vv.height + vv.offsetTop)
+ if (hidden > KEYBOARD_MIN_PX) onShow()
+ else onHide()
+ }
+
+ update()
+ vv.addEventListener('resize', update)
+ vv.addEventListener('scroll', update)
return () => {
- Keyboard.removeAllListeners()
+ vv.removeEventListener('resize', update)
+ vv.removeEventListener('scroll', update)
+ onHide()
}
}, [])