refactor: implement immersive mode for mobile keyboard layout and update related styles

This commit is contained in:
2026-07-11 21:18:06 +10:00
parent afeb45f59d
commit 7800e7ced7
7 changed files with 70 additions and 18 deletions

View File

@@ -1,3 +1,5 @@
{
"dependencies": {}
"dependencies": {
"@capacitor/status-bar": "8.0.1"
}
}

View File

@@ -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<HTMLDivElement>(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 (
<Page ref={roomViewRef}>
@@ -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 && (
<div className={composerCss.FollowingFloat}>
<div className={composerCss.FollowingFloatHit}>
<RoomViewFollowing room={room} />
@@ -171,6 +171,12 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
</>
)}
</div>
{!immersive &&
(hideActivity ? (
<RoomViewFollowingPlaceholder />
) : (
<RoomViewFollowing room={room} />
))}
</Box>
</>
)}

View File

@@ -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: {

View File

@@ -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 };
};

View File

@@ -9,11 +9,11 @@ type ClientLayoutProps = {
};
export function ClientLayout({ nav, children }: ClientLayoutProps) {
const { keyboardOpen } = useMobileKeyboardLayout();
const { immersive } = useMobileKeyboardLayout();
return (
<Box grow="Yes">
{!keyboardOpen && <Box shrink="No">{nav}</Box>}
{!immersive && <Box shrink="No">{nav}</Box>}
<Box grow="Yes">{children}</Box>
<DockedCallPanel />
</Box>

View File

@@ -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;