import * as Clipboard from "expo-clipboard";import * as Haptics from "expo-haptics";import { KeyboardAvoidingLegendList } from "@legendapp/list/keyboard";import { type LegendListRef } from "@legendapp/list/react-native";import type { ThreadId, TurnId } from "@t3tools/contracts";import { SymbolView } from "expo-symbols";import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";import { Markdown, type CustomRenderers, type NodeStyleOverrides, type PartialMarkdownTheme,} from "react-native-nitro-markdown";import { Image, Linking, type NativeScrollEvent, type NativeSyntheticEvent, Pressable, ScrollView, StyleSheet, Text as NativeText, type ColorValue, useColorScheme, useWindowDimensions, View,} from "react-native";import { TouchableOpacity } from "react-native-gesture-handler";import ImageViewing from "react-native-image-viewing";import { useSafeAreaInsets } from "react-native-safe-area-context";import { useThemeColor } from "../../lib/useThemeColor";import { hasNativeSelectableMarkdownText, SelectableMarkdownText, type NativeMarkdownTextStyle, type SelectableMarkdownSkill,} from "../../native/SelectableMarkdownText"; import { AppText as Text } from "../../components/AppText";import { EmptyState } from "../../components/EmptyState";import { CopyTextButton } from "../../components/CopyTextButton";import { parseReviewCommentMessageSegments, type ReviewInlineComment,} from "../review/reviewCommentSelection";import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface";import { buildNativeReviewDiffData, createNativeReviewDiffTheme, NATIVE_REVIEW_DIFF_CONTENT_WIDTH, NATIVE_REVIEW_DIFF_ROW_HEIGHT, NATIVE_REVIEW_DIFF_STYLE,} from "../review/nativeReviewDiffAdapter";import { buildReviewParsedDiff } from "../review/reviewModel";import { cn } from "../../lib/cn";import type { MobileLayoutVariant } from "../../lib/mobileLayout";import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links";import { deriveThreadFeedPresentation, type ThreadFeedEntry, type ThreadFeedLatestTurn,} from "../../lib/threadActivity";import { isThreadFeedNearEnd } from "../../lib/threadFeedLayout";import { relativeTime } from "../../lib/time";import { messageImageUrl } from "./threadPresentation"; const THREAD_FEED_END_THRESHOLD = 80;const MESSAGE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit",}); function formatMessageTime(input: string): string { const timestamp = Date.parse(input); if (Number.isNaN(timestamp)) { return ""; } return MESSAGE_TIME_FORMATTER.format(timestamp);} export interface ThreadFeedProps { readonly threadId: ThreadId; readonly feed: ReadonlyArray<ThreadFeedEntry>; readonly httpBaseUrl: string | null; readonly bearerToken: string | null; readonly agentLabel: string; readonly latestTurn: ThreadFeedLatestTurn | null; readonly contentTopInset?: number; readonly contentBottomInset?: number; readonly layoutVariant?: MobileLayoutVariant; readonly composerExpanded?: boolean; readonly skills?: ReadonlyArray<SelectableMarkdownSkill>;} function stripShellWrapper(value: string): string { const trimmed = value.trim(); const match = trimmed.match(/^\/bin\/zsh -lc ['"]?([\s\S]*?)['"]?$/); return (match?.[1] ?? trimmed).trim();} function compactActivityDetail(detail: string | null): string | null { if (!detail) { return null; } const cleaned = stripShellWrapper(detail).replace(/\s+/g, " ").trim(); return cleaned.length > 0 ? cleaned : null;} function buildActivityRows( activities: Extract<ThreadFeedEntry, { type: "activity-group" }>["activities"],) { return activities.map((activity) => ({ ...activity, detail: compactActivityDetail(activity.detail), }));} const MAX_VISIBLE_WORK_LOG_ENTRIES = 1; const MARKDOWN_COLORS = { light: { body: "#111111", strong: "#000000", link: "#2563eb", blockquoteBorder: "rgba(0, 0, 0, 0.08)", blockquoteBackground: "rgba(0, 0, 0, 0.02)", codeBackground: "rgba(0, 0, 0, 0.04)", codeText: "#262626", horizontalRule: "rgba(0, 0, 0, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.22)", userCodeText: "#ffffff", userFenceBackground: "rgba(0, 0, 0, 0.16)", userFenceText: "#ffffff", }, dark: { body: "#e5e5e5", strong: "#f5f5f5", link: "#60a5fa", blockquoteBorder: "rgba(255, 255, 255, 0.1)", blockquoteBackground: "rgba(255, 255, 255, 0.03)", codeBackground: "rgba(255, 255, 255, 0.06)", codeText: "#e5e5e5", horizontalRule: "rgba(255, 255, 255, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.18)", userCodeText: "#ffffff", userFenceBackground: "rgba(0, 0, 0, 0.28)", userFenceText: "#ffffff", },} as const; interface MarkdownStyleSets { readonly user: MarkdownStyleSet; readonly assistant: MarkdownStyleSet;} interface MarkdownStyleSet { readonly theme: PartialMarkdownTheme; readonly styles: NodeStyleOverrides; readonly renderers: CustomRenderers; readonly nativeTextStyle: NativeMarkdownTextStyle;} interface ReviewCommentColors { readonly background: ColorValue; readonly border: ColorValue; readonly mutedBackground: ColorValue; readonly text: ColorValue; readonly mutedText: ColorValue; readonly codeBackground: ColorValue;} const failedMarkdownFaviconHosts = new Set<string>();const markdownLinkStyles = StyleSheet.create({ favicon: { width: 14, height: 14, borderRadius: 3, marginHorizontal: 3, transform: [{ translateY: 2 }], }, file: { borderRadius: 5, borderWidth: StyleSheet.hairlineWidth, fontFamily: "DMSans_500Medium", fontSize: 13, lineHeight: 20, paddingHorizontal: 6, paddingVertical: 2, }, fileIcon: { width: 15, height: 15, marginRight: 4, transform: [{ translateY: 2 }], },}); const MarkdownExternalLink = memo(function MarkdownExternalLink(props: { readonly children: ReactNode; readonly color: string; readonly host: string; readonly href: string;}) { const [failed, setFailed] = useState(() => failedMarkdownFaviconHosts.has(props.host)); return ( <NativeText onPress={() => { void Linking.openURL(props.href); }} style={{ color: props.color, fontFamily: "DMSans_400Regular", textDecorationLine: "none", }} > {!failed ? ( <Image source={{ uri: `https://www.google.com/s2/favicons?domain=${encodeURIComponent(props.host)}&sz=32`, }} style={markdownLinkStyles.favicon} onError={() => { failedMarkdownFaviconHosts.add(props.host); setFailed(true); }} /> ) : ( <NativeText style={{ color: props.color }}>{" ◉ "}</NativeText> )} {props.children} </NativeText> );}); function useReviewCommentColors(): ReviewCommentColors { const colorScheme = useColorScheme(); const isDark = colorScheme === "dark"; const background = isDark ? "#151515" : "#ffffff"; const border = isDark ? "#2a2a2a" : "#d7d7d7"; const mutedBackground = isDark ? "#242424" : "#f2f2f2"; const text = isDark ? "#f3f3f3" : "#111111"; const mutedText = isDark ? "#8f8f8f" : "#666666"; const codeBackground = isDark ? "#0f0f0f" : "#ffffff"; return useMemo( () => ({ background, border, mutedBackground, text, mutedText, codeBackground, }), [background, border, codeBackground, mutedBackground, mutedText, text], );} function useMarkdownStyles(): MarkdownStyleSets { const colorScheme = useColorScheme(); const colors = MARKDOWN_COLORS[colorScheme === "dark" ? "dark" : "light"]; const inlineChipBackground = String(useThemeColor("--color-subtle")); const inlineSkillBackground = String(useThemeColor("--color-inline-skill-background")); const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); return useMemo(() => { const markdownBodyColor = colors.body; const markdownStrongColor = colors.strong; const markdownLinkColor = colors.link; const markdownBlockquoteBg = colors.blockquoteBackground; const markdownBlockquoteBorder = colors.blockquoteBorder; const markdownCodeBg = colors.codeBackground; const markdownCodeText = colors.codeText; const markdownHrColor = colors.horizontalRule; const markdownUserBodyColor = colors.userBody; const markdownUserCodeBg = colors.userCodeBackground; const markdownUserCodeText = colors.userCodeText; const markdownUserFenceBg = colors.userFenceBackground; const markdownUserFenceText = colors.userFenceText; const baseTheme: PartialMarkdownTheme = { colors: { text: markdownBodyColor, heading: markdownStrongColor, link: markdownLinkColor, blockquote: markdownBlockquoteBorder, border: markdownHrColor, surfaceLight: markdownBlockquoteBg, accent: markdownLinkColor, tableBorder: markdownHrColor, tableHeader: markdownBlockquoteBg, tableHeaderText: markdownStrongColor, tableRowOdd: "transparent", tableRowEven: "transparent", }, spacing: { xs: 4, s: 4, m: 8, l: 8, xl: 16, }, fontSizes: { s: 13, m: 15, h1: 20, h2: 18, h3: 16, h4: 14, h5: 14, h6: 14, }, fontFamilies: { regular: "DMSans_400Regular", heading: "DMSans_700Bold", mono: "ui-monospace", }, headingWeight: "700", borderRadius: { s: 4, m: 8, l: 12, }, showCodeLanguage: false, }; const baseStyles: NodeStyleOverrides = { document: { flexShrink: 1 }, paragraph: { marginTop: 0, marginBottom: 10 }, list: { marginTop: 4, marginBottom: 8 }, list_item: { marginTop: 0, marginBottom: 4 }, task_list_item: { marginTop: 0, marginBottom: 4 }, text: { lineHeight: 22 }, bold: { fontWeight: "700", color: markdownStrongColor, fontFamily: "DMSans_700Bold", }, italic: { fontStyle: "italic" }, link: { color: markdownLinkColor, textDecorationLine: "underline" as const, }, blockquote: { borderLeftWidth: 2, borderLeftColor: markdownBlockquoteBorder, paddingLeft: 11, paddingVertical: 2, marginLeft: 0, marginVertical: 10, }, heading: { fontFamily: "DMSans_700Bold", color: markdownStrongColor, marginTop: 18, marginBottom: 8, }, horizontal_rule: { backgroundColor: markdownHrColor, height: 1, marginVertical: 12, }, }; const createMarkdownRenderers = ( inlineBackgroundColor: string, inlineTextColor: string, blockBackgroundColor: string, blockTextColor: string, ): CustomRenderers => ({ link: ({ children, href = "" }) => { const presentation = resolveMarkdownLinkPresentation(href); if (presentation.kind === "file") { return ( <NativeText style={[ markdownLinkStyles.file, { backgroundColor: inlineBackgroundColor, borderColor: markdownHrColor, color: inlineTextColor, }, ]} > <Image source={markdownFileIconSource(presentation.icon)} style={markdownLinkStyles.fileIcon} /> {presentation.label} </NativeText> ); } if (presentation.kind === "external") { return ( <MarkdownExternalLink href={presentation.href} host={presentation.host} color={markdownLinkColor} > {children} </MarkdownExternalLink> ); } const linkHref = presentation.href; return ( <NativeText onPress={ linkHref ? () => { void Linking.openURL(linkHref); } : undefined } style={{ color: markdownLinkColor, textDecorationLine: "underline", }} > {children} </NativeText> ); }, list: ({ node, Renderer, ordered = false, start = 1 }) => ( <View style={{ marginTop: 2, marginBottom: 8 }}> {node.children?.map((child, index) => { const childKey = `${child.type}:${child.beg ?? "unknown"}:${child.end ?? "unknown"}`; if (child.type === "task_list_item") { return ( <Renderer key={childKey} node={child} depth={1} inListItem parentIsText={false} /> ); } return ( <View key={childKey} style={{ flexDirection: "row", alignItems: "flex-start", marginBottom: 3, }} > <NativeText style={{ width: ordered ? 22 : 12, marginRight: 5, color: inlineTextColor, fontFamily: "DMSans_400Regular", fontSize: 15, lineHeight: 22, textAlign: ordered ? "right" : "center", }} > {ordered ? `${start + index}.` : "•"} </NativeText> <View style={{ flex: 1, minWidth: 0 }}> <Renderer node={child} depth={1} inListItem parentIsText={false} /> </View> </View> ); })} </View> ), code_inline: ({ content }) => { const value = content ?? ""; const wrapsPoorly = value.length > 24 || value.includes("/") || value.includes("\\") || value.includes(":"); return ( <NativeText style={{ color: inlineTextColor, fontFamily: "ui-monospace", fontSize: 12, lineHeight: 18, ...(wrapsPoorly ? { opacity: 0.82 } : { backgroundColor: inlineBackgroundColor, borderRadius: 4, paddingHorizontal: 3, }), }} > {value} </NativeText> ); }, code_block: ({ content, language }) => ( <View style={{ backgroundColor: blockBackgroundColor, borderRadius: 10, borderWidth: 1, borderColor: markdownHrColor, marginVertical: 12, overflow: "hidden", }} >Lines 1–500 of 1570. Load more
import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { KeyboardAvoidingLegendList } from "@legendapp/list/keyboard"; import { type LegendListRef } from "@legendapp/list/react-native"; import type { ThreadId, TurnId } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { Markdown, type CustomRenderers, type NodeStyleOverrides, type PartialMarkdownTheme, } from "react-native-nitro-markdown"; import { Image, Linking, type NativeScrollEvent, type NativeSyntheticEvent, Pressable, ScrollView, StyleSheet, Text as NativeText, type ColorValue, useColorScheme, useWindowDimensions, View, } from "react-native"; import { TouchableOpacity } from "react-native-gesture-handler"; import ImageViewing from "react-native-image-viewing"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; import { hasNativeSelectableMarkdownText, SelectableMarkdownText, type NativeMarkdownTextStyle, type SelectableMarkdownSkill, } from "../../native/SelectableMarkdownText"; import { AppText as Text } from "../../components/AppText"; import { EmptyState } from "../../components/EmptyState"; import { CopyTextButton } from "../../components/CopyTextButton"; import { parseReviewCommentMessageSegments, type ReviewInlineComment, } from "../review/reviewCommentSelection"; import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface"; import { buildNativeReviewDiffData, createNativeReviewDiffTheme, NATIVE_REVIEW_DIFF_CONTENT_WIDTH, NATIVE_REVIEW_DIFF_ROW_HEIGHT, NATIVE_REVIEW_DIFF_STYLE, } from "../review/nativeReviewDiffAdapter"; import { buildReviewParsedDiff } from "../review/reviewModel"; import { cn } from "../../lib/cn"; import type { MobileLayoutVariant } from "../../lib/mobileLayout"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links"; import { deriveThreadFeedPresentation, type ThreadFeedEntry, type ThreadFeedLatestTurn, } from "../../lib/threadActivity"; import { isThreadFeedNearEnd } from "../../lib/threadFeedLayout"; import { relativeTime } from "../../lib/time"; import { messageImageUrl } from "./threadPresentation"; const THREAD_FEED_END_THRESHOLD = 80; const MESSAGE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", }); function formatMessageTime(input: string): string { const timestamp = Date.parse(input); if (Number.isNaN(timestamp)) { return ""; } return MESSAGE_TIME_FORMATTER.format(timestamp); } export interface ThreadFeedProps { readonly threadId: ThreadId; readonly feed: ReadonlyArray<ThreadFeedEntry>; readonly httpBaseUrl: string | null; readonly bearerToken: string | null; readonly agentLabel: string; readonly latestTurn: ThreadFeedLatestTurn | null; readonly contentTopInset?: number; readonly contentBottomInset?: number; readonly layoutVariant?: MobileLayoutVariant; readonly composerExpanded?: boolean; readonly skills?: ReadonlyArray<SelectableMarkdownSkill>; } function stripShellWrapper(value: string): string { const trimmed = value.trim(); const match = trimmed.match(/^\/bin\/zsh -lc ['"]?([\s\S]*?)['"]?$/); return (match?.[1] ?? trimmed).trim(); } function compactActivityDetail(detail: string | null): string | null { if (!detail) { return null; } const cleaned = stripShellWrapper(detail).replace(/\s+/g, " ").trim(); return cleaned.length > 0 ? cleaned : null; } function buildActivityRows( activities: Extract<ThreadFeedEntry, { type: "activity-group" }>["activities"], ) { return activities.map((activity) => ({ ...activity, detail: compactActivityDetail(activity.detail), })); } const MAX_VISIBLE_WORK_LOG_ENTRIES = 1; const MARKDOWN_COLORS = { light: { body: "#111111", strong: "#000000", link: "#2563eb", blockquoteBorder: "rgba(0, 0, 0, 0.08)", blockquoteBackground: "rgba(0, 0, 0, 0.02)", codeBackground: "rgba(0, 0, 0, 0.04)", codeText: "#262626", horizontalRule: "rgba(0, 0, 0, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.22)", userCodeText: "#ffffff", userFenceBackground: "rgba(0, 0, 0, 0.16)", userFenceText: "#ffffff", }, dark: { body: "#e5e5e5", strong: "#f5f5f5", link: "#60a5fa", blockquoteBorder: "rgba(255, 255, 255, 0.1)", blockquoteBackground: "rgba(255, 255, 255, 0.03)", codeBackground: "rgba(255, 255, 255, 0.06)", codeText: "#e5e5e5", horizontalRule: "rgba(255, 255, 255, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.18)", userCodeText: "#ffffff", userFenceBackground: "rgba(0, 0, 0, 0.28)", userFenceText: "#ffffff", }, } as const; interface MarkdownStyleSets { readonly user: MarkdownStyleSet; readonly assistant: MarkdownStyleSet; } interface MarkdownStyleSet { readonly theme: PartialMarkdownTheme; readonly styles: NodeStyleOverrides; readonly renderers: CustomRenderers; readonly nativeTextStyle: NativeMarkdownTextStyle; } interface ReviewCommentColors { readonly background: ColorValue; readonly border: ColorValue; readonly mutedBackground: ColorValue; readonly text: ColorValue; readonly mutedText: ColorValue; readonly codeBackground: ColorValue; } const failedMarkdownFaviconHosts = new Set<string>(); const markdownLinkStyles = StyleSheet.create({ favicon: { width: 14, height: 14, borderRadius: 3, marginHorizontal: 3, transform: [{ translateY: 2 }], }, file: { borderRadius: 5, borderWidth: StyleSheet.hairlineWidth, fontFamily: "DMSans_500Medium", fontSize: 13, lineHeight: 20, paddingHorizontal: 6, paddingVertical: 2, }, fileIcon: { width: 15, height: 15, marginRight: 4, transform: [{ translateY: 2 }], }, }); const MarkdownExternalLink = memo(function MarkdownExternalLink(props: { readonly children: ReactNode; readonly color: string; readonly host: string; readonly href: string; }) { const [failed, setFailed] = useState(() => failedMarkdownFaviconHosts.has(props.host)); return ( <NativeText onPress={() => { void Linking.openURL(props.href); }} style={{ color: props.color, fontFamily: "DMSans_400Regular", textDecorationLine: "none", }} > {!failed ? ( <Image source={{ uri: `https://www.google.com/s2/favicons?domain=${encodeURIComponent(props.host)}&sz=32`, }} style={markdownLinkStyles.favicon} onError={() => { failedMarkdownFaviconHosts.add(props.host); setFailed(true); }} /> ) : ( <NativeText style={{ color: props.color }}>{" ◉ "}</NativeText> )} {props.children} </NativeText> ); }); function useReviewCommentColors(): ReviewCommentColors { const colorScheme = useColorScheme(); const isDark = colorScheme === "dark"; const background = isDark ? "#151515" : "#ffffff"; const border = isDark ? "#2a2a2a" : "#d7d7d7"; const mutedBackground = isDark ? "#242424" : "#f2f2f2"; const text = isDark ? "#f3f3f3" : "#111111"; const mutedText = isDark ? "#8f8f8f" : "#666666"; const codeBackground = isDark ? "#0f0f0f" : "#ffffff"; return useMemo( () => ({ background, border, mutedBackground, text, mutedText, codeBackground, }), [background, border, codeBackground, mutedBackground, mutedText, text], ); } function useMarkdownStyles(): MarkdownStyleSets { const colorScheme = useColorScheme(); const colors = MARKDOWN_COLORS[colorScheme === "dark" ? "dark" : "light"]; const inlineChipBackground = String(useThemeColor("--color-subtle")); const inlineSkillBackground = String(useThemeColor("--color-inline-skill-background")); const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); return useMemo(() => { const markdownBodyColor = colors.body; const markdownStrongColor = colors.strong; const markdownLinkColor = colors.link; const markdownBlockquoteBg = colors.blockquoteBackground; const markdownBlockquoteBorder = colors.blockquoteBorder; const markdownCodeBg = colors.codeBackground; const markdownCodeText = colors.codeText; const markdownHrColor = colors.horizontalRule; const markdownUserBodyColor = colors.userBody; const markdownUserCodeBg = colors.userCodeBackground; const markdownUserCodeText = colors.userCodeText; const markdownUserFenceBg = colors.userFenceBackground; const markdownUserFenceText = colors.userFenceText; const baseTheme: PartialMarkdownTheme = { colors: { text: markdownBodyColor, heading: markdownStrongColor, link: markdownLinkColor, blockquote: markdownBlockquoteBorder, border: markdownHrColor, surfaceLight: markdownBlockquoteBg, accent: markdownLinkColor, tableBorder: markdownHrColor, tableHeader: markdownBlockquoteBg, tableHeaderText: markdownStrongColor, tableRowOdd: "transparent", tableRowEven: "transparent", }, spacing: { xs: 4, s: 4, m: 8, l: 8, xl: 16, }, fontSizes: { s: 13, m: 15, h1: 20, h2: 18, h3: 16, h4: 14, h5: 14, h6: 14, }, fontFamilies: { regular: "DMSans_400Regular", heading: "DMSans_700Bold", mono: "ui-monospace", }, headingWeight: "700", borderRadius: { s: 4, m: 8, l: 12, }, showCodeLanguage: false, }; const baseStyles: NodeStyleOverrides = { document: { flexShrink: 1 }, paragraph: { marginTop: 0, marginBottom: 10 }, list: { marginTop: 4, marginBottom: 8 }, list_item: { marginTop: 0, marginBottom: 4 }, task_list_item: { marginTop: 0, marginBottom: 4 }, text: { lineHeight: 22 }, bold: { fontWeight: "700", color: markdownStrongColor, fontFamily: "DMSans_700Bold", }, italic: { fontStyle: "italic" }, link: { color: markdownLinkColor, textDecorationLine: "underline" as const, }, blockquote: { borderLeftWidth: 2, borderLeftColor: markdownBlockquoteBorder, paddingLeft: 11, paddingVertical: 2, marginLeft: 0, marginVertical: 10, }, heading: { fontFamily: "DMSans_700Bold", color: markdownStrongColor, marginTop: 18, marginBottom: 8, }, horizontal_rule: { backgroundColor: markdownHrColor, height: 1, marginVertical: 12, }, }; const createMarkdownRenderers = ( inlineBackgroundColor: string, inlineTextColor: string, blockBackgroundColor: string, blockTextColor: string, ): CustomRenderers => ({ link: ({ children, href = "" }) => { const presentation = resolveMarkdownLinkPresentation(href); if (presentation.kind === "file") { return ( <NativeText style={[ markdownLinkStyles.file, { backgroundColor: inlineBackgroundColor, borderColor: markdownHrColor, color: inlineTextColor, }, ]} > <Image source={markdownFileIconSource(presentation.icon)} style={markdownLinkStyles.fileIcon} /> {presentation.label} </NativeText> ); } if (presentation.kind === "external") { return ( <MarkdownExternalLink href={presentation.href} host={presentation.host} color={markdownLinkColor} > {children} </MarkdownExternalLink> ); } const linkHref = presentation.href; return ( <NativeText onPress={ linkHref ? () => { void Linking.openURL(linkHref); } : undefined } style={{ color: markdownLinkColor, textDecorationLine: "underline", }} > {children} </NativeText> ); }, list: ({ node, Renderer, ordered = false, start = 1 }) => ( <View style={{ marginTop: 2, marginBottom: 8 }}> {node.children?.map((child, index) => { const childKey = `${child.type}:${child.beg ?? "unknown"}:${child.end ?? "unknown"}`; if (child.type === "task_list_item") { return ( <Renderer key={childKey} node={child} depth={1} inListItem parentIsText={false} /> ); } return ( <View key={childKey} style={{ flexDirection: "row", alignItems: "flex-start", marginBottom: 3, }} > <NativeText style={{ width: ordered ? 22 : 12, marginRight: 5, color: inlineTextColor, fontFamily: "DMSans_400Regular", fontSize: 15, lineHeight: 22, textAlign: ordered ? "right" : "center", }} > {ordered ? `${start + index}.` : "•"} </NativeText> <View style={{ flex: 1, minWidth: 0 }}> <Renderer node={child} depth={1} inListItem parentIsText={false} /> </View> </View> ); })} </View> ), code_inline: ({ content }) => { const value = content ?? ""; const wrapsPoorly = value.length > 24 || value.includes("/") || value.includes("\\") || value.includes(":"); return ( <NativeText style={{ color: inlineTextColor, fontFamily: "ui-monospace", fontSize: 12, lineHeight: 18, ...(wrapsPoorly ? { opacity: 0.82 } : { backgroundColor: inlineBackgroundColor, borderRadius: 4, paddingHorizontal: 3, }), }} > {value} </NativeText> ); }, code_block: ({ content, language }) => ( <View style={{ backgroundColor: blockBackgroundColor, borderRadius: 10, borderWidth: 1, borderColor: markdownHrColor, marginVertical: 12, overflow: "hidden", }} >