diff --git a/apps/mobile/deps/react-native-screens-5.0.0-t3.30.tgz b/apps/mobile/deps/react-native-screens-5.0.0-t3.30.tgz deleted file mode 100644 index 581307cf91be1..0000000000000 Binary files a/apps/mobile/deps/react-native-screens-5.0.0-t3.30.tgz and /dev/null differ diff --git a/apps/mobile/deps/react-native-screens-5.0.0-t3.31.tgz b/apps/mobile/deps/react-native-screens-5.0.0-t3.31.tgz new file mode 100644 index 0000000000000..8f21f0d7a67eb Binary files /dev/null and b/apps/mobile/deps/react-native-screens-5.0.0-t3.31.tgz differ diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 441ee7beb6d35..44a19af33786d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -134,7 +134,7 @@ "react-native-pdf-jsi": "5.0.0", "react-native-reanimated": "4.7.1", "react-native-safe-area-context": "~5.10.1", - "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.30.tgz", + "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.31.tgz", "react-native-shiki-engine": "^0.3.12", "react-native-svg": "15.16.0", "react-native-webview": "14.0.1", diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 13240c54a975f..f74f13acd50b1 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -149,7 +149,7 @@ import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass"; import { deriveLayout } from "./lib/layout"; import { useNativeLayoutMetrics } from "./native/native-layout-metrics"; import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; -import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface"; +import { HORIZONTAL_FORM_SHEET_OPTIONS, formSheetDetents } from "./native/sheet-surface"; import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; import { useComposerAttachmentUploadWorker } from "./state/composer-attachment-uploads"; @@ -210,6 +210,15 @@ const LEGAL_DOCUMENT_HEADER_OPTIONS: AppScreenOptions = { const NESTED_NAVIGATOR_OPTIONS = { nativeContentInsetHorizontally: false, headerShown: false }; +// Thread settings keep the Duo's vertical bar so cancel, search, filter and confirm sit in the +// rail. The picker stack spans the sheet so its header reaches the rail, and insets its content. +const THREAD_SETTINGS_SHEET_OPTIONS = { + presentation: "formSheet" as const, + sheetAllowedDetents: [1], + sheetGrabberVisible: true, + nativeContentInsetHorizontally: false, +}; + const SettingsContentStack = createV5SheetStackNavigator({ initialRouteName: "Settings", screenOptions: { @@ -368,9 +377,7 @@ const SettingsContentStack = createV5SheetStackNavigator({ ...(Platform.OS === "android" ? { presentation: "card" as const } : { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [1], - sheetGrabberVisible: true, + ...THREAD_SETTINGS_SHEET_OPTIONS, }), }, }), @@ -513,9 +520,7 @@ const NewTaskSheetStack = createNativeStackNavigator({ ...(Platform.OS === "android" ? { presentation: "card" as const } : { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [1], - sheetGrabberVisible: true, + ...THREAD_SETTINGS_SHEET_OPTIONS, }), }, }), @@ -734,8 +739,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: PullRequestFilesSheet, options: { ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined), - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.6, 0.92], + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.6), sheetGrabberVisible: true, sheetCornerRadius: Platform.OS === "android" ? 28 : undefined, }, @@ -744,8 +749,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: PullRequestReviewersScreen, options: { ...DETAIL_HEADER_OPTIONS, - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.75, 0.92], + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.75), sheetGrabberVisible: true, headerBackVisible: false, }, @@ -759,8 +764,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ options: { ...(Platform.OS === "android" ? { presentation: "fullScreenModal" as const } - : { ...SHEET_GLASS_HEADER_OPTIONS, ...FORM_SHEET_PRESENTATION_OPTIONS }), - sheetAllowedDetents: Platform.OS === "android" ? undefined : [0.75, 0.92], + : { ...SHEET_GLASS_HEADER_OPTIONS, ...HORIZONTAL_FORM_SHEET_OPTIONS }), + sheetAllowedDetents: Platform.OS === "android" ? undefined : formSheetDetents(0.75), sheetGrabberVisible: Platform.OS !== "android", }, }), @@ -768,8 +773,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: PullRequestReviewSheet, options: { ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined), - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.75, 0.92], + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.75), sheetGrabberVisible: true, }, }), @@ -816,8 +821,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ // formSheet; use a full-screen modal there instead. ...(Platform.OS === "android" ? { presentation: "fullScreenModal" as const } - : FORM_SHEET_PRESENTATION_OPTIONS), - sheetAllowedDetents: Platform.OS === "android" ? undefined : [0.55, 0.92], + : HORIZONTAL_FORM_SHEET_OPTIONS), + sheetAllowedDetents: Platform.OS === "android" ? undefined : formSheetDetents(0.55), sheetGrabberVisible: Platform.OS !== "android", }, }), @@ -856,36 +861,34 @@ const RootStackConfig = createWorkspaceStackNavigator({ ...(Platform.OS === "android" ? { presentation: "card" as const } : { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [1], - sheetGrabberVisible: true, + ...THREAD_SETTINGS_SHEET_OPTIONS, }), }, }), ThreadQueue: createNativeStackScreen({ screen: ThreadQueueSheet, options: { - ...FORM_SHEET_PRESENTATION_OPTIONS, + ...HORIZONTAL_FORM_SHEET_OPTIONS, headerShown: false, - sheetAllowedDetents: [0.65, 0.95], + sheetAllowedDetents: formSheetDetents(0.65, 0.95), sheetGrabberVisible: true, }, }), ThreadBackgroundWork: createNativeStackScreen({ screen: ThreadBackgroundWorkSheet, options: { - ...FORM_SHEET_PRESENTATION_OPTIONS, + ...HORIZONTAL_FORM_SHEET_OPTIONS, headerShown: false, - sheetAllowedDetents: [0.5, 0.9], + sheetAllowedDetents: formSheetDetents(0.5, 0.9), sheetGrabberVisible: true, }, }), ThreadAgents: createNativeStackScreen({ screen: ThreadAgentsSheet, options: { - ...FORM_SHEET_PRESENTATION_OPTIONS, + ...HORIZONTAL_FORM_SHEET_OPTIONS, headerShown: false, - sheetAllowedDetents: [0.5, 0.9], + sheetAllowedDetents: formSheetDetents(0.5, 0.9), sheetGrabberVisible: true, }, }), @@ -893,8 +896,9 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: GitOverviewSheet, linking: `${THREAD_LINKING_PREFIX}/git`, options: { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.55, 0.92], + ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined), + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.55), sheetGrabberVisible: true, }, }), @@ -902,8 +906,9 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: ThreadPullRequestsSheet, linking: `${THREAD_LINKING_PREFIX}/pull-requests`, options: { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.55, 0.92], + ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined), + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.55), sheetGrabberVisible: true, }, }), @@ -912,8 +917,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ linking: `${THREAD_LINKING_PREFIX}/git/commit`, options: { ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined), - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.8, 0.92], + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.8), sheetGrabberVisible: true, }, }), @@ -921,8 +926,9 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: GitBranchesSheet, linking: `${THREAD_LINKING_PREFIX}/git/branches`, options: { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.55, 0.92], + ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined), + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.55), sheetGrabberVisible: true, }, }), @@ -930,8 +936,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ screen: GitConfirmSheet, linking: `${THREAD_LINKING_PREFIX}/git-confirm`, options: { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.45, 0.7], + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: [0.55, 0.7], sheetGrabberVisible: true, }, }), @@ -964,8 +970,8 @@ const RootStackConfig = createWorkspaceStackNavigator({ ...(Platform.OS === "android" ? { headerShown: false } : SHEET_SOLID_HEADER_OPTIONS), title: "Set up T3 Connect", gestureEnabled: true, - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.6, 0.95], + ...HORIZONTAL_FORM_SHEET_OPTIONS, + sheetAllowedDetents: formSheetDetents(0.6, 0.95), sheetGrabberVisible: true, }, }), @@ -977,7 +983,7 @@ const RootStackConfig = createWorkspaceStackNavigator({ ...(Platform.OS === "android" ? { presentation: "card" as const, headerShown: false } : { - ...FORM_SHEET_PRESENTATION_OPTIONS, + ...HORIZONTAL_FORM_SHEET_OPTIONS, sheetAllowedDetents: [0.55, 0.7], sheetGrabberVisible: true, }), @@ -991,7 +997,7 @@ const RootStackConfig = createWorkspaceStackNavigator({ // native header stack mounts without remounting the pairing form. ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined), title: "Add Environment", - ...FORM_SHEET_PRESENTATION_OPTIONS, + ...HORIZONTAL_FORM_SHEET_OPTIONS, sheetAllowedDetents: [0.55, 0.7], sheetGrabberVisible: true, }, @@ -1009,6 +1015,7 @@ const RootStackConfig = createWorkspaceStackNavigator({ ), options: { ...NESTED_NAVIGATOR_OPTIONS, + sheetPrefersHorizontalBars: true, gestureEnabled: true, headerShown: false, }, @@ -1066,8 +1073,8 @@ export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navig return usesWorkspaceFlowScreens ? { presentation: "card" } : { - ...FORM_SHEET_PRESENTATION_OPTIONS, - sheetAllowedDetents: [0.92], + presentation: "formSheet", + sheetAllowedDetents: [1], sheetGrabberVisible: true, }; }} diff --git a/apps/mobile/src/components/ui/Sheet.tsx b/apps/mobile/src/components/ui/Sheet.tsx index 21ee41a5c5569..b8876c4deb0e2 100644 --- a/apps/mobile/src/components/ui/Sheet.tsx +++ b/apps/mobile/src/components/ui/Sheet.tsx @@ -5,6 +5,7 @@ import { useResolveClassNames } from "uniwind"; import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; import { NativeHeader, NativeScreen } from "../../native/ThemedScreenStack"; +import { formSheetDetents } from "../../native/sheet-surface"; const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); @@ -21,8 +22,10 @@ export function Sheet(props: SheetProps) { return ( [ withNativeGlassHeaderItem({ - axisBehavior: "horizontalOnly", + axisBehavior: "automatic", accessibilityLabel: "Filter settings environments and projects", icon: { name: filterIcon, type: "sfSymbol" }, label: "", @@ -121,7 +121,7 @@ export function SettingsEnvironmentFilterHeader(props: { ...(closeSettings ? [ withNativeGlassHeaderItem({ - axisBehavior: "horizontalOnly", + axisBehavior: "automatic", accessibilityLabel: "Close settings", icon: { name: "xmark", type: "sfSymbol" }, identifier: "settings-close", diff --git a/apps/mobile/src/features/threads/ThreadPullRequestsSheet.tsx b/apps/mobile/src/features/threads/ThreadPullRequestsSheet.tsx index c9075f851ee59..dcf70291e9b74 100644 --- a/apps/mobile/src/features/threads/ThreadPullRequestsSheet.tsx +++ b/apps/mobile/src/features/threads/ThreadPullRequestsSheet.tsx @@ -2,15 +2,14 @@ import { useNavigation, type StaticScreenProps } from "@react-navigation/native" import { LegendList } from "@legendapp/list/react-native"; import { useMemo } from "react"; import { Platform, View } from "react-native"; -import { ScreenStack } from "react-native-screens"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AndroidSheetHeader } from "../../components/ui/AndroidScreenHeader"; import { AppText as Text } from "../../components/ui/AppText"; import { ScreenContent } from "../../components/ui/ScreenContent"; -import { NativeSheetContent } from "../../native/NativeSheetContent"; -import { NativeStackScreenOptions, nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; -import { NativeHeader, NativeScreen } from "../../native/ThemedScreenStack"; +import { SheetHeader } from "../../components/ui/SheetHeader"; +import { Toolbar } from "../../components/ui/Toolbar"; +import { NativeStackScreenOptions } from "../../native/StackHeader"; import { useOpenPullRequest } from "../../native/useOpenPullRequest"; import { useThreadSelection } from "../../state/use-thread-selection"; import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; @@ -21,7 +20,6 @@ import { } from "./LinkedPullRequestList"; import { useLinkedPullRequestActions } from "./useLinkedPullRequestActions"; -const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); const TITLE = "Linked pull requests"; export function ThreadPullRequestsSheet( @@ -92,47 +90,19 @@ export function ThreadPullRequestsSheet( ); if (Platform.OS === "ios") { - // A plain formSheet screen never renders a stack header, so it comes from - // a nested native stack inside the sheet (same shape as the git sheet). return ( - - - {content} - navigation.goBack()} /> + {actions.canLink ? ( + + - - + + ) : null} + {content} ); } diff --git a/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx b/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx index eab14dc38ed0f..720d7d01ff3dc 100644 --- a/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx +++ b/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx @@ -85,6 +85,7 @@ import { createNativeMailSearchToolbarItem, NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET, } from "../layout/native-mail-search-toolbar"; +import { useNativeLayoutMetrics } from "../../native/native-layout-metrics"; import { useNativeMailSearchToolbar } from "../../native/use-native-mail-search-toolbar"; import { ModelRow, ChoiceRow } from "./ThreadSettingsRows"; import { @@ -847,10 +848,8 @@ function ThreadSettingsOptionsItem(props: { .get(session.environmentId) ?.providers.find((provider) => provider.instanceId === session.providerInstanceId) ?? null) : null; - const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); - const bottomToolbarInset = usesNativeMailSearchToolbar - ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET - : 0; + // Both iOS search layouts float a bottom toolbar over the list's last rows. + const bottomToolbarInset = Platform.OS === "ios" ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET : 0; return ( @@ -1211,11 +1210,17 @@ function useThreadSettingsPickerPresentation() { return value; } +// With a Duo vertical bar, filter and search join cancel and confirm in the rail. +function useThreadSettingsMailSearchToolbar() { + const metrics = useNativeLayoutMetrics(); + return useNativeMailSearchToolbar() && (metrics?.verticalBarEdge ?? "none") === "none"; +} + function ThreadSettingsModelsScreen() { const session = useThreadSettingsSession(); const presentation = useThreadSettingsPickerPresentation(); const navigation = useNavigation>(); - const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); + const usesNativeMailSearchToolbar = useThreadSettingsMailSearchToolbar(); const hasCustomCatalogFilter = session.providerFilter !== null || session.showLegacy; const commitAndClose = useEffectEvent(() => { if (!session.commitPendingModel()) return; @@ -1335,6 +1340,7 @@ function ThreadSettingsModelsScreen() { headerSearchBarOptions: Platform.OS === "ios" && !usesNativeMailSearchToolbar ? { + allowToolbarIntegration: true, autoCapitalize: "none", hideNavigationBar: false, obscureBackground: false, @@ -1378,7 +1384,6 @@ function ThreadSettingsModelsScreen() { ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle" } - separateBackground title="Model filters" > @@ -1414,6 +1419,7 @@ function ThreadSettingsModelsScreen() { ) : null} + ) : null} @@ -1444,6 +1450,9 @@ function ThreadSettingsChoiceScreen() { ); } +// Filter and search share the sheet's toolbar, below cancel and confirm in the Duo rail. +const BOTTOM_TOOLBAR_OPTIONS = { nativeKeepsBottomToolbar: true }; + function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) { const theme = useUniwindTheme(); const solidSheetBackground = theme["--color-sheet-solid"]; @@ -1458,6 +1467,7 @@ function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) initialRouteName="ThreadSettingsModels" screenOptions={{ animation: Platform.OS === "android" ? "default" : "slide_from_right", + ...BOTTOM_TOOLBAR_OPTIONS, contentStyle: { backgroundColor: solidSheetBackground }, gestureEnabled: true, headerBackButtonDisplayMode: "minimal", diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx index d1d39357f2fa8..4075a49e9774a 100644 --- a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx @@ -5,6 +5,7 @@ import { Platform, Pressable, ScrollView, useWindowDimensions, View } from "reac import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AndroidSheetHeader } from "../../../components/ui/AndroidScreenHeader"; +import { SheetHeader } from "../../../components/ui/SheetHeader"; import { ScreenContent } from "../../../components/ui/ScreenContent"; import { NativeStackScreenOptions } from "../../../native/StackHeader"; import { AppText as Text, AppTextInput as TextInput } from "../../../components/ui/AppText"; @@ -81,11 +82,18 @@ export function GitBranchesSheet(_props: GitBranchesSheetProps) { onBack={() => navigation.goBack()} hideBottomBorder /> - ) : null} + ) : ( + navigation.goBack()} + /> + )} - {content} + {content} - - - {content} - - - + navigation.goBack()} + /> + {content} ); } diff --git a/apps/mobile/src/native/NativeHeaderToolbar.tsx b/apps/mobile/src/native/NativeHeaderToolbar.tsx index 2e6e7059880b9..3f7f98a4b2324 100644 --- a/apps/mobile/src/native/NativeHeaderToolbar.tsx +++ b/apps/mobile/src/native/NativeHeaderToolbar.tsx @@ -42,6 +42,10 @@ type NativeStackHeaderAxisBehavior = Extract< NativeStackHeaderItem, { type: "button" } >["axisBehavior"]; +type NativeStackHeaderVisibilityPriority = Extract< + NativeStackHeaderItem, + { type: "button" } +>["visibilityPriority"]; type NativeToolbarItem = NativeStackHeaderItem | HeaderBarButtonSearchBarPlacementItem; type NativeStackOptionsWithToolbar = NativeStackNavigationOptions & { unstable_headerToolbarItems?: () => NativeToolbarItem[]; @@ -166,6 +170,7 @@ function convertToolbarChild(child: ReactNode): NativeToolbarItem | null { ? child.props.variant : "plain", identifier: typeof child.props.identifier === "string" ? child.props.identifier : undefined, + visibilityPriority: child.props.visibilityPriority as NativeStackHeaderVisibilityPriority, }; } @@ -277,6 +282,7 @@ function NativeHeaderToolbarButton(_props: { readonly tintColor?: ColorValue; readonly variant?: "plain" | "done" | "prominent"; readonly identifier?: string; + readonly visibilityPriority?: NativeStackHeaderVisibilityPriority; }) { return null; } diff --git a/apps/mobile/src/native/NativeSheetContent.tsx b/apps/mobile/src/native/NativeSheetContent.tsx deleted file mode 100644 index ec44dc0dde2b6..0000000000000 --- a/apps/mobile/src/native/NativeSheetContent.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { ReactNode } from "react"; -import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; - -import { useNativeWorkspaceColumnsSupported } from "./NativeWorkspaceColumns"; - -export function NativeSheetContent(props: { readonly children: ReactNode }) { - const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported(); - if (!usesNativeWorkspaceColumns) return <>{props.children}; - - return ( - - - {props.children} - - - ); -} diff --git a/apps/mobile/src/native/StackHeader.tsx b/apps/mobile/src/native/StackHeader.tsx index 698a345035cca..d5de421db0c65 100644 --- a/apps/mobile/src/native/StackHeader.tsx +++ b/apps/mobile/src/native/StackHeader.tsx @@ -20,6 +20,7 @@ export type AppNativeStackNavigationOptions = Omit< "headerTintColor" | "unstable_headerLeftItems" | "unstable_headerRightItems" > & { readonly nativeContentInsetHorizontally?: boolean; + readonly nativeKeepsBottomToolbar?: boolean; readonly nativeSidebarFilter?: NativeStackHeaderItemMenu; readonly headerTintColor?: string | ColorValue; readonly unstable_headerCenterItems?: unknown; diff --git a/apps/mobile/src/native/V5StackHeader.ios.tsx b/apps/mobile/src/native/V5StackHeader.ios.tsx index 85419d80353e5..a04a6064e2bac 100644 --- a/apps/mobile/src/native/V5StackHeader.ios.tsx +++ b/apps/mobile/src/native/V5StackHeader.ios.tsx @@ -31,6 +31,7 @@ type Options = NativeStackNavigationOptions & { readonly headerSubtitleStyle?: { color?: string; fontSize?: number; fontFamily?: string }; readonly unstable_headerCenterItems?: () => NativeStackHeaderItem[]; readonly unstable_headerToolbarItems?: () => ToolbarItem[]; + readonly nativeKeepsBottomToolbar?: boolean; }; type MenuElementInput = @@ -128,6 +129,8 @@ function convertItems(items: NativeStackHeaderItem[], prefix: string): HeaderIte axisBehavior: item.axisBehavior, icon: convertIcon(item.icon), onPress: item.disabled ? undefined : item.onPress, + visibilityPriority: + item.visibilityPriority === "default" ? "standard" : item.visibilityPriority, variant: item.variant, tintColor: item.tintColor, hidesSharedBackground: item.hidesSharedBackground, @@ -211,9 +214,11 @@ export function V5StackHeader(props: { } // Duo keeps compose at the bottom of a visible sidebar. On a single panel, // the primary column owns the rail too; its separate UIToolbar is inaccessible. + // A sheet's own UIToolbar shows at the foot of its rail, so it can keep its bottom items. if ( nativeMetrics && nativeMetrics.verticalBarEdge !== "none" && + !options.nativeKeepsBottomToolbar && (!props.primary || nativeMetrics.horizontalSizeClass === "compact") ) { trailing.push(...bottom); diff --git a/apps/mobile/src/native/createV5StackNavigator.ios.tsx b/apps/mobile/src/native/createV5StackNavigator.ios.tsx index 6433ab5002019..1c8cea900618f 100644 --- a/apps/mobile/src/native/createV5StackNavigator.ios.tsx +++ b/apps/mobile/src/native/createV5StackNavigator.ios.tsx @@ -369,6 +369,7 @@ export function V5SheetStackView(props: V5StackViewProps) { initialDetentIndex={options.sheetInitialDetentIndex} largestUndimmedDetentIndex={options.sheetLargestUndimmedDetentIndex} prefersGrabberVisible={options.sheetGrabberVisible} + prefersHorizontalBars={options.sheetPrefersHorizontalBars} preferredCornerRadius={options.sheetCornerRadius} prefersScrollingExpandsWhenScrolledToEdge={options.sheetExpandsWhenScrolledToEdge} nativeContainerStyle={{ diff --git a/apps/mobile/src/native/sheet-surface.ts b/apps/mobile/src/native/sheet-surface.ts index 7867b2b77e81f..9de5943a008e0 100644 --- a/apps/mobile/src/native/sheet-surface.ts +++ b/apps/mobile/src/native/sheet-surface.ts @@ -1,3 +1,11 @@ -export const FORM_SHEET_PRESENTATION_OPTIONS = { +import { Platform } from "react-native"; + +export const HORIZONTAL_FORM_SHEET_OPTIONS = { presentation: "formSheet" as const, + sheetPrefersHorizontalBars: true, }; + +export function formSheetDetents(initialDetent: number, androidExpandedDetent = 0.92) { + // Duo moves the status bar above the sheet only at the maximum native detent. + return [initialDetent, Platform.OS === "ios" ? 1 : androidExpandedDetent]; +} diff --git a/docs/operations/mobile-screens-fork.md b/docs/operations/mobile-screens-fork.md index cd21aa551370b..614aa4280cbd8 100644 --- a/docs/operations/mobile-screens-fork.md +++ b/docs/operations/mobile-screens-fork.md @@ -1,7 +1,7 @@ # Building the mobile Screens dependency The mobile v5 stack uses a committed tarball from the -[T3 Screens fork](https://github.com/pingdotgg/react-native-screens/tree/t3-v5.0.0-t3.30). +[T3 Screens fork](https://github.com/pingdotgg/react-native-screens/tree/t3-v5.0.0-t3.31). The fork owns the integration changes; the app does not patch this package. Each archive includes compiled JavaScript, declarations, native source and `t3-fork.json` recording its version, source commit and upstream base. diff --git a/patches/@react-navigation%2Fnative-stack@7.21.0.patch b/patches/@react-navigation%2Fnative-stack@7.21.0.patch index d221ce6a8754e..3fc4b61604e33 100644 --- a/patches/@react-navigation%2Fnative-stack@7.21.0.patch +++ b/patches/@react-navigation%2Fnative-stack@7.21.0.patch @@ -1,5 +1,5 @@ diff --git a/lib/module/views/NativeStackView.native.js b/lib/module/views/NativeStackView.native.js -index 6882090..011ad0e 100644 +index 6882090..f1e14e1 100644 --- a/lib/module/views/NativeStackView.native.js +++ b/lib/module/views/NativeStackView.native.js @@ -17,8 +17,12 @@ const ANDROID_DEFAULT_HEADER_HEIGHT = 56; @@ -15,7 +15,15 @@ index 6882090..011ad0e 100644 index, focused, shouldFreeze, -@@ -305,7 +309,8 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({ +@@ -144,6 +148,7 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({ + sheetAllowedDetents = [1.0], + sheetLargestUndimmedDetentIndex = -1, + sheetGrabberVisible = false, ++ sheetPrefersHorizontalBars = false, + sheetCornerRadius = -1.0, + sheetElevation = 24, + sheetExpandsWhenScrolledToEdge = true, +@@ -305,7 +310,8 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({ } } }); @@ -25,7 +33,15 @@ index 6882090..011ad0e 100644 route: route, navigation: navigation, children: /*#__PURE__*/_jsx(ScreenStackItem, { -@@ -427,6 +432,7 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({ +@@ -333,6 +339,7 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({ + sheetAllowedDetents: sheetAllowedDetents, + sheetLargestUndimmedDetentIndex: sheetLargestUndimmedDetentIndex, + sheetGrabberVisible: sheetGrabberVisible, ++ sheetPrefersHorizontalBars: sheetPrefersHorizontalBars, + sheetInitialDetentIndex: sheetInitialDetentIndex, + sheetCornerRadius: sheetCornerRadius, + sheetElevation: sheetElevation, +@@ -427,6 +434,7 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({ }); }); export function NativeStackView({ @@ -33,7 +49,7 @@ index 6882090..011ad0e 100644 state, navigation, descriptors, -@@ -444,6 +450,17 @@ export function NativeStackView({ +@@ -444,6 +452,17 @@ export function NativeStackView({ return /*#__PURE__*/_jsx(SafeAreaProviderCompat, { children: /*#__PURE__*/_jsx(ScreenStack, { style: styles.container, @@ -51,7 +67,7 @@ index 6882090..011ad0e 100644 children: state.routes.concat(state.preloadedRoutes).map((route, index) => { const descriptor = descriptors[route.key] ?? preloadedDescriptors[route.key]; const isFocused = state.index === index; -@@ -460,6 +477,7 @@ export function NativeStackView({ +@@ -460,6 +479,7 @@ export function NativeStackView({ // due to component being unmounted. To avoid this, we don't freeze the previous screen there const shouldFreeze = isFabric() ? !isPreloaded && !isFocused && !isBelowFocused && !isModalOnIos : !isPreloaded && !isFocused && !isModalOnIos; return /*#__PURE__*/_jsx(SceneView, { @@ -60,7 +76,7 @@ index 6882090..011ad0e 100644 focused: isFocused, shouldFreeze: shouldFreeze, diff --git a/lib/module/views/useHeaderConfigProps.js b/lib/module/views/useHeaderConfigProps.js -index 08055b2..8718847 100644 +index 08055b2..ccf1fde 100644 --- a/lib/module/views/useHeaderConfigProps.js +++ b/lib/module/views/useHeaderConfigProps.js @@ -18,6 +18,12 @@ const processBarButtonItems = (items, colors, fonts) => { @@ -174,10 +190,19 @@ index 08055b2..8718847 100644 headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts), experimental_userInterfaceStyle: dark ? 'dark' : 'light' diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.ts -index 27a3d8e..3adf86e 100644 +index 27a3d8e..7312e4e 100644 --- a/lib/typescript/src/types.d.ts +++ b/lib/typescript/src/types.d.ts -@@ -903,6 +903,8 @@ type SharedHeaderItem = { +@@ -694,6 +694,8 @@ export type NativeStackNavigationOptions = { + * @platform ios + */ + sheetGrabberVisible?: boolean; ++ /** Requests horizontal bars on iOS 27.1+. Defaults to UIKit automatic placement. */ ++ sheetPrefersHorizontalBars?: boolean; + /** + * The largest sheet detent for which a view underneath won't be dimmed. + * Works only when `presentation` is set to `formSheet`. +@@ -903,6 +905,8 @@ type SharedHeaderItem = { * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/sharesbackground */ sharesBackground?: boolean; @@ -186,7 +211,7 @@ index 27a3d8e..3adf86e 100644 /** * An identifier used to match items across transitions. * Only available from iOS 26.0 and later. -@@ -1148,6 +1150,13 @@ export type NativeStackHeaderItemSpacing = { +@@ -1148,6 +1152,13 @@ export type NativeStackHeaderItemSpacing = { */ export type NativeStackHeaderItemCustom = { type: 'custom'; @@ -232,10 +257,19 @@ index d533cd9..857b7ad 100644 //# sourceMappingURL=NativeStackView.native.d.ts.map \ No newline at end of file diff --git a/src/types.tsx b/src/types.tsx -index 5d63f41..e9c53ee 100644 +index 5d63f41..f949a11 100644 --- a/src/types.tsx +++ b/src/types.tsx -@@ -959,6 +959,8 @@ type SharedHeaderItem = { +@@ -746,6 +746,8 @@ export type NativeStackNavigationOptions = { + * @platform ios + */ + sheetGrabberVisible?: boolean; ++ /** Requests horizontal bars on iOS 27.1+. Defaults to UIKit automatic placement. */ ++ sheetPrefersHorizontalBars?: boolean; + /** + * The largest sheet detent for which a view underneath won't be dimmed. + * Works only when `presentation` is set to `formSheet`. +@@ -959,6 +961,8 @@ type SharedHeaderItem = { * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/sharesbackground */ sharesBackground?: boolean; @@ -244,7 +278,7 @@ index 5d63f41..e9c53ee 100644 /** * An identifier used to match items across transitions. * Only available from iOS 26.0 and later. -@@ -1213,6 +1215,13 @@ export type NativeStackHeaderItemSpacing = { +@@ -1213,6 +1217,13 @@ export type NativeStackHeaderItemSpacing = { */ export type NativeStackHeaderItemCustom = { type: 'custom'; @@ -259,7 +293,7 @@ index 5d63f41..e9c53ee 100644 * A React Element to display as the item. */ diff --git a/src/views/NativeStackView.native.tsx b/src/views/NativeStackView.native.tsx -index aa13bc7..ab4a9d1 100644 +index aa13bc7..225daf9 100644 --- a/src/views/NativeStackView.native.tsx +++ b/src/views/NativeStackView.native.tsx @@ -52,7 +52,14 @@ function isFabric() { @@ -285,7 +319,15 @@ index aa13bc7..ab4a9d1 100644 index, focused, shouldFreeze, -@@ -434,7 +442,7 @@ const SceneView = React.memo(function SceneView({ +@@ -199,6 +207,7 @@ const SceneView = React.memo(function SceneView({ + sheetAllowedDetents = [1.0], + sheetLargestUndimmedDetentIndex = -1, + sheetGrabberVisible = false, ++ sheetPrefersHorizontalBars = false, + sheetCornerRadius = -1.0, + sheetElevation = 24, + sheetExpandsWhenScrolledToEdge = true, +@@ -434,7 +443,7 @@ const SceneView = React.memo(function SceneView({ ); return ( @@ -294,7 +336,15 @@ index aa13bc7..ab4a9d1 100644 @@ -308,7 +358,7 @@ index aa13bc7..ab4a9d1 100644 state: StackNavigationState; navigation: NativeStackNavigationHelpers; descriptors: NativeStackDescriptorMap; -@@ -586,6 +595,7 @@ type Props = { +@@ -586,6 +597,7 @@ type Props = { }; export function NativeStackView({ @@ -316,7 +366,7 @@ index aa13bc7..ab4a9d1 100644 state, navigation, descriptors, -@@ -633,6 +643,7 @@ export function NativeStackView({ +@@ -633,6 +645,7 @@ export function NativeStackView({ return (