import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useLocalSearchParams } from "expo-router"; import Stack from "expo-router/stack"; import { SymbolView } from "expo-symbols"; import { memo, type ReactElement, useCallback, useMemo } from "react"; import { ActivityIndicator, Pressable, ScrollView, type NativeSyntheticEvent, Text as NativeText, StyleSheet, useColorScheme, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText"; import { environmentCatalog } from "../../connection/catalog"; import { useEnvironmentPresentation } from "../../state/presentation"; import { useAtomCommand } from "../../state/use-atom-command"; import { useThemeColor } from "../../lib/useThemeColor"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useThreadDraftForThread } from "../../state/use-thread-composer-state"; import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; import { useReviewCacheForThread } from "./reviewState"; import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface"; import { NATIVE_REVIEW_DIFF_CONTENT_WIDTH, NATIVE_REVIEW_DIFF_ROW_HEIGHT, } from "./nativeReviewDiffAdapter"; import { useReviewDiffData } from "./useReviewDiffData"; import { useReviewFileVisibility } from "./reviewFileVisibility"; import { useReviewSections } from "./useReviewSections"; import { useNativeReviewDiffBridge } from "./useNativeReviewDiffBridge"; import { useReviewCommentSelectionController } from "./useReviewCommentSelectionController"; import { resolveReviewAvailability } from "./reviewAvailability"; const IOS_NAV_BAR_HEIGHT = 44; const REVIEW_HEADER_SPACING = 0; const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) { return ( Partial diff {props.notice} ); }); function ReviewSelectionActionBar(props: { readonly bottomInset: number; readonly title: string | null; readonly onOpenComment: (() => void) | null; readonly onClear: () => void; }) { if (!props.title) { return null; } const content = ( <> {props.title} ); return ( {props.onOpenComment ? ( {content} ) : ( {content} )} ); } export function ReviewSheet() { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const headerForeground = String(useThemeColor("--color-foreground")); const headerMuted = String(useThemeColor("--color-foreground-muted")); const headerIcon = String(useThemeColor("--color-icon")); const { environmentId, threadId } = useLocalSearchParams<{ environmentId: EnvironmentId; threadId: ThreadId; }>(); const environment = useEnvironmentPresentation(environmentId); const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); const isEnvironmentReady = environment.presentation?.connection.phase === "connected"; const { draftMessage } = useThreadDraftForThread({ environmentId, threadId }); const reviewCache = useReviewCacheForThread({ environmentId, threadId }); const selectedTheme = colorScheme === "dark" ? "dark" : "light"; const topContentInset = insets.top + IOS_NAV_BAR_HEIGHT; const { error, loadingGitDiffs, loadingTurnIds, reviewSections, selectedSection, refreshSelectedSection, selectSection, } = useReviewSections({ enabled: isEnvironmentReady, environmentId, threadId, reviewCache, }); const { headerDiffSummary, nativeReviewDiffData, parsedDiff, pendingReviewCommentCount } = useReviewDiffData({ threadKey: reviewCache.threadKey, selectedSection, draftMessage, }); const NativeReviewDiffView = resolveNativeReviewDiffView()!; const reviewFiles = parsedDiff.kind === "files" ? parsedDiff.files : []; const fileVisibility = useReviewFileVisibility({ threadKey: reviewCache.threadKey, sectionId: selectedSection?.id ?? null, files: reviewFiles, cachedExpandedFileIds: selectedSection?.id ? reviewCache.expandedFileIdsBySection[selectedSection.id] : undefined, cachedViewedFileIds: selectedSection?.id ? reviewCache.viewedFileIdsBySection[selectedSection.id] : undefined, }); const { collapsedFileIds, toggleExpandedFile, toggleViewedFile, viewedFileIds } = fileVisibility; const commentSelection = useReviewCommentSelectionController({ environmentId, threadId, selectedSection, nativeReviewDiffData, }); const nativeBridge = useNativeReviewDiffBridge({ threadKey: reviewCache.threadKey, sectionId: selectedSection?.id ?? null, diff: selectedSection?.diff, data: nativeReviewDiffData, scheme: selectedTheme, collapsedFileIds, viewedFileIds, selectedRowIds: commentSelection.selectedRowIds, canHighlight: parsedDiff.kind === "files", }); const handleNativeToggleFile = useCallback( (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => { const { fileId } = event.nativeEvent; if (fileId) { toggleExpandedFile(fileId); } }, [toggleExpandedFile], ); const handleNativeToggleViewedFile = useCallback( (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => { const { fileId } = event.nativeEvent; if (fileId) { toggleViewedFile(fileId); } }, [toggleViewedFile], ); const parsedDiffNotice = parsedDiff.kind === "files" || parsedDiff.kind === "raw" ? parsedDiff.notice : null; const hasCachedSelectedDiff = selectedSection?.diff != null; const hasAnyCachedDiff = reviewSections.some((section) => section.diff != null); const { showConnectionNotice, showSectionToolbar } = resolveReviewAvailability({ hasEnvironmentPresentation: environment.isReady, isEnvironmentConnected: isEnvironmentReady, hasCachedSelectedDiff, hasAnyCachedDiff, }); const handleRetryEnvironment = useCallback(() => { void retryEnvironment(environmentId); }, [environmentId, retryEnvironment]); const listHeader = useMemo(() => { const children: ReactElement[] = []; if (error) { children.push( Review unavailable {error} , ); } if (parsedDiffNotice) { children.push(); } if (children.length === 0) { return null; } return <>{children}; }, [error, parsedDiffNotice]); return ( <> ( Files Changed {headerDiffSummary.additions && headerDiffSummary.deletions ? ( <> {headerDiffSummary.additions} {headerDiffSummary.deletions} {pendingReviewCommentCount > 0 ? ( {pendingReviewCommentCount} pending ) : null} ) : ( {selectedSection?.title ?? "Review changes"} {pendingReviewCommentCount > 0 ? ( {pendingReviewCommentCount} pending ) : null} )} ), }} /> {showSectionToolbar ? ( {reviewSections.map((section) => ( selectSection(section.id)} subtitle={section.subtitle ?? undefined} > {section.title} ))} void refreshSelectedSection()} subtitle="Reload current diff" > Refresh ) : null} {showConnectionNotice ? ( ) : selectedSection && parsedDiff.kind === "files" ? ( {listHeader} ) : ( {listHeader} {!selectedSection ? ( No review diffs This thread has no ready turn diffs and the worktree diff is empty. ) : selectedSection.isLoading && selectedSection.diff === null ? ( Loading diff… ) : parsedDiff.kind === "empty" ? ( No changes {selectedSection.subtitle ?? "This diff is empty."} ) : parsedDiff.kind === "raw" ? ( {parsedDiff.reason} {parsedDiff.text} ) : null} )} ); }