| 1 | import { AppActivityIndicator } from "../../components/AppActivityIndicator"; |
| 2 | import { MaterialListRow } from "../../components/MaterialListRow"; |
| 3 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; |
| 4 | import { shouldCheckoutNewTaskBranch } from "./new-task-context-presentation"; |
| 5 | import type { VcsRef } from "@t3tools/client-runtime/state/vcs"; |
| 6 | import { AuthSourceControlWriteScope, resolveEnvironmentMachineKind } from "@t3tools/contracts"; |
| 7 | import { LegendList } from "@legendapp/list/react-native"; |
| 8 | import { |
| 9 | isAtomCommandInterrupted, |
| 10 | squashAtomCommandFailure, |
| 11 | } from "@t3tools/client-runtime/state/runtime"; |
| 12 | import * as Haptics from "expo-haptics"; |
| 13 | import { useNavigation } from "@react-navigation/native"; |
| 14 | import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; |
| 15 | import { Alert, Platform, Pressable, ScrollView, TextInput, View } from "react-native"; |
| 16 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 17 | |
| 18 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 19 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; |
| 20 | import { SymbolView } from "../../components/AppSymbol"; |
| 21 | import { AppText as Text } from "../../components/AppText"; |
| 22 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; |
| 23 | import { ThemedSwitch } from "../../components/ThemedSwitch"; |
| 24 | import { cn } from "../../lib/cn"; |
| 25 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 26 | import { useServerConfigs } from "../../state/entities"; |
| 27 | import { useEnvironmentScope } from "../../state/session"; |
| 28 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 29 | import { vcsEnvironment } from "../../state/vcs"; |
| 30 | import { |
| 31 | createNativeMailSearchToolbarItem, |
| 32 | NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET, |
| 33 | } from "../layout/native-mail-search-toolbar"; |
| 34 | import { useNativeMailSearchToolbar } from "../../native/use-native-mail-search-toolbar"; |
| 35 | import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider"; |
| 36 | import { checkoutNewTaskBranch } from "./checkout-new-task-branch"; |
| 37 | |
| 38 | function SelectionRow(props: { |
| 39 | readonly icon?: "arrow.triangle.branch" | ReactNode; |
| 40 | readonly onPress: () => void; |
| 41 | readonly disabled?: boolean; |
| 42 | readonly selected: boolean; |
| 43 | readonly isLast?: boolean; |
| 44 | readonly subtitle?: string; |
| 45 | readonly title: string; |
| 46 | }) { |
| 47 | if (Platform.OS === "android") { |
| 48 | return ( |
| 49 | <MaterialListRow |
| 50 | className="bg-grouped-card" |
| 51 | title={props.title} |
| 52 | subtitle={props.subtitle} |
| 53 | leading={ |
| 54 | props.icon === "arrow.triangle.branch" ? ( |
| 55 | <SymbolView |
| 56 | name="arrow.triangle.branch" |
| 57 | size={24} |
| 58 | tintColorClassName="accent-icon-muted" |
| 59 | /> |
| 60 | ) : ( |
| 61 | props.icon |
| 62 | ) |
| 63 | } |
| 64 | trailing={ |
| 65 | props.selected ? ( |
| 66 | <SymbolView name="checkmark" size={20} tintColorClassName="accent-focus" /> |
| 67 | ) : null |
| 68 | } |
| 69 | accessibilityRole="radio" |
| 70 | accessibilityState={{ checked: props.selected }} |
| 71 | disabled={props.disabled} |
| 72 | onPress={props.onPress} |
| 73 | /> |
| 74 | ); |
| 75 | } |
| 76 | return ( |
| 77 | <Pressable |
| 78 | accessibilityLabel={[props.title, props.subtitle].filter(Boolean).join(", ")} |
| 79 | accessibilityRole="radio" |
| 80 | accessibilityState={{ checked: props.selected }} |
| 81 | className={cn( |
| 82 | "min-h-14 flex-row items-center gap-3 bg-grouped-card px-4 py-3 active:bg-subtle", |
| 83 | !props.isLast && "border-b border-border-subtle", |
| 84 | )} |
| 85 | disabled={props.disabled} |
| 86 | onPress={props.onPress} |
| 87 | style={{ opacity: props.disabled ? 0.45 : 1 }} |
| 88 | > |
| 89 | {props.icon === "arrow.triangle.branch" ? ( |
| 90 | <SymbolView |
| 91 | name="arrow.triangle.branch" |
| 92 | size={17} |
| 93 | tintColorClassName="accent-icon-muted" |
| 94 | type="monochrome" |
| 95 | /> |
| 96 | ) : ( |
| 97 | (props.icon ?? null) |
| 98 | )} |
| 99 | <View className="min-w-0 flex-1 gap-0.5"> |
| 100 | <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}> |
| 101 | {props.title} |
| 102 | </Text> |
| 103 | {props.subtitle ? ( |
| 104 | <Text className="text-xs text-foreground-muted" numberOfLines={1}> |
| 105 | {props.subtitle} |
| 106 | </Text> |
| 107 | ) : null} |
| 108 | </View> |
| 109 | {props.selected ? ( |
| 110 | <SymbolView |
| 111 | name="checkmark" |
| 112 | size={16} |
| 113 | tintColorClassName="accent-icon" |
| 114 | type="monochrome" |
| 115 | weight="semibold" |
| 116 | /> |
| 117 | ) : null} |
| 118 | </Pressable> |
| 119 | ); |
| 120 | } |
| 121 | |
| 122 | function ToggleRow(props: { |
| 123 | readonly title: string; |
| 124 | readonly value: boolean; |
| 125 | readonly onValueChange: (value: boolean) => void; |
| 126 | }) { |
| 127 | return ( |
| 128 | <View className="min-h-14 flex-row items-center gap-3 bg-grouped-card px-4 py-3"> |
| 129 | <Text |
| 130 | className={cn( |
| 131 | "min-w-0 flex-1 text-base text-foreground", |
| 132 | Platform.OS !== "android" && "font-t3-medium", |
| 133 | )} |
| 134 | numberOfLines={1} |
| 135 | > |
| 136 | {props.title} |
| 137 | </Text> |
| 138 | <ThemedSwitch |
| 139 | accessibilityLabel={props.title} |
| 140 | onValueChange={props.onValueChange} |
| 141 | value={props.value} |
| 142 | /> |
| 143 | </View> |
| 144 | ); |
| 145 | } |
| 146 | |
| 147 | function BranchSelectionRow(props: { |
| 148 | readonly badge: string | null; |
| 149 | readonly branch: VcsRef; |
| 150 | readonly disabled: boolean; |
| 151 | readonly isFirst: boolean; |
| 152 | readonly isLast: boolean; |
| 153 | readonly onSelect: (branch: VcsRef) => void; |
| 154 | readonly selected: boolean; |
| 155 | }) { |
| 156 | const onPress = useCallback(() => props.onSelect(props.branch), [props.branch, props.onSelect]); |
| 157 | |
| 158 | return ( |
| 159 | <View |
| 160 | className={cn( |
| 161 | props.isFirst && |
| 162 | (Platform.OS === "android" |
| 163 | ? "overflow-hidden rounded-t-[28px]" |
| 164 | : "overflow-hidden rounded-t-2xl"), |
| 165 | props.isLast && |
| 166 | (Platform.OS === "android" |
| 167 | ? "overflow-hidden rounded-b-[28px]" |
| 168 | : "overflow-hidden rounded-b-2xl"), |
| 169 | )} |
| 170 | > |
| 171 | <SelectionRow |
| 172 | icon="arrow.triangle.branch" |
| 173 | disabled={props.disabled} |
| 174 | isLast={props.isLast} |
| 175 | onPress={onPress} |
| 176 | selected={props.selected} |
| 177 | subtitle={props.badge ? props.badge.toUpperCase() : undefined} |
| 178 | title={props.branch.name} |
| 179 | /> |
| 180 | </View> |
| 181 | ); |
| 182 | } |
| 183 | |
| 184 | function PickerSurface(props: { readonly children: ReactNode }) { |
| 185 | return ( |
| 186 | <View |
| 187 | className={ |
| 188 | Platform.OS === "android" |
| 189 | ? "overflow-hidden rounded-[28px] bg-grouped-card" |
| 190 | : "overflow-hidden rounded-2xl bg-grouped-card" |
| 191 | } |
| 192 | > |
| 193 | {props.children} |
| 194 | </View> |
| 195 | ); |
| 196 | } |
| 197 | |
| 198 | export function NewTaskEnvironmentPickerRouteScreen() { |
| 199 | const flow = useNewTaskFlow(); |
| 200 | const navigation = useNavigation(); |
| 201 | const insets = useSafeAreaInsets(); |
| 202 | const serverConfigs = useServerConfigs(); |
| 203 | return ( |
| 204 | <View className="flex-1 bg-sheet" collapsable={false}> |
| 205 | <NativeStackScreenOptions |
| 206 | options={{ |
| 207 | headerShown: Platform.OS !== "android", |
| 208 | title: "Environment", |
| 209 | }} |
| 210 | /> |
| 211 | {Platform.OS === "android" ? ( |
| 212 | <AndroidScreenHeader |
| 213 | title="Environment" |
| 214 | hideBottomBorder |
| 215 | onBack={() => navigation.goBack()} |
| 216 | /> |
| 217 | ) : null} |
| 218 | <MaterialScreenContent> |
| 219 | <ScrollView |
| 220 | contentInsetAdjustmentBehavior="automatic" |
| 221 | contentContainerStyle={{ |
| 222 | paddingBottom: Math.max(insets.bottom, 16) + 16, |
| 223 | paddingHorizontal: 16, |
| 224 | paddingTop: 16, |
| 225 | }} |
| 226 | showsVerticalScrollIndicator={false} |
| 227 | > |
| 228 | <PickerSurface> |
| 229 | {flow.environments.map((environment, index) => ( |
| 230 | <SelectionRow |
| 231 | key={String(environment.environmentId)} |
| 232 | icon={ |
| 233 | <EnvironmentMachineSymbol |
| 234 | kind={resolveEnvironmentMachineKind( |
| 235 | serverConfigs.get(environment.environmentId) ?? null, |
| 236 | )} |
| 237 | size={Platform.OS === "android" ? 24 : 17} |
| 238 | tintColorClassName="accent-icon-muted" |
| 239 | /> |
| 240 | } |
| 241 | isLast={index === flow.environments.length - 1} |
| 242 | disabled={flow.switchingToEnvironmentId !== null} |
| 243 | onPress={() => { |
| 244 | void Haptics.selectionAsync(); |
| 245 | void flow.switchEnvironment(environment.environmentId).then((switched) => { |
| 246 | if (switched) navigation.goBack(); |
| 247 | }); |
| 248 | }} |
| 249 | selected={flow.selectedEnvironmentId === environment.environmentId} |
| 250 | title={environment.environmentLabel} |
| 251 | /> |
| 252 | ))} |
| 253 | </PickerSurface> |
| 254 | </ScrollView> |
| 255 | </MaterialScreenContent> |
| 256 | </View> |
| 257 | ); |
| 258 | } |
| 259 | |
| 260 | export function NewTaskBranchPickerRouteScreen() { |
| 261 | const flow = useNewTaskFlow(); |
| 262 | const canWriteSourceControl = useEnvironmentScope( |
| 263 | flow.selectedProject?.environmentId ?? null, |
| 264 | AuthSourceControlWriteScope, |
| 265 | ); |
| 266 | const navigation = useNavigation(); |
| 267 | const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false }); |
| 268 | const [switchingBranchName, setSwitchingBranchName] = useState<string | null>(null); |
| 269 | const selectingBranchNameRef = useRef<string | null>(null); |
| 270 | const allowSelectionNavigationRef = useRef(false); |
| 271 | const mountedRef = useRef(true); |
| 272 | |
| 273 | useEffect(() => { |
| 274 | mountedRef.current = true; |
| 275 | return () => { |
| 276 | mountedRef.current = false; |
| 277 | flow.setBranchQuery(""); |
| 278 | }; |
| 279 | }, [flow.setBranchQuery]); |
| 280 | |
| 281 | useEffect( |
| 282 | () => |
| 283 | navigation.addListener("beforeRemove", (event) => { |
| 284 | if (selectingBranchNameRef.current !== null && !allowSelectionNavigationRef.current) { |
| 285 | event.preventDefault(); |
| 286 | } |
| 287 | }), |
| 288 | [navigation], |
| 289 | ); |
| 290 | |
| 291 | const selectBranch = useCallback( |
| 292 | async (branch: VcsRef) => { |
| 293 | const needsCheckout = shouldCheckoutNewTaskBranch({ |
| 294 | branchIsCurrent: branch.current, |
| 295 | branchWorktreePath: branch.worktreePath, |
| 296 | workspaceMode: flow.workspaceMode, |
| 297 | }); |
| 298 | if (selectingBranchNameRef.current !== null || (needsCheckout && !canWriteSourceControl)) { |
| 299 | return; |
| 300 | } |
| 301 | selectingBranchNameRef.current = branch.name; |
| 302 | void Haptics.selectionAsync(); |
| 303 | |
| 304 | try { |
| 305 | if (!flow.selectedProject) return; |
| 306 | setSwitchingBranchName(branch.name); |
| 307 | const result = await checkoutNewTaskBranch({ |
| 308 | branch, |
| 309 | project: flow.selectedProject, |
| 310 | workspaceMode: flow.workspaceMode, |
| 311 | switchRef, |
| 312 | }); |
| 313 | if (result._tag === "Failure") { |
| 314 | if (mountedRef.current && navigation.isFocused() && !isAtomCommandInterrupted(result)) { |
| 315 | const error = squashAtomCommandFailure(result); |
| 316 | Alert.alert( |
| 317 | "Could not switch branch", |
| 318 | error instanceof Error ? error.message : "The branch could not be checked out.", |
| 319 | ); |
| 320 | } |
| 321 | return; |
| 322 | } |
| 323 | |
| 324 | // The checkout has already changed the repository. Persist the matching |
| 325 | // draft selection even if the native sheet was dismissed while the |
| 326 | // command was in flight; only visible-screen work is focus-gated below. |
| 327 | flow.selectBranch(result.value); |
| 328 | if (!mountedRef.current || !navigation.isFocused()) { |
| 329 | return; |
| 330 | } |
| 331 | flow.setBranchQuery(""); |
| 332 | allowSelectionNavigationRef.current = true; |
| 333 | navigation.goBack(); |
| 334 | } finally { |
| 335 | selectingBranchNameRef.current = null; |
| 336 | allowSelectionNavigationRef.current = false; |
| 337 | if (mountedRef.current) { |
| 338 | setSwitchingBranchName(null); |
| 339 | } |
| 340 | } |
| 341 | }, |
| 342 | [ |
| 343 | canWriteSourceControl, |
| 344 | flow.selectBranch, |
| 345 | flow.selectedProject, |
| 346 | flow.setBranchQuery, |
| 347 | flow.workspaceMode, |
| 348 | navigation, |
| 349 | switchRef, |
| 350 | ], |
| 351 | ); |
| 352 | |
| 353 | return ( |
| 354 | <BranchPickerScreen |
| 355 | title={flow.workspaceMode === "worktree" ? "Base branch" : "Branch"} |
| 356 | project={flow.selectedProject} |
| 357 | branches={flow.filteredBranches} |
| 358 | selectedBranchName={ |
| 359 | flow.selectedBranchName ?? |
| 360 | flow.availableBranches.find((branch) => branch.current)?.name ?? |
| 361 | flow.availableBranches.find((branch) => branch.isDefault)?.name ?? |
| 362 | null |
| 363 | } |
| 364 | query={flow.branchQuery} |
| 365 | onQueryChange={flow.setBranchQuery} |
| 366 | loading={flow.branchesLoading} |
| 367 | error={flow.branchesError} |
| 368 | refreshing={flow.branchesFetchingNextPage} |
| 369 | hasMore={flow.hasMoreBranches} |
| 370 | onRefresh={flow.loadBranches} |
| 371 | onLoadMore={flow.loadMoreBranches} |
| 372 | selectionDisabled={switchingBranchName !== null} |
| 373 | onSelect={selectBranch} |
| 374 | worktree={ |
| 375 | flow.workspaceMode === "worktree" |
| 376 | ? { |
| 377 | startFromOrigin: flow.startFromOrigin, |
| 378 | onChangeStartFromOrigin: flow.setStartFromOrigin, |
| 379 | } |
| 380 | : undefined |
| 381 | } |
| 382 | /> |
| 383 | ); |
| 384 | } |
| 385 | |
| 386 | /** The searchable branch screen shared by new threads and scheduled tasks. */ |
| 387 | export function BranchPickerScreen(props: { |
| 388 | readonly title: string; |
| 389 | readonly project: EnvironmentProject | null; |
| 390 | readonly branches: readonly VcsRef[]; |
| 391 | readonly selectedBranchName: string | null; |
| 392 | readonly query: string; |
| 393 | readonly onQueryChange: (query: string) => void; |
| 394 | readonly loading: boolean; |
| 395 | readonly error: string | null; |
| 396 | readonly refreshing: boolean; |
| 397 | readonly hasMore: boolean; |
| 398 | readonly onRefresh: () => void; |
| 399 | readonly onLoadMore: () => void; |
| 400 | readonly selectionDisabled?: boolean; |
| 401 | readonly onSelect: (branch: VcsRef) => void; |
| 402 | readonly worktree?: { |
| 403 | readonly startFromOrigin: boolean; |
| 404 | readonly onChangeStartFromOrigin: (value: boolean) => void; |
| 405 | }; |
| 406 | }) { |
| 407 | const navigation = useNavigation(); |
| 408 | const insets = useSafeAreaInsets(); |
| 409 | const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); |
| 410 | const selectedBranchName = |
| 411 | props.selectedBranchName ?? |
| 412 | props.branches.find((branch) => branch.current)?.name ?? |
| 413 | props.branches.find((branch) => branch.isDefault)?.name ?? |
| 414 | null; |
| 415 | const branchListContentStyle = useMemo( |
| 416 | () => ({ |
| 417 | paddingBottom: usesNativeMailSearchToolbar |
| 418 | ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET + 16 |
| 419 | : Platform.OS === "ios" |
| 420 | ? 16 |
| 421 | : Math.max(insets.bottom, 16) + 16, |
| 422 | paddingHorizontal: 16, |
| 423 | paddingTop: 16, |
| 424 | }), |
| 425 | [insets.bottom, usesNativeMailSearchToolbar], |
| 426 | ); |
| 427 | |
| 428 | const renderBranch = useCallback( |
| 429 | ({ item, index }: { readonly item: VcsRef; readonly index: number }) => ( |
| 430 | <BranchSelectionRow |
| 431 | badge={branchBadgeLabel({ branch: item, project: props.project })} |
| 432 | branch={item} |
| 433 | disabled={props.selectionDisabled ?? false} |
| 434 | isFirst={index === 0} |
| 435 | isLast={index === props.branches.length - 1} |
| 436 | onSelect={props.onSelect} |
| 437 | selected={selectedBranchName === item.name} |
| 438 | /> |
| 439 | ), |
| 440 | [ |
| 441 | props.branches.length, |
| 442 | props.project, |
| 443 | props.onSelect, |
| 444 | selectedBranchName, |
| 445 | props.selectionDisabled, |
| 446 | ], |
| 447 | ); |
| 448 | |
| 449 | const branchListHeader = props.worktree ? ( |
| 450 | <View |
| 451 | className={cn( |
| 452 | "mb-3 overflow-hidden", |
| 453 | Platform.OS === "android" ? "rounded-[28px]" : "rounded-2xl", |
| 454 | )} |
| 455 | > |
| 456 | <ToggleRow |
| 457 | onValueChange={props.worktree.onChangeStartFromOrigin} |
| 458 | title="Start from origin" |
| 459 | value={props.worktree.startFromOrigin} |
| 460 | /> |
| 461 | </View> |
| 462 | ) : null; |
| 463 | |
| 464 | const branchContent = |
| 465 | props.branches.length === 0 ? ( |
| 466 | <ScrollView |
| 467 | className="flex-1 bg-sheet android:bg-sheet-solid" |
| 468 | contentInsetAdjustmentBehavior="automatic" |
| 469 | contentContainerStyle={{ flexGrow: 1, paddingHorizontal: 16, paddingTop: 16 }} |
| 470 | scrollEnabled={false} |
| 471 | showsVerticalScrollIndicator={false} |
| 472 | > |
| 473 | {branchListHeader} |
| 474 | <View |
| 475 | className="flex-1 items-center justify-center gap-3 px-4" |
| 476 | style={{ |
| 477 | marginBottom: usesNativeMailSearchToolbar |
| 478 | ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET |
| 479 | : 0, |
| 480 | }} |
| 481 | > |
| 482 | {props.loading ? <AppActivityIndicator /> : null} |
| 483 | <Text className="text-center text-sm text-foreground-muted"> |
| 484 | {props.loading |
| 485 | ? "Loading branches…" |
| 486 | : props.error |
| 487 | ? props.error |
| 488 | : props.query |
| 489 | ? "No matching branches" |
| 490 | : "No branches available"} |
| 491 | </Text> |
| 492 | {!props.loading && props.error ? ( |
| 493 | <Pressable |
| 494 | accessibilityRole="button" |
| 495 | className="rounded-full bg-card px-4 py-2 active:opacity-70" |
| 496 | onPress={props.onRefresh} |
| 497 | > |
| 498 | <Text className="text-sm font-t3-medium text-foreground">Try again</Text> |
| 499 | </Pressable> |
| 500 | ) : null} |
| 501 | </View> |
| 502 | </ScrollView> |
| 503 | ) : ( |
| 504 | <LegendList |
| 505 | alwaysBounceVertical={false} |
| 506 | automaticallyAdjustsScrollIndicatorInsets |
| 507 | automaticallyAdjustKeyboardInsets={Platform.OS === "ios"} |
| 508 | className="flex-1 bg-sheet android:bg-sheet-solid" |
| 509 | contentInsetAdjustmentBehavior="automatic" |
| 510 | contentContainerStyle={branchListContentStyle} |
| 511 | data={props.branches} |
| 512 | keyboardDismissMode={Platform.OS === "ios" ? "interactive" : "on-drag"} |
| 513 | keyboardShouldPersistTaps="handled" |
| 514 | keyExtractor={(branch) => |
| 515 | `${branch.remoteName ?? "local"}:${branch.name}:${branch.worktreePath ?? ""}` |
| 516 | } |
| 517 | ListHeaderComponent={branchListHeader} |
| 518 | ListFooterComponent={ |
| 519 | props.refreshing ? ( |
| 520 | <View className="items-center py-4"> |
| 521 | <AppActivityIndicator /> |
| 522 | </View> |
| 523 | ) : null |
| 524 | } |
| 525 | onEndReached={props.hasMore ? props.onLoadMore : undefined} |
| 526 | onEndReachedThreshold={0.35} |
| 527 | renderItem={renderBranch} |
| 528 | showsVerticalScrollIndicator={false} |
| 529 | /> |
| 530 | ); |
| 531 | |
| 532 | if (Platform.OS === "android") { |
| 533 | return ( |
| 534 | <View className="flex-1 bg-sheet" collapsable={false}> |
| 535 | <NativeStackScreenOptions options={{ headerShown: false }} /> |
| 536 | <AndroidScreenHeader |
| 537 | title={props.title} |
| 538 | hideBottomBorder |
| 539 | onBack={() => navigation.goBack()} |
| 540 | /> |
| 541 | <View className="bg-header px-4 pb-3 pt-1"> |
| 542 | <TextInput |
| 543 | autoCapitalize="none" |
| 544 | autoCorrect={false} |
| 545 | accessibilityLabel="Find a branch" |
| 546 | className="h-12 rounded-full border border-input-border bg-input px-4 font-sans text-base text-foreground" |
| 547 | selectionColorClassName="accent-focus/32" |
| 548 | cursorColorClassName="accent-focus" |
| 549 | selectionHandleColorClassName="accent-focus" |
| 550 | onChangeText={props.onQueryChange} |
| 551 | placeholder="Find a branch" |
| 552 | placeholderTextColorClassName="accent-placeholder" |
| 553 | value={props.query} |
| 554 | /> |
| 555 | </View> |
| 556 | <MaterialScreenContent>{branchContent}</MaterialScreenContent> |
| 557 | </View> |
| 558 | ); |
| 559 | } |
| 560 | |
| 561 | return ( |
| 562 | <> |
| 563 | <NativeStackScreenOptions |
| 564 | options={{ |
| 565 | headerShown: true, |
| 566 | title: props.title, |
| 567 | unstable_headerToolbarItems: usesNativeMailSearchToolbar |
| 568 | ? () => [ |
| 569 | createNativeMailSearchToolbarItem({ |
| 570 | onSearchTextChange: props.onQueryChange, |
| 571 | placeholder: "Find a branch", |
| 572 | searchTextChangeId: "new-task-branch-search-text", |
| 573 | showsSearchDismissButton: true, |
| 574 | }), |
| 575 | ] |
| 576 | : undefined, |
| 577 | headerSearchBarOptions: usesNativeMailSearchToolbar |
| 578 | ? undefined |
| 579 | : { |
| 580 | allowToolbarIntegration: true, |
| 581 | autoCapitalize: "none", |
| 582 | hideNavigationBar: false, |
| 583 | obscureBackground: false, |
| 584 | placeholder: "Find a branch", |
| 585 | onChangeText: (event) => { |
| 586 | props.onQueryChange(event.nativeEvent.text); |
| 587 | }, |
| 588 | onCancelButtonPress: () => { |
| 589 | props.onQueryChange(""); |
| 590 | }, |
| 591 | }, |
| 592 | }} |
| 593 | /> |
| 594 | {usesNativeMailSearchToolbar ? null : ( |
| 595 | <NativeHeaderToolbar placement="bottom"> |
| 596 | <NativeHeaderToolbar.SearchBarSlot /> |
| 597 | </NativeHeaderToolbar> |
| 598 | )} |
| 599 | {branchContent} |
| 600 | </> |
| 601 | ); |
| 602 | } |