280 lines · 8.3 KB · TypeScript
RawDownload
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";import { resolveMediaSource } from "@t3tools/client-runtime/media-source";import { getBrowseDirectoryPath } from "@t3tools/client-runtime/state/projects";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 { useFontFamily } from "../../lib/useFontFamily";import {  resolveMarkdownFontSizes,  resolveNativeMarkdownTypography,} from "../../lib/appearancePreferences";import { useUniwindTheme } from "../../lib/useUniwindTheme";import {  ThreadMarkdownImage,  ThreadMarkdownImageUnavailable,} from "../threads/ThreadMarkdownImage";import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";import {  hasNativeSelectableMarkdownText,  SelectableMarkdownText,  type MarkdownImageRenderer,  type NativeMarkdownTextStyle,} from "../../native/SelectableMarkdownText";import { resolveWorkspaceFilePath } from "./filePath"; interface MarkdownPreviewStyles {  readonly theme: PartialMarkdownTheme;  readonly styles: NodeStyleOverrides;  readonly renderers: CustomRenderers;  readonly nativeTextStyle: NativeMarkdownTextStyle;} function useMarkdownPreviewStyles(renderImage?: MarkdownImageRenderer): MarkdownPreviewStyles {  const { appearance } = useAppearancePreferences();  const markdownFontSizes = useMemo(    () => resolveMarkdownFontSizes(appearance.baseFontSize),    [appearance.baseFontSize],  );  const nativeMarkdownTypography = useMemo(    () => resolveNativeMarkdownTypography(appearance.baseFontSize),    [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 blockquoteBackground = theme["--color-md-blockquote-bg"];  const codeBackground = theme["--color-md-code-bg"];  const codeText = theme["--color-md-code-text"];  const horizontalRule = theme["--color-md-hr"];  const regularFontFamily = useFontFamily("regular");  const mediumFontFamily = useFontFamily("medium");  const boldFontFamily = useFontFamily("bold");   return useMemo(() => {    const renderers: CustomRenderers = {      link: ({ href, children }) => (        <NativeText          className="font-t3-medium"          onPress={() => {            if (href) {              void tryOpenExternalUrl(href, "markdown-link");            }          }}          style={{            color: link,            textDecorationLine: "none",          }}        >          {children}        </NativeText>      ),      image: ({ node }) =>        node.href && renderImage          ? (renderImage({              href: node.href,              alt: node.alt ?? null,              title: node.title ?? null,            }) ?? undefined)          : undefined,    };     return {      theme: {        colors: {          text: body,          heading: strong,          link,          blockquote: blockquoteBorder,          border: horizontalRule,          surface: "transparent",          surfaceLight: blockquoteBackground,          accent: link,          tableBorder: horizontalRule,          tableHeader: blockquoteBackground,          tableHeaderText: strong,          tableRowOdd: blockquoteBackground,          tableRowEven: "transparent",          code: codeText,          codeBackground,        },      },      styles: {        text: {          color: body,          fontFamily: regularFontFamily,          fontSize: markdownFontSizes.m,          lineHeight: markdownFontSizes.bodyLineHeight,        },        heading: {          color: strong,          fontFamily: boldFontFamily,        },        strong: {          color: strong,          fontFamily: boldFontFamily,        },        link: {          color: link,          fontFamily: mediumFontFamily,        },        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: regularFontFamily,        headingFontFamily: boldFontFamily,        boldFontFamily,      },    };  }, [    blockquoteBackground,    blockquoteBorder,    body,    codeBackground,    codeText,    horizontalRule,    link,    markdownFontSizes,    mediumFontFamily,    nativeMarkdownTypography,    regularFontFamily,    renderImage,    strong,    boldFontFamily,  ]);} export function FileMarkdownPreview(props: {  readonly cwd: string;  readonly environmentId: EnvironmentId;  readonly markdown: string;  readonly relativePath: string;  readonly threadId: ThreadId;  readonly onRefresh?: () => Promise<void> | 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 markdownDirectory = useMemo(    () => getBrowseDirectoryPath(resolveWorkspaceFilePath(props.cwd, props.relativePath)),    [props.cwd, props.relativePath],  );  const renderImage = useCallback<MarkdownImageRenderer>(    (image) => {      const media = resolveMediaSource(image.href, {        threadId: props.threadId,        workspaceRoot: markdownDirectory,        imageEmbed: true,      });      if (media?.access === "direct") {        return null;      }      if (media === null || media.kind !== "image" || media.access === "unavailable") {        return <ThreadMarkdownImageUnavailable alt={image.alt} />;      }      return (        <ThreadMarkdownImage          environmentId={props.environmentId}          resource={media.resource}          alt={image.alt}          srcFragment={media.srcFragment}          onPressPreview={() => undefined}        />      );    },    [markdownDirectory, props.environmentId, props.threadId],  );  const styles = useMarkdownPreviewStyles(renderImage);  const onLinkPress = useCallback((href: string) => {    void tryOpenExternalUrl(href, "markdown-link");  }, []);   return (    <ScrollView      className="flex-1 bg-sheet"      contentContainerStyle={{ padding: 18 }}      refreshControl={        props.onRefresh ? (          <RefreshControl            refreshing={isPullRefreshing}            onRefresh={() => void handlePullToRefresh()}          />        ) : undefined      }    >      <View className="mx-auto w-full max-w-[760px]">        {hasNativeSelectableMarkdownText() ? (          <SelectableMarkdownText            markdown={props.markdown}            onLinkPress={onLinkPress}            renderImage={renderImage}            textStyle={styles.nativeTextStyle}          />        ) : (          <Markdown            options={{ gfm: true }}            renderers={styles.renderers}            styles={styles.styles}            theme={styles.theme}          >            {props.markdown}          </Markdown>        )}      </View>    </ScrollView>  );}