import { useLocalSearchParams, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; import { TextInputWrapper } from "expo-paste-input"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Pressable, ScrollView, View, useColorScheme, useWindowDimensions } from "react-native"; import { KeyboardAvoidingView } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import ImageViewing from "react-native-image-viewing"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; import { ControlPill } from "../../components/ControlPill"; import { cn } from "../../lib/cn"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages"; import { useThemeColor } from "../../lib/useThemeColor"; import { useNativePaste } from "../../lib/useNativePaste"; import { setPendingConnectionError } from "../../state/use-remote-environment-registry"; import { appendReviewCommentToDraft } from "../../state/use-thread-composer-state"; import { clearReviewCommentTarget, formatReviewCommentContext, getReviewUnifiedLineNumber, getSelectedReviewCommentLines, useReviewCommentTarget, } from "./reviewCommentSelection"; import { changeTone, DiffTokenText, REVIEW_DIFF_LINE_HEIGHT, REVIEW_MONO_FONT_FAMILY, ReviewChangeBar, } from "./reviewDiffRendering"; import { highlightReviewSelectedLines, type ReviewDiffTheme, type ReviewHighlightedToken, } from "./shikiReviewHighlighter"; const REVIEW_COMMENT_PREVIEW_MAX_LINES = 5; export function ReviewCommentComposerSheet() { const router = useRouter(); const insets = useSafeAreaInsets(); const { width } = useWindowDimensions(); const colorScheme = useColorScheme(); const iconTint = String(useThemeColor("--color-icon")); const target = useReviewCommentTarget(); const { environmentId, threadId } = useLocalSearchParams<{ environmentId: EnvironmentId; threadId: ThreadId; }>(); const [commentText, setCommentText] = useState(""); const [highlightedLinesById, setHighlightedLinesById] = useState< Record> >({}); const [attachments, setAttachments] = useState>([]); const [previewImageUri, setPreviewImageUri] = useState(null); const selectedLines = useMemo( () => (target ? getSelectedReviewCommentLines(target) : []), [target], ); const firstLine = selectedLines[0] ?? null; const lastLine = selectedLines[selectedLines.length - 1] ?? null; const firstNumber = firstLine ? getReviewUnifiedLineNumber(firstLine) : null; const lastNumber = lastLine ? getReviewUnifiedLineNumber(lastLine) : null; const selectedTheme = (colorScheme === "dark" ? "dark" : "light") satisfies ReviewDiffTheme; const canSubmit = commentText.trim().length > 0 && target !== null && !!environmentId && !!threadId; const selectionLabel = selectedLines.length === 1 ? firstNumber !== null ? `Line ${firstNumber}` : "File comment" : firstNumber !== null && lastNumber !== null ? `Lines ${firstNumber}-${lastNumber}` : `${selectedLines.length} lines selected`; const previewHeight = Math.max( Math.min(selectedLines.length, REVIEW_COMMENT_PREVIEW_MAX_LINES) * REVIEW_DIFF_LINE_HEIGHT, REVIEW_DIFF_LINE_HEIGHT, ); const previewViewportWidth = Math.max(width - 40, 280); const dismissComposer = useCallback(() => { clearReviewCommentTarget(); router.dismiss(); }, [router]); const handleNativePaste = useNativePaste((uris) => { void (async () => { try { const images = await convertPastedImagesToAttachments({ uris, existingCount: attachments.length, }); if (images.length > 0) { setAttachments((current) => [...current, ...images]); } } catch (error) { console.error("[review comment] error converting pasted images", error); } })(); }); useEffect(() => { if (!target || selectedLines.length === 0) { setHighlightedLinesById({}); return; } let cancelled = false; void highlightReviewSelectedLines({ filePath: target.filePath, lines: selectedLines, theme: selectedTheme, }) .then((next) => { if (!cancelled) { setHighlightedLinesById(next); } }) .catch(() => { if (!cancelled) { setHighlightedLinesById({}); } }); return () => { cancelled = true; }; }, [selectedLines, selectedTheme, target]); async function handlePickImages(): Promise { const result = await pickComposerImages({ existingCount: attachments.length }); if (result.images.length > 0) { setAttachments((current) => [...current, ...result.images]); } if (result.error) { setPendingConnectionError(result.error); } } return ( Add Comment {!target ? ( No selection Select a diff line or range first. ) : ( {selectionLabel} {target.filePath} REVIEW_COMMENT_PREVIEW_MAX_LINES} nestedScrollEnabled keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={ selectedLines.length > REVIEW_COMMENT_PREVIEW_MAX_LINES } style={{ height: previewHeight }} > {selectedLines.map((line) => { const lineNumber = getReviewUnifiedLineNumber(line); return ( {lineNumber ?? ""} ); })} Comment {attachments.length > 0 ? ( { setAttachments((current) => current.filter((image) => image.id !== imageId), ); }} /> ) : null} )} {target ? ( void handlePickImages()} /> { if (!target || !environmentId || !threadId || commentText.trim().length === 0) { return; } appendReviewCommentToDraft({ environmentId, threadId, text: formatReviewCommentContext(target, commentText), attachments, }); setAttachments([]); dismissComposer(); }} /> ) : null} setPreviewImageUri(null)} swipeToCloseEnabled doubleTapToZoomEnabled /> ); }