fix(android): apply env(safe-area-inset-bottom) so 3-button nav stops covering bottom-anchored UI

This commit is contained in:
heaven 2026-05-27 22:43:28 +03:00
parent aca33f470d
commit 443213b4b6
6 changed files with 47 additions and 13 deletions

View file

@ -228,8 +228,17 @@ export const handleBar = style({
// up over the inline form. The DirectSelfRow ending up immediately // up over the inline form. The DirectSelfRow ending up immediately
// above the keyboard would block the user's view of the form they're // above the keyboard would block the user's view of the form they're
// typing into. // typing into.
//
// `paddingBottom: env(safe-area-inset-bottom)` lifts DirectSelfRow /
// WorkspaceFooter above Android 3-button nav in edge-to-edge mode.
// The inset zone paints in the curtain's `Background.Container` bg
// (curtain has `overflow: hidden`), so the system bar reads as a
// continuation of the curtain tone — no visible seam. The keyboard
// `height: 0; overflow: hidden` collapse above also clips the
// padding region — keyboard handling preserved.
export const bottomPinnedSlot = style({ export const bottomPinnedSlot = style({
flexShrink: 0, flexShrink: 0,
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
}); });
// Segment button (Direct / Channels / Bots). // Segment button (Direct / Channels / Bots).

View file

@ -1,10 +1,6 @@
import { globalStyle, style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
import { color, toRem } from 'folds'; import { color, toRem } from 'folds';
import { import { Editor, EditorTextarea, EditorTextareaScroll } from '../../components/editor/Editor.css';
Editor,
EditorTextarea,
EditorTextareaScroll,
} from '../../components/editor/Editor.css';
import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe'; import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
// Main chat composer — Dawn canon (stream-v2-dawn.jsx line 285-307): a // Main chat composer — Dawn canon (stream-v2-dawn.jsx line 285-307): a
@ -28,6 +24,13 @@ export const ChatComposer = style({});
// slide/fade transition driven by the `data-hidden` attribute set from // slide/fade transition driven by the `data-hidden` attribute set from
// React state. CSS class (not inline `transition`) so the // React state. CSS class (not inline `transition`) so the
// `prefers-reduced-motion` media query can disable the motion. // `prefers-reduced-motion` media query can disable the motion.
//
// `env(safe-area-inset-bottom)` for Android 3-button-nav clearance lives
// on the INNER `ChatComposer` div (see `RoomView.tsx`), not here. The
// wrapper is the `ResizeObserver` target for `composerHeight`, and
// `entry.contentRect` returns content-box (excludes the observed node's
// own padding); padding here would inflate border-box without growing
// the reported height, letting the last message slip behind the inset.
export const ComposerOverlay = style({ export const ComposerOverlay = style({
position: 'absolute', position: 'absolute',
left: 0, left: 0,

View file

@ -174,8 +174,13 @@ export function RoomView({ eventId }: { eventId?: string }) {
> >
<div <div
className={css.ChatComposer} className={css.ChatComposer}
// `calc(GAP + env(safe-area-inset-bottom))` on the bottom —
// see `ComposerOverlay` css for why the inset lives here
// and not on the wrapper.
style={{ style={{
padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}`, padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} calc(${toRem(
VOJO_HORSESHOE_GAP_PX
)} + env(safe-area-inset-bottom, 0px))`,
}} }}
> >
{tombstoneEvent ? ( {tombstoneEvent ? (

View file

@ -14,6 +14,12 @@ export const RoomViewTyping = style([
DefaultReset, DefaultReset,
{ {
padding: `0 ${config.space.S500}`, padding: `0 ${config.space.S500}`,
// Lift typing text above the Android 3-button nav for the
// composer-hidden window (thread drawer open / scroll-hide). The
// main composer sits at `bottom: 0` with its own inset and fully
// covers this strip at rest, so the duplicate inset is invisible
// in normal flow.
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
width: '100%', width: '100%',
backgroundColor: color.SurfaceVariant.Container, backgroundColor: color.SurfaceVariant.Container,
color: color.Surface.OnContainer, color: color.Surface.OnContainer,

View file

@ -1,9 +1,6 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds'; import { color, config, toRem } from 'folds';
import { import { VOJO_HORSESHOE_GAP_PX, VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
VOJO_HORSESHOE_GAP_PX,
VOJO_HORSESHOE_RADIUS_PX,
} from '../../styles/horseshoe';
// Desktop wrapper for the resizable thread drawer. Sizing and the // Desktop wrapper for the resizable thread drawer. Sizing and the
// absolutely-positioned resize handle live here; the inner aside // absolutely-positioned resize handle live here; the inner aside
@ -231,9 +228,15 @@ export const ThreadCounterText = style({
// reapplied to the inner wrap in `ThreadDrawer.tsx` so the same // reapplied to the inner wrap in `ThreadDrawer.tsx` so the same
// `globalStyle` rules (`Surface.Container` bg, 32px radius, dark // `globalStyle` rules (`Surface.Container` bg, 32px radius, dark
// touch-hover gate) reach the Editor inside. // touch-hover gate) reach the Editor inside.
//
// Bottom side adds `env(safe-area-inset-bottom)` so the Send button
// clears Android 3-button nav in edge-to-edge mode, mirroring the
// main `ChatComposer` in `RoomView.tsx`.
export const ThreadComposer = style({ export const ThreadComposer = style({
flexShrink: 0, flexShrink: 0,
padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}`, padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} calc(${toRem(
VOJO_HORSESHOE_GAP_PX
)} + env(safe-area-inset-bottom, 0px))`,
}); });
// Bubble chrome itself lives in `Channel.css.ts` and applies via the // Bubble chrome itself lives in `Channel.css.ts` and applies via the

View file

@ -17,8 +17,16 @@ import { color } from 'folds';
// Bottom inset is owned per-component: surfaces that anchor // Bottom inset is owned per-component: surfaces that anchor
// interactive content at the screen bottom and need 3-button-nav / // interactive content at the screen bottom and need 3-button-nav /
// home-indicator clearance read `env(safe-area-inset-bottom)` // home-indicator clearance read `env(safe-area-inset-bottom)`
// themselves (e.g. `SyncIndicator.css.ts`). The chat surface / // themselves (e.g. `SyncIndicator.css.ts`,
// composer extend flush to `body_bottom` by design. // `StreamHeader.bottomPinnedSlot` for DirectSelfRow / WorkspaceFooter,
// `RoomView.tsx` ChatComposer inner div, the `panelContent` rules in
// MobileSettings / ChannelsWorkspace / MobileMediaViewer horseshoes).
// The chat overlay wrapper (`ComposerOverlay`) itself stays `bottom: 0`
// flush — the inset lives on the inner card padding so
// `ResizeObserver.contentRect` keeps `composerHeight` in sync with
// the visible overlay height for `RoomTimeline.bottomOverlayHeight`.
// Bot widgets are responsible for their own gesture-pill clearance
// (see `BotShell.css.ts::Shell`).
globalStyle(':root', { globalStyle(':root', {
vars: { vars: {
'--vojo-safe-top': 'env(safe-area-inset-top, 0px)', '--vojo-safe-top': 'env(safe-area-inset-top, 0px)',