Skip to content

Commit b68d568

Browse files
fsherzhelezkov
andauthored
feat: add markdown support (#13)
* feat: add markdown support * fix: odd rendering cases (render markdown inside of a View instead of Text) * bump deps according to recommendation log --------- Co-authored-by: Nick Zhelezkov <[email protected]>
1 parent 2f4e938 commit b68d568

16 files changed

+825
-210
lines changed

example/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
"web": "expo start --web"
1010
},
1111
"dependencies": {
12-
"@expo/metro-runtime": "~3.2.1",
12+
"@expo/metro-runtime": "~3.2.3",
1313
"@solana/web3.js": "^1.95.2",
14-
"expo": "~51.0.22",
14+
"expo": "~51.0.39",
1515
"expo-status-bar": "~1.12.1",
1616
"react": "18.2.0",
1717
"react-dom": "18.2.0",
18-
"react-native": "0.74.3",
18+
"react-native": "0.74.5",
1919
"react-native-svg": "15.2.0",
2020
"react-native-web": "~0.19.10"
2121
},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"@react-native-community/datetimepicker": "^8.2.0",
160160
"@react-native-picker/picker": "^2.7.7",
161161
"@shopify/restyle": "^2.4.4",
162+
"react-native-markdown-display": "^7.0.2",
162163
"react-native-modal-datetime-picker": "^17.1.0"
163164
}
164165
}

src/ui/components/SimpleMarkdown.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { PropsWithChildren } from 'react';
2+
import { Linking, type StyleProp, type TextStyle } from 'react-native';
3+
import Markdown, {
4+
MarkdownIt,
5+
type RenderRules,
6+
} from 'react-native-markdown-display';
7+
import { type ColorVars, useTheme } from '../theme';
8+
import { confirmLinkTransition } from '../utils';
9+
import { Text } from './Text';
10+
11+
interface Props {
12+
text: string;
13+
baseTextVariant?: 'caption' | 'text';
14+
baseColor?: keyof ColorVars;
15+
}
16+
17+
const LinkWithConfirm = ({
18+
href,
19+
children,
20+
style,
21+
}: PropsWithChildren<{ href: string; style?: StyleProp<TextStyle> }>) => {
22+
return (
23+
<Text
24+
style={style}
25+
onPress={async () => {
26+
const isOpenable = await Linking.canOpenURL(href);
27+
28+
if (!isOpenable) {
29+
return;
30+
}
31+
32+
confirmLinkTransition(href, {
33+
onOk: () => {
34+
Linking.openURL(href);
35+
},
36+
onCancel: () => {},
37+
});
38+
}}
39+
>
40+
{children}
41+
</Text>
42+
);
43+
};
44+
45+
const rules: RenderRules = {
46+
image: () => null,
47+
link: (node, children, _, styles) => {
48+
const href = node.attributes.href;
49+
50+
return (
51+
<LinkWithConfirm key={node.key} href={href} style={styles.link}>
52+
{children}
53+
</LinkWithConfirm>
54+
);
55+
},
56+
};
57+
58+
export const SimpleMarkdown = ({
59+
text,
60+
baseTextVariant = 'text',
61+
baseColor = 'textPrimary',
62+
}: Props) => {
63+
const theme = useTheme();
64+
const baseTextStyles = {
65+
...theme.textVariants[baseTextVariant],
66+
color: theme.colors[baseColor],
67+
};
68+
69+
const commonHeadingParagraphStyle = {
70+
...baseTextStyles,
71+
marginTop: 0,
72+
marginBottom: theme.spacing['1'],
73+
};
74+
75+
const commonCodeBlockStyle = {
76+
...baseTextStyles,
77+
borderWidth: 0,
78+
backgroundColor: 'transparent',
79+
marginLeft: 0,
80+
paddingLeft: 0,
81+
borderRadius: 0,
82+
};
83+
84+
return (
85+
<Markdown
86+
rules={rules}
87+
markdownit={MarkdownIt({
88+
typographer: true,
89+
linkify: true,
90+
})}
91+
style={{
92+
text: baseTextStyles,
93+
heading1: commonHeadingParagraphStyle,
94+
heading2: commonHeadingParagraphStyle,
95+
heading3: commonHeadingParagraphStyle,
96+
heading4: commonHeadingParagraphStyle,
97+
heading5: commonHeadingParagraphStyle,
98+
heading6: commonHeadingParagraphStyle,
99+
paragraph: commonHeadingParagraphStyle,
100+
code_block: commonCodeBlockStyle,
101+
code_inline: commonCodeBlockStyle,
102+
fence: commonCodeBlockStyle,
103+
blockquote: {
104+
backgroundColor: 'transparent',
105+
borderLeftWidth: 0,
106+
paddingHorizontal: 0,
107+
paddingVertical: 0,
108+
marginLeft: 0,
109+
},
110+
table: {
111+
borderWidth: 0,
112+
borderRadius: 0,
113+
},
114+
td: {
115+
padding: 0,
116+
paddingHorizontal: theme.spacing['0.5'],
117+
paddingVertical: theme.spacing['1'],
118+
},
119+
th: {
120+
padding: 0,
121+
paddingHorizontal: theme.spacing['0.5'],
122+
paddingVertical: theme.spacing['1'],
123+
},
124+
tr: {
125+
borderBottomWidth: 0,
126+
},
127+
ordered_list_icon: baseTextStyles,
128+
hr: {
129+
marginVertical: theme.spacing['1'],
130+
},
131+
}}
132+
>
133+
{text}
134+
</Markdown>
135+
);
136+
};

src/ui/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export { Button } from './Button';
44
export { InputContainer } from './InputContainer';
55
export { Link } from './Link';
66
export { PickerModal } from './PickerModal';
7+
export { SimpleMarkdown } from './SimpleMarkdown';
78
export { Snackbar } from './Snackbar';
89
export { Text } from './Text';

src/ui/hooks/useIsolatedLayoutPropNormalizer.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
SingleValueActionComponent,
99
} from '@dialectlabs/blinks-core';
1010
import { useCallback, useMemo } from 'react';
11-
import { Alert, Linking } from 'react-native';
11+
import { Linking } from 'react-native';
1212
import type { IsolatedLayoutProps } from '../types';
13+
import { confirmLinkTransition } from '../utils';
1314
import { buttonLabelMap, buttonVariantMap } from './ui-mappers';
1415

1516
const SOFT_LIMIT_FORM_INPUTS = 10;
@@ -49,25 +50,13 @@ export const useIsolatedLayoutPropNormalizer = ({
4950
}
5051

5152
if (extra.type === 'external-link') {
52-
Alert.alert(
53-
'External Link',
54-
`This action redirects to the website: ${extra.data.externalLink}, the link will open in your browser`,
55-
[
56-
{
57-
text: 'Cancel',
58-
style: 'cancel',
59-
onPress: () => extra.onCancel?.(),
60-
},
61-
{
62-
text: 'OK',
63-
isPreferred: true,
64-
onPress: () => {
65-
Linking.openURL(extra.data.externalLink);
66-
extra.onNext();
67-
},
68-
},
69-
],
70-
);
53+
confirmLinkTransition(extra.data.externalLink, {
54+
onOk: () => {
55+
Linking.openURL(extra.data.externalLink);
56+
extra.onNext();
57+
},
58+
onCancel: () => extra.onCancel?.(),
59+
});
7160
}
7261
},
7362
};

src/ui/hooks/useLayoutPropNormalizer.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
SingleValueActionComponent,
99
} from '@dialectlabs/blinks-core';
1010
import { useMemo } from 'react';
11-
import { Alert, Linking } from 'react-native';
11+
import { Linking } from 'react-native';
1212
import type { LayoutProps } from '../types';
13+
import { confirmLinkTransition } from '../utils';
1314
import { buttonLabelMap, buttonVariantMap } from './ui-mappers';
1415

1516
const SOFT_LIMIT_BUTTONS = 10;
@@ -86,25 +87,13 @@ export const useLayoutPropNormalizer = ({
8687
}
8788

8889
if (extra.type === 'external-link') {
89-
Alert.alert(
90-
'External Link',
91-
`This action redirects to the website: ${extra.data.externalLink}, the link will open in your browser`,
92-
[
93-
{
94-
text: 'Cancel',
95-
style: 'cancel',
96-
onPress: () => extra.onCancel?.(),
97-
},
98-
{
99-
text: 'OK',
100-
isPreferred: true,
101-
onPress: () => {
102-
Linking.openURL(extra.data.externalLink);
103-
extra.onNext();
104-
},
105-
},
106-
],
107-
);
90+
confirmLinkTransition(extra.data.externalLink, {
91+
onOk: () => {
92+
Linking.openURL(extra.data.externalLink);
93+
extra.onNext();
94+
},
95+
onCancel: () => extra.onCancel?.(),
96+
});
10897
}
10998
},
11099
};

src/ui/layout/ActionInput/ActionCheckboxGroup.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useMemo, useState } from 'react';
22
import { TouchableOpacity } from 'react-native';
3-
import { Box, Text } from '../../components';
3+
import { Box, SimpleMarkdown, Text } from '../../components';
44
import { CheckBoxIcon } from '../../icons';
55
import type {
66
BorderRadiiVars,
@@ -217,13 +217,13 @@ export const ActionCheckboxGroup = ({
217217
/>
218218
)}
219219
{finalDescription && (
220-
<Text
221-
color={getDescriptionColor(state.valid, touched)}
222-
variant="caption"
223-
{...standaloneProps.text}
224-
>
225-
{finalDescription}
226-
</Text>
220+
<Box {...standaloneProps.text}>
221+
<SimpleMarkdown
222+
text={finalDescription}
223+
baseTextVariant="caption"
224+
baseColor={getDescriptionColor(state.valid, touched)}
225+
/>
226+
</Box>
227227
)}
228228
</Box>
229229
);

src/ui/layout/ActionInput/ActionDateInput.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useMemo, useState } from 'react';
22
import { TouchableOpacity } from 'react-native';
33
import DateTimePickerModal from 'react-native-modal-datetime-picker';
4-
import { Box, InputContainer, Text } from '../../components';
4+
import { Box, InputContainer, SimpleMarkdown, Text } from '../../components';
55
import { CalendarIcon } from '../../icons';
66
import { useTheme } from '../../theme';
77
import type { InputProps } from '../../types';
@@ -145,13 +145,13 @@ const DateInput = ({
145145
)}
146146
</InputContainer>
147147
{finalDescription && (
148-
<Text
149-
color={getDescriptionColor(isValid, isTouched)}
150-
py={1}
151-
variant="caption"
152-
>
153-
{finalDescription}
154-
</Text>
148+
<Box py={1}>
149+
<SimpleMarkdown
150+
text={finalDescription}
151+
baseTextVariant="caption"
152+
baseColor={getDescriptionColor(isValid, isTouched)}
153+
/>
154+
</Box>
155155
)}
156156
<DateTimePickerModal
157157
minimumDate={minDate ?? undefined}

src/ui/layout/ActionInput/ActionDateTimeInput.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type ReactNode, useEffect, useMemo, useState } from 'react';
22
import { TouchableOpacity } from 'react-native';
33
import DateTimePickerModal from 'react-native-modal-datetime-picker';
4-
import { Box, InputContainer, Text } from '../../components';
4+
import { Box, InputContainer, SimpleMarkdown, Text } from '../../components';
55
import { CalendarIcon, ClockIcon } from '../../icons';
66
import type {
77
BorderRadiiVars,
@@ -241,14 +241,13 @@ const DateTimeInput = ({
241241
)}
242242
</Box>
243243
{finalDescription && (
244-
<Text
245-
color={getDescriptionColor(isValid, isTouched)}
246-
variant="caption"
247-
py={1}
248-
{...standaloneProps.text}
249-
>
250-
{finalDescription}
251-
</Text>
244+
<Box py={1} {...standaloneProps.text}>
245+
<SimpleMarkdown
246+
text={finalDescription}
247+
baseTextVariant="caption"
248+
baseColor={getDescriptionColor(isValid, isTouched)}
249+
/>
250+
</Box>
252251
)}
253252
<DateTimePickerModal
254253
date={displayedDate}

src/ui/layout/ActionInput/ActionNumberInput.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
TextInput,
55
type TextInputChangeEventData,
66
} from 'react-native';
7-
import { Box, InputContainer, Text } from '../../components';
7+
import { Box, InputContainer, SimpleMarkdown } from '../../components';
88
import NumberIcon from '../../icons/NumberIcon';
99
import { useTheme } from '../../theme';
1010
import type { InputProps } from '../../types';
@@ -139,13 +139,13 @@ export const ActionNumberInput = ({
139139
)}
140140
</InputContainer>
141141
{finalDescription && (
142-
<Text
143-
color={getDescriptionColor(isValid, isTouched)}
144-
variant="caption"
145-
py={1}
146-
>
147-
{finalDescription}
148-
</Text>
142+
<Box py={1}>
143+
<SimpleMarkdown
144+
text={finalDescription}
145+
baseTextVariant="caption"
146+
baseColor={getDescriptionColor(isValid, isTouched)}
147+
/>
148+
</Box>
149149
)}
150150
</Box>
151151
);

0 commit comments

Comments
 (0)