diff --git a/apps/mobile/deps/react-native-screens-5.0.0-t3.45.tgz b/apps/mobile/deps/react-native-screens-5.0.0-t3.45.tgz deleted file mode 100644 index 2e076b177f3f..000000000000 Binary files a/apps/mobile/deps/react-native-screens-5.0.0-t3.45.tgz and /dev/null differ diff --git a/apps/mobile/deps/react-native-screens-5.0.0-t3.48.tgz b/apps/mobile/deps/react-native-screens-5.0.0-t3.48.tgz new file mode 100644 index 000000000000..f1eb3858e986 Binary files /dev/null and b/apps/mobile/deps/react-native-screens-5.0.0-t3.48.tgz differ diff --git a/apps/mobile/package.json b/apps/mobile/package.json index e91ec70d0ba1..f5e817165b95 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -127,7 +127,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.45.tgz", + "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.48.tgz", "react-native-svg": "15.16.0", "react-native-webview": "14.0.1", "react-native-worklets": "0.13.0", diff --git a/apps/mobile/src/components/ui/AndroidScreenHeader.tsx b/apps/mobile/src/components/ui/AndroidScreenHeader.tsx index 1b259f1ecbfa..7931717682fa 100644 --- a/apps/mobile/src/components/ui/AndroidScreenHeader.tsx +++ b/apps/mobile/src/components/ui/AndroidScreenHeader.tsx @@ -1,7 +1,7 @@ import { AppActivityIndicator } from "./AppActivityIndicator"; import { Column, Row, Text as NativeText, RNHostView } from "@expo/ui/jetpack-compose"; import { weight } from "@expo/ui/jetpack-compose/modifiers"; -import { useState, type ReactNode } from "react"; +import { use, useState, type ReactNode } from "react"; import { View } from "react-native"; import type { AppSymbolName } from "./AppSymbol"; @@ -12,6 +12,7 @@ import { splitAndroidHeaderActions } from "./headerMenu.android"; import { useScaledTextRole } from "../../features/settings/appearance/useScaledTextRole"; import { useToolbarLayout } from "./useToolbarLayout"; import { useControlTheme } from "./useControlTheme"; +import { RoutedFormSheetContext } from "./RoutedFormSheetContext"; export interface AndroidHeaderAction { readonly accessibilityLabel: string; @@ -50,7 +51,8 @@ export function AndroidScreenHeader(props: { const { colors } = useControlTheme(); const titleTypography = useScaledTextRole("title"); const subtitleTypography = useScaledTextRole("label"); - const { paddingTop, paddingBottom } = useToolbarLayout(props.embedded); + const inFormSheet = use(RoutedFormSheetContext); + const { paddingTop, paddingBottom } = useToolbarLayout(props.embedded || inFormSheet); const [headerWidth, setHeaderWidth] = useState(0); const { visibleActions, overflowActions } = splitAndroidHeaderActions( props.actions ?? [], @@ -67,6 +69,12 @@ export function AndroidScreenHeader(props: { borderBottomWidth: props.hideBottomBorder ? 0 : undefined, }} > + {inFormSheet ? ( + + ) : null} - {/* Android FormSheet does not bound flex children. Material drags target the first scroll view, so place content before the header. */} - - {props.children} - setIsOpen(false)} hideBottomBorder /> - - + + {/* Android FormSheet does not bound flex children. Material drags target the first scroll view, so place content before the header. */} + + {props.children} + setIsOpen(false)} + hideBottomBorder + /> + + + ); } diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 98bd85d8d9e7..53d2fb3e1d71 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -1,3 +1,7 @@ +import { + WorkspaceColumnsRendererContext, + WorkspacePresentationHostContext, +} from "./workspace-presentation-context"; import { useAndroidFloatingTabs } from "../settings/appearance/AppearancePreferencesProvider"; import { SafeAreaInsetsContext, useSafeAreaInsets } from "react-native-safe-area-context"; import { AdaptiveWorkspaceContext, useAdaptiveWorkspaceLayout } from "./adaptive-workspace-context"; @@ -176,6 +180,7 @@ function AdaptiveWorkspaceLayoutContent( const floatingTabsEnabled = useAndroidFloatingTabs(); const projectGroupingMode = props.projectGroupingMode; const nativeWorkspace = use(NativeWorkspaceModeContext); + const usesPresentationHost = use(WorkspacePresentationHostContext); const windowDimensions = useWindowDimensions(); const nativeMetrics = useNativeLayoutMetrics(); const { width: viewportWidth, height } = nativeMetrics ?? windowDimensions; @@ -839,181 +844,185 @@ function AdaptiveWorkspaceLayoutContent( ); } - return ( - ( + - - - - - {shouldRenderNavigationRail ? ( - - ) : null} - {shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? ( - + {shouldRenderNavigationRail ? ( + + ) : null} + {shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? ( + + + ( + + )} + > + - - ( - - )} - > - - - - - - - {/* Keep the pill under the content throughout the sidebar's clipping animation. */} - {floatingTabsEnabled && !workspacePage ? ( - - ) : null} - - + + + + + + {/* Keep the pill under the content throughout the sidebar's clipping animation. */} + {floatingTabsEnabled && !workspacePage ? ( + ) : null} - + + ) : null} + + + {/* Android holds the content still, so its floating controls keep its width. */} + + - - {/* Android holds the content still, so its floating controls keep its width. */} - - - {/* Compact Settings keeps the bottom navigation under all of its pages. */} - - - {props.children} - - - {floatingTabsEnabled && - (compactAndroidSettings || (layout.usesSplitView && workspacePage)) ? ( - - ) : compactAndroidSettings ? ( - - ) : null} - - - - - - {shouldRenderNavigationRail && navigationRailWidth > 0 ? ( - // The rail reserves its slot above and draws over the panes, so it can widen over - // the list in place. It takes the whole workspace only while it is open. - - + {children} + + + {floatingTabsEnabled && + (compactAndroidSettings || (layout.usesSplitView && workspacePage)) ? ( + - - ) : null} - + ) : compactAndroidSettings ? ( + + ) : null} + + + + + + {shouldRenderNavigationRail && navigationRailWidth > 0 ? ( + // The rail reserves its slot above and draws over the panes, so it can widen over + // the list in place. It takes the whole workspace only while it is open. + + + + ) : null} + + ); + + return ( + + + + {usesPresentationHost ? ( + + {props.children} + + ) : ( + renderColumns(props.children) + )} diff --git a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.android.tsx b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.android.tsx new file mode 100644 index 000000000000..765b23243798 --- /dev/null +++ b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.android.tsx @@ -0,0 +1,152 @@ +import { + createNavigatorFactory, + StackRouter, + useNavigationBuilder, + type NavigatorTypeBagBase, + type ParamListBase, + type StackActionHelpers, + type StackNavigationState, + type StackRouterOptions, + type StaticConfig, + type TypedNavigator, +} from "@react-navigation/native"; +import { + NativeStackView, + type NativeStackNavigationEventMap, + type NativeStackNavigationOptions, + type NativeStackNavigatorProps, + type NativeStackTypeBag, +} from "@react-navigation/native-stack"; +import { use, type ComponentProps } from "react"; + +import { RoutedFormSheetContext } from "../../components/ui/RoutedFormSheetContext"; + +import { projectStackPresentation } from "../../native/workspace-stack-projection"; +import { + WorkspaceColumnsRendererContext, + WorkspacePresentationHostContext, +} from "./workspace-presentation-context"; + +type ViewProps = ComponentProps; +type Descriptor = ViewProps["descriptors"][string]; +type Route = ViewProps["state"]["routes"][number]; + +function WorkspaceStackView(props: ViewProps) { + const renderColumns = use(WorkspaceColumnsRendererContext); + if (!renderColumns) return ; + + const isOverlay = (route: Route) => { + const descriptor = props.descriptors[route.key] ?? props.describe(route, true); + const presentation = descriptor.options.presentation; + return presentation !== undefined && presentation !== "card"; + }; + const projection = projectStackPresentation(props.state, isOverlay); + const workspaceRoutes = projection.workspace; + const visibleRoute = workspaceRoutes.at(-1); + if (!visibleRoute) return ; + + const visibleDescriptor = props.descriptors[visibleRoute.key]!; + const baseRoute = { key: `${props.state.key}:workspace`, name: "Workspace" }; + const baseDescriptor: Descriptor = { + ...visibleDescriptor, + options: { headerShown: false, freezeOnBlur: false }, + render: () => + renderColumns( + !isOverlay(route)), + }} + />, + ), + }; + const routes = [baseRoute, ...projection.overlays]; + return ( + + ); +} + +function WorkspaceStackNavigator({ + id, + initialRouteName, + UNSTABLE_routeNamesChangeBehavior, + children, + layout, + screenListeners, + screenOptions, + screenLayout, + UNSTABLE_router, + ...rest +}: NativeStackNavigatorProps) { + const { state, describe, descriptors, navigation, NavigationContent } = useNavigationBuilder< + StackNavigationState, + StackRouterOptions, + StackActionHelpers, + NativeStackNavigationOptions, + NativeStackNavigationEventMap + >(StackRouter, { + id, + initialRouteName, + UNSTABLE_routeNamesChangeBehavior, + children, + layout, + screenListeners, + screenOptions, + screenLayout, + UNSTABLE_router, + }); + return ( + + + [ + key, + descriptor.options.presentation === "formSheet" + ? { + ...descriptor, + render: () => ( + {descriptor.render()} + ), + } + : descriptor, + ]), + )} + navigation={navigation} + /> + + + ); +} + +type WorkspaceTypeBag< + ParamList extends ParamListBase, + NavigatorID extends string | undefined, +> = Omit, "Navigator"> & { + Navigator: typeof WorkspaceStackNavigator; +}; + +export function createWorkspaceStackNavigator< + const ParamList extends ParamListBase, + const NavigatorID extends string | undefined = string | undefined, + const TypeBag extends NavigatorTypeBagBase = WorkspaceTypeBag, + const Config extends StaticConfig = StaticConfig, +>(config?: Config): TypedNavigator { + return createNavigatorFactory(WorkspaceStackNavigator)(config); +} diff --git a/apps/mobile/src/features/layout/workspace-presentation-context.tsx b/apps/mobile/src/features/layout/workspace-presentation-context.tsx new file mode 100644 index 000000000000..ec1544dce821 --- /dev/null +++ b/apps/mobile/src/features/layout/workspace-presentation-context.tsx @@ -0,0 +1,7 @@ +import { createContext, type ReactElement, type ReactNode } from "react"; + +/** Android's native presentation stack surrounds all workspace columns. */ +export const WorkspacePresentationHostContext = createContext(false); +export const WorkspaceColumnsRendererContext = createContext< + ((children: ReactNode) => ReactElement) | null +>(null); diff --git a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx index be0912a6ebea..68ea161b0110 100644 --- a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx @@ -1,3 +1,4 @@ +import { ScreenContent } from "../../../components/ui/ScreenContent"; import { SheetHeader } from "../../../components/ui/SheetHeader"; import { Button } from "../../../components/ui/Button"; import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; @@ -84,145 +85,149 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { onCancel={() => navigation.goBack()} accessibilityLabel="Cancel commit" /> - - {isDefaultRef ? ( - - You’re on the default branch. You can create a new branch when committing. - - ) : null} - - Commit message - - - Leave empty to generate a message automatically. - - - - - - Changed files - - {selectedFiles.length} of {allFiles.length} selected · + - {formatDiffCount(selectedInsertions)} −{formatDiffCount(selectedDeletions)} - - - {allFiles.length > 0 ? ( -