import { useCallback, useMemo, useState } from "react"; import { Markdown, type CustomRenderers, type NodeStyleOverrides, type PartialMarkdownTheme, } from "react-native-nitro-markdown"; import { RefreshControl, ScrollView, Text as NativeText, View } from "react-native"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography, } from "../../lib/appearancePreferences"; import { useThemeColor } from "../../lib/useThemeColor"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { hasNativeSelectableMarkdownText, SelectableMarkdownText, type NativeMarkdownTextStyle, } from "../../native/SelectableMarkdownText"; interface MarkdownPreviewStyles { readonly theme: PartialMarkdownTheme; readonly styles: NodeStyleOverrides; readonly renderers: CustomRenderers; readonly nativeTextStyle: NativeMarkdownTextStyle; } function useMarkdownPreviewStyles(): MarkdownPreviewStyles { const { appearance } = useAppearancePreferences(); const markdownFontSizes = useMemo( () => resolveMarkdownFontSizes(appearance.baseFontSize), [appearance.baseFontSize], ); const nativeMarkdownTypography = useMemo( () => resolveNativeMarkdownTypography(appearance.baseFontSize), [appearance.baseFontSize], ); const body = String(useThemeColor("--color-md-body")); const strong = String(useThemeColor("--color-md-strong")); const link = String(useThemeColor("--color-md-link")); const blockquoteBorder = String(useThemeColor("--color-md-blockquote-border")); const blockquoteBackground = String(useThemeColor("--color-md-blockquote-bg")); const codeBackground = String(useThemeColor("--color-md-code-bg")); const codeText = String(useThemeColor("--color-md-code-text")); const horizontalRule = String(useThemeColor("--color-md-hr")); return useMemo(() => { const renderers: CustomRenderers = { link: ({ href, children }) => ( { if (href) { void tryOpenExternalUrl(href, "markdown-link"); } }} style={{ color: link, fontFamily: "DMSans_500Medium", textDecorationLine: "none", }} > {children} ), }; return { theme: { colors: { text: body, heading: strong, link, blockquote: blockquoteBorder, border: horizontalRule, surfaceLight: blockquoteBackground, accent: link, tableBorder: horizontalRule, tableHeader: blockquoteBackground, tableHeaderText: strong, code: codeText, codeBackground, }, }, styles: { text: { color: body, fontFamily: "DMSans_400Regular", fontSize: markdownFontSizes.m, lineHeight: markdownFontSizes.bodyLineHeight, }, heading: { color: strong, fontFamily: "DMSans_700Bold", }, strong: { color: strong, fontFamily: "DMSans_700Bold", }, link: { color: link, fontFamily: "DMSans_500Medium", }, blockquote: { backgroundColor: blockquoteBackground, borderLeftColor: blockquoteBorder, borderLeftWidth: 3, paddingLeft: 12, }, code: { backgroundColor: codeBackground, color: codeText, fontFamily: "ui-monospace", }, codeBlock: { backgroundColor: codeBackground, borderRadius: 12, color: codeText, fontFamily: "ui-monospace", padding: 12, }, hr: { backgroundColor: horizontalRule, }, }, renderers, nativeTextStyle: { color: body, strongColor: strong, mutedColor: body, linkColor: link, inlineCodeColor: codeText, codeColor: codeText, codeBackgroundColor: codeBackground, codeBlockBackgroundColor: codeBackground, fileTextColor: codeText, skillTextColor: codeText, quoteMarkerColor: blockquoteBorder, dividerColor: horizontalRule, fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, fontFamily: "DMSans_400Regular", headingFontFamily: "DMSans_700Bold", boldFontFamily: "DMSans_700Bold", }, }; }, [ blockquoteBackground, blockquoteBorder, body, codeBackground, codeText, horizontalRule, link, markdownFontSizes, nativeMarkdownTypography, strong, ]); } export function FileMarkdownPreview(props: { readonly markdown: string; readonly onRefresh?: () => Promise | void; }) { const [isPullRefreshing, setIsPullRefreshing] = useState(false); const handlePullToRefresh = useCallback(async () => { if (!props.onRefresh) { return; } setIsPullRefreshing(true); try { await props.onRefresh(); } finally { setIsPullRefreshing(false); } }, [props.onRefresh]); const styles = useMarkdownPreviewStyles(); const onLinkPress = useCallback((href: string) => { void tryOpenExternalUrl(href, "markdown-link"); }, []); return ( void handlePullToRefresh()} /> ) : undefined } > {hasNativeSelectableMarkdownText() ? ( ) : ( {props.markdown} )} ); }