mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5
6import {toShortUrl} from '#/lib/strings/url-helpers'
7import {isNative} from '#/platform/detection'
8import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
9import {useInteractionState} from '#/components/hooks/useInteractionState'
10import {InlineLinkText, LinkProps} from '#/components/Link'
11import {ProfileHoverCard} from '#/components/ProfileHoverCard'
12import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
13import {Text, TextProps} from '#/components/Typography'
14
15const WORD_WRAP = {wordWrap: 1}
16
17export function RichText({
18 testID,
19 value,
20 style,
21 numberOfLines,
22 disableLinks,
23 selectable,
24 enableTags = false,
25 authorHandle,
26 onLinkPress,
27}: TextStyleProp &
28 Pick<TextProps, 'selectable'> & {
29 value: RichTextAPI | string
30 testID?: string
31 numberOfLines?: number
32 disableLinks?: boolean
33 enableTags?: boolean
34 authorHandle?: string
35 onLinkPress?: LinkProps['onPress']
36 }) {
37 const richText = React.useMemo(
38 () =>
39 value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
40 [value],
41 )
42 const styles = [a.leading_snug, flatten(style)]
43
44 const {text, facets} = richText
45
46 if (!facets?.length) {
47 if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
48 return (
49 <Text
50 selectable={selectable}
51 testID={testID}
52 style={[
53 {
54 fontSize: 26,
55 lineHeight: 30,
56 },
57 ]}
58 // @ts-ignore web only -prf
59 dataSet={WORD_WRAP}>
60 {text}
61 </Text>
62 )
63 }
64 return (
65 <Text
66 selectable={selectable}
67 testID={testID}
68 style={styles}
69 numberOfLines={numberOfLines}
70 // @ts-ignore web only -prf
71 dataSet={WORD_WRAP}>
72 {text}
73 </Text>
74 )
75 }
76
77 const els = []
78 let key = 0
79 // N.B. must access segments via `richText.segments`, not via destructuring
80 for (const segment of richText.segments()) {
81 const link = segment.link
82 const mention = segment.mention
83 const tag = segment.tag
84 if (
85 mention &&
86 AppBskyRichtextFacet.validateMention(mention).success &&
87 !disableLinks
88 ) {
89 els.push(
90 <ProfileHoverCard key={key} inline did={mention.did}>
91 <InlineLinkText
92 selectable={selectable}
93 to={`/profile/${mention.did}`}
94 style={[...styles, {pointerEvents: 'auto'}]}
95 // @ts-ignore TODO
96 dataSet={WORD_WRAP}
97 onPress={onLinkPress}>
98 {segment.text}
99 </InlineLinkText>
100 </ProfileHoverCard>,
101 )
102 } else if (link && AppBskyRichtextFacet.validateLink(link).success) {
103 if (disableLinks) {
104 els.push(toShortUrl(segment.text))
105 } else {
106 els.push(
107 <InlineLinkText
108 selectable={selectable}
109 key={key}
110 to={link.uri}
111 style={[...styles, {pointerEvents: 'auto'}]}
112 // @ts-ignore TODO
113 dataSet={WORD_WRAP}
114 shareOnLongPress
115 onPress={onLinkPress}>
116 {toShortUrl(segment.text)}
117 </InlineLinkText>,
118 )
119 }
120 } else if (
121 !disableLinks &&
122 enableTags &&
123 tag &&
124 AppBskyRichtextFacet.validateTag(tag).success
125 ) {
126 els.push(
127 <RichTextTag
128 key={key}
129 text={segment.text}
130 tag={tag.tag}
131 style={styles}
132 selectable={selectable}
133 authorHandle={authorHandle}
134 />,
135 )
136 } else {
137 els.push(segment.text)
138 }
139 key++
140 }
141
142 return (
143 <Text
144 selectable={selectable}
145 testID={testID}
146 style={styles}
147 numberOfLines={numberOfLines}
148 // @ts-ignore web only -prf
149 dataSet={WORD_WRAP}>
150 {els}
151 </Text>
152 )
153}
154
155function RichTextTag({
156 text,
157 tag,
158 style,
159 selectable,
160 authorHandle,
161}: {
162 text: string
163 tag: string
164 selectable?: boolean
165 authorHandle?: string
166} & TextStyleProp) {
167 const t = useTheme()
168 const {_} = useLingui()
169 const control = useTagMenuControl()
170 const {
171 state: hovered,
172 onIn: onHoverIn,
173 onOut: onHoverOut,
174 } = useInteractionState()
175 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
176 const {
177 state: pressed,
178 onIn: onPressIn,
179 onOut: onPressOut,
180 } = useInteractionState()
181
182 const open = React.useCallback(() => {
183 control.open()
184 }, [control])
185
186 /*
187 * N.B. On web, this is wrapped in another pressable comopnent with a11y
188 * labels, etc. That's why only some of these props are applied here.
189 */
190
191 return (
192 <React.Fragment>
193 <TagMenu control={control} tag={tag} authorHandle={authorHandle}>
194 <Text
195 selectable={selectable}
196 {...native({
197 accessibilityLabel: _(msg`Hashtag: #${tag}`),
198 accessibilityHint: _(msg`Click here to open tag menu for #${tag}`),
199 accessibilityRole: isNative ? 'button' : undefined,
200 onPress: open,
201 onPressIn: onPressIn,
202 onPressOut: onPressOut,
203 })}
204 {...web({
205 onMouseEnter: onHoverIn,
206 onMouseLeave: onHoverOut,
207 })}
208 // @ts-ignore
209 onFocus={onFocus}
210 onBlur={onBlur}
211 style={[
212 style,
213 {
214 pointerEvents: 'auto',
215 color: t.palette.primary_500,
216 },
217 web({
218 cursor: 'pointer',
219 }),
220 (hovered || focused || pressed) && {
221 ...web({outline: 0}),
222 textDecorationLine: 'underline',
223 textDecorationColor: t.palette.primary_500,
224 },
225 ]}>
226 {text}
227 </Text>
228 </TagMenu>
229 </React.Fragment>
230 )
231}