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}
)}
>
);
}