import type { VcsRef } from "@t3tools/client-runtime/state/vcs"; import { resolveEnvironmentMachineKind } from "@t3tools/contracts"; import { LegendList } from "@legendapp/list/react-native"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import * as Haptics from "expo-haptics"; import { useNavigation } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, TextInput, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; import { ThemedSwitch } from "../../components/ThemedSwitch"; import { cn } from "../../lib/cn"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { useServerConfigs } from "../../state/entities"; import { useAtomCommand } from "../../state/use-atom-command"; import { vcsEnvironment } from "../../state/vcs"; import { createNativeMailSearchToolbarItem, NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET, NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, } from "../layout/native-mail-search-toolbar"; import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider"; import { checkoutNewTaskBranch } from "./checkout-new-task-branch"; function SelectionRow(props: { readonly icon?: "arrow.triangle.branch" | ReactNode; readonly onPress: () => void; readonly disabled?: boolean; readonly selected: boolean; readonly isLast?: boolean; readonly subtitle?: string; readonly title: string; }) { return ( {props.icon === "arrow.triangle.branch" ? ( ) : ( (props.icon ?? null) )} {props.title} {props.subtitle ? ( {props.subtitle} ) : null} {props.selected ? ( ) : null} ); } function ToggleRow(props: { readonly title: string; readonly value: boolean; readonly onValueChange: (value: boolean) => void; }) { return ( {props.title} ); } function BranchSelectionRow(props: { readonly badge: string | null; readonly branch: VcsRef; readonly disabled: boolean; readonly isFirst: boolean; readonly isLast: boolean; readonly onSelect: (branch: VcsRef) => void; readonly selected: boolean; }) { const onPress = useCallback(() => props.onSelect(props.branch), [props.branch, props.onSelect]); return ( ); } function PickerSurface(props: { readonly children: ReactNode }) { return {props.children}; } export function NewTaskEnvironmentPickerRouteScreen() { const flow = useNewTaskFlow(); const navigation = useNavigation(); const insets = useSafeAreaInsets(); const serverConfigs = useServerConfigs(); return ( {Platform.OS === "android" ? ( navigation.goBack()} /> ) : null} {flow.environments.map((environment, index) => ( } isLast={index === flow.environments.length - 1} onPress={() => { void Haptics.selectionAsync(); flow.selectEnvironment(environment.environmentId); navigation.goBack(); }} selected={flow.selectedEnvironmentId === environment.environmentId} title={environment.environmentLabel} /> ))} ); } export function NewTaskBranchPickerRouteScreen() { const flow = useNewTaskFlow(); const navigation = useNavigation(); const insets = useSafeAreaInsets(); const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false }); const [switchingBranchName, setSwitchingBranchName] = useState(null); const selectingBranchNameRef = useRef(null); const allowSelectionNavigationRef = useRef(false); const mountedRef = useRef(true); const screenTitle = flow.workspaceMode === "worktree" ? "Base branch" : "Branch"; const usesNativeMailSearchToolbar = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; const selectedBranchName = flow.selectedBranchName ?? flow.availableBranches.find((branch) => branch.current)?.name ?? flow.availableBranches.find((branch) => branch.isDefault)?.name ?? null; const branchListContentStyle = useMemo( () => ({ paddingBottom: usesNativeMailSearchToolbar ? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET + 16 : Platform.OS === "ios" ? 16 : Math.max(insets.bottom, 16) + 16, paddingHorizontal: 16, paddingTop: 12, }), [insets.bottom, usesNativeMailSearchToolbar], ); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; flow.setBranchQuery(""); }; }, [flow.setBranchQuery]); useEffect( () => navigation.addListener("beforeRemove", (event) => { if (selectingBranchNameRef.current !== null && !allowSelectionNavigationRef.current) { event.preventDefault(); } }), [navigation], ); const selectBranch = useCallback( async (branch: VcsRef) => { if (selectingBranchNameRef.current !== null) { return; } selectingBranchNameRef.current = branch.name; void Haptics.selectionAsync(); try { if (!flow.selectedProject) return; setSwitchingBranchName(branch.name); const result = await checkoutNewTaskBranch({ branch, project: flow.selectedProject, workspaceMode: flow.workspaceMode, switchRef, }); if (result._tag === "Failure") { if (mountedRef.current && navigation.isFocused() && !isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); Alert.alert( "Could not switch branch", error instanceof Error ? error.message : "The branch could not be checked out.", ); } return; } // The checkout has already changed the repository. Persist the matching // draft selection even if the native sheet was dismissed while the // command was in flight; only visible-screen work is focus-gated below. flow.selectBranch(result.value); if (!mountedRef.current || !navigation.isFocused()) { return; } flow.setBranchQuery(""); allowSelectionNavigationRef.current = true; navigation.goBack(); } finally { selectingBranchNameRef.current = null; allowSelectionNavigationRef.current = false; if (mountedRef.current) { setSwitchingBranchName(null); } } }, [ flow.selectBranch, flow.selectedProject, flow.setBranchQuery, flow.workspaceMode, navigation, switchRef, ], ); const renderBranch = useCallback( ({ item, index }: { readonly item: VcsRef; readonly index: number }) => ( ), [ flow.filteredBranches.length, flow.selectedProject, selectBranch, selectedBranchName, switchingBranchName, ], ); const branchListHeader = flow.workspaceMode === "worktree" ? ( ) : null; const branchContent = flow.filteredBranches.length === 0 ? ( {branchListHeader} {flow.branchesLoading ? : null} {flow.branchesLoading ? "Loading branches…" : flow.branchesError ? flow.branchesError : flow.branchQuery ? "No matching branches" : "No branches available"} {!flow.branchesLoading && flow.branchesError ? ( Try again ) : null} ) : ( `${branch.remoteName ?? "local"}:${branch.name}:${branch.worktreePath ?? ""}` } ListHeaderComponent={branchListHeader} ListFooterComponent={ flow.branchesFetchingNextPage ? ( ) : null } onEndReached={flow.hasMoreBranches ? flow.loadMoreBranches : undefined} onEndReachedThreshold={0.35} renderItem={renderBranch} showsVerticalScrollIndicator={false} /> ); if (Platform.OS === "android") { return ( navigation.goBack()} /> {branchContent} ); } return ( <> [ createNativeMailSearchToolbarItem({ onSearchTextChange: flow.setBranchQuery, placeholder: "Find a branch", searchTextChangeId: "new-task-branch-search-text", showsSearchDismissButton: true, }), ] : undefined, headerSearchBarOptions: usesNativeMailSearchToolbar ? undefined : { allowToolbarIntegration: true, autoCapitalize: "none", hideNavigationBar: false, obscureBackground: false, placeholder: "Find a branch", onChangeText: (event) => { flow.setBranchQuery(event.nativeEvent.text); }, onCancelButtonPress: () => { flow.setBranchQuery(""); }, }, }} /> {usesNativeMailSearchToolbar ? null : ( )} {branchContent} ); }