From 9498d7b4e40b465b67219c0225ea52f0be6cdf5f Mon Sep 17 00:00:00 2001 From: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:38:38 -0400 Subject: [PATCH 1/2] fix(mobile): support pre-Liquid-Glass iOS bottom toolbar - Fall back to standard search and toolbar primitives on older iOS - Preserve bottom-toolbar spacing and list content insets --- .../archive/ArchivedThreadsScreen.tsx | 14 ++- .../features/files/ThreadFilesRouteScreen.tsx | 8 +- apps/mobile/src/features/home/HomeHeader.tsx | 97 +++++++++---------- apps/mobile/src/features/home/HomeScreen.tsx | 9 +- .../layout/native-mail-search-toolbar.ts | 11 +++ apps/mobile/src/native/StackHeader.tsx | 9 +- 6 files changed, 89 insertions(+), 59 deletions(-) diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 916802e9faf..01440007bc6 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -29,7 +29,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { @@ -70,7 +73,8 @@ function ArchivedThreadsHeader(props: { const searchIconColor = useThemeColor("--color-icon"); const searchTextColor = useThemeColor("--color-foreground"); const usesNativeChrome = Platform.OS === "ios"; - const usesCompactMailToolbar = Platform.OS === "ios" && width < 700; + const usesCompactMailToolbar = + Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; const androidFilterActions = useMemo( () => [ { @@ -272,7 +276,11 @@ function ArchivedThreadsHeader(props: { ...(usesNativeChrome ? { allowToolbarIntegration: true, - placement: "integratedButton" as const, + // "integratedButton" is an iOS 26 search-bar placement; + // pre-glass iOS keeps the default pull-down placement. + ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED + ? { placement: "integratedButton" as const } + : null), } : { placement: "stacked" as const, diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 012f99536d2..7f5105aac17 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -29,7 +29,10 @@ import { useAdaptiveWorkspacePaneRole, useRegisterWorkspaceInspector, } from "../layout/AdaptiveWorkspaceLayout"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; import { ReviewHighlighterProvider } from "../review/ReviewHighlighterProvider"; import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; @@ -354,7 +357,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { ); } - const usesCompactMailToolbar = Platform.OS === "ios" && !layout.usesSplitView; + const usesCompactMailToolbar = + Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; return ( <> diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 4265107912b..12d5eaca473 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -13,7 +13,10 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { buildHomeListFilterMenu, @@ -320,9 +323,11 @@ function IosHomeHeader(props: HomeHeaderProps) { }), ] : undefined, - unstable_headerToolbarItems: - Platform.OS === "ios" - ? () => [ + // The keys below are set per-branch (not `undefined`) so a later + // reapply cannot clobber options owned by NativeHeaderToolbar. + ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED + ? { + unstable_headerToolbarItems: () => [ createNativeMailSearchToolbarItem({ composeButtonId: "home-new-task", composeSystemImageName: "square.and.pencil", @@ -336,14 +341,14 @@ function IosHomeHeader(props: HomeHeaderProps) { placeholder: "Search", searchTextChangeId: "home-search-text", }), - ] - : undefined, - headerSearchBarOptions: - Platform.OS === "ios" - ? undefined - : { + ], + } + : { + // Pre-Liquid-Glass iOS: standard pull-down search in the nav + // bar; create + sort live in the plain bottom toolbar below. + headerSearchBarOptions: { ref: searchBarRef, - allowToolbarIntegration: true, + autoCapitalize: "none" as const, hideNavigationBar: false, placeholder: "Search", onCancelButtonPress: () => { @@ -353,21 +358,11 @@ function IosHomeHeader(props: HomeHeaderProps) { props.onSearchQueryChange(event.nativeEvent.text); }, }, + }), }} /> - {Platform.OS === "ios" ? null : ( - - - - )} - - {Platform.OS === "ios" ? null : ( + {NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? null : ( ) : null} - - Sort projects - {PROJECT_SORT_OPTIONS.map((option) => ( - props.onProjectSortOrderChange(option.value)} - > - {option.label} - - ))} - + {threadListV2Enabled ? null : ( + + Sort projects + {PROJECT_SORT_OPTIONS.map((option) => ( + props.onProjectSortOrderChange(option.value)} + > + {option.label} + + ))} + + )} - - Sort threads - {THREAD_SORT_OPTIONS.map((option) => ( - props.onThreadSortOrderChange(option.value)} - > - {option.label} - - ))} - + {threadListV2Enabled ? null : ( + + Sort threads + {THREAD_SORT_OPTIONS.map((option) => ( + props.onThreadSortOrderChange(option.value)} + > + {option.label} + + ))} + + )} - - - + { const item = convertToolbarChild(child); if (item) { + if (item.type === "spacing") { + // Native inserts spacing items at `index`, treating a missing index + // as 0 — which would move the spacer in front of earlier siblings. + (item as { index?: number }).index = items.length; + } items.push(item); } }); @@ -440,6 +446,7 @@ function NativeHeaderToolbarLabel(_props: { readonly children?: ReactNode }) { NativeHeaderToolbarLabel.displayName = "NativeHeaderToolbarLabel"; function NativeHeaderToolbarSpacer(_props: { + readonly flexible?: boolean; readonly sharesBackground?: boolean; readonly width?: number; }) { From 2488a2d49f9a1139a538ffdfc21a343f849927f4 Mon Sep 17 00:00:00 2001 From: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:32:21 -0400 Subject: [PATCH 2/2] fix(mobile): clear fallback toolbar in home layouts --- apps/mobile/src/features/home/HomeScreen.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index c55488490f1..79e452572c1 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -102,6 +102,7 @@ interface HomeScreenProps { /* ─── Layout constants ───────────────────────────────────────────────── */ const ESTIMATED_THREAD_ROW_HEIGHT = 72; +const PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT = 44; /** * Top spacing between the list and the Android custom header. The Android * header (AndroidHomeHeader) is rendered in-flow above this screen and @@ -189,6 +190,9 @@ export function HomeScreen(props: HomeScreenProps) { const listRef = useRef(null); const insets = useSafeAreaInsets(); const accentColor = useThemeColor("--color-icon-muted"); + const iosBottomToolbarClearance = NATIVE_LIQUID_GLASS_SUPPORTED + ? 0 + : PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT; const effectiveGroupDisplayStates = useMemo(() => { const next = new Map(groupDisplayStates); if (!AsyncResult.isSuccess(preferencesResult)) { @@ -810,7 +814,7 @@ export function HomeScreen(props: HomeScreenProps) { @@ -949,7 +953,7 @@ export function HomeScreen(props: HomeScreenProps) { contentContainerStyle={{ paddingBottom: Platform.OS === "ios" - ? Math.max(insets.bottom, 24) + 96 + ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance : Math.max(insets.bottom, 16) + 88, }} /> @@ -996,13 +1000,13 @@ export function HomeScreen(props: HomeScreenProps) { // "never". paddingBottom: Platform.OS === "ios" - ? Math.max(insets.bottom, 24) + (NATIVE_LIQUID_GLASS_SUPPORTED ? 24 : 68) + ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance : Math.max(insets.bottom, 16) + 88, }} scrollIndicatorInsets={ Platform.OS === "ios" ? { - bottom: Math.max(insets.bottom, 16) + (NATIVE_LIQUID_GLASS_SUPPORTED ? 24 : 68), + bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance, top: 0, } : undefined