1import type { HeaderBarButtonMailSearchToolbarItem } from "react-native-screens";
2import { useId } from "react";
3import { ActivityIndicator, Platform, View } from "react-native";
4import { createNativeHeaderMenu } from "./nativeHeaderMenu.ios";
5import { ScreenHeaderButton } from "./ScreenHeaderButton";
6import { NativeHeaderToolbar, NativeStackScreenOptions } from "../native/StackHeader";
7import { useNativeWorkspaceColumnsSupported } from "../native/NativeWorkspaceColumns";
8import { useAdaptiveWorkspaceLayout } from "../features/layout/AdaptiveWorkspaceLayout";
9import {
10 createNativeMailSearchToolbarItem,
11 NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
12} from "../features/layout/native-mail-search-toolbar";
13import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
14import type { ScreenHeaderMenuItem, ScreenHeaderProps } from "./ScreenHeader.types";
15import type { AppSymbolName } from "./AppSymbol";
16import { useNativeMailSearchToolbar } from "../native/use-native-mail-search-toolbar";
17
18function iosIcon(icon: AppSymbolName) {
19 return typeof icon === "string" ? icon : icon.ios;
20}
21
22type MailMenu = NonNullable<HeaderBarButtonMailSearchToolbarItem["filterMenu"]>;
23function mailMenuItems(items: ReadonlyArray<ScreenHeaderMenuItem>): MailMenu["items"] {
24 return items.map((item) =>
25 "items" in item
26 ? { type: "submenu", title: item.title ?? "", items: mailMenuItems(item.items) }
27 : {
28 type: "action",
29 title: item.title,
30 state: item.selected ? "on" : "off",
31 disabled: item.disabled,
32 onPress: item.onPress,
33 },
34 );
35}
36
37export function ScreenHeader(props: ScreenHeaderProps) {
38 const usesNativeWorkspaceColumns = useNativeWorkspaceColumnsSupported();
39 const headerId = useId();
40 const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout();
41 const { themeVariables } = useAppearancePreferences();
42 const usesNativeMailSearchToolbar = useNativeMailSearchToolbar();
43 const { search, menus } = props;
44 const menu = menus?.[0];
45 const compactSearch =
46 search !== undefined &&
47 (search.compactToolbar ?? !layout.usesSplitView) &&
48 usesNativeMailSearchToolbar;
49 const visibleMenus = compactSearch ? (menus?.slice(1) ?? []) : (menus ?? []);
50 const refresh = search?.refreshInToolbar ? search.onRefresh : undefined;
51 return (
52 <>
53 <NativeStackScreenOptions
54 optionsVersion={[
55 props.optionsVersion,
56 compactSearch ? menu : undefined,
57 compactSearch ? refresh : undefined,
58 ]}
59 options={{
60 headerShown: true,
61 headerBackVisible:
62 !layout.usesSplitView ||
63 !props.backInSplitView ||
64 !(props.backInSplitView.onPress || props.onBack),
65 title: props.title,
66 unstable_headerSubtitle: props.subtitle || undefined,
67 ...(props.matchSearchSurface
68 ? { contentStyle: { backgroundColor: themeVariables["--color-sheet-solid"] } }
69 : undefined),
70 ...(search
71 ? {
72 unstable_headerToolbarItems: compactSearch
73 ? () => [
74 createNativeMailSearchToolbarItem({
75 showsSearchDismissButton: true,
76 placeholder: search.compactPlaceholder ?? search.placeholder,
77 onSearchTextChange: search.onChangeText,
78 searchTextChangeId: `${headerId}-search-text`,
79 ...(refresh
80 ? {
81 composeButtonId: `${headerId}-refresh`,
82 composeSystemImageName: "arrow.clockwise",
83 onComposePress: refresh,
84 }
85 : undefined),
86 ...(menu
87 ? {
88 filterButtonId: `${headerId}-filter`,
89 filterSystemImageName: iosIcon(menu.icon),
90 filterMenu: { title: menu.title, items: mailMenuItems(menu.items) },
91 }
92 : undefined),
93 }),
94 ]
95 : () => [],
96 headerSearchBarOptions: compactSearch
97 ? undefined
98 : {
99 allowToolbarIntegration: true,
100 ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED && search.mode === "inline"
101 ? { placement: "integratedButton" as const }
102 : undefined),
103 autoCapitalize: "none",
104 hideNavigationBar: false,
105 ...(search.mode === "inline" ? { obscureBackground: false } : undefined),
106 placeholder: search.placeholder,
107 onChangeText: (event) => search.onChangeText(event.nativeEvent.text),
108 onCancelButtonPress: () => search.onChangeText(""),
109 },
110 }
111 : undefined),
112 ...props.options,
113 }}
114 />
115 {layout.usesSplitView && (props.sidebar !== false || props.backInSplitView) ? (
116 <NativeHeaderToolbar placement="left">
117 {props.backInSplitView && (props.backInSplitView.onPress || props.onBack) ? (
118 <ScreenHeaderButton
119 {...props.backInSplitView}
120 onPress={props.backInSplitView.onPress ?? props.onBack}
121 />
122 ) : null}
123 {props.sidebar !== false &&
124 (!usesNativeWorkspaceColumns ||
125 (Platform.OS === "ios" && Platform.isPad) ||
126 !panes.primarySidebarVisible) ? (
127 <ScreenHeaderButton
128 axisBehavior={usesNativeWorkspaceColumns ? "horizontalOnly" : undefined}
129 accessibilityLabel={
130 panes.primarySidebarVisible
131 ? `Maximize ${props.title.toLowerCase()}`
132 : "Show threads"
133 }
134 icon={
135 panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"
136 }
137 onPress={togglePrimarySidebar}
138 separateBackground={
139 props.backInSplitView ? props.backInSplitView.separateBackground : true
140 }
141 />
142 ) : null}
143 </NativeHeaderToolbar>
144 ) : null}
145 {(props.actions?.length ||
146 (!compactSearch && refresh) ||
147 visibleMenus.length ||
148 props.trailing) &&
149 props.options?.unstable_headerRightItems === undefined ? (
150 <NativeHeaderToolbar placement="right">
151 {props.actions?.map(({ loading, ...action }) =>
152 loading ? (
153 <NativeHeaderToolbar.Custom key={action.accessibilityLabel}>
154 <View
155 accessibilityLabel={action.accessibilityLabel}
156 className="size-9 items-center justify-center"
157 >
158 <ActivityIndicator color={action.tintColor} />
159 </View>
160 </NativeHeaderToolbar.Custom>
161 ) : (
162 <ScreenHeaderButton
163 key={action.accessibilityLabel}
164 {...action}
165 icon={iosIcon(action.icon)}
166 separateBackground
167 />
168 ),
169 )}
170 {refresh && !compactSearch ? (
171 <ScreenHeaderButton
172 accessibilityLabel={search?.refreshAccessibilityLabel}
173 icon="arrow.clockwise"
174 onPress={refresh}
175 separateBackground
176 />
177 ) : null}
178 {visibleMenus.map(createNativeHeaderMenu)}
179 {props.trailing}
180 </NativeHeaderToolbar>
181 ) : null}
182 </>
183 );
184}
185
186export type {
187 ScreenHeaderAction,
188 ScreenHeaderMenu,
189 ScreenHeaderMenuItem,
190 ScreenHeaderProps,
191 ScreenHeaderSearch,
192} from "./ScreenHeader.types";