Skip to content

Commit 3d3e847

Browse files
author
Julius Marminge
committed
Switch mobile thread feed to Nitro markdown
- Replace react-native-markdown-display with react-native-nitro-markdown - Update thread markdown styling/rendering and lockfile deps
1 parent a77ea8f commit 3d3e847

3 files changed

Lines changed: 217 additions & 147 deletions

File tree

apps/mobile/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"react-native-gesture-handler": "~2.30.0",
4646
"react-native-image-viewing": "^0.2.2",
4747
"react-native-keyboard-controller": "1.20.7",
48-
"react-native-markdown-display": "^7.0.2",
48+
"react-native-nitro-markdown": "^0.5.3",
49+
"react-native-nitro-modules": "^0.35.4",
4950
"react-native-reanimated": "4.2.1",
5051
"react-native-safe-area-context": "~5.6.2",
5152
"react-native-screens": "~4.23.0",

apps/mobile/src/features/threads/ThreadFeed.tsx

Lines changed: 206 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@ import { KeyboardAvoidingLegendList } from "@legendapp/list/keyboard";
44
import { type LegendListRef } from "@legendapp/list/react-native";
55
import { SymbolView } from "expo-symbols";
66
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
7-
import Markdown from "react-native-markdown-display";
8-
import { Image, Pressable, ScrollView, View } from "react-native";
7+
import {
8+
Markdown,
9+
type CustomRenderers,
10+
type NodeStyleOverrides,
11+
type PartialMarkdownTheme,
12+
} from "react-native-nitro-markdown";
13+
import {
14+
Image,
15+
Pressable,
16+
ScrollView,
17+
Text as NativeText,
18+
type ColorValue,
19+
View,
20+
} from "react-native";
921
import { TouchableOpacity } from "react-native-gesture-handler";
1022
import ImageViewing from "react-native-image-viewing";
1123
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -73,9 +85,19 @@ function buildActivityRows(
7385

7486
const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;
7587

88+
function toMarkdownThemeColor(value: ColorValue): string {
89+
return value as string;
90+
}
91+
7692
interface MarkdownStyleSets {
77-
readonly user: Record<string, any>;
78-
readonly assistant: Record<string, any>;
93+
readonly user: MarkdownStyleSet;
94+
readonly assistant: MarkdownStyleSet;
95+
}
96+
97+
interface MarkdownStyleSet {
98+
readonly theme: PartialMarkdownTheme;
99+
readonly styles: NodeStyleOverrides;
100+
readonly renderers: CustomRenderers;
79101
}
80102

81103
function useMarkdownStyles(): MarkdownStyleSets {
@@ -93,137 +115,193 @@ function useMarkdownStyles(): MarkdownStyleSets {
93115
const userFenceText = useThemeColor("--color-md-user-fence-text");
94116

95117
return useMemo(() => {
96-
const base = {
97-
body: {
98-
color: bodyColor,
99-
fontSize: 15,
100-
lineHeight: 22,
101-
fontFamily: "DMSans_400Regular",
118+
const markdownBodyColor = toMarkdownThemeColor(bodyColor);
119+
const markdownStrongColor = toMarkdownThemeColor(strongColor);
120+
const markdownLinkColor = toMarkdownThemeColor(linkColor);
121+
const markdownBlockquoteBg = toMarkdownThemeColor(blockquoteBg);
122+
const markdownBlockquoteBorder = toMarkdownThemeColor(blockquoteBorder);
123+
const markdownCodeBg = toMarkdownThemeColor(codeBg);
124+
const markdownCodeText = toMarkdownThemeColor(codeText);
125+
const markdownHrColor = toMarkdownThemeColor(hrColor);
126+
const markdownUserCodeBg = toMarkdownThemeColor(userCodeBg);
127+
const markdownUserCodeText = toMarkdownThemeColor(userCodeText);
128+
const markdownUserFenceBg = toMarkdownThemeColor(userFenceBg);
129+
const markdownUserFenceText = toMarkdownThemeColor(userFenceText);
130+
131+
const baseTheme: PartialMarkdownTheme = {
132+
colors: {
133+
text: markdownBodyColor,
134+
heading: markdownStrongColor,
135+
link: markdownLinkColor,
136+
blockquote: markdownBlockquoteBorder,
137+
border: markdownHrColor,
138+
surfaceLight: markdownBlockquoteBg,
139+
accent: markdownLinkColor,
140+
tableBorder: markdownHrColor,
141+
tableHeader: markdownBlockquoteBg,
142+
tableHeaderText: markdownStrongColor,
143+
tableRowOdd: "transparent",
144+
tableRowEven: "transparent",
145+
},
146+
spacing: {
147+
xs: 4,
148+
s: 4,
149+
m: 8,
150+
l: 8,
151+
xl: 16,
102152
},
153+
fontSizes: {
154+
s: 13,
155+
m: 15,
156+
h1: 22,
157+
h2: 19,
158+
h3: 17,
159+
h4: 15,
160+
h5: 15,
161+
h6: 15,
162+
},
163+
fontFamilies: {
164+
regular: "DMSans_400Regular",
165+
heading: "DMSans_700Bold",
166+
mono: "ui-monospace",
167+
},
168+
headingWeight: "700",
169+
borderRadius: {
170+
s: 4,
171+
m: 8,
172+
l: 12,
173+
},
174+
showCodeLanguage: false,
175+
};
176+
177+
const baseStyles: NodeStyleOverrides = {
178+
document: { flexShrink: 1 },
103179
paragraph: { marginTop: 0, marginBottom: 8 },
104-
bullet_list: { marginTop: 4, marginBottom: 4 },
105-
ordered_list: { marginTop: 4, marginBottom: 4 },
106-
list_item: { marginTop: 0, marginBottom: 4, flexDirection: "row" as const },
107-
strong: { fontWeight: "700" as const, color: strongColor, fontFamily: "DMSans_700Bold" },
108-
em: { fontStyle: "italic" as const },
109-
link: { color: linkColor, textDecorationLine: "underline" as const },
180+
list: { marginTop: 4, marginBottom: 4 },
181+
list_item: { marginTop: 0, marginBottom: 4 },
182+
task_list_item: { marginTop: 0, marginBottom: 4 },
183+
bold: { fontWeight: "700", color: markdownStrongColor, fontFamily: "DMSans_700Bold" },
184+
italic: { fontStyle: "italic" },
185+
link: { color: markdownLinkColor, textDecorationLine: "underline" as const },
110186
blockquote: {
111187
borderLeftWidth: 3,
112-
borderLeftColor: blockquoteBorder,
113-
backgroundColor: blockquoteBg,
188+
borderLeftColor: markdownBlockquoteBorder,
189+
backgroundColor: markdownBlockquoteBg,
114190
paddingLeft: 12,
115191
paddingVertical: 6,
116192
marginLeft: 0,
117193
marginVertical: 4,
118194
borderRadius: 4,
119195
},
120-
heading1: {
121-
fontSize: 22,
122-
lineHeight: 28,
123-
fontWeight: "800" as const,
124-
fontFamily: "DMSans_700Bold",
125-
color: strongColor,
126-
marginTop: 16,
127-
marginBottom: 8,
128-
},
129-
heading2: {
130-
fontSize: 19,
131-
lineHeight: 26,
132-
fontWeight: "700" as const,
196+
heading: {
133197
fontFamily: "DMSans_700Bold",
134-
color: strongColor,
135-
marginTop: 14,
136-
marginBottom: 6,
137-
},
138-
heading3: {
139-
fontSize: 17,
140-
lineHeight: 24,
141-
fontWeight: "700" as const,
142-
fontFamily: "DMSans_700Bold",
143-
color: strongColor,
198+
color: markdownStrongColor,
144199
marginTop: 12,
145-
marginBottom: 4,
146-
},
147-
heading4: {
148-
fontSize: 15,
149-
lineHeight: 22,
150-
fontWeight: "700" as const,
151-
fontFamily: "DMSans_700Bold",
152-
color: strongColor,
153-
marginTop: 10,
154-
marginBottom: 4,
200+
marginBottom: 6,
155201
},
156-
hr: {
157-
backgroundColor: hrColor,
202+
horizontal_rule: {
203+
backgroundColor: markdownHrColor,
158204
height: 1,
159205
marginVertical: 12,
160206
},
161207
};
162208

163-
const user = {
164-
...base,
165-
paragraph: { marginTop: 0, marginBottom: 0 },
166-
code_inline: {
167-
backgroundColor: userCodeBg,
168-
color: userCodeText,
169-
borderRadius: 5,
170-
paddingHorizontal: 5,
171-
paddingVertical: 1,
172-
fontFamily: "ui-monospace",
173-
fontSize: 13,
174-
},
175-
code_block: {
176-
backgroundColor: userFenceBg,
177-
color: userFenceText,
178-
borderRadius: 12,
179-
padding: 12,
180-
fontFamily: "ui-monospace",
181-
fontSize: 13,
182-
lineHeight: 19,
183-
},
184-
fence: {
185-
backgroundColor: userFenceBg,
186-
color: userFenceText,
187-
borderRadius: 12,
188-
padding: 12,
189-
fontFamily: "ui-monospace",
190-
fontSize: 13,
191-
lineHeight: 19,
209+
const createCodeRenderers = (
210+
inlineBackgroundColor: string,
211+
inlineTextColor: string,
212+
blockBackgroundColor: string,
213+
blockTextColor: string,
214+
): CustomRenderers => ({
215+
code_inline: ({ content }) => (
216+
<NativeText
217+
style={{
218+
backgroundColor: inlineBackgroundColor,
219+
color: inlineTextColor,
220+
borderRadius: 5,
221+
paddingHorizontal: 5,
222+
paddingVertical: 1,
223+
fontFamily: "ui-monospace",
224+
fontSize: 13,
225+
}}
226+
>
227+
{content}
228+
</NativeText>
229+
),
230+
code_block: ({ content }) => (
231+
<View
232+
style={{
233+
backgroundColor: blockBackgroundColor,
234+
borderRadius: 12,
235+
padding: 12,
236+
marginVertical: 8,
237+
}}
238+
>
239+
<ScrollView horizontal showsHorizontalScrollIndicator={false} bounces={false}>
240+
<NativeText
241+
selectable
242+
style={{
243+
color: blockTextColor,
244+
fontFamily: "ui-monospace",
245+
fontSize: 13,
246+
lineHeight: 19,
247+
}}
248+
>
249+
{content}
250+
</NativeText>
251+
</ScrollView>
252+
</View>
253+
),
254+
});
255+
256+
const userTheme: PartialMarkdownTheme = {
257+
...baseTheme,
258+
colors: {
259+
...baseTheme.colors,
260+
code: markdownUserCodeText,
261+
codeBackground: markdownUserCodeBg,
262+
border: markdownUserFenceBg,
192263
},
193264
};
265+
const userStyles: NodeStyleOverrides = {
266+
...baseStyles,
267+
paragraph: { marginTop: 0, marginBottom: 0 },
268+
};
194269

195-
const assistant = {
196-
...base,
197-
code_inline: {
198-
backgroundColor: codeBg,
199-
color: codeText,
200-
borderRadius: 5,
201-
paddingHorizontal: 5,
202-
paddingVertical: 1,
203-
fontFamily: "ui-monospace",
204-
fontSize: 13,
270+
const assistantTheme: PartialMarkdownTheme = {
271+
...baseTheme,
272+
colors: {
273+
...baseTheme.colors,
274+
code: markdownCodeText,
275+
codeBackground: markdownCodeBg,
276+
border: markdownCodeBg,
205277
},
206-
code_block: {
207-
backgroundColor: codeBg,
208-
color: codeText,
209-
borderRadius: 12,
210-
padding: 12,
211-
fontFamily: "ui-monospace",
212-
fontSize: 13,
213-
lineHeight: 19,
278+
};
279+
const assistantStyles: NodeStyleOverrides = {
280+
...baseStyles,
281+
};
282+
283+
return {
284+
user: {
285+
theme: userTheme,
286+
styles: userStyles,
287+
renderers: createCodeRenderers(
288+
markdownUserCodeBg,
289+
markdownUserCodeText,
290+
markdownUserFenceBg,
291+
markdownUserFenceText,
292+
),
214293
},
215-
fence: {
216-
backgroundColor: codeBg,
217-
color: codeText,
218-
borderRadius: 12,
219-
padding: 12,
220-
fontFamily: "ui-monospace",
221-
fontSize: 13,
222-
lineHeight: 19,
294+
assistant: {
295+
theme: assistantTheme,
296+
styles: assistantStyles,
297+
renderers: createCodeRenderers(
298+
markdownCodeBg,
299+
markdownCodeText,
300+
markdownCodeBg,
301+
markdownCodeText,
302+
),
223303
},
224304
};
225-
226-
return { user, assistant };
227305
}, [
228306
blockquoteBg,
229307
blockquoteBorder,
@@ -267,7 +345,14 @@ function renderFeedEntry(
267345
<View className="mb-3.5 items-end gap-1.5">
268346
<View className="max-w-[85%] gap-2 rounded-[22px] rounded-br-[10px] border border-blue-300/50 bg-blue-50/80 px-4 py-4 dark:border-blue-400/20 dark:bg-blue-500/12">
269347
{message.text.trim().length > 0 ? (
270-
<Markdown style={styles}>{message.text}</Markdown>
348+
<Markdown
349+
options={{ gfm: true }}
350+
renderers={styles.renderers}
351+
styles={styles.styles}
352+
theme={styles.theme}
353+
>
354+
{message.text}
355+
</Markdown>
271356
) : null}
272357
{attachments.map((attachment) => {
273358
const uri = messageImageUrl(props.httpBaseUrl, attachment.id);
@@ -308,7 +393,16 @@ function renderFeedEntry(
308393

309394
return (
310395
<View className="mb-3.5 gap-1.5 px-1">
311-
{message.text.trim().length > 0 ? <Markdown style={styles}>{message.text}</Markdown> : null}
396+
{message.text.trim().length > 0 ? (
397+
<Markdown
398+
options={{ gfm: true }}
399+
renderers={styles.renderers}
400+
styles={styles.styles}
401+
theme={styles.theme}
402+
>
403+
{message.text}
404+
</Markdown>
405+
) : null}
312406
{attachments.map((attachment) => {
313407
const uri = messageImageUrl(props.httpBaseUrl, attachment.id);
314408
if (!uri) {

0 commit comments

Comments
 (0)