| author | |
| committer | |
| log | 323dc321a2f8bec2160e6e35f94d07c5bb949084 |
| tree | 6a92017e2c68979f6862d385d09b048455fbd4c9 |
| parent | 6154b46cb86b038f1b54c32690a794ac639520ca |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 96 insertions(+), 61 deletions(-)
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+11-3| ... | ... | @@ -29,7 +29,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 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: { |
| 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: { |
| 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, |
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+6-2| ... | ... | @@ -29,7 +29,10 @@ import { |
| 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) { |
| 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 | <> |
apps/mobile/src/features/home/HomeHeader.tsx+47-50| ... | ... | @@ -13,7 +13,10 @@ import { useThemeColor } from "../../lib/useThemeColor"; |
| 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) { |
| 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) { |
| 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) { |
| 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) { |
| 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" |
apps/mobile/src/features/home/HomeScreen.tsx+13-5| ... | ... | @@ -109,6 +109,7 @@ interface HomeScreenProps { |
| 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) { |
| 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) { |
| 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) { |
| 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) { |
| 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 |
apps/mobile/src/features/layout/native-mail-search-toolbar.ts+11| ... | ... | @@ -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" |
apps/mobile/src/native/StackHeader.tsx+8-1| ... | ... | @@ -340,7 +340,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { |
| 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[] { |
| 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 }) { |
| 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 | }) { |