forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React 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|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[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
32export function RichText({
33 testID,
34 value,
35 style,
36 numberOfLines,
37 disableLinks,
38 selectable,
39 enableTags = false,
40 authorHandle,
41 onLinkPress,
42 interactiveStyle,
43 emojiMultiplier = 1.85,
44 onLayout,
45 onTextLayout,
46 shouldProxyLinks,
47}: RichTextProps) {
48 const richText = React.useMemo(
49 () =>
50 value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
51 [value],
52 )
53
54 const plainStyles = [a.leading_snug, style]
55 const interactiveStyles = [plainStyles, interactiveStyle]
56
57 const {text, facets} = richText
58
59 if (!facets?.length) {
60 if (isOnlyEmoji(text)) {
61 const flattenedStyle = flatten(style) ?? {}
62 const fontSize =
63 (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
64 return (
65 <Text
66 emoji
67 selectable={selectable}
68 testID={testID}
69 style={[plainStyles, {fontSize}]}
70 onLayout={onLayout}
71 onTextLayout={onTextLayout}
72 // @ts-ignore web only -prf
73 dataSet={WORD_WRAP}>
74 {text}
75 </Text>
76 )
77 }
78 return (
79 <Text
80 emoji
81 selectable={selectable}
82 testID={testID}
83 style={plainStyles}
84 numberOfLines={numberOfLines}
85 onLayout={onLayout}
86 onTextLayout={onTextLayout}
87 // @ts-ignore web only -prf
88 dataSet={WORD_WRAP}>
89 {text}
90 </Text>
91 )
92 }
93
94 const els = []
95 let key = 0
96 // N.B. must access segments via `richText.segments`, not via destructuring
97 for (const segment of richText.segments()) {
98 const link = segment.link
99 const mention = segment.mention
100 const tag = segment.tag
101 if (
102 mention &&
103 AppBskyRichtextFacet.validateMention(mention).success &&
104 !disableLinks
105 ) {
106 els.push(
107 <ProfileHoverCard key={key} did={mention.did}>
108 <InlineLinkText
109 selectable={selectable}
110 to={`/profile/${mention.did}`}
111 style={interactiveStyles}
112 // @ts-ignore TODO
113 dataSet={WORD_WRAP}
114 shouldProxy={shouldProxyLinks}
115 onPress={onLinkPress}>
116 {segment.text}
117 </InlineLinkText>
118 </ProfileHoverCard>,
119 )
120 } else if (link && AppBskyRichtextFacet.validateLink(link).success) {
121 const isValidLink = URL_REGEX.test(link.uri)
122 if (!isValidLink || disableLinks) {
123 els.push(toShortUrl(segment.text))
124 } else {
125 els.push(
126 <InlineLinkText
127 selectable={selectable}
128 key={key}
129 to={link.uri}
130 style={interactiveStyles}
131 // @ts-ignore TODO
132 dataSet={WORD_WRAP}
133 shareOnLongPress
134 shouldProxy={shouldProxyLinks}
135 onPress={onLinkPress}
136 emoji>
137 {toShortUrl(segment.text)}
138 </InlineLinkText>,
139 )
140 }
141 } else if (
142 !disableLinks &&
143 enableTags &&
144 tag &&
145 AppBskyRichtextFacet.validateTag(tag).success
146 ) {
147 els.push(
148 <RichTextTag
149 key={key}
150 display={segment.text}
151 tag={tag.tag}
152 textStyle={interactiveStyles}
153 authorHandle={authorHandle}
154 />,
155 )
156 } else {
157 els.push(segment.text)
158 }
159 key++
160 }
161
162 return (
163 <Text
164 emoji
165 selectable={selectable}
166 testID={testID}
167 style={plainStyles}
168 numberOfLines={numberOfLines}
169 onLayout={onLayout}
170 onTextLayout={onTextLayout}
171 // @ts-ignore web only -prf
172 dataSet={WORD_WRAP}>
173 {els}
174 </Text>
175 )
176}