Open History to see the last commit
1import { useState } from "react";
2import { Pressable, ScrollView, Text } from "react-native";
3import type { MarkdownStyle } from "react-native-enriched-markdown";
4
5import { Menu } from "../components/ui/Menu";
6import { useMarkdownCodeHighlight } from "../features/threads/markdownCodeHighlightState";
7import { copyTextWithHaptic } from "../lib/copyTextWithHaptic";
8import type { HighlightScheme } from "../lib/highlights";
9import { CodeBlockFrame, CodeBlockHeaderButton } from "./CodeBlockFrame";
10import { fencedCodeMarkdown } from "./codeBlockMarkdown";
11import type { NativeMarkdownTextStyle } from "./SelectableMarkdownText.types";
12
13// Header names from enriched-markdown's CodeBlockLanguages.cpp that are not the fence capitalized.
14const FENCE_DISPLAY_NAMES: Readonly<Record<string, string>> = {
15 cc: "C++",
16 cpp: "C++",
17 cxx: "C++",
18 cs: "C#",
19 csharp: "C#",
20 graphql: "GraphQL",
21 objc: "Objective-C",
22 objectivec: "Objective-C",
23 php: "PHP",
24 rb: "Ruby",
25 scss: "SCSS",
26 sql: "SQL",
27 toml: "TOML",
28 xml: "XML",
29};
30
31// The native block's long-press menu, with its labels and iOS symbols.
32const CODE_MENU_ACTIONS = [
33 { id: "copy-code", title: "Copy", image: "doc.on.doc" },
34 { id: "copy-markdown", title: "Copy as Markdown", image: "doc.text" },
35];
36
37/** A fence in a language enriched-markdown cannot colour, drawn like its native block. */
38export function HighlightedCodeBlock(props: {
39 readonly code: string;
40 readonly fence: string;
41 readonly language: string;
42 /** False only while the library withholds an unclosed fence (`streamingAnimation`, which T3 never enables, so always true today); highlighting waits for it. */
43 readonly complete: boolean;
44 readonly scheme: HighlightScheme;
45 readonly textStyle: NativeMarkdownTextStyle;
46 readonly codeStyle: NonNullable<MarkdownStyle["codeBlock"]>;
47}) {
48 const { code, codeStyle, textStyle } = props;
49 const [wrapped, setWrapped] = useState(false);
50 const lines = useMarkdownCodeHighlight({
51 code,
52 language: props.language,
53 theme: props.scheme,
54 enabled: props.complete,
55 });
56 const fence = props.fence.toLowerCase();
57 let offset = 0;
58 const text = (
59 // Not selectable: as in the native block, a long-press opens the copy menu instead.
60 <Text
61 style={{
62 color: codeStyle.color,
63 fontFamily: codeStyle.fontFamily,
64 fontSize: codeStyle.fontSize,
65 lineHeight: codeStyle.lineHeight,
66 }}
67 >
68 {lines?.flatMap((line, lineIndex) => {
69 const separator = lineIndex > 0 ? "\n" : "";
70 offset += separator.length;
71 return [
72 separator,
73 ...line.map((token) => {
74 const start = offset;
75 offset += token.content.length;
76 return (
77 <Text key={start} style={token.color ? { color: token.color } : undefined}>
78 {token.content}
79 </Text>
80 );
81 }),
82 ];
83 }) ?? code}
84 </Text>
85 );
86 // Inside the scroller: on iOS an ancestor JS responder disables a ScrollView's panning.
87 const content = (
88 <Menu
89 shouldOpenOnLongPress
90 actions={CODE_MENU_ACTIONS}
91 style={{ flexGrow: 1 }}
92 onPressAction={({ nativeEvent }) => {
93 copyTextWithHaptic(
94 nativeEvent.event === "copy-markdown" ? fencedCodeMarkdown(code, props.fence) : code,
95 );
96 }}
97 >
98 <Pressable style={{ padding: codeStyle.padding }}>{text}</Pressable>
99 </Menu>
100 );
101 return (
102 <CodeBlockFrame
103 label={FENCE_DISPLAY_NAMES[fence] ?? fence.charAt(0).toUpperCase() + fence.slice(1)}
104 textStyle={textStyle}
105 actions={
106 <>
107 <CodeBlockHeaderButton
108 icon="text.alignleft"
109 label={wrapped ? "Disable line wrap" : "Wrap lines"}
110 textStyle={textStyle}
111 active={wrapped}
112 onPress={() => setWrapped((value) => !value)}
113 />
114 <CodeBlockHeaderButton
115 icon="doc.on.doc"
116 label="Copy code"
117 textStyle={textStyle}
118 onPress={() => copyTextWithHaptic(code)}
119 />
120 </>
121 }
122 >
123 {wrapped ? (
124 content
125 ) : (
126 <ScrollView
127 horizontal
128 // As in the thread feed: with the keyboard open, the first long-press reaches the copy menu.
129 keyboardShouldPersistTaps="always"
130 contentContainerStyle={{ flexGrow: 1 }}
131 >
132 {content}
133 </ScrollView>
134 )}
135 </CodeBlockFrame>
136 );
137}