import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { StackActions, useIsFocused, useNavigation, type StaticScreenProps, } from "@react-navigation/native"; import { SymbolView } from "../../components/AppSymbol"; import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; import { useEffect, useRef } from "react"; import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { cn } from "../../lib/cn"; import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; import { AppText as Text } from "../../components/AppText"; import { ProjectFavicon } from "../../components/ProjectFavicon"; import { useProjects } from "../../state/entities"; import type { WorkspaceState } from "../../state/workspaceModel"; import { useWorkspaceState } from "../../state/workspace"; import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; import { useIncomingShare } from "../sharing/IncomingShareProvider"; import { useNewTaskFlow } from "./new-task-flow-provider"; import { getProjectScopeSelectionTarget } from "./new-task-project-selection"; type NewTaskRouteParams = { readonly incomingShareId?: string | string[]; }; function deriveProjectEmptyState(catalogState: WorkspaceState): { readonly title: string; readonly detail: string; readonly loading: boolean; } { if (catalogState.isLoadingConnections) { return { title: "Loading environments", detail: "Checking saved environments on this device.", loading: true, }; } if (!catalogState.hasConnections) { return { title: "No environments connected", detail: "Add an environment before creating a task.", loading: false, }; } if ( (catalogState.connectionState === "available" || catalogState.connectionState === "offline" || catalogState.connectionState === "error") && !catalogState.hasLoadedShellSnapshot ) { return { title: "Environment unavailable", detail: catalogState.connectionError ?? "The saved environment is offline. Check the URL or start the environment, then retry.", loading: false, }; } if ( catalogState.hasConnectingEnvironment && !catalogState.hasLoadedShellSnapshot && catalogState.connectionError === null ) { return { title: "Connecting to environment", detail: "Loading projects from the saved environment.", loading: true, }; } return { title: "No projects found", detail: "The connected environment did not report any projects.", loading: false, }; } export function NewTaskRouteScreen({ route }: StaticScreenProps) { const projects = useProjects(); const { projectScopes, selectedEnvironmentId, setProject } = useNewTaskFlow(); const { state: catalogState } = useWorkspaceState(); const navigation = useNavigation(); const isFocused = useIsFocused(); const { layout } = useAdaptiveWorkspaceLayout(); const insets = useSafeAreaInsets(); const { getShare, releaseShareReservation } = useIncomingShare(); const routeShareId = Array.isArray(route.params?.incomingShareId) ? route.params.incomingShareId[0] : route.params?.incomingShareId; const incomingShare = routeShareId ? getShare(routeShareId) : null; const incomingShareSubtitle = incomingShare ? incomingShare.attachments.length === 0 ? "Choose a project for what you shared" : incomingShare.attachments.length === 1 ? `Choose a project for the ${incomingShare.attachments[0]?.type === "image" ? "image" : "file"} you shared` : `Choose a project for the ${incomingShare.attachments.length} ${incomingShare.attachments.every((attachment) => attachment.type === "image") ? "images" : "files"} you shared` : null; const screenTitle = incomingShare ? "Start a task" : "Choose project"; const projectEmptyState = deriveProjectEmptyState(catalogState); const resumedDestinationKeyRef = useRef(null); const reservedDestinationProject = incomingShare?.destination ? (projects.find( (project) => project.environmentId === incomingShare.destination?.environmentId && project.id === incomingShare.destination?.projectId, ) ?? null) : null; async function selectProject(project: EnvironmentProject): Promise { if (incomingShare?.destination && !reservedDestinationProject) { try { await releaseShareReservation(incomingShare.id, incomingShare.destination); } catch (error) { Alert.alert( "Could not change project", error instanceof Error ? error.message : "The shared content reservation could not be updated.", ); return; } } const state = navigation.getState(); const previousRoute = state?.routes[state.index - 1]; if (previousRoute?.name === "NewTaskDraft") { setProject(project); navigation.goBack(); return; } navigation.dispatch( StackActions.push("NewTaskDraft", { environmentId: project.environmentId, projectId: project.id, title: project.title, incomingShareId: incomingShare?.id, }), ); } useEffect(() => { const destination = incomingShare?.destination; if (!destination) { resumedDestinationKeyRef.current = null; return; } if (!isFocused) { // Returning from the reserved draft is a fresh resume attempt. Keeping // this latch set would leave every project row disabled with no route. resumedDestinationKeyRef.current = null; return; } const destinationKey = `${incomingShare.id}:${destination.environmentId}:${destination.projectId}`; if (resumedDestinationKeyRef.current === destinationKey) { return; } if (!reservedDestinationProject) { return; } resumedDestinationKeyRef.current = destinationKey; navigation.dispatch( StackActions.push("NewTaskDraft", { environmentId: reservedDestinationProject.environmentId, projectId: reservedDestinationProject.id, title: reservedDestinationProject.title, incomingShareId: incomingShare.id, }), ); }, [incomingShare, isFocused, navigation, reservedDestinationProject]); return ( {Platform.OS === "android" ? ( <> {/* Android renders its own in-screen header instead of the native bar. */} navigation.goBack() : undefined} actions={ catalogState.hasReadyEnvironment ? [ { accessibilityLabel: "Add project", icon: "plus", onPress: () => navigation.dispatch(StackActions.push("AddProject")), }, ] : [] } /> ) : ( <> {layout.usesSplitView ? ( navigation.goBack()} separateBackground /> ) : null} {catalogState.hasReadyEnvironment ? ( navigation.dispatch(StackActions.push("AddProject"))} separateBackground /> ) : null} )} {projectScopes.length === 0 ? ( {projectEmptyState.loading ? ( ) : null} {projectEmptyState.title} {projectEmptyState.detail} {!catalogState.hasReadyEnvironment ? ( navigation.navigate("ConnectionsNew")} > Add environment ) : ( navigation.dispatch(StackActions.push("AddProject"))} > Add new project )} ) : ( {projectScopes.map((scope, scopeIndex) => { const hasMultipleProjects = scope.projects.length > 1; const selectionTarget = getProjectScopeSelectionTarget(scope, selectedEnvironmentId); return ( 0 && "border-t border-border-subtle")} > void selectProject(selectionTarget)} className="flex-row items-center gap-3 bg-card px-4 py-3.5" > {scope.title} {hasMultipleProjects ? `${scope.projects.length} workspaces` : selectionTarget.workspaceRoot} ); })} )} ); }