import { settlePromise } from "@t3tools/client-runtime/state/runtime"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { resolveMobileMarkdownMediaSource } from "../../lib/markdownMediaSource"; import { getBrowseDirectoryPath } from "@t3tools/client-runtime/state/projects"; import { maskMarkdownFrontmatter } from "@t3tools/shared/markdownFrontmatter"; import { useState } from "react"; import { StackActions, useNavigation } from "@react-navigation/native"; import { RefreshControl, ScrollView, View } from "react-native"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; import { FONT_FAMILIES } from "../../lib/fontFamilies"; import { resolveNativeMarkdownTypography } from "../../lib/appearancePreferences"; import { useUniwindTheme } from "../../lib/useUniwindTheme"; import { ThreadMarkdownImage, ThreadMarkdownImageView, ThreadMarkdownImageUnavailable, } from "../threads/ThreadMarkdownImage"; import { ThreadMarkdownVideo } from "../threads/ThreadMarkdownVideo"; import { markdownFileEmbedLabel, resolveMarkdownMediaPreview } from "../../lib/markdownMedia"; import { normalizeNativeMarkdownUrl, resolveMarkdownLinkPresentation, } from "../../lib/markdownLinks"; import { AppText as Text } from "../../components/ui/AppText"; import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; import { useMarkdownImageSource } from "../../native/useMarkdownImageSource"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { SelectableMarkdownText, type MarkdownImageRenderer, type NativeMarkdownTextStyle, } from "../../native/SelectableMarkdownText"; import { fileRoutePathSegments, resolveWorkspaceFilePath } from "./filePath"; interface MarkdownPreviewStyles { readonly nativeTextStyle: NativeMarkdownTextStyle; } export function useMarkdownPreviewStyles(): MarkdownPreviewStyles { const { appearance } = useAppearancePreferences(); const nativeMarkdownTypography = resolveNativeMarkdownTypography(appearance.baseFontSize); const theme = useUniwindTheme(); const body = theme["--color-md-body"]; const strong = theme["--color-md-strong"]; const link = theme["--color-md-link"]; const blockquoteBorder = theme["--color-md-blockquote-border"]; const codeBackground = theme["--color-md-code-bg"]; const codeText = theme["--color-md-code-text"]; const horizontalRule = theme["--color-md-hr"]; const regularFontFamily = FONT_FAMILIES.regular; const boldFontFamily = FONT_FAMILIES.bold; return { nativeTextStyle: { color: body, strongColor: strong, mutedColor: body, linkColor: link, inlineCodeColor: codeText, codeColor: codeText, codeBackgroundColor: codeBackground, codeBlockBackgroundColor: codeBackground, fileTextColor: codeText, quoteMarkerColor: blockquoteBorder, dividerColor: horizontalRule, fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, fontFamily: regularFontFamily, headingFontFamily: boldFontFamily, boldFontFamily, }, }; } export function FileMarkdownPreview(props: { readonly cwd: string; readonly captured?: boolean; readonly environmentId: EnvironmentId; readonly markdown: string; readonly relativePath: string; readonly threadId: ThreadId | null; readonly onRefresh?: () => Promise | void; }) { const navigation = useNavigation(); const [isPullRefreshing, setIsPullRefreshing] = useState(false); const [expandedFile, setExpandedFile] = useState(null); const [expandedVideo, setExpandedVideo] = useState(null); const handlePullToRefresh = async () => { if (!props.onRefresh) { return; } setIsPullRefreshing(true); // RefreshControl only needs the spinner cleared; the caller surfaces its own errors. await settlePromise(async () => props.onRefresh?.()); setIsPullRefreshing(false); }; const markdownDirectory = getBrowseDirectoryPath( resolveWorkspaceFilePath(props.cwd, props.relativePath), ); const renderImage: MarkdownImageRenderer = (image) => { const media = resolveMobileMarkdownMediaSource(image.href, { threadId: props.threadId ?? undefined, workspaceRoot: markdownDirectory, imageEmbed: true, }); const preview = resolveMarkdownMediaPreview(image.href, { environmentId: props.environmentId, threadId: props.threadId ?? undefined, workspaceRoot: markdownDirectory, captured: props.captured, imageEmbed: true, }); const fileLink = resolveMarkdownLinkPresentation(image.href); if ( (preview && preview.kind !== "image" && preview.kind !== "video") || (!props.captured && fileLink.kind === "file" && fileLink.line !== undefined) ) { const name = preview?.source.name ?? (fileLink.kind === "file" ? fileLink.label : "Open file"); return ( openMedia(image.href, true)} style={{ color: styles.nativeTextStyle.linkColor }} > {name} ); } if (media?.access === "direct") { if (media.kind === "video") { return ( ); } return ( ); } if (props.captured || media === null || media.access === "unavailable") { return ; } if (media.kind === "video") { return preview?.kind === "video" ? ( ) : ( ); } return ( ); }; const styles = useMarkdownPreviewStyles(); const resolveImageSource = useMarkdownImageSource({ environmentId: props.environmentId, threadId: props.threadId, workspaceRoot: markdownDirectory, captured: props.captured, }); const openMedia = (href: string, imageEmbed = false) => { const media = resolveMarkdownMediaPreview(href, { environmentId: props.environmentId, threadId: props.threadId ?? undefined, workspaceRoot: markdownDirectory, captured: props.captured, imageEmbed, }); if (!media || media.kind === "file") { const fileLink = resolveMarkdownLinkPresentation(href); if (props.captured || fileLink.kind !== "file") return false; const params = { environmentId: String(props.environmentId), path: fileRoutePathSegments(resolveWorkspaceFilePath(markdownDirectory, fileLink.path)), ...(fileLink.line ? { line: String(fileLink.line) } : {}), }; if (props.threadId) { navigation.navigate("ThreadFile", { ...params, threadId: String(props.threadId) }); } else { navigation.dispatch(StackActions.push("NewTaskFile", { ...params, cwd: props.cwd })); } return true; } if (media.kind === "video") setExpandedVideo(media.source); else setExpandedFile(media.source); return true; }; const onImagePress = (href: string) => { openMedia(href, true); }; const onLinkPress = (href: string) => { if (!openMedia(href)) void tryOpenExternalUrl(href, "markdown-link"); }; return ( void handlePullToRefresh()} /> ) : undefined } > markdownFileEmbedLabel( href, { environmentId: props.environmentId, threadId: props.threadId ?? undefined, workspaceRoot: markdownDirectory, captured: props.captured, }, false, ) } onLinkPress={onLinkPress} renderImage={renderImage} resolveImageSource={resolveImageSource} onImagePress={onImagePress} textStyle={styles.nativeTextStyle} /> setExpandedFile(null)} /> setExpandedVideo(null)} /> ); }