Bluesky app fork with some witchin' additions 馃挮
1import {useMemo} from 'react'
2import {type StyleProp, type TextStyle} from 'react-native'
3import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
4
5import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets'
6import {toShortUrl} from '#/lib/strings/url-helpers'
7import {atoms as a, flatten, type TextStyleProp} from '#/alf'
8import {isOnlyEmoji} from '#/alf/typography'
9import {InlineLinkText, type LinkProps} from '#/components/Link'
10import {ProfileHoverCard} from '#/components/ProfileHoverCard'
11import {RichTextTag} from '#/components/RichTextTag'
12import {Text, type TextProps} from '#/components/Typography'
13
14const WORD_WRAP = {wordWrap: 1}
15// lifted from facet detection in `RichText` impl, _without_ `gm` flags
16const URL_REGEX =
17 /(^|\s|\()(?!javascript:)([a-z][a-z0-9+.-]*:\/\/[\S]+|(?:[a-z0-9]+\.)+[a-z0-9]+(:[0-9]+)?[\S]*|[a-z][a-z0-9+.-]*:[^\s()]+)/i
18
19export type RichTextProps = TextStyleProp &
20 Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
21 value: RichTextAPI | string
22 testID?: string
23 numberOfLines?: number
24 disableLinks?: boolean
25 enableTags?: boolean
26 authorHandle?: string
27 onLinkPress?: LinkProps['onPress']
28 interactiveStyle?: StyleProp<TextStyle>
29 emojiMultiplier?: number
30 shouldProxyLinks?: boolean
31 /**
32 * DANGEROUS: Disable facet lexicon validation
33 *
34 * `detectFacetsWithoutResolution()` generates technically invalid facets,
35 * with a handle in place of the DID. This means that RichText that uses it
36 * won't be able to render links.
37 *
38 * Use with care - only use if you're rendering facets you're generating yourself.
39 */
40 disableMentionFacetValidation?: true
41 }
42
43export function RichText({
44 testID,
45 value,
46 style,
47 numberOfLines,
48 disableLinks,
49 selectable,
50 enableTags = false,
51 authorHandle,
52 onLinkPress,
53 interactiveStyle,
54 emojiMultiplier = 1.85,
55 onLayout,
56 onTextLayout,
57 shouldProxyLinks,
58 disableMentionFacetValidation,
59}: RichTextProps) {
60 const richText = useMemo(() => {
61 if (value instanceof RichTextAPI) {
62 return value
63 } else {
64 const rt = new RichTextAPI({text: value})
65 detectFacetsWithoutResolution(rt)
66 return rt
67 }
68 }, [value])
69
70 const plainStyles = [a.leading_snug, style]
71 const interactiveStyles = [plainStyles, interactiveStyle]
72
73 const {text, facets} = richText
74
75 if (!facets?.length) {
76 if (isOnlyEmoji(text)) {
77 const flattenedStyle = flatten(style) ?? {}
78 const fontSize =
79 (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
80 return (
81 <Text
82 emoji
83 selectable={selectable}
84 testID={testID}
85 style={[plainStyles, {fontSize}]}
86 onLayout={onLayout}
87 onTextLayout={onTextLayout}
88 // @ts-ignore web only -prf
89 dataSet={WORD_WRAP}>
90 {text}
91 </Text>
92 )
93 }
94 return (
95 <Text
96 emoji
97 selectable={selectable}
98 testID={testID}
99 style={plainStyles}
100 numberOfLines={numberOfLines}
101 onLayout={onLayout}
102 onTextLayout={onTextLayout}
103 // @ts-ignore web only -prf
104 dataSet={WORD_WRAP}>
105 {text}
106 </Text>
107 )
108 }
109
110 const els = []
111 let key = 0
112 // N.B. must access segments via `richText.segments`, not via destructuring
113 for (const segment of richText.segments()) {
114 const link = segment.link
115 const mention = segment.mention
116 const tag = segment.tag
117
118 if (
119 mention &&
120 (disableMentionFacetValidation ||
121 AppBskyRichtextFacet.validateMention(mention).success) &&
122 !disableLinks
123 ) {
124 els.push(
125 <ProfileHoverCard key={key} did={mention.did}>
126 <InlineLinkText
127 selectable={selectable}
128 to={`/profile/${mention.did}`}
129 style={interactiveStyles}
130 // @ts-ignore TODO
131 dataSet={WORD_WRAP}
132 shouldProxy={shouldProxyLinks}
133 onPress={onLinkPress}>
134 {segment.text}
135 </InlineLinkText>
136 </ProfileHoverCard>,
137 )
138 } else if (link && AppBskyRichtextFacet.validateLink(link).success) {
139 const isValidLink = URL_REGEX.test(link.uri)
140 if (!isValidLink || disableLinks) {
141 els.push(toShortUrl(segment.text))
142 } else {
143 els.push(
144 <InlineLinkText
145 selectable={selectable}
146 key={key}
147 to={link.uri}
148 style={interactiveStyles}
149 // @ts-ignore TODO
150 dataSet={WORD_WRAP}
151 shareOnLongPress
152 shouldProxy={shouldProxyLinks}
153 onPress={onLinkPress}
154 emoji>
155 {toShortUrl(segment.text)}
156 </InlineLinkText>,
157 )
158 }
159 } else if (
160 !disableLinks &&
161 enableTags &&
162 tag &&
163 AppBskyRichtextFacet.validateTag(tag).success
164 ) {
165 els.push(
166 <RichTextTag
167 key={key}
168 display={segment.text}
169 tag={tag.tag}
170 textStyle={interactiveStyles}
171 authorHandle={authorHandle}
172 />,
173 )
174 } else {
175 els.push(segment.text)
176 }
177 key++
178 }
179
180 return (
181 <Text
182 emoji
183 selectable={selectable}
184 testID={testID}
185 style={plainStyles}
186 numberOfLines={numberOfLines}
187 onLayout={onLayout}
188 onTextLayout={onTextLayout}
189 // @ts-ignore web only -prf
190 dataSet={WORD_WRAP}>
191 {els}
192 </Text>
193 )
194}