From 7800e7ced76b35f5749a5d6bf6e0d16853e9b376 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sat, 11 Jul 2026 21:18:06 +1000 Subject: [PATCH] refactor: implement immersive mode for mobile keyboard layout and update related styles --- overlay/package-additions.json | 4 ++- overlay/src/app/features/room/RoomView.tsx | 16 +++++++--- .../features/room/RoomViewFollowing.css.ts | 24 +++++++++----- .../src/app/hooks/useMobileKeyboardLayout.ts | 32 ++++++++++++++++++- overlay/src/app/pages/client/ClientLayout.tsx | 4 +-- overlay/src/index.css | 5 +++ package.json | 3 +- 7 files changed, 70 insertions(+), 18 deletions(-) diff --git a/overlay/package-additions.json b/overlay/package-additions.json index 18a1e41..6b719eb 100644 --- a/overlay/package-additions.json +++ b/overlay/package-additions.json @@ -1,3 +1,5 @@ { - "dependencies": {} + "dependencies": { + "@capacitor/status-bar": "8.0.1" + } } diff --git a/overlay/src/app/features/room/RoomView.tsx b/overlay/src/app/features/room/RoomView.tsx index 2b891ae..8213401 100644 --- a/overlay/src/app/features/room/RoomView.tsx +++ b/overlay/src/app/features/room/RoomView.tsx @@ -14,7 +14,7 @@ import { RoomTimeline } from './RoomTimeline'; import { RoomViewTyping } from './RoomViewTyping'; import { RoomTombstone } from './RoomTombstone'; import { RoomInput } from './RoomInput'; -import { RoomViewFollowing } from './RoomViewFollowing'; +import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing'; import { Page } from '../../components/page'; import { RoomViewHeader } from './RoomViewHeader'; import { useKeyDown } from '../../hooks/useKeyDown'; @@ -65,7 +65,7 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) { const roomViewRef = useRef(null); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); - const { isLandscape, keyboardOpen } = useMobileKeyboardLayout(); + const { immersive } = useMobileKeyboardLayout(); const { roomId } = room; const editor = useEditor(); @@ -104,9 +104,9 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) { ) ); - const flushComposer = keyboardOpen || isLandscape; + const flushComposer = immersive; const composerPadding = flushComposer ? '0' : `0 ${config.space.S400}`; - const showHeader = !keyboardOpen; + const showHeader = !immersive; return ( @@ -134,7 +134,7 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) { data-composer-flush={flushComposer ? 'true' : undefined} className={composerCss.ComposerDock} > - {!hideActivity && ( + {immersive && !hideActivity && (
@@ -171,6 +171,12 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) { )}
+ {!immersive && + (hideActivity ? ( + + ) : ( + + ))} )} diff --git a/overlay/src/app/features/room/RoomViewFollowing.css.ts b/overlay/src/app/features/room/RoomViewFollowing.css.ts index 58a326a..748ecab 100644 --- a/overlay/src/app/features/room/RoomViewFollowing.css.ts +++ b/overlay/src/app/features/room/RoomViewFollowing.css.ts @@ -1,12 +1,11 @@ import { style } from '@vanilla-extract/css'; import { recipe } from '@vanilla-extract/recipes'; -import { DefaultReset, color, config } from 'folds'; +import { DefaultReset, color, config, toRem } from 'folds'; export const RoomViewFollowingPlaceholder = style([ DefaultReset, { - display: 'none', - height: 0, + height: toRem(28), }, ]); @@ -14,13 +13,22 @@ export const RoomViewFollowing = recipe({ base: [ DefaultReset, { - minHeight: 0, - padding: `${config.space.S100} ${config.space.S200}`, - width: 'auto', - maxWidth: '100%', - backgroundColor: 'transparent', + minHeight: toRem(28), + padding: `0 ${config.space.S400}`, + width: '100%', + backgroundColor: color.Surface.Container, color: color.Surface.OnContainer, outline: 'none', + selectors: { + // Compact floating chip while the keyboard is open. + '[data-composer-flush="true"] &': { + minHeight: 0, + width: 'auto', + maxWidth: '100%', + padding: `${config.space.S100} ${config.space.S200}`, + backgroundColor: 'transparent', + }, + }, }, ], variants: { diff --git a/overlay/src/app/hooks/useMobileKeyboardLayout.ts b/overlay/src/app/hooks/useMobileKeyboardLayout.ts index a64e194..e63b5d0 100644 --- a/overlay/src/app/hooks/useMobileKeyboardLayout.ts +++ b/overlay/src/app/hooks/useMobileKeyboardLayout.ts @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react'; +import { isCapacitorNative } from '../utils/tauri'; const KEYBOARD_HEIGHT_THRESHOLD_PX = 120; +const IMMERSIVE_CLASS = 'immersive-landscape-keyboard'; const getIsLandscape = (): boolean => { if (typeof window === 'undefined') return false; @@ -24,9 +26,25 @@ const getKeyboardOpen = (): boolean => { return tag === 'INPUT' || tag === 'TEXTAREA'; }; +const setNativeStatusBarHidden = async (hidden: boolean) => { + if (!isCapacitorNative()) return; + try { + const { StatusBar } = await import('@capacitor/status-bar'); + if (hidden) { + await StatusBar.hide(); + } else { + await StatusBar.show(); + } + } catch { + // Plugin may be missing in some builds; CSS safe-area collapse still applies. + } +}; + export type MobileKeyboardLayout = { isLandscape: boolean; keyboardOpen: boolean; + /** Landscape + keyboard: compact immersive room chrome. */ + immersive: boolean; }; /** @@ -37,6 +55,8 @@ export const useMobileKeyboardLayout = (): MobileKeyboardLayout => { const [isLandscape, setIsLandscape] = useState(getIsLandscape); const [keyboardOpen, setKeyboardOpen] = useState(getKeyboardOpen); + const immersive = isLandscape && keyboardOpen; + useEffect(() => { const sync = () => { setIsLandscape(getIsLandscape()); @@ -63,5 +83,15 @@ export const useMobileKeyboardLayout = (): MobileKeyboardLayout => { }; }, []); - return { isLandscape, keyboardOpen }; + useEffect(() => { + document.documentElement.classList.toggle(IMMERSIVE_CLASS, immersive); + void setNativeStatusBarHidden(immersive); + + return () => { + document.documentElement.classList.remove(IMMERSIVE_CLASS); + void setNativeStatusBarHidden(false); + }; + }, [immersive]); + + return { isLandscape, keyboardOpen, immersive }; }; diff --git a/overlay/src/app/pages/client/ClientLayout.tsx b/overlay/src/app/pages/client/ClientLayout.tsx index 19577ad..4da43e7 100644 --- a/overlay/src/app/pages/client/ClientLayout.tsx +++ b/overlay/src/app/pages/client/ClientLayout.tsx @@ -9,11 +9,11 @@ type ClientLayoutProps = { }; export function ClientLayout({ nav, children }: ClientLayoutProps) { - const { keyboardOpen } = useMobileKeyboardLayout(); + const { immersive } = useMobileKeyboardLayout(); return ( - {!keyboardOpen && {nav}} + {!immersive && {nav}} {children} diff --git a/overlay/src/index.css b/overlay/src/index.css index fe1a70b..e211462 100644 --- a/overlay/src/index.css +++ b/overlay/src/index.css @@ -97,6 +97,11 @@ body { font-variant-ligatures: no-contextual; } +/* Landscape + keyboard: reclaim status-bar inset while chrome is hidden. */ +html.immersive-landscape-keyboard { + --safe-area-inset-top: 0px; +} + /* Prevent zalgo text and combining diacritics from overflowing line boundaries */ p, span, div, a, button, li, td, th, h1, h2, h3, h4, h5, h6 { line-height: 1.5; diff --git a/package.json b/package.json index 7f14bad..6e1f3af 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "@capacitor/android": "8.3.0", "@capacitor/cli": "8.3.0", "@capacitor/core": "8.3.0", - "@capacitor/local-notifications": "8.0.2" + "@capacitor/local-notifications": "8.0.2", + "@capacitor/status-bar": "8.0.1" }, "devDependencies": { "node-fetch": "3.3.2",