import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Alert, Pressable, Text, View } from 'react-native' import { SafeAreaView, useSafeAreaFrame } from 'react-native-safe-area-context' import { ChevronLeft, Save } from 'lucide-react-native' import { useWorkspaceRouteHandoff } from '../navigation/workspace-route-handoff' import { getWorktreeLabel } from '../session/worktree-label' import { colors, spacing } from '../theme/mobile-theme' import { useForceReconnect } from '../transport/host-client-hooks' import { useWorkspaceClient } from '../transport/use-workspace-client' import { connectionRetryAction } from '../transport/connection-retry-action' import { loadMobileFilePreview, previewError, saveMobileTerminalArtifactPreview, type MobileFilePreviewSource, type MobileFilePreviewResult } from './mobile-file-preview-request' import { ConfirmModal } from '../components/ConfirmModal' import { MobileFilePreviewBody } from './MobileFilePreviewBody' import { displayNameFromPreviewPath, type MobileFilePreviewRouteState } from './mobile-file-preview-route' import { previewSourceFromRoute, sourceKeyForPreview } from './mobile-file-preview-source' import { normalizeMobileFilePreviewLineColumn } from './mobile-file-preview-line-column' import { hasUnsavedMobileTerminalArtifactDraft, isEditableMobileTerminalArtifactPreview, shouldKeepDirtyDraftOnPreviewLoadResult } from './mobile-file-preview-editability' import { filePreviewStyles as styles } from './mobile-file-preview-styles' import { mobileMarkdownFileLink } from './mobile-markdown-file-link' import { useMobileFilePreviewBack } from './use-mobile-file-preview-back' type Props = { route: MobileFilePreviewRouteState } export function MobileFilePreviewScreen({ route }: Props) { // Why: every workspace screen uses the handoff so it shares the route chunk; else the bundle overruns its asset budget. const router = useWorkspaceRouteHandoff() const previewParams = route.ok ? route.params : null const { client, state: connState } = useWorkspaceClient(previewParams?.hostId) const forceReconnect = useForceReconnect() const [preview, setPreview] = useState(() => route.ok ? { status: 'loading', message: 'Loading preview...' } : previewError(route.message) ) const [draftContent, setDraftContent] = useState('') const [savedContent, setSavedContent] = useState('') const [saveError, setSaveError] = useState('') const [saving, setSaving] = useState(false) const draftContentRef = useRef(draftContent) const savedContentRef = useRef(savedContent) const draftSourceKeyRef = useRef(null) const { width, height } = useSafeAreaFrame() const routePreviewSource = useMemo( () => (previewParams ? previewSourceFromRoute(previewParams) : null), [previewParams] ) const [previewSource, setPreviewSource] = useState( routePreviewSource ) const previewSourceKey = useMemo(() => sourceKeyForPreview(previewSource), [previewSource]) const routePreviewSourceKey = useMemo( () => sourceKeyForPreview(routePreviewSource), [routePreviewSource] ) const previewSourceKeyRef = useRef(previewSourceKey) const lineColumn = useMemo( () => previewParams ? normalizeMobileFilePreviewLineColumn(previewParams.line, previewParams.column) : null, [previewParams] ) useEffect(() => { setPreviewSource(routePreviewSource) draftSourceKeyRef.current = null }, [routePreviewSource]) useEffect(() => { previewSourceKeyRef.current = previewSourceKey }, [previewSourceKey]) useEffect(() => { draftContentRef.current = draftContent }, [draftContent]) useEffect(() => { savedContentRef.current = savedContent }, [savedContent]) const loadPreview = useCallback(async () => { const loadSourceKey = previewSourceKey if (!previewParams || !previewSource || loadSourceKey !== routePreviewSourceKey) { setPreview(previewError(route.ok ? 'Unable to load preview' : route.message)) return } const preserveDirtyDraft = draftSourceKeyRef.current === previewSourceKey && draftContentRef.current !== savedContentRef.current if (!client || connState !== 'connected') { if (preserveDirtyDraft) { setSaveError('Waiting for desktop...') return } setPreview({ status: 'waiting', message: 'Waiting for desktop...', reconnect: true }) return } if (!preserveDirtyDraft) { setPreview({ status: 'loading', message: 'Loading preview...' }) } setSaveError('') try { const result = await loadMobileFilePreview(client, previewSource, undefined, { onTerminalArtifactSourceRefreshed: setPreviewSource, refreshGrant: true }) if (previewSourceKeyRef.current !== loadSourceKey) { return } if (shouldKeepDirtyDraftOnPreviewLoadResult(preserveDirtyDraft, result)) { setSaveError(result.message) return } const loadedContent = result.status === 'ready' && result.kind !== 'image' && result.kind !== 'media' ? result.content : result.status === 'empty' ? '' : null if (loadedContent !== null) { if (!preserveDirtyDraft) { setDraftContent(loadedContent) setSavedContent(loadedContent) } draftSourceKeyRef.current = previewSourceKey } setPreview(result) } catch (err) { const message = err instanceof Error ? err.message : 'Unable to load preview' if (preserveDirtyDraft) { setSaveError(message) return } setPreview(previewError(message)) } }, [ client, connState, previewParams, previewSource, previewSourceKey, route, routePreviewSourceKey ]) useEffect(() => { void loadPreview() }, [loadPreview]) const retry = useMemo( () => connectionRetryAction({ hostId: previewParams?.hostId, needsReconnect: preview.status === 'waiting' || (preview.status === 'error' && preview.reconnect) || connState !== 'connected', forceReconnect, reload: () => void loadPreview() }), [connState, forceReconnect, loadPreview, preview, previewParams] ) const displayPath = previewParams?.source === 'terminalArtifact' ? (previewParams.absolutePath ?? '') : (previewParams?.relativePath ?? '') const title = previewParams?.name ?? displayNameFromPreviewPath(displayPath) const worktreeLabel = getWorktreeLabel( previewParams?.worktreeName, previewParams?.worktreeId ?? '' ) const meta = previewParams ? `${worktreeLabel} - ${displayPath}` : 'Preview' const isEditableTerminalArtifact = previewSource?.source === 'terminalArtifact' && isEditableMobileTerminalArtifactPreview(preview, previewSource.readOnly === true) const canSaveArtifact = isEditableTerminalArtifact && draftSourceKeyRef.current === previewSourceKey && draftContent !== savedContent const hasUnsavedTerminalArtifactDraft = hasUnsavedMobileTerminalArtifactDraft({ source: previewSource?.source, draftSourceKey: draftSourceKeyRef.current, previewSourceKey, draftContent, savedContent }) const saveArtifact = useCallback(async () => { if (!client || previewSource?.source !== 'terminalArtifact' || !canSaveArtifact || saving) { return } setSaving(true) setSaveError('') try { const result = await saveMobileTerminalArtifactPreview(client, previewSource, draftContent, { baseContent: savedContent, onTerminalArtifactSourceRefreshed: setPreviewSource }) if (result.status === 'saved') { setSavedContent(draftContent) } else { setSaveError(saveErrorMessageFromPreviewResult(result)) } } catch (err) { const message = err instanceof Error ? err.message : 'Unable to save file' setSaveError(message) } finally { setSaving(false) } }, [canSaveArtifact, client, draftContent, previewSource, savedContent, saving]) const leave = useCallback(() => router.back(), [router]) const { confirmingDiscard, requestBack, stay, discard } = useMobileFilePreviewBack({ hasUnsavedDraft: hasUnsavedTerminalArtifactDraft, leave }) return ( [styles.backButton, pressed && styles.backButtonPressed]} onPress={requestBack} hitSlop={8} accessibilityRole="button" accessibilityLabel="Back to files" > {title || 'Preview'} {meta} {isEditableTerminalArtifact ? ( void saveArtifact()} disabled={!canSaveArtifact || saving} accessibilityLabel="Save terminal artifact" > ) : null} setPreview({ status: 'error', message: 'Unable to load preview', reconnect: false }) } onRetry={retry} onOpenFile={(pathText) => { const href = previewParams && mobileMarkdownFileLink(previewParams, pathText) if (href) { router.push(href) } else { Alert.alert('Unable to open file', 'Choose a file inside this workspace.') } }} /> ) } function saveErrorMessageFromPreviewResult(result: MobileFilePreviewResult): string { return result.status === 'error' || result.status === 'waiting' ? result.message : 'Unable to save file' }