Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 106 additions & 96 deletions apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ import {
} from "../../native/v5-workspace-context";

import { useNativeLayoutMetrics } from "../../native/native-layout-metrics";
import {
WorkspaceColumnsRendererContext,
WorkspacePresentationHostContext,
} from "./workspace-presentation-context";

interface AdaptiveWorkspaceContextValue {
readonly layout: Layout;
Expand Down Expand Up @@ -254,6 +258,7 @@ function AdaptiveWorkspaceLayoutContent(
) {
const projectGroupingMode = props.projectGroupingMode;
const nativeWorkspace = use(NativeWorkspaceModeContext);
const usesPresentationHost = use(WorkspacePresentationHostContext);
const windowDimensions = useWindowDimensions();
const nativeMetrics = useNativeLayoutMetrics();
const { width: viewportWidth, height } = nativeMetrics ?? windowDimensions;
Expand Down Expand Up @@ -699,108 +704,113 @@ function AdaptiveWorkspaceLayoutContent(
</HomeListOptionsProvider>
);
}
return (
<HomeListOptionsProvider
projectGroupingMode={projectGroupingMode}
savedFilters={props.savedThreadListFilters}
const renderColumns = (children: ReactNode) => (
<View
testID="adaptive-workspace-layout"
className={Platform.OS === "android" ? "flex-1 flex-row bg-header" : "flex-1 flex-row"}
>
<AdaptiveWorkspaceContext.Provider value={contextValue}>
<View
testID="adaptive-workspace-layout"
className={Platform.OS === "android" ? "flex-1 flex-row bg-header" : "flex-1 flex-row"}
{shouldRenderNavigationRail ? (
<Animated.View
className="self-stretch overflow-hidden"
accessibilityElementsHidden={navigationRailWidth === 0}
importantForAccessibility={navigationRailWidth > 0 ? "auto" : "no-hide-descendants"}
pointerEvents={navigationRailWidth > 0 ? "auto" : "none"}
style={railAnimatedStyle}
>
{shouldRenderNavigationRail ? (
<Animated.View
className="self-stretch overflow-hidden"
accessibilityElementsHidden={navigationRailWidth === 0}
importantForAccessibility={navigationRailWidth > 0 ? "auto" : "no-hide-descendants"}
pointerEvents={navigationRailWidth > 0 ? "auto" : "none"}
style={railAnimatedStyle}
>
<AndroidWorkspaceNavigation rail onSelect={revealPrimarySidebar} />
</Animated.View>
) : null}
{shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? (
<Animated.View
className="self-stretch overflow-hidden"
accessibilityElementsHidden={!panes.primarySidebarVisible}
collapsable={false}
importantForAccessibility={
panes.primarySidebarVisible ? "auto" : "no-hide-descendants"
}
pointerEvents={panes.primarySidebarVisible ? "auto" : "none"}
style={sidebarAnimatedStyle}
>
<View className="flex-1" style={{ width: layout.listPaneWidth }}>
<RenderErrorBoundary
renderFallback={(fallback) => (
<RenderFailureView
{...fallback}
title="The sidebar couldn't be displayed"
exit={{ label: "Open settings", onPress: handleOpenSettings }}
/>
)}
>
<AndroidWorkspaceSidebar
onRequestVisibility={revealPrimarySidebar}
selectedPullRequestKey={props.selectedPullRequestKey ?? null}
>
<AndroidHomeFabLayout sidebar onStartNewTask={handleStartNewTask}>
<ThreadNavigationSidebar
width={layout.listPaneWidth}
visible={panes.primarySidebarVisible}
onRequestVisibility={revealPrimarySidebar}
selectedThreadKey={selectedThreadKey}
onOpenSettings={handleOpenSettings}
onOpenEnvironmentSettings={handleOpenEnvironmentSettings}
onNewThreadInProject={handleNewThreadInProject}
onNewThreadOnBranch={handleNewThreadOnBranch}
onSelectThread={handleSelectThread}
onSearchQueryChange={setPrimarySidebarSearchQuery}
searchQuery={primarySidebarSearchQuery}
/>
</AndroidHomeFabLayout>
</AndroidWorkspaceSidebar>
</RenderErrorBoundary>
</View>
</Animated.View>
) : null}
<View
className={
Platform.OS === "android"
? "flex-1 overflow-hidden bg-header"
: "flex-1 overflow-hidden bg-screen"
}
collapsable={false}
>
<View
collapsable={false}
style={
contentSettledWidth !== null
? {
flex: 1,
width: contentSettledWidth,
}
: { flex: 1 }
}
<AndroidWorkspaceNavigation rail onSelect={revealPrimarySidebar} />
</Animated.View>
) : null}
{shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? (
<Animated.View
className="self-stretch overflow-hidden"
accessibilityElementsHidden={!panes.primarySidebarVisible}
collapsable={false}
importantForAccessibility={panes.primarySidebarVisible ? "auto" : "no-hide-descendants"}
pointerEvents={panes.primarySidebarVisible ? "auto" : "none"}
style={sidebarAnimatedStyle}
>
<View className="flex-1" style={{ width: layout.listPaneWidth }}>
<RenderErrorBoundary
renderFallback={(fallback) => (
<RenderFailureView
{...fallback}
title="The sidebar couldn't be displayed"
exit={{ label: "Open settings", onPress: handleOpenSettings }}
/>
)}
>
<WorkspaceContentWidthContext
value={layout.usesSplitView ? renderedContentWidth : null}
<AndroidWorkspaceSidebar
onRequestVisibility={revealPrimarySidebar}
selectedPullRequestKey={props.selectedPullRequestKey ?? null}
>
{props.children}
</WorkspaceContentWidthContext>
</View>
<AndroidHomeFabLayout sidebar onStartNewTask={handleStartNewTask}>
<ThreadNavigationSidebar
width={layout.listPaneWidth}
visible={panes.primarySidebarVisible}
onRequestVisibility={revealPrimarySidebar}
selectedThreadKey={selectedThreadKey}
onOpenSettings={handleOpenSettings}
onOpenEnvironmentSettings={handleOpenEnvironmentSettings}
onNewThreadInProject={handleNewThreadInProject}
onNewThreadOnBranch={handleNewThreadOnBranch}
onSelectThread={handleSelectThread}
onSearchQueryChange={setPrimarySidebarSearchQuery}
searchQuery={primarySidebarSearchQuery}
/>
</AndroidHomeFabLayout>
</AndroidWorkspaceSidebar>
</RenderErrorBoundary>
</View>
<WorkspaceInspectorPane
pathname={props.pathname}
renderedInspectorWidth={renderedInspectorWidth}
active={workspaceInspector?.active ?? false}
panes={panes}
renderInspector={workspaceInspector?.render}
setAuxiliaryPaneWidth={setAuxiliaryPaneWidth}
onClosed={handleWorkspaceInspectorClosed}
/>
</Animated.View>
) : null}
<View
className={
Platform.OS === "android"
? "flex-1 overflow-hidden bg-header"
: "flex-1 overflow-hidden bg-screen"
}
collapsable={false}
>
<View
collapsable={false}
style={
contentSettledWidth !== null
? {
flex: 1,
width: contentSettledWidth,
}
: { flex: 1 }
}
>
<WorkspaceContentWidthContext value={layout.usesSplitView ? renderedContentWidth : null}>
{children}
</WorkspaceContentWidthContext>
</View>
</View>
<WorkspaceInspectorPane
pathname={props.pathname}
renderedInspectorWidth={renderedInspectorWidth}
active={workspaceInspector?.active ?? false}
panes={panes}
renderInspector={workspaceInspector?.render}
setAuxiliaryPaneWidth={setAuxiliaryPaneWidth}
onClosed={handleWorkspaceInspectorClosed}
/>
</View>
);
return (
<HomeListOptionsProvider
projectGroupingMode={projectGroupingMode}
savedFilters={props.savedThreadListFilters}
>
<AdaptiveWorkspaceContext.Provider value={contextValue}>
{usesPresentationHost ? (
<WorkspaceColumnsRendererContext value={renderColumns}>
{props.children}
</WorkspaceColumnsRendererContext>
) : (
renderColumns(props.children)
)}
</AdaptiveWorkspaceContext.Provider>
</HomeListOptionsProvider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 { projectStackPresentation } from "../../native/workspace-stack-projection";
import {
WorkspaceColumnsRendererContext,
WorkspacePresentationHostContext,
} from "./workspace-presentation-context";

type ViewProps = ComponentProps<typeof NativeStackView>;
type Descriptor = ViewProps["descriptors"][string];
type Route = ViewProps["state"]["routes"][number];

function WorkspaceStackView(props: ViewProps) {
const renderColumns = use(WorkspaceColumnsRendererContext);
if (!renderColumns) return <NativeStackView {...props} />;

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 <NativeStackView {...props} />;

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(
<NativeStackView
{...props}
state={{
...props.state,
routes: workspaceRoutes,
index: workspaceRoutes.length - 1,
preloadedRoutes: props.state.preloadedRoutes.filter((route) => !isOverlay(route)),
}}
/>,
)}
</>
),
};
const routes = [baseRoute, ...projection.overlays];
return (
<NativeStackView
{...props}
presentationEnvelope
state={{
...props.state,
routes,
index: routes.length - 1,
preloadedRoutes: props.state.preloadedRoutes.filter(isOverlay),
}}
descriptors={{ ...props.descriptors, [baseRoute.key]: baseDescriptor }}
/>
);
}

function WorkspaceStackNavigator({
id,
initialRouteName,
UNSTABLE_routeNamesChangeBehavior,
children,
layout,
screenListeners,
screenOptions,
screenLayout,
UNSTABLE_router,
...rest
}: NativeStackNavigatorProps) {
const { state, describe, descriptors, navigation, NavigationContent } = useNavigationBuilder<
StackNavigationState<ParamListBase>,
StackRouterOptions,
StackActionHelpers<ParamListBase>,
NativeStackNavigationOptions,
NativeStackNavigationEventMap
>(StackRouter, {
id,
initialRouteName,
UNSTABLE_routeNamesChangeBehavior,
children,
layout,
screenListeners,
screenOptions,
screenLayout,
UNSTABLE_router,
});
return (
<WorkspacePresentationHostContext value>
<NavigationContent>
<WorkspaceStackView
{...rest}
state={state}
describe={describe}
descriptors={descriptors}
navigation={navigation}
/>
</NavigationContent>
</WorkspacePresentationHostContext>
);
}

type WorkspaceTypeBag<
ParamList extends ParamListBase,
NavigatorID extends string | undefined,
> = Omit<NativeStackTypeBag<ParamList, NavigatorID>, "Navigator"> & {
Navigator: typeof WorkspaceStackNavigator;
};

export function createWorkspaceStackNavigator<
const ParamList extends ParamListBase,
const NavigatorID extends string | undefined = string | undefined,
const TypeBag extends NavigatorTypeBagBase = WorkspaceTypeBag<ParamList, NavigatorID>,
const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
>(config?: Config): TypedNavigator<TypeBag, Config> {
return createNavigatorFactory(WorkspaceStackNavigator)(config);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext, type ReactNode } from "react";

/** Android's native presentation stack surrounds all workspace columns. */
export const WorkspacePresentationHostContext = createContext(false);
export const WorkspaceColumnsRendererContext = createContext<
((children: ReactNode) => ReactNode) | null
>(null);
Loading
Loading