Unified · Split
| @@ -29,7 +29,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; · 28 unmodified lines | ||
| 29 | 29 | import { relativeTime } from "../../lib/time"; |
| 30 | 30 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 31 | 31 | 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"; | |
| 33 | 36 | import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; |
| 34 | 37 | |
| 35 | 38 | export interface ArchivedThreadsHeaderEnvironment { |
| @@ -70,7 +73,8 @@ function ArchivedThreadsHeader(props: { · 34 unmodified lines | ||
| 70 | 73 | const searchIconColor = useThemeColor("--color-icon"); |
| 71 | 74 | const searchTextColor = useThemeColor("--color-foreground"); |
| 72 | 75 | 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; | |
| 74 | 78 | const androidFilterActions = useMemo<MenuAction[]>( |
| 75 | 79 | () => [ |
| 76 | 80 | { |
| @@ -272,7 +276,11 @@ function ArchivedThreadsHeader(props: { · 195 unmodified lines | ||
| 272 | 276 | ...(usesNativeChrome |
| 273 | 277 | ? { |
| 274 | 278 | 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), | |
| 276 | 284 | } |
| 277 | 285 | : { |
| 278 | 286 | placement: "stacked" as const, |
| @@ -29,7 +29,10 @@ import { · 28 unmodified lines | ||
| 29 | 29 | useAdaptiveWorkspacePaneRole, |
| 30 | 30 | useRegisterWorkspaceInspector, |
| 31 | 31 | } 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"; | |
| 33 | 36 | import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; |
| 34 | 37 | import { ReviewHighlighterProvider } from "../review/ReviewHighlighterProvider"; |
| 35 | 38 | import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; |
| @@ -354,7 +357,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { · 318 unmodified lines | ||
| 354 | 357 | ); |
| 355 | 358 | } |
| 356 | 359 | |
| 357 | − const usesCompactMailToolbar = Platform.OS === "ios" && !layout.usesSplitView; | |
| 360 | + const usesCompactMailToolbar = | |
| 361 | + Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; | |
| 358 | 362 | |
| 359 | 363 | return ( |
| 360 | 364 | <> |
| @@ -13,7 +13,10 @@ import { useThemeColor } from "../../lib/useThemeColor"; · 12 unmodified lines | ||
| 13 | 13 | import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; |
| 14 | 14 | import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; |
| 15 | 15 | 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"; | |
| 17 | 20 | import type { HomeProjectSortOrder } from "./homeThreadList"; |
| 18 | 21 | import { |
| 19 | 22 | buildHomeListFilterMenu, |
| @@ -320,9 +323,11 @@ function IosHomeHeader(props: HomeHeaderProps) { · 300 unmodified lines | ||
| 320 | 323 | }), |
| 321 | 324 | ] |
| 322 | 325 | : 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: () => [ | |
| 326 | 331 | createNativeMailSearchToolbarItem({ |
| 327 | 332 | composeButtonId: "home-new-task", |
| 328 | 333 | composeSystemImageName: "square.and.pencil", |
| @@ -336,14 +341,14 @@ function IosHomeHeader(props: HomeHeaderProps) { · 7 unmodified lines | ||
| 336 | 341 | placeholder: "Search", |
| 337 | 342 | searchTextChangeId: "home-search-text", |
| 338 | 343 | }), |
| 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: { | |
| 345 | 350 | ref: searchBarRef, |
| 346 | − allowToolbarIntegration: true, | |
| 351 | + autoCapitalize: "none" as const, | |
| 347 | 352 | hideNavigationBar: false, |
| 348 | 353 | placeholder: "Search", |
| 349 | 354 | onCancelButtonPress: () => { |
| @@ -353,21 +358,11 @@ function IosHomeHeader(props: HomeHeaderProps) { · 3 unmodified lines | ||
| 353 | 358 | props.onSearchQueryChange(event.nativeEvent.text); |
| 354 | 359 | }, |
| 355 | 360 | }, |
| 361 | + }), | |
| 356 | 362 | }} |
| 357 | 363 | /> |
| 358 | 364 | |
| 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 : ( | |
| 371 | 366 | <NativeHeaderToolbar placement="bottom"> |
| 372 | 367 | <NativeHeaderToolbar.Menu |
| 373 | 368 | accessibilityLabel="Filter and sort threads" |
| @@ -421,35 +416,37 @@ function IosHomeHeader(props: HomeHeaderProps) { · 47 unmodified lines | ||
| 421 | 416 | </NativeHeaderToolbar.Menu> |
| 422 | 417 | ) : null} |
| 423 | 418 | |
| 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 | + )} | |
| 436 | 433 | |
| 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 | + )} | |
| 449 | 448 | </NativeHeaderToolbar.Menu> |
| 450 | − <NativeHeaderToolbar.Spacer width={8} sharesBackground={false} /> | |
| 451 | − <NativeHeaderToolbar.SearchBarSlot /> | |
| 452 | − <NativeHeaderToolbar.Spacer width={8} sharesBackground={false} /> | |
| 449 | + <NativeHeaderToolbar.Spacer flexible /> | |
| 453 | 450 | <NativeHeaderToolbar.Button |
| 454 | 451 | accessibilityLabel="New task" |
| 455 | 452 | icon="square.and.pencil" |
| @@ -109,6 +109,7 @@ interface HomeScreenProps { · 108 unmodified lines | ||
| 109 | 109 | /* ─── Layout constants ───────────────────────────────────────────────── */ |
| 110 | 110 | |
| 111 | 111 | const ESTIMATED_THREAD_ROW_HEIGHT = 72; |
| 112 | +const PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT = 44; | |
| 112 | 113 | /** |
| 113 | 114 | * Top spacing between the list and the Android custom header. The Android |
| 114 | 115 | * header (AndroidHomeHeader) is rendered in-flow above this screen and |
| @@ -196,6 +197,10 @@ export function HomeScreen(props: HomeScreenProps) { · 81 unmodified lines | ||
| 196 | 197 | const listRef = useRef<LegendListRef | null>(null); |
| 197 | 198 | const insets = useSafeAreaInsets(); |
| 198 | 199 | 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; | |
| 199 | 204 | const searchEnvironmentIds = useMemo( |
| 200 | 205 | () => |
| 201 | 206 | props.selectedEnvironmentId === null |
| @@ -877,7 +882,7 @@ export function HomeScreen(props: HomeScreenProps) { · 675 unmodified lines | ||
| 877 | 882 | <View |
| 878 | 883 | className="flex-1 items-center justify-center bg-screen px-8" |
| 879 | 884 | style={{ |
| 880 | − paddingBottom: Math.max(insets.bottom, 24), | |
| 885 | + paddingBottom: Math.max(insets.bottom, 24) + iosBottomToolbarClearance, | |
| 881 | 886 | paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0, |
| 882 | 887 | }} |
| 883 | 888 | > |
| @@ -1019,7 +1024,7 @@ export function HomeScreen(props: HomeScreenProps) { · 135 unmodified lines | ||
| 1019 | 1024 | contentContainerStyle={{ |
| 1020 | 1025 | paddingBottom: |
| 1021 | 1026 | Platform.OS === "ios" |
| 1022 | − ? Math.max(insets.bottom, 24) + 96 | |
| 1027 | + ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance | |
| 1023 | 1028 | : Math.max(insets.bottom, 16) + 88, |
| 1024 | 1029 | }} |
| 1025 | 1030 | /> |
| @@ -1060,16 +1065,19 @@ export function HomeScreen(props: HomeScreenProps) { · 34 unmodified lines | ||
| 1060 | 1065 | scrollEventThrottle={16} |
| 1061 | 1066 | contentContainerStyle={{ |
| 1062 | 1067 | // 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". | |
| 1064 | 1072 | paddingBottom: |
| 1065 | 1073 | Platform.OS === "ios" |
| 1066 | − ? Math.max(insets.bottom, 24) + 24 | |
| 1074 | + ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance | |
| 1067 | 1075 | : Math.max(insets.bottom, 16) + 88, |
| 1068 | 1076 | }} |
| 1069 | 1077 | scrollIndicatorInsets={ |
| 1070 | 1078 | Platform.OS === "ios" |
| 1071 | 1079 | ? { |
| 1072 | − bottom: Math.max(insets.bottom, 16) + 24, | |
| 1080 | + bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance, | |
| 1073 | 1081 | top: 0, |
| 1074 | 1082 | } |
| 1075 | 1083 | : undefined |
| @@ -1,5 +1,16 @@ | ||
| 1 | 1 | import type { HeaderBarButtonMailSearchToolbarItem } from "react-native-screens"; |
| 2 | 2 | |
| 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 | + | |
| 3 | 14 | type NativeMailSearchToolbarInput = Omit< |
| 4 | 15 | HeaderBarButtonMailSearchToolbarItem, |
| 5 | 16 | "type" | "useFallbackSearchField" |
| @@ -340,7 +340,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { · 339 unmodified lines | ||
| 340 | 340 | return { |
| 341 | 341 | type: "spacing", |
| 342 | 342 | spacing: typeof child.props.width === "number" ? child.props.width : 8, |
| 343 | − }; | |
| 343 | + flexible: Boolean(child.props.flexible), | |
| 344 | + } as NativeStackHeaderItem; | |
| 344 | 345 | } |
| 345 | 346 | |
| 346 | 347 | return null; |
| @@ -351,6 +352,11 @@ function collectToolbarItems(children: ReactNode): NativeStackHeaderItem[] { · 4 unmodified lines | ||
| 351 | 352 | Children.forEach(children, (child) => { |
| 352 | 353 | const item = convertToolbarChild(child); |
| 353 | 354 | 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 | + } | |
| 354 | 360 | items.push(item); |
| 355 | 361 | } |
| 356 | 362 | }); |
| @@ -440,6 +446,7 @@ function NativeHeaderToolbarLabel(_props: { readonly children?: ReactNode }) { · 83 unmodified lines | ||
| 440 | 446 | NativeHeaderToolbarLabel.displayName = "NativeHeaderToolbarLabel"; |
| 441 | 447 | |
| 442 | 448 | function NativeHeaderToolbarSpacer(_props: { |
| 449 | + readonly flexible?: boolean; | |
| 443 | 450 | readonly sharesBackground?: boolean; |
| 444 | 451 | readonly width?: number; |
| 445 | 452 | }) { |