fix(android): pad content root by WindowInsets.tappableElement so 3-button nav clears UI while gesture mode stays edge-to-edge
This commit is contained in:
parent
53acca3755
commit
61fdf06126
10 changed files with 103 additions and 70 deletions
|
|
@ -1,11 +1,16 @@
|
|||
package chat.vojo.app;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
|
|
@ -86,6 +91,60 @@ public class MainActivity extends BridgeActivity {
|
|||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||
controller.setAppearanceLightStatusBars(false);
|
||||
controller.setAppearanceLightNavigationBars(false);
|
||||
|
||||
// 3-button nav clearance. Reads `tappableElement` (= 0 in gesture
|
||||
// mode, = nav-bar height in 3-button mode) and applies it as
|
||||
// padding on the activity's content root — NOT on the WebView
|
||||
// itself. The WebView is a child of the content root, so root
|
||||
// padding shrinks the WebView's layout area; in 3-button mode
|
||||
// the WebView ends above the nav bar and the activity
|
||||
// windowBackground strip behind it paints `splash_bg` (#0d0e11)
|
||||
// which matches the dark body bg, so system icons read as
|
||||
// continuous with the chat surface. Gesture mode stays
|
||||
// edge-to-edge (padding = 0). Left/right are included for
|
||||
// landscape 3-button mode where the nav bar rotates to a side.
|
||||
//
|
||||
// CRITICAL: the listener MUST live on the content root, not on
|
||||
// the WebView. Attaching it to the WebView replaces WebView's
|
||||
// internal `OnApplyWindowInsetsListener` (the one Chromium uses
|
||||
// to feed CSS `env(safe-area-inset-*)`), which silently breaks
|
||||
// `env(safe-area-inset-top)` → `--vojo-safe-top` → every top-
|
||||
// anchored UI clearance for the status bar. Padding the parent
|
||||
// leaves WebView's window-insets pipeline untouched while still
|
||||
// shrinking its visual area.
|
||||
//
|
||||
// Why `tappableElement` and not `systemBars()` / `navigationBars()`:
|
||||
// both report ~24-32 dp in gesture mode too (the pill area),
|
||||
// which would lift UI in fullscreen — exactly the regression
|
||||
// commit 443213b4 had. `tappableElement` is the type defined as
|
||||
// «system UI regions where the user can tap», which means 0 in
|
||||
// gesture mode and the nav-bar in 3-button mode.
|
||||
//
|
||||
// Capacitor 8.3's bundled `SystemBars` plugin attaches its own
|
||||
// inset listener on `webView.getParent()` (CoordinatorLayout one
|
||||
// level deeper). Since `index.html` declares `viewport-fit=cover`,
|
||||
// Capacitor takes the passthrough branch and doesn't pad — no
|
||||
// compound-padding with our listener. If `viewport-fit=cover` is
|
||||
// ever removed, set `plugins.SystemBars.insetsHandling = "disable"`
|
||||
// in `capacitor.config.ts` to avoid double-lift.
|
||||
//
|
||||
// Fallback for API < 29 (Android 7-9): `tappableElement` did
|
||||
// not exist before Q. AndroidX backports the call to API 24+ as
|
||||
// `getSystemWindowInsets()` (≈ `navigationBars()`); the explicit
|
||||
// gate makes the intent visible at the call site.
|
||||
final View contentRoot = findViewById(android.R.id.content);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(contentRoot, (v, windowInsets) -> {
|
||||
final int typeMask = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
? WindowInsetsCompat.Type.tappableElement()
|
||||
: WindowInsetsCompat.Type.navigationBars();
|
||||
Insets ins = windowInsets.getInsets(typeMask);
|
||||
v.setPadding(ins.left, 0, ins.right, ins.bottom);
|
||||
// Do NOT consume — propagate to WebView (CSS env() pipeline)
|
||||
// and Capacitor's SystemBars listener so they still see
|
||||
// unmodified insets.
|
||||
return windowInsets;
|
||||
});
|
||||
ViewCompat.requestApplyInsets(contentRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -238,17 +238,8 @@ export const handleBar = style({
|
|||
// up over the inline form. The DirectSelfRow ending up immediately
|
||||
// above the keyboard would block the user's view of the form they're
|
||||
// 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({
|
||||
flexShrink: 0,
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
});
|
||||
|
||||
// Segment button (Direct / Channels / Bots).
|
||||
|
|
|
|||
|
|
@ -55,8 +55,11 @@ export const silhouette = style({
|
|||
});
|
||||
|
||||
// Top-anchored — handle + viewer body reveal top-down as silhouette
|
||||
// grows. `padding-bottom: env(safe-area-inset-bottom)` keeps the
|
||||
// download / share row clear of the Android nav bar.
|
||||
// grows. Android nav-bar clearance is owned by `MainActivity.java`'s
|
||||
// WindowInsets listener (the WebView is already sized above the nav
|
||||
// bar), so no `padding-bottom: env(...)` here — that would stack on
|
||||
// top of the native padding and lift the download / share row above
|
||||
// its visible host.
|
||||
export const panelContent = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
|
@ -65,7 +68,6 @@ export const panelContent = style({
|
|||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
});
|
||||
|
||||
// 20px drag-to-close band. The ONLY drag-to-close origin — touches
|
||||
|
|
|
|||
|
|
@ -24,13 +24,6 @@ export const ChatComposer = style({});
|
|||
// slide/fade transition driven by the `data-hidden` attribute set from
|
||||
// React state. CSS class (not inline `transition`) so the
|
||||
// `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({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|||
import { useEditor } from '../../components/editor';
|
||||
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
||||
import { RoomTimeline } from './RoomTimeline';
|
||||
import { RoomViewTyping } from './RoomViewTyping';
|
||||
import { RoomTombstone } from './RoomTombstone';
|
||||
import { RoomInput } from './RoomInput';
|
||||
import { Page } from '../../components/page';
|
||||
|
|
@ -157,7 +156,6 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
|||
bottomOverlayHeight={composerHeight}
|
||||
onComposerHiddenChange={setComposerHidden}
|
||||
/>
|
||||
<RoomViewTyping room={room} />
|
||||
</Box>
|
||||
{!threadDrawerOpen && (
|
||||
<div
|
||||
|
|
@ -174,13 +172,8 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
|||
>
|
||||
<div
|
||||
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={{
|
||||
padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} calc(${toRem(
|
||||
VOJO_HORSESHOE_GAP_PX
|
||||
)} + env(safe-area-inset-bottom, 0px))`,
|
||||
padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}`,
|
||||
}}
|
||||
>
|
||||
{tombstoneEvent ? (
|
||||
|
|
|
|||
|
|
@ -228,15 +228,9 @@ export const ThreadCounterText = style({
|
|||
// reapplied to the inner wrap in `ThreadDrawer.tsx` so the same
|
||||
// `globalStyle` rules (`Surface.Container` bg, 32px radius, dark
|
||||
// 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({
|
||||
flexShrink: 0,
|
||||
padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} calc(${toRem(
|
||||
VOJO_HORSESHOE_GAP_PX
|
||||
)} + env(safe-area-inset-bottom, 0px))`,
|
||||
padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}`,
|
||||
});
|
||||
|
||||
// Bubble chrome itself lives in `Channel.css.ts` and applies via the
|
||||
|
|
|
|||
|
|
@ -117,15 +117,13 @@ export const appBody = style({
|
|||
// Background: `SurfaceVariant.Container` (Dawn bg = #181a20) — the
|
||||
// chat-pane tone, same as the Settings PageNav inside (set via
|
||||
// `surface="surfaceVariant"`). Load-bearing: the silhouette's bg
|
||||
// shows through every panel gap that Settings's PageNav doesn't
|
||||
// cover — the 20px drag-handle band at the top and the
|
||||
// `env(safe-area-inset-bottom)` padding inside `panelContent` at the
|
||||
// gesture-pill strip. With `Background.Container` (#0d0e11) here, the
|
||||
// user saw dark stripes at both seams on Samsung S24 edge-to-edge;
|
||||
// matching silhouette to the PageNav tone closes them. Same idea as
|
||||
// commit 77bb72d which dynamically retunes `--vojo-safe-area-bg`
|
||||
// while a Room is mounted to keep the system-bar strips and the chat
|
||||
// surface in lockstep.
|
||||
// shows through the 20px drag-handle band at the top so the seam
|
||||
// matches the PageNav tone. With `Background.Container` (#0d0e11)
|
||||
// here, the user saw a dark stripe at that seam on Samsung S24
|
||||
// edge-to-edge; matching silhouette to the PageNav tone closes it.
|
||||
// Same idea as commit 77bb72d which dynamically retunes
|
||||
// `--vojo-safe-area-bg` while a Room is mounted to keep the
|
||||
// system-bar strips and the chat surface in lockstep.
|
||||
export const silhouette = style({
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
|
|
@ -144,21 +142,17 @@ export const silhouette = style({
|
|||
// are visible at the very first pixel of drag — gives the user
|
||||
// immediate "I'm opening Settings" feedback.
|
||||
//
|
||||
// `padding-bottom: env(safe-area-inset-bottom)` keeps the panel
|
||||
// content (Settings menu) clear of the Android nav bar in edge-to-edge
|
||||
// mode. `box-sizing: border-box` makes the inline `height: railHeightPx`
|
||||
// include the inset, so the panel's measured height includes the
|
||||
// reserved inset and the bottom of the visible content sits above the
|
||||
// system icons.
|
||||
// Android nav-bar clearance is owned by `MainActivity.java`'s
|
||||
// WindowInsets listener (the WebView is already sized above the nav
|
||||
// bar). No `padding-bottom: env(...)` here — that would stack on top
|
||||
// of the native padding and clip the Settings menu's bottom rows.
|
||||
export const panelContent = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
});
|
||||
|
||||
// Drag handle band — sits at the TOP of `panelContent` so it appears
|
||||
|
|
|
|||
|
|
@ -12,13 +12,17 @@ const slide = keyframes({
|
|||
|
||||
// Container is taller than the visible bar so the drop-shadow halo isn't
|
||||
// clipped by `overflow: hidden`. The bar itself sits flush to the bottom
|
||||
// edge of the safe-area inset (the 26px above is a transparent
|
||||
// pointer-events: none zone where only the glow renders).
|
||||
// edge of the WebView's visible area (the 26px above is a transparent
|
||||
// pointer-events: none zone where only the glow renders). Bottom inset
|
||||
// is owned by `MainActivity.java`'s WindowInsets listener — the WebView
|
||||
// is already sized to clear the 3-button nav bar, so `bottom: 0` lands
|
||||
// flush above the nav rather than under it. Side insets stay `env(...)`
|
||||
// for display-cutout clearance in landscape.
|
||||
export const root = style({
|
||||
position: 'fixed',
|
||||
left: 'env(safe-area-inset-left, 0px)',
|
||||
right: 'env(safe-area-inset-right, 0px)',
|
||||
bottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
bottom: 0,
|
||||
height: '28px',
|
||||
pointerEvents: 'none',
|
||||
// Z400 sits above app chrome and below folds modals/popouts (which use
|
||||
|
|
|
|||
|
|
@ -89,8 +89,11 @@ export const silhouette = style({
|
|||
willChange: 'height, border-top-left-radius, border-top-right-radius',
|
||||
});
|
||||
|
||||
// Top-anchored panel content. Padding-bottom reserves Android nav-bar
|
||||
// inset so the create-space row never tucks under the gesture pill.
|
||||
// Top-anchored panel content. Android nav-bar clearance is owned by
|
||||
// `MainActivity.java`'s WindowInsets listener (the WebView is already
|
||||
// sized above the nav bar), so no `padding-bottom: env(...)` here —
|
||||
// that would stack on top of the native padding and clip the
|
||||
// create-space row.
|
||||
export const panelContent = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
|
@ -99,7 +102,6 @@ export const panelContent = style({
|
|||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
||||
});
|
||||
|
||||
// 20px drag-to-close band at the top of the silhouette. The ONLY
|
||||
|
|
|
|||
|
|
@ -14,19 +14,20 @@ import { color } from 'folds';
|
|||
// headers don't double-pad. On web `env()` resolves to `0px`, so the
|
||||
// var is a no-op there.
|
||||
//
|
||||
// Bottom inset is owned per-component: surfaces that anchor
|
||||
// interactive content at the screen bottom and need 3-button-nav /
|
||||
// home-indicator clearance read `env(safe-area-inset-bottom)`
|
||||
// themselves (e.g. `SyncIndicator.css.ts`,
|
||||
// `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`).
|
||||
// Bottom inset is owned by `MainActivity.java::onCreate`'s
|
||||
// `OnApplyWindowInsetsListener` — it reads
|
||||
// `WindowInsetsCompat.Type.tappableElement()` (= 0 in gesture mode,
|
||||
// = nav-bar height in 3-button mode) and applies it as bottom
|
||||
// padding on the activity's content root (NOT on the WebView; that
|
||||
// would replace WebView's internal listener and break
|
||||
// `env(safe-area-inset-top)`). The web side stays unaware: in
|
||||
// gesture mode the WebView extends edge-to-edge, in 3-button mode
|
||||
// it is sized above the nav bar and the activity windowBackground
|
||||
// strip below paints the system icons over a tone-matched bg.
|
||||
// Don't add `padding-bottom: env(safe-area-inset-bottom)` to any
|
||||
// component — Chromium maps that env() to `systemBars()` which is
|
||||
// non-zero in gesture mode too, and it stacks on top of the native
|
||||
// padding, double-lifting the UI.
|
||||
globalStyle(':root', {
|
||||
vars: {
|
||||
'--vojo-safe-top': 'env(safe-area-inset-top, 0px)',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue