import { useState, useEffect, useCallback, useMemo } from 'react' import { FlatList, Image, View, Text, ScrollView, Pressable, Platform, ActivityIndicator, type ListRenderItem } from 'react-native' import { Copy, MessageSquare, Send } from 'lucide-react-native' import { MobileSyntaxSegments } from '../components/MobileSyntaxSegments' import { buildPlainMobileDiffSyntaxLines, highlightMobileCode, highlightMobileDiffLines, resolveMobileSyntaxLanguage } from './mobile-file-syntax' import { MobileHtmlPreview } from '../components/MobileHtmlPreview' import { colors } from '../theme/mobile-theme' import { styles } from './mobile-session-styles' import type { DiffComment } from '../../../src/shared/diff-comment-types' import type { DiffCommentActions, DiffSyntaxState, FileDocState, FileSyntaxState, RenderableDiffLine } from './mobile-session-route-types' import { DiffLineRow } from './MobileSessionDiffLineRow' export function FileReader({ doc, title, relativePath, language, diffCommentActions }: { doc: FileDocState | undefined title: string relativePath: string language?: string diffCommentActions?: DiffCommentActions }) { const syntaxLanguage = useMemo( () => resolveMobileSyntaxLanguage(relativePath || title, language), [language, relativePath, title] ) const [fileSyntax, setFileSyntax] = useState(null) const [diffSyntax, setDiffSyntax] = useState(null) const [activeCommentLine, setActiveCommentLine] = useState(null) const [commentDraft, setCommentDraft] = useState('') const plainDiffLines = useMemo( () => doc?.status === 'ready' && doc.kind === 'diff' ? buildPlainMobileDiffSyntaxLines(doc.lines) : [], [doc] ) const diffCommentsForFile = useMemo( () => diffCommentActions?.comments.filter( (comment) => comment.filePath === relativePath && comment.source !== 'markdown' ) ?? [], [diffCommentActions?.comments, relativePath] ) const diffCommentsByLine = useMemo(() => { const map = new Map() for (const comment of diffCommentsForFile) { const list = map.get(comment.lineNumber) ?? [] list.push(comment) map.set(comment.lineNumber, list) } for (const list of map.values()) { list.sort((a, b) => a.createdAt - b.createdAt) } return map }, [diffCommentsForFile]) const startComment = useCallback((lineNumber: number) => { setActiveCommentLine(lineNumber) setCommentDraft('') }, []) const cancelComment = useCallback(() => { setActiveCommentLine(null) setCommentDraft('') }, []) const submitComment = useCallback( (lineNumber: number) => { if (!diffCommentActions) { return } void diffCommentActions.onAdd(relativePath, lineNumber, commentDraft).then((added) => { if (added) { setActiveCommentLine(null) setCommentDraft('') } }) }, [commentDraft, diffCommentActions, relativePath] ) const renderDiffLine: ListRenderItem = useCallback( ({ item, index }) => ( { if (diffCommentActions) { void diffCommentActions.onDelete(commentId) } }} /> ), [ activeCommentLine, cancelComment, commentDraft, diffCommentActions, diffCommentsByLine, startComment, submitComment, title ] ) useEffect(() => { if (doc?.status !== 'ready') { return undefined } // Why: defer highlighting one tick so large files show as plain text immediately before colors are applied. const timer = setTimeout(() => { // file + html share the syntax-segment source view (html's "Source" toggle). if (doc.kind === 'file' || doc.kind === 'html') { setFileSyntax({ doc, language: syntaxLanguage, segments: highlightMobileCode(doc.content, syntaxLanguage).segments }) return } if (doc.kind === 'diff') { setDiffSyntax({ doc, language: syntaxLanguage, lines: highlightMobileDiffLines(doc.lines, syntaxLanguage) }) } // image: no syntax highlighting. }, 0) return () => clearTimeout(timer) }, [doc, syntaxLanguage]) if (!doc || doc.status === 'loading') { return ( ) } if (doc.status === 'error') { return ( {doc.message} ) } if (doc.kind === 'diff') { const activeDiffSyntax = diffSyntax?.doc === doc && diffSyntax.language === syntaxLanguage ? diffSyntax.lines : null const commentCount = diffCommentActions?.comments.length ?? 0 const unsentCommentCount = diffCommentActions?.comments.filter((comment) => !comment.sentAt).length ?? 0 const commentsBusy = diffCommentActions?.busy === true const canCopyNotes = commentCount > 0 && !commentsBusy const canSendNotes = unsentCommentCount > 0 && !commentsBusy return ( {diffCommentActions ? ( {commentCount === 0 ? 'No review notes' : `${commentCount} review ${commentCount === 1 ? 'note' : 'notes'}`} void diffCommentActions.onCopyAll()} accessibilityLabel="Copy review notes" > Copy Send ) : null} `${index}:${line.kind}:${line.oldLineNumber ?? ''}:${line.newLineNumber ?? ''}` } renderItem={renderDiffLine} initialNumToRender={32} maxToRenderPerBatch={48} windowSize={7} removeClippedSubviews={Platform.OS !== 'web'} keyboardShouldPersistTaps="handled" /> ) } if (doc.kind === 'image') { return ( ) } const renderSourceText = (content: string) => ( ) if (doc.kind === 'html') { return ( renderSourceText(doc.content)} /> ) } return renderSourceText(doc.content) }