History

fix(mobile): support pre-Liquid-Glass iOS bottom toolbar (#4984)

Gabriel De Andrade committed 3 months ago

Commit 323dc321 parent 6154b466 files · +96 −61Browse files

Unified · Split

6 files
  1. apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +11 −3
  2. apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +6 −2
  3. apps/mobile/src/features/home/HomeHeader.tsx +47 −50
  4. apps/mobile/src/features/home/HomeScreen.tsx +13 −5
  5. apps/mobile/src/features/layout/native-mail-search-toolbar.ts +11 −0
  6. apps/mobile/src/native/StackHeader.tsx +8 −1
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+11−3
@@ -29,7 +29,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; · 28 unmodified lines
2929 import { relativeTime } from "../../lib/time";
3030 import { useThemeColor } from "../../lib/useThemeColor";
3131 import { ThreadSwipeable } from "../home/thread-swipe-actions";
32−import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar";
32+import {
33+ createNativeMailSearchToolbarItem,
34+ NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
35+} from "../layout/native-mail-search-toolbar";
3336 import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList";
3437
3538 export interface ArchivedThreadsHeaderEnvironment {
@@ -70,7 +73,8 @@ function ArchivedThreadsHeader(props: { · 34 unmodified lines
7073 const searchIconColor = useThemeColor("--color-icon");
7174 const searchTextColor = useThemeColor("--color-foreground");
7275 const usesNativeChrome = Platform.OS === "ios";
73− const usesCompactMailToolbar = Platform.OS === "ios" && width < 700;
76+ const usesCompactMailToolbar =
77+ Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;
7478 const androidFilterActions = useMemo<MenuAction[]>(
7579 () => [
7680 {
@@ -272,7 +276,11 @@ function ArchivedThreadsHeader(props: { · 195 unmodified lines
272276 ...(usesNativeChrome
273277 ? {
274278 allowToolbarIntegration: true,
275− placement: "integratedButton" as const,
279+ // "integratedButton" is an iOS 26 search-bar placement;
280+ // pre-glass iOS keeps the default pull-down placement.
281+ ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED
282+ ? { placement: "integratedButton" as const }
283+ : null),
276284 }
277285 : {
278286 placement: "stacked" as const,
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+6−2
@@ -29,7 +29,10 @@ import { · 28 unmodified lines
2929 useAdaptiveWorkspacePaneRole,
3030 useRegisterWorkspaceInspector,
3131 } from "../layout/AdaptiveWorkspaceLayout";
32−import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar";
32+import {
33+ createNativeMailSearchToolbarItem,
34+ NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
35+} from "../layout/native-mail-search-toolbar";
3336 import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
3437 import { ReviewHighlighterProvider } from "../review/ReviewHighlighterProvider";
3538 import { ThreadRouteScreen } from "../threads/ThreadRouteScreen";
@@ -354,7 +357,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { · 318 unmodified lines
354357 );
355358 }
356359
357− const usesCompactMailToolbar = Platform.OS === "ios" && !layout.usesSplitView;
360+ const usesCompactMailToolbar =
361+ Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;
358362
359363 return (
360364 <>
apps/mobile/src/features/home/HomeHeader.tsx+47−50
@@ -13,7 +13,10 @@ import { useThemeColor } from "../../lib/useThemeColor"; · 12 unmodified lines
1313 import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
1414 import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
1515 import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
16−import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar";
16+import {
17+ createNativeMailSearchToolbarItem,
18+ NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
19+} from "../layout/native-mail-search-toolbar";
1720 import type { HomeProjectSortOrder } from "./homeThreadList";
1821 import {
1922 buildHomeListFilterMenu,
@@ -320,9 +323,11 @@ function IosHomeHeader(props: HomeHeaderProps) { · 300 unmodified lines
320323 }),
321324 ]
322325 : undefined,
323− unstable_headerToolbarItems:
324− Platform.OS === "ios"
325− ? () => [
326+ // The keys below are set per-branch (not `undefined`) so a later
327+ // reapply cannot clobber options owned by NativeHeaderToolbar.
328+ ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED
329+ ? {
330+ unstable_headerToolbarItems: () => [
326331 createNativeMailSearchToolbarItem({
327332 composeButtonId: "home-new-task",
328333 composeSystemImageName: "square.and.pencil",
@@ -336,14 +341,14 @@ function IosHomeHeader(props: HomeHeaderProps) { · 7 unmodified lines
336341 placeholder: "Search",
337342 searchTextChangeId: "home-search-text",
338343 }),
339− ]
340− : undefined,
341− headerSearchBarOptions:
342− Platform.OS === "ios"
343− ? undefined
344− : {
344+ ],
345+ }
346+ : {
347+ // Pre-Liquid-Glass iOS: standard pull-down search in the nav
348+ // bar; create + sort live in the plain bottom toolbar below.
349+ headerSearchBarOptions: {
345350 ref: searchBarRef,
346− allowToolbarIntegration: true,
351+ autoCapitalize: "none" as const,
347352 hideNavigationBar: false,
348353 placeholder: "Search",
349354 onCancelButtonPress: () => {
@@ -353,21 +358,11 @@ function IosHomeHeader(props: HomeHeaderProps) { · 3 unmodified lines
353358 props.onSearchQueryChange(event.nativeEvent.text);
354359 },
355360 },
361+ }),
356362 }}
357363 />
358364
359− {Platform.OS === "ios" ? null : (
360− <NativeHeaderToolbar placement="right">
361− <NativeHeaderToolbar.Button
362− accessibilityLabel="Open settings"
363− icon="gearshape"
364− onPress={props.onOpenSettings}
365− separateBackground
366− />
367− </NativeHeaderToolbar>
368− )}
369−
370− {Platform.OS === "ios" ? null : (
365+ {NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? null : (
371366 <NativeHeaderToolbar placement="bottom">
372367 <NativeHeaderToolbar.Menu
373368 accessibilityLabel="Filter and sort threads"
@@ -421,35 +416,37 @@ function IosHomeHeader(props: HomeHeaderProps) { · 47 unmodified lines
421416 </NativeHeaderToolbar.Menu>
422417 ) : null}
423418
424− <NativeHeaderToolbar.Menu title="Sort projects">
425− <NativeHeaderToolbar.Label>Sort projects</NativeHeaderToolbar.Label>
426− {PROJECT_SORT_OPTIONS.map((option) => (
427− <NativeHeaderToolbar.MenuAction
428− key={option.value}
429− isOn={props.projectSortOrder === option.value}
430− onPress={() => props.onProjectSortOrderChange(option.value)}
431− >
432− <NativeHeaderToolbar.Label>{option.label}</NativeHeaderToolbar.Label>
433− </NativeHeaderToolbar.MenuAction>
434− ))}
435− </NativeHeaderToolbar.Menu>
419+ {threadListV2Enabled ? null : (
420+ <NativeHeaderToolbar.Menu title="Sort projects">
421+ <NativeHeaderToolbar.Label>Sort projects</NativeHeaderToolbar.Label>
422+ {PROJECT_SORT_OPTIONS.map((option) => (
423+ <NativeHeaderToolbar.MenuAction
424+ key={option.value}
425+ isOn={props.projectSortOrder === option.value}
426+ onPress={() => props.onProjectSortOrderChange(option.value)}
427+ >
428+ <NativeHeaderToolbar.Label>{option.label}</NativeHeaderToolbar.Label>
429+ </NativeHeaderToolbar.MenuAction>
430+ ))}
431+ </NativeHeaderToolbar.Menu>
432+ )}
436433
437− <NativeHeaderToolbar.Menu title="Sort threads">
438− <NativeHeaderToolbar.Label>Sort threads</NativeHeaderToolbar.Label>
439− {THREAD_SORT_OPTIONS.map((option) => (
440− <NativeHeaderToolbar.MenuAction
441− key={option.value}
442− isOn={props.threadSortOrder === option.value}
443− onPress={() => props.onThreadSortOrderChange(option.value)}
444− >
445− <NativeHeaderToolbar.Label>{option.label}</NativeHeaderToolbar.Label>
446− </NativeHeaderToolbar.MenuAction>
447− ))}
448− </NativeHeaderToolbar.Menu>
434+ {threadListV2Enabled ? null : (
435+ <NativeHeaderToolbar.Menu title="Sort threads">
436+ <NativeHeaderToolbar.Label>Sort threads</NativeHeaderToolbar.Label>
437+ {THREAD_SORT_OPTIONS.map((option) => (
438+ <NativeHeaderToolbar.MenuAction
439+ key={option.value}
440+ isOn={props.threadSortOrder === option.value}
441+ onPress={() => props.onThreadSortOrderChange(option.value)}
442+ >
443+ <NativeHeaderToolbar.Label>{option.label}</NativeHeaderToolbar.Label>
444+ </NativeHeaderToolbar.MenuAction>
445+ ))}
446+ </NativeHeaderToolbar.Menu>
447+ )}
449448 </NativeHeaderToolbar.Menu>
450− <NativeHeaderToolbar.Spacer width={8} sharesBackground={false} />
451− <NativeHeaderToolbar.SearchBarSlot />
452− <NativeHeaderToolbar.Spacer width={8} sharesBackground={false} />
449+ <NativeHeaderToolbar.Spacer flexible />
453450 <NativeHeaderToolbar.Button
454451 accessibilityLabel="New task"
455452 icon="square.and.pencil"
apps/mobile/src/features/home/HomeScreen.tsx+13−5
@@ -109,6 +109,7 @@ interface HomeScreenProps { · 108 unmodified lines
109109 /* ─── Layout constants ───────────────────────────────────────────────── */
110110
111111 const ESTIMATED_THREAD_ROW_HEIGHT = 72;
112+const PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT = 44;
112113 /**
113114 * Top spacing between the list and the Android custom header. The Android
114115 * header (AndroidHomeHeader) is rendered in-flow above this screen and
@@ -196,6 +197,10 @@ export function HomeScreen(props: HomeScreenProps) { · 81 unmodified lines
196197 const listRef = useRef<LegendListRef | null>(null);
197198 const insets = useSafeAreaInsets();
198199 const accentColor = useThemeColor("--color-icon-muted");
200+ const iosBottomToolbarClearance =
201+ Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED
202+ ? PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT
203+ : 0;
199204 const searchEnvironmentIds = useMemo(
200205 () =>
201206 props.selectedEnvironmentId === null
@@ -877,7 +882,7 @@ export function HomeScreen(props: HomeScreenProps) { · 675 unmodified lines
877882 <View
878883 className="flex-1 items-center justify-center bg-screen px-8"
879884 style={{
880− paddingBottom: Math.max(insets.bottom, 24),
885+ paddingBottom: Math.max(insets.bottom, 24) + iosBottomToolbarClearance,
881886 paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,
882887 }}
883888 >
@@ -1019,7 +1024,7 @@ export function HomeScreen(props: HomeScreenProps) { · 135 unmodified lines
10191024 contentContainerStyle={{
10201025 paddingBottom:
10211026 Platform.OS === "ios"
1022− ? Math.max(insets.bottom, 24) + 96
1027+ ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance
10231028 : Math.max(insets.bottom, 16) + 88,
10241029 }}
10251030 />
@@ -1060,16 +1065,19 @@ export function HomeScreen(props: HomeScreenProps) { · 34 unmodified lines
10601065 scrollEventThrottle={16}
10611066 contentContainerStyle={{
10621067 // Android reserves room for the floating new-task FAB
1063− // (56 button + 16 gap + bottom inset).
1068+ // (56 button + 16 gap + bottom inset). Pre-glass iOS shows a
1069+ // standard 44pt bottom toolbar that overlays the list and is not
1070+ // reflected in insets while contentInsetAdjustmentBehavior is
1071+ // "never".
10641072 paddingBottom:
10651073 Platform.OS === "ios"
1066− ? Math.max(insets.bottom, 24) + 24
1074+ ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance
10671075 : Math.max(insets.bottom, 16) + 88,
10681076 }}
10691077 scrollIndicatorInsets={
10701078 Platform.OS === "ios"
10711079 ? {
1072− bottom: Math.max(insets.bottom, 16) + 24,
1080+ bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance,
10731081 top: 0,
10741082 }
10751083 : undefined
apps/mobile/src/features/layout/native-mail-search-toolbar.ts+11−0
@@ -1,5 +1,16 @@
11 import type { HeaderBarButtonMailSearchToolbarItem } from "react-native-screens";
22
3+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
4+
5+/**
6+ * The patched mail-style toolbar is built natively from iOS 26 Liquid Glass
7+ * UIKit (`UIGlassEffect`) with no earlier fallback: pre-26 the native side
8+ * silently drops the item and hides the navigation toolbar entirely. Screens
9+ * that send it must fall back to standard search/toolbar primitives when this
10+ * is false.
11+ */
12+export const NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED = NATIVE_LIQUID_GLASS_SUPPORTED;
13+
314 type NativeMailSearchToolbarInput = Omit<
415 HeaderBarButtonMailSearchToolbarItem,
516 "type" | "useFallbackSearchField"
apps/mobile/src/native/StackHeader.tsx+8−1
@@ -340,7 +340,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { · 339 unmodified lines
340340 return {
341341 type: "spacing",
342342 spacing: typeof child.props.width === "number" ? child.props.width : 8,
343− };
343+ flexible: Boolean(child.props.flexible),
344+ } as NativeStackHeaderItem;
344345 }
345346
346347 return null;
@@ -351,6 +352,11 @@ function collectToolbarItems(children: ReactNode): NativeStackHeaderItem[] { · 4 unmodified lines
351352 Children.forEach(children, (child) => {
352353 const item = convertToolbarChild(child);
353354 if (item) {
355+ if (item.type === "spacing") {
356+ // Native inserts spacing items at `index`, treating a missing index
357+ // as 0 — which would move the spacer in front of earlier siblings.
358+ (item as { index?: number }).index = items.length;
359+ }
354360 items.push(item);
355361 }
356362 });
@@ -440,6 +446,7 @@ function NativeHeaderToolbarLabel(_props: { readonly children?: ReactNode }) { · 83 unmodified lines
440446 NativeHeaderToolbarLabel.displayName = "NativeHeaderToolbarLabel";
441447
442448 function NativeHeaderToolbarSpacer(_props: {
449+ readonly flexible?: boolean;
443450 readonly sharesBackground?: boolean;
444451 readonly width?: number;
445452 }) {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t