| 1 | import type { HeaderBarButtonMailSearchToolbarItem } from "react-native-screens"; |
| 2 | import { useId } from "react"; |
| 3 | import { ActivityIndicator, Platform, View } from "react-native"; |
| 4 | import { createNativeHeaderMenu } from "./nativeHeaderMenu.ios"; |
| 5 | import { ScreenHeaderButton } from "./ScreenHeaderButton"; |
| 6 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../native/StackHeader"; |
| 7 | import { useNativeWorkspaceColumnsSupported } from "../native/NativeWorkspaceColumns"; |
| 8 | import { useAdaptiveWorkspaceLayout } from "../features/layout/AdaptiveWorkspaceLayout"; |
| 9 | import { |
| 10 | createNativeMailSearchToolbarItem, |
| 11 | NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, |
| 12 | } from "../features/layout/native-mail-search-toolbar"; |
| 13 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 14 | import type { ScreenHeaderMenuItem, ScreenHeaderProps } from "./ScreenHeader.types"; |
| 15 | import type { AppSymbolName } from "./AppSymbol"; |
| 16 | import { useNativeMailSearchToolbar } from "../native/use-native-mail-search-toolbar"; |
| 17 | |
| 18 | function iosIcon(icon: AppSymbolName) { |
| 19 | return typeof icon === "string" ? icon : icon.ios; |
| 20 | } |
| 21 | |
| 22 | type MailMenu = NonNullable<HeaderBarButtonMailSearchToolbarItem["filterMenu"]>; |
| 23 | function 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 | |
| 37 | export 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 | |
| 186 | export type { |
| 187 | ScreenHeaderAction, |
| 188 | ScreenHeaderMenu, |
| 189 | ScreenHeaderMenuItem, |
| 190 | ScreenHeaderProps, |
| 191 | ScreenHeaderSearch, |
| 192 | } from "./ScreenHeader.types"; |