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