1import { useNavigation } from "@react-navigation/native";
2import { use, useCallback, useMemo } from "react";
3import { Platform, View, useWindowDimensions } from "react-native";
4import { useKeyboardState } from "react-native-keyboard-controller";
5import { SafeAreaInsetsContext, useSafeAreaInsets } from "react-native-safe-area-context";
6
7import { CompactBrandTitle } from "../../components/CompactBrandTitle";
8import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
9import { NativeStackScreenOptions } from "../../native/StackHeader";
10import { NativePrimaryColumnContext } from "../../native/v5-workspace-context";
11import {
12 useNativeColumnLayoutMetrics,
13 useNativeLayoutMetrics,
14} from "../../native/native-layout-metrics";
15import { HomeRouteScreen } from "../home/HomeRouteScreen";
16import { dispatchHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
17import { WorkspaceConnectionTitle } from "../home/WorkspaceConnectionTitle";
18import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
19import { PullRequestsScreen } from "../pull-requests/PullRequestsScreen";
20import {
21 WorkspaceTabsContext,
22 WorkspaceTabsProvider,
23 useWorkspaceTabs,
24} from "./WorkspaceTabsContext";
25import { WorkspaceToolbar } from "./WorkspaceToolbar";
26import { WORKSPACE_DOCK, workspaceToolbarHeight } from "./workspace-tabs-state";
27import { WorkspaceSidebarTabs } from "./WorkspaceSidebarTabs";
28import { useSidebarWorkspaceTabs, useSystemWorkspaceTabBar } from "./useSystemWorkspaceTabBar";
29
30const TAB_TITLES = {
31 threads: "Threads",
32 pullRequests: "Pull requests",
33};
34
35/** Brand and connection status occupy the same leading slot in both roots. */
36function WorkspaceRootTitle(props: {
37 readonly maxWidth: number;
38 readonly onOpenEnvironments: () => void;
39}) {
40 const { state } = useWorkspaceTabs()!;
41 return (
42 <View style={{ width: props.maxWidth, minHeight: 32, justifyContent: "center" }}>
43 <WorkspaceConnectionTitle
44 brand={
45 <CompactBrandTitle accessibilityLabel={`The Knife, ${TAB_TITLES[state.selectedTab]}`} />
46 }
47 maxWidth={props.maxWidth}
48 onPress={props.onOpenEnvironments}
49 />
50 </View>
51 );
52}
53
54function WorkspaceRootContent() {
55 const navigation = useNavigation();
56 const { width: windowWidth } = useWindowDimensions();
57 const { layout } = useAdaptiveWorkspaceLayout();
58 const primaryColumn = use(NativePrimaryColumnContext);
59 const columnMetrics = useNativeColumnLayoutMetrics();
60 const nativeMetrics = useNativeLayoutMetrics();
61 const systemTabBar = useSystemWorkspaceTabBar();
62 const sidebarTabs = useSidebarWorkspaceTabs();
63 const sidebarNavigation =
64 primaryColumn !== null &&
65 (layout.usesSplitView ||
66 (Platform.OS === "ios" && Platform.isPad) ||
67 (nativeMetrics !== null && nativeMetrics.verticalBarEdge !== "none"));
68 const width = columnMetrics?.width ?? windowWidth;
69 const insets = useSafeAreaInsets();
70 const keyboardHeight = useKeyboardState((keyboard) => keyboard.height);
71 const workspace = useWorkspaceTabs()!;
72 const { state } = workspace;
73 const renderHeaderTitle = useCallback(
74 () => (
75 <WorkspaceRootTitle
76 maxWidth={Math.max(0, width - 160)}
77 onOpenEnvironments={() =>
78 navigation.navigate("SettingsSheet", {
79 screen: "SettingsContent",
80 params: { screen: "SettingsEnvironments" },
81 })
82 }
83 />
84 ),
85 [width, navigation],
86 );
87 const tab = state.tabs[state.selectedTab];
88 const bottomClearance =
89 Platform.OS === "ios"
90 ? Math.max(insets.bottom, keyboardHeight) +
91 (systemTabBar || sidebarTabs || tab.searchMode !== "resting"
92 ? 16
93 : WORKSPACE_DOCK.height + WORKSPACE_DOCK.bottomGap + 16)
94 : keyboardHeight > 0 && tab.searchMode !== "resting"
95 ? keyboardHeight + 64
96 : Math.max(insets.bottom, 12) + workspaceToolbarHeight(tab.searchMode) + 16;
97 const context = useMemo(() => ({ ...workspace, bottomClearance }), [workspace, bottomClearance]);
98
99 return (
100 <WorkspaceTabsContext value={context}>
101 <View
102 className="flex-1 bg-screen"
103 style={sidebarTabs ? { paddingTop: insets.top } : undefined}
104 >
105 {Platform.OS === "ios" ? (
106 <NativeStackScreenOptions
107 optionsVersion={[state.selectedTab, state.selectionRevision, width, sidebarNavigation]}
108 options={{
109 title: TAB_TITLES[state.selectedTab],
110 headerShown: true,
111 unstable_headerLeftItems: () => [],
112 headerTitle: renderHeaderTitle,
113 }}
114 />
115 ) : (
116 <AndroidScreenHeader
117 title="The Knife"
118 titleContent={renderHeaderTitle()}
119 actions={[
120 {
121 accessibilityLabel:
122 state.selectedTab === "threads" ? "Search threads" : "Search pull requests",
123 icon: "magnifyingglass",
124 onPress: () => {
125 dispatchHardwareKeyboardCommand("focusSearch");
126 },
127 },
128 {
129 accessibilityLabel: "Settings",
130 icon: "gearshape",
131 onPress: () =>
132 navigation.navigate("SettingsSheet", {
133 screen: "SettingsContent",
134 params: { screen: "Settings" },
135 }),
136 },
137 ]}
138 />
139 )}
140 {sidebarTabs ? <WorkspaceSidebarTabs /> : null}
141 {/* The pinned picker already clears the navigation bar; lists must not inset it again. */}
142 <SafeAreaInsetsContext value={sidebarTabs ? { ...insets, top: 0 } : insets}>
143 {state.selectedTab === "threads" ? <HomeRouteScreen /> : <PullRequestsScreen />}
144 </SafeAreaInsetsContext>
145 <WorkspaceToolbar
146 sidebar={sidebarNavigation}
147 onNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
148 />
149 </View>
150 </WorkspaceTabsContext>
151 );
152}
153
154/** Keep workspace state mounted as the native columns expand and collapse. */
155export function WorkspaceRootScreen() {
156 const { layout } = useAdaptiveWorkspaceLayout();
157 const workspace = useWorkspaceTabs();
158 return Platform.OS === "android" && layout.usesSplitView ? (
159 <HomeRouteScreen />
160 ) : workspace ? (
161 <WorkspaceRootContent />
162 ) : (
163 <WorkspaceTabsProvider>
164 <WorkspaceRootContent />
165 </WorkspaceTabsProvider>
166 );
167}