import * as Haptics from "expo-haptics";import { KeyboardAwareLegendList } from "@legendapp/list/keyboard";import { type LegendListRef } from "@legendapp/list/react-native";import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts";import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList";import { formatElapsed } from "@t3tools/shared/orchestrationTiming";import { SymbolView } from "../../components/AppSymbol";import { HeaderHeightContext } from "@react-navigation/elements";import { useNavigation } from "@react-navigation/native";import { memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode, type RefObject,} from "react";import { Markdown, type CustomRenderers, type NodeStyleOverrides, type PartialMarkdownTheme,} from "react-native-nitro-markdown";import { ActivityIndicator, Image, Linking, Platform, type LayoutChangeEvent, 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 Animated, { FadeIn, FadeInUp, LinearTransition, type SharedValue,} from "react-native-reanimated";import { useThemeColor } from "../../lib/useThemeColor";import { useFontFamily } from "../../lib/useFontFamily";import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";import { hasNativeSelectableMarkdownText, SelectableMarkdownText, type NativeMarkdownTextStyle, type SelectableMarkdownSkill,} from "../../native/SelectableMarkdownText"; import { AppText as Text } from "../../components/AppText";import { CopyTextButton } from "../../components/CopyTextButton";import { parseReviewCommentMessageSegments, type ReviewInlineComment,} from "../review/reviewCommentSelection";import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter";import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface";import { buildNativeReviewDiffData, createNativeReviewDiffTheme, NATIVE_REVIEW_DIFF_CONTENT_WIDTH,} from "../review/nativeReviewDiffAdapter";import { buildReviewParsedDiff } from "../review/reviewModel";import { cn } from "../../lib/cn";import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout";import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography,} from "../../lib/appearancePreferences";import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface";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 type { ThreadContentPresentation } from "./threadContentPresentation";import { ThreadWorkGroupToggle, ThreadWorkLog } from "./thread-work-log";import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState";import { useAssetUrl } from "../../state/assets";import { resolveWorkspaceRelativeFilePath } from "../files/filePath"; 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);} // Rows shift when content above them grows (streaming text, work-log folds);// animating the container position turns those jumps into slides.const FEED_ITEM_LAYOUT_TRANSITION = LinearTransition.duration(180); // Entering animations must only play for rows born just now — LegendList// remounts rows when they scroll back into view, and replaying an entrance for// old content would be its own kind of jank.const FRESH_ENTRY_WINDOW_MS = 3_000;function isFreshTimestamp(input: string): boolean { const timestamp = Date.parse(input); return Number.isFinite(timestamp) && Date.now() - timestamp < FRESH_ENTRY_WINDOW_MS;} export interface ThreadFeedProps { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly workspaceRoot?: string | null; readonly feed: ReadonlyArray<ThreadFeedEntry>; readonly contentPresentation: ThreadContentPresentation; readonly agentLabel: string; readonly latestTurn: ThreadFeedLatestTurn | null; readonly activeWorkStartedAt: string | null; readonly listRef: RefObject<LegendListRef | null>; readonly freeze: SharedValue<boolean>; readonly anchorMessageId: MessageId | null; readonly contentInsetEndAdjustment: SharedValue<number>; readonly contentTopInset?: number; readonly contentBottomInset?: number; readonly contentMaxWidth?: number; readonly layoutVariant?: LayoutVariant; readonly usesAutomaticContentInsets?: boolean; readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void; readonly skills?: ReadonlyArray<SelectableMarkdownSkill>;} function MessageAttachmentImage(props: { readonly environmentId: EnvironmentId; readonly attachmentId: string; readonly className: string; readonly onPressImage: (uri: string, headers?: Record<string, string>) => void;}) { const uri = useAssetUrl(props.environmentId, { _tag: "attachment", attachmentId: props.attachmentId, }); if (uri === null) { return ( <View className={`${props.className} items-center justify-center`}> <ActivityIndicator /> </View> ); } return ( <TouchableOpacity activeOpacity={0.7} onPress={() => props.onPressImage(uri)}> <Image source={{ uri }} className={props.className} resizeMode="cover" /> </TouchableOpacity> );} 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", inlineCodeText: "#5f6368", horizontalRule: "rgba(0, 0, 0, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.22)", userCodeText: "#ffffff", userInlineCodeText: "rgba(255, 255, 255, 0.82)", 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", inlineCodeText: "#b8bcc2", horizontalRule: "rgba(255, 255, 255, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.18)", userCodeText: "#ffffff", userInlineCodeText: "rgba(255, 255, 255, 0.82)", userFenceBackground: "rgba(0, 0, 0, 0.28)", userFenceText: "#ffffff", },} as const; const MARKDOWN_MONO_FONT = Platform.select({ ios: "ui-monospace", android: "monospace", default: "monospace",}); 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({ inlineIcon: { width: 14, height: 14, marginHorizontal: 3, transform: [{ translateY: 2 }], }, favicon: { borderRadius: 3, },}); 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 className="font-sans" onPress={() => { void Linking.openURL(props.href); }} style={{ color: props.color, textDecorationLine: "none", }} > {!failed ? ( <Image source={{ uri: `https://www.google.com/s2/favicons?domain=${encodeURIComponent(props.host)}&sz=32`, }} style={[markdownLinkStyles.inlineIcon, markdownLinkStyles.favicon]} onError={() => { failedMarkdownFaviconHosts.add(props.host); setFailed(true); }} /> ) : ( <NativeText style={{ color: props.color }}>{" ◉ "}</NativeText> )} {props.children} </NativeText> );}); function MarkdownCodeBlock(props: { readonly backgroundColor: string; readonly borderColor: string; readonly content: string; readonly copyTintColor: ColorValue; readonly headerTextColor: string; readonly fontSize: number; readonly highlightCode: boolean; readonly language?: string | null; readonly lineHeight: number; readonly textColor: string; readonly theme: ReviewDiffTheme;}) { const content = props.content.replace(/\n$/, ""); const languageLabel = props.language?.trim() || "text"; const highlighted = useMarkdownCodeHighlight({ code: content, enabled: props.highlightCode && Boolean(props.language?.trim()), language: props.language, theme: props.theme, }); let tokenOffset = 0; return ( <View className="my-3 min-w-0 max-w-full self-stretch overflow-hidden rounded-lg border" style={{ backgroundColor: props.backgroundColor, borderColor: props.borderColor }} > <View className="flex-row items-center justify-between gap-2 border-b py-1 pr-1.5 pl-3.5" style={{ borderBottomColor: props.borderColor }} > <NativeText className="flex-1 font-mono uppercase opacity-70" numberOfLines={1} style={{ color: props.headerTextColor, fontSize: props.fontSize, ...(Platform.OS === "android" ? { includeFontPadding: false } : null), }} > {languageLabel} </NativeText> <CopyTextButton accessibilityLabel="Copy code" text={content} tintColor={props.copyTintColor} buttonSize={32} iconSize={16} /> </View> <ScrollView horizontal bounces={false} nestedScrollEnabled={Platform.OS === "android"} showsHorizontalScrollIndicator={false} contentContainerClassName="px-3.5 py-3" > <NativeText selectable className="font-mono" style={{ color: props.textColor, fontSize: props.fontSize, lineHeight: props.lineHeight, ...(Platform.OS === "android" ? { includeFontPadding: false } : null), }} > {highlighted ? highlighted.map((line, lineIndex) => { const lineStartOffset = tokenOffset; const lineText = line.map((token) => token.content).join(""); const renderedLine = ( <NativeText key={`line:${lineStartOffset}:${lineText}`}> {line.map((token) => { const startOffset = tokenOffset; tokenOffset += token.content.length; const fontStyle = token.fontStyle !== null && (token.fontStyle & 1) === 1 ? ("italic" as const) : ("normal" as const); const fontWeight = token.fontStyle !== null && (token.fontStyle & 2) === 2 ? ("700" as const) : ("400" as const); return ( <NativeText key={`${startOffset}:${token.content}:${token.color ?? ""}:${ token.fontStyle ?? "" }`} style={{ color: token.color ?? props.textColor, fontStyle, fontWeight, }} > {token.content} </NativeText> ); })} {lineIndex + 1 < highlighted.length ? "\n" : ""} </NativeText> ); if (lineIndex + 1 < highlighted.length) { tokenOffset += 1; } return renderedLine; }) : content} </NativeText> </ScrollView> </View> );} 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(onLinkPress: (href: string) => void): MarkdownStyleSets { const colorScheme = useColorScheme(); const { appearance } = useAppearancePreferences(); const markdownFontSizes = useMemo( () => resolveMarkdownFontSizes(appearance.baseFontSize), [appearance.baseFontSize], ); const nativeMarkdownTypography = useMemo( () => resolveNativeMarkdownTypography(appearance.baseFontSize), [appearance.baseFontSize], ); const themeMode = colorScheme === "dark" ? "dark" : "light"; const colors = MARKDOWN_COLORS[themeMode]; const iconSubtleColor = String(useThemeColor("--color-icon-subtle")); const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); const userBubbleForegroundMuted = String(useThemeColor("--color-user-bubble-foreground-muted")); const regularFontFamily = useFontFamily("regular"); const boldFontFamily = useFontFamily("bold"); 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 markdownInlineCodeText = colors.inlineCodeText; const markdownHrColor = colors.horizontalRule; const markdownUserBodyColor = colors.userBody; const markdownUserCodeBg = colors.userCodeBackground; const markdownUserCodeText = colors.userCodeText; const markdownUserInlineCodeText = colors.userInlineCodeText; const markdownUserFenceBg = colors.userFenceBackground; const markdownUserFenceText = colors.userFenceText; const baseTheme: PartialMarkdownTheme = { colors: { text: markdownBodyColor, heading: markdownStrongColor, link: markdownLinkColor, blockquote: markdownBlockquoteBorder, border: markdownHrColor, surface: "transparent", 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: markdownFontSizes.s, m: markdownFontSizes.m, h1: markdownFontSizes.h1, h2: markdownFontSizes.h2, h3: markdownFontSizes.h3, h4: markdownFontSizes.h4, h5: markdownFontSizes.h5, h6: markdownFontSizes.h6, }, fontFamilies: { regular: regularFontFamily, heading: boldFontFamily, mono: MARKDOWN_MONO_FONT, }, headingWeight: "700",Lines 1–500 of 1837. Load more
import * as Haptics from "expo-haptics"; import { KeyboardAwareLegendList } from "@legendapp/list/keyboard"; import { type LegendListRef } from "@legendapp/list/react-native"; import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts"; import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; import { formatElapsed } from "@t3tools/shared/orchestrationTiming"; import { SymbolView } from "../../components/AppSymbol"; import { HeaderHeightContext } from "@react-navigation/elements"; import { useNavigation } from "@react-navigation/native"; import { memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode, type RefObject, } from "react"; import { Markdown, type CustomRenderers, type NodeStyleOverrides, type PartialMarkdownTheme, } from "react-native-nitro-markdown"; import { ActivityIndicator, Image, Linking, Platform, type LayoutChangeEvent, 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 Animated, { FadeIn, FadeInUp, LinearTransition, type SharedValue, } from "react-native-reanimated"; import { useThemeColor } from "../../lib/useThemeColor"; import { useFontFamily } from "../../lib/useFontFamily"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { hasNativeSelectableMarkdownText, SelectableMarkdownText, type NativeMarkdownTextStyle, type SelectableMarkdownSkill, } from "../../native/SelectableMarkdownText"; import { AppText as Text } from "../../components/AppText"; import { CopyTextButton } from "../../components/CopyTextButton"; import { parseReviewCommentMessageSegments, type ReviewInlineComment, } from "../review/reviewCommentSelection"; import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter"; import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface"; import { buildNativeReviewDiffData, createNativeReviewDiffTheme, NATIVE_REVIEW_DIFF_CONTENT_WIDTH, } from "../review/nativeReviewDiffAdapter"; import { buildReviewParsedDiff } from "../review/reviewModel"; import { cn } from "../../lib/cn"; import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout"; import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography, } from "../../lib/appearancePreferences"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; 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 type { ThreadContentPresentation } from "./threadContentPresentation"; import { ThreadWorkGroupToggle, ThreadWorkLog } from "./thread-work-log"; import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; import { useAssetUrl } from "../../state/assets"; import { resolveWorkspaceRelativeFilePath } from "../files/filePath"; 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); } // Rows shift when content above them grows (streaming text, work-log folds); // animating the container position turns those jumps into slides. const FEED_ITEM_LAYOUT_TRANSITION = LinearTransition.duration(180); // Entering animations must only play for rows born just now — LegendList // remounts rows when they scroll back into view, and replaying an entrance for // old content would be its own kind of jank. const FRESH_ENTRY_WINDOW_MS = 3_000; function isFreshTimestamp(input: string): boolean { const timestamp = Date.parse(input); return Number.isFinite(timestamp) && Date.now() - timestamp < FRESH_ENTRY_WINDOW_MS; } export interface ThreadFeedProps { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly workspaceRoot?: string | null; readonly feed: ReadonlyArray<ThreadFeedEntry>; readonly contentPresentation: ThreadContentPresentation; readonly agentLabel: string; readonly latestTurn: ThreadFeedLatestTurn | null; readonly activeWorkStartedAt: string | null; readonly listRef: RefObject<LegendListRef | null>; readonly freeze: SharedValue<boolean>; readonly anchorMessageId: MessageId | null; readonly contentInsetEndAdjustment: SharedValue<number>; readonly contentTopInset?: number; readonly contentBottomInset?: number; readonly contentMaxWidth?: number; readonly layoutVariant?: LayoutVariant; readonly usesAutomaticContentInsets?: boolean; readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void; readonly skills?: ReadonlyArray<SelectableMarkdownSkill>; } function MessageAttachmentImage(props: { readonly environmentId: EnvironmentId; readonly attachmentId: string; readonly className: string; readonly onPressImage: (uri: string, headers?: Record<string, string>) => void; }) { const uri = useAssetUrl(props.environmentId, { _tag: "attachment", attachmentId: props.attachmentId, }); if (uri === null) { return ( <View className={`${props.className} items-center justify-center`}> <ActivityIndicator /> </View> ); } return ( <TouchableOpacity activeOpacity={0.7} onPress={() => props.onPressImage(uri)}> <Image source={{ uri }} className={props.className} resizeMode="cover" /> </TouchableOpacity> ); } 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", inlineCodeText: "#5f6368", horizontalRule: "rgba(0, 0, 0, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.22)", userCodeText: "#ffffff", userInlineCodeText: "rgba(255, 255, 255, 0.82)", 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", inlineCodeText: "#b8bcc2", horizontalRule: "rgba(255, 255, 255, 0.08)", userBody: "#ffffff", userCodeBackground: "rgba(255, 255, 255, 0.18)", userCodeText: "#ffffff", userInlineCodeText: "rgba(255, 255, 255, 0.82)", userFenceBackground: "rgba(0, 0, 0, 0.28)", userFenceText: "#ffffff", }, } as const; const MARKDOWN_MONO_FONT = Platform.select({ ios: "ui-monospace", android: "monospace", default: "monospace", }); 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({ inlineIcon: { width: 14, height: 14, marginHorizontal: 3, transform: [{ translateY: 2 }], }, favicon: { borderRadius: 3, }, }); 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 className="font-sans" onPress={() => { void Linking.openURL(props.href); }} style={{ color: props.color, textDecorationLine: "none", }} > {!failed ? ( <Image source={{ uri: `https://www.google.com/s2/favicons?domain=${encodeURIComponent(props.host)}&sz=32`, }} style={[markdownLinkStyles.inlineIcon, markdownLinkStyles.favicon]} onError={() => { failedMarkdownFaviconHosts.add(props.host); setFailed(true); }} /> ) : ( <NativeText style={{ color: props.color }}>{" ◉ "}</NativeText> )} {props.children} </NativeText> ); }); function MarkdownCodeBlock(props: { readonly backgroundColor: string; readonly borderColor: string; readonly content: string; readonly copyTintColor: ColorValue; readonly headerTextColor: string; readonly fontSize: number; readonly highlightCode: boolean; readonly language?: string | null; readonly lineHeight: number; readonly textColor: string; readonly theme: ReviewDiffTheme; }) { const content = props.content.replace(/\n$/, ""); const languageLabel = props.language?.trim() || "text"; const highlighted = useMarkdownCodeHighlight({ code: content, enabled: props.highlightCode && Boolean(props.language?.trim()), language: props.language, theme: props.theme, }); let tokenOffset = 0; return ( <View className="my-3 min-w-0 max-w-full self-stretch overflow-hidden rounded-lg border" style={{ backgroundColor: props.backgroundColor, borderColor: props.borderColor }} > <View className="flex-row items-center justify-between gap-2 border-b py-1 pr-1.5 pl-3.5" style={{ borderBottomColor: props.borderColor }} > <NativeText className="flex-1 font-mono uppercase opacity-70" numberOfLines={1} style={{ color: props.headerTextColor, fontSize: props.fontSize, ...(Platform.OS === "android" ? { includeFontPadding: false } : null), }} > {languageLabel} </NativeText> <CopyTextButton accessibilityLabel="Copy code" text={content} tintColor={props.copyTintColor} buttonSize={32} iconSize={16} /> </View> <ScrollView horizontal bounces={false} nestedScrollEnabled={Platform.OS === "android"} showsHorizontalScrollIndicator={false} contentContainerClassName="px-3.5 py-3" > <NativeText selectable className="font-mono" style={{ color: props.textColor, fontSize: props.fontSize, lineHeight: props.lineHeight, ...(Platform.OS === "android" ? { includeFontPadding: false } : null), }} > {highlighted ? highlighted.map((line, lineIndex) => { const lineStartOffset = tokenOffset; const lineText = line.map((token) => token.content).join(""); const renderedLine = ( <NativeText key={`line:${lineStartOffset}:${lineText}`}> {line.map((token) => { const startOffset = tokenOffset; tokenOffset += token.content.length; const fontStyle = token.fontStyle !== null && (token.fontStyle & 1) === 1 ? ("italic" as const) : ("normal" as const); const fontWeight = token.fontStyle !== null && (token.fontStyle & 2) === 2 ? ("700" as const) : ("400" as const); return ( <NativeText key={`${startOffset}:${token.content}:${token.color ?? ""}:${ token.fontStyle ?? "" }`} style={{ color: token.color ?? props.textColor, fontStyle, fontWeight, }} > {token.content} </NativeText> ); })} {lineIndex + 1 < highlighted.length ? "\n" : ""} </NativeText> ); if (lineIndex + 1 < highlighted.length) { tokenOffset += 1; } return renderedLine; }) : content} </NativeText> </ScrollView> </View> ); } 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(onLinkPress: (href: string) => void): MarkdownStyleSets { const colorScheme = useColorScheme(); const { appearance } = useAppearancePreferences(); const markdownFontSizes = useMemo( () => resolveMarkdownFontSizes(appearance.baseFontSize), [appearance.baseFontSize], ); const nativeMarkdownTypography = useMemo( () => resolveNativeMarkdownTypography(appearance.baseFontSize), [appearance.baseFontSize], ); const themeMode = colorScheme === "dark" ? "dark" : "light"; const colors = MARKDOWN_COLORS[themeMode]; const iconSubtleColor = String(useThemeColor("--color-icon-subtle")); const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); const userBubbleForegroundMuted = String(useThemeColor("--color-user-bubble-foreground-muted")); const regularFontFamily = useFontFamily("regular"); const boldFontFamily = useFontFamily("bold"); 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 markdownInlineCodeText = colors.inlineCodeText; const markdownHrColor = colors.horizontalRule; const markdownUserBodyColor = colors.userBody; const markdownUserCodeBg = colors.userCodeBackground; const markdownUserCodeText = colors.userCodeText; const markdownUserInlineCodeText = colors.userInlineCodeText; const markdownUserFenceBg = colors.userFenceBackground; const markdownUserFenceText = colors.userFenceText; const baseTheme: PartialMarkdownTheme = { colors: { text: markdownBodyColor, heading: markdownStrongColor, link: markdownLinkColor, blockquote: markdownBlockquoteBorder, border: markdownHrColor, surface: "transparent", 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: markdownFontSizes.s, m: markdownFontSizes.m, h1: markdownFontSizes.h1, h2: markdownFontSizes.h2, h3: markdownFontSizes.h3, h4: markdownFontSizes.h4, h5: markdownFontSizes.h5, h6: markdownFontSizes.h6, }, fontFamilies: { regular: regularFontFamily, heading: boldFontFamily, mono: MARKDOWN_MONO_FONT, }, headingWeight: "700",