1import { StackActions, useNavigation } from "@react-navigation/native";
2import { useMemo } from "react";
3import { Platform } from "react-native";
4import type { AppNativeStackNavigationOptions } from "../../native/StackHeader";
5import { useNativeWorkspaceColumnsSupported } from "../../native/NativeWorkspaceColumns";
6import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
7import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
8import { dispatchHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
9import {
10 ThreadGitControls,
11 useThreadGitCenterHeaderItems,
12 useThreadGitRightHeaderItems,
13} from "./ThreadGitControls";
14
15type NativeHeaderItems = ReadonlyArray<Record<string, unknown>>;
16
17export function useThreadHeaderOptions(props: {
18 readonly title: string;
19 readonly subtitle: string;
20 readonly headerColor: string;
21 readonly usesNativeHeaderGlass: boolean;
22 readonly gitControls: Parameters<typeof ThreadGitControls>[0];
23 readonly onReturnToThread?: () => void;
24}) {
25 const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported();
26 const usesDuoHeader = usesNativeWorkspaceColumns && Platform.OS === "ios" && !Platform.isPad;
27 const navigation = useNavigation();
28 const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout();
29 const threadCenterHeaderItems = useThreadGitCenterHeaderItems(props.gitControls);
30 const compactRightHeaderItems = useThreadGitRightHeaderItems(props.gitControls);
31 const splitLeftHeaderItems = useMemo<NativeHeaderItems>(
32 () => [
33 ...(!usesDuoHeader
34 ? [
35 {
36 // Match Mail's split-view detail toolbar: the first detail action sits
37 // inside the content pane, not flush against the sidebar divider.
38 spacing: 18,
39 type: "spacing" as const,
40 },
41 ]
42 : []),
43 ...(props.onReturnToThread
44 ? [
45 withNativeGlassHeaderItem({
46 accessibilityLabel: "Return to chat",
47 icon: { name: "chevron.left", type: "sfSymbol" as const },
48 identifier: "thread-left-return",
49 onPress: props.onReturnToThread,
50 type: "button" as const,
51 }),
52 ]
53 : []),
54 withNativeGlassHeaderItem({
55 axisBehavior: usesNativeWorkspaceColumns ? "horizontalOnly" : undefined,
56 accessibilityLabel: panes.primarySidebarVisible
57 ? "Maximize content"
58 : "Show thread sidebar",
59 icon: {
60 name: panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left",
61 type: "sfSymbol" as const,
62 },
63 identifier: "thread-left-sidebar",
64 label: panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar",
65 onPress: togglePrimarySidebar,
66 type: "button" as const,
67 }),
68 ...(!usesDuoHeader
69 ? [
70 withNativeGlassHeaderItem({
71 accessibilityLabel: "New task",
72 icon: { name: "square.and.pencil", type: "sfSymbol" as const },
73 identifier: "thread-left-new-task",
74 label: "New task",
75 onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }),
76 type: "button" as const,
77 }),
78 ]
79 : []),
80 ],
81 [
82 panes.primarySidebarVisible,
83 props.onReturnToThread,
84 navigation,
85 togglePrimarySidebar,
86 usesDuoHeader,
87 usesNativeWorkspaceColumns,
88 ],
89 );
90 // Deep links / cold starts land with Thread as the ONLY route, where the
91 // native back button does not render. Provide an explicit Home escape for
92 // that case; when history exists the native back button is used instead.
93 const canGoBack = navigation.canGoBack();
94 const compactHomeHeaderItems = useMemo<NativeHeaderItems>(
95 () => [
96 withNativeGlassHeaderItem({
97 accessibilityLabel: "Go to threads list",
98 icon: { name: "list.bullet", type: "sfSymbol" as const },
99 identifier: "thread-left-home",
100 onPress: () => navigation.dispatch(StackActions.replace("Home")),
101 type: "button" as const,
102 }),
103 ],
104 [navigation],
105 );
106
107 const duoRightHeaderItems = useMemo<NativeHeaderItems>(
108 () => [
109 ...threadCenterHeaderItems,
110 { type: "spacing", spacing: 8 },
111 ...(layout.usesSplitView &&
112 Platform.OS === "ios" &&
113 !Platform.isPad &&
114 !panes.primarySidebarVisible
115 ? [
116 withNativeGlassHeaderItem({
117 type: "button" as const,
118 axisBehavior: "verticalPreferred",
119 identifier: "thread-right-search",
120 label: "Search threads",
121 accessibilityLabel: "Search threads",
122 icon: { name: "magnifyingglass", type: "sfSymbol" as const },
123 onPress: () => {
124 dispatchHardwareKeyboardCommand("focusSearch");
125 },
126 }),
127 ]
128 : []),
129 withNativeGlassHeaderItem({
130 type: "button" as const,
131 axisBehavior: "verticalPreferred",
132 identifier: "thread-right-new-task",
133 label: "New task",
134 accessibilityLabel: "New task",
135 icon: { name: "square.and.pencil", type: "sfSymbol" as const },
136 onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }),
137 }),
138 ],
139 [navigation, threadCenterHeaderItems, layout.usesSplitView, panes.primarySidebarVisible],
140 );
141
142 const options: AppNativeStackNavigationOptions = {
143 headerShown: true,
144 headerTitle: props.title,
145 headerTitleStyle: props.usesNativeHeaderGlass
146 ? {
147 fontSize: 17,
148 fontWeight: "800",
149 }
150 : undefined,
151 title: props.title,
152 headerBackVisible: !layout.usesSplitView,
153 // Compact uses the NATIVE back button when a previous route exists;
154 // deep links / cold starts get an explicit Home button instead.
155 // Split view always uses its custom left items.
156 unstable_headerLeftItems: layout.usesSplitView
157 ? () => splitLeftHeaderItems
158 : canGoBack
159 ? undefined
160 : () => compactHomeHeaderItems,
161 // Search lives in the persistent sidebar, so the split header keeps
162 // the git controls on the RIGHT (no center items — center space is
163 // reserved for future breadcrumbs/status).
164 unstable_headerRightItems: () =>
165 usesDuoHeader
166 ? duoRightHeaderItems
167 : layout.usesSplitView
168 ? threadCenterHeaderItems
169 : compactRightHeaderItems,
170 unstable_headerToolbarItems: () => [],
171 unstable_headerSubtitle: props.usesNativeHeaderGlass ? props.subtitle : undefined,
172 contentStyle: undefined,
173 };
174 const { environmentId, threadId, gitStatus } = props.gitControls;
175 return {
176 options,
177 // Header item factories are stabilized, so the native header only re-reads them when
178 // this version changes. Keying on the items keeps the Git menu status live; the menu
179 // callbacks also read state the items do not display (a "Push" item runs `push` or
180 // `commit_push` depending on the default ref), so that state is keyed too.
181 optionsVersion: [
182 splitLeftHeaderItems,
183 threadCenterHeaderItems,
184 compactRightHeaderItems,
185 duoRightHeaderItems,
186 usesDuoHeader,
187 environmentId,
188 threadId,
189 gitStatus?.isDefaultRef,
190 gitStatus?.refName,
191 gitStatus?.pr?.url,
192 ],
193 sidebar: false,
194 fallback:
195 !layout.usesSplitView && !props.usesNativeHeaderGlass ? (
196 <ThreadGitControls {...props.gitControls} showActionControls />
197 ) : null,
198 };
199}