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