mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 257 lines 6.8 kB view raw
1import React from 'react' 2import {TextStyle} from 'react-native' 3import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' 4import {msg} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6import {useNavigation} from '@react-navigation/native' 7 8import {NavigationProp} from '#/lib/routes/types' 9import {toShortUrl} from '#/lib/strings/url-helpers' 10import {isNative} from '#/platform/detection' 11import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf' 12import {useInteractionState} from '#/components/hooks/useInteractionState' 13import {InlineLinkText, LinkProps} from '#/components/Link' 14import {ProfileHoverCard} from '#/components/ProfileHoverCard' 15import {TagMenu, useTagMenuControl} from '#/components/TagMenu' 16import {Text, TextProps} from '#/components/Typography' 17 18const WORD_WRAP = {wordWrap: 1} 19 20export type RichTextProps = TextStyleProp & 21 Pick<TextProps, 'selectable'> & { 22 value: RichTextAPI | string 23 testID?: string 24 numberOfLines?: number 25 disableLinks?: boolean 26 enableTags?: boolean 27 authorHandle?: string 28 onLinkPress?: LinkProps['onPress'] 29 interactiveStyle?: TextStyle 30 emojiMultiplier?: number 31 } 32 33export function RichText({ 34 testID, 35 value, 36 style, 37 numberOfLines, 38 disableLinks, 39 selectable, 40 enableTags = false, 41 authorHandle, 42 onLinkPress, 43 interactiveStyle, 44 emojiMultiplier = 1.85, 45}: RichTextProps) { 46 const richText = React.useMemo( 47 () => 48 value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), 49 [value], 50 ) 51 52 const flattenedStyle = flatten(style) 53 const plainStyles = [a.leading_snug, flattenedStyle] 54 const interactiveStyles = [ 55 a.leading_snug, 56 a.pointer_events_auto, 57 flatten(interactiveStyle), 58 flattenedStyle, 59 ] 60 61 const {text, facets} = richText 62 63 if (!facets?.length) { 64 if (isOnlyEmoji(text)) { 65 const fontSize = 66 (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier 67 return ( 68 <Text 69 selectable={selectable} 70 testID={testID} 71 style={[plainStyles, {fontSize}]} 72 // @ts-ignore web only -prf 73 dataSet={WORD_WRAP}> 74 {text} 75 </Text> 76 ) 77 } 78 return ( 79 <Text 80 selectable={selectable} 81 testID={testID} 82 style={plainStyles} 83 numberOfLines={numberOfLines} 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} inline 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 onPress={onLinkPress}> 112 {segment.text} 113 </InlineLinkText> 114 </ProfileHoverCard>, 115 ) 116 } else if (link && AppBskyRichtextFacet.validateLink(link).success) { 117 if (disableLinks) { 118 els.push(toShortUrl(segment.text)) 119 } else { 120 els.push( 121 <InlineLinkText 122 selectable={selectable} 123 key={key} 124 to={link.uri} 125 style={interactiveStyles} 126 // @ts-ignore TODO 127 dataSet={WORD_WRAP} 128 shareOnLongPress 129 onPress={onLinkPress}> 130 {toShortUrl(segment.text)} 131 </InlineLinkText>, 132 ) 133 } 134 } else if ( 135 !disableLinks && 136 enableTags && 137 tag && 138 AppBskyRichtextFacet.validateTag(tag).success 139 ) { 140 els.push( 141 <RichTextTag 142 key={key} 143 text={segment.text} 144 tag={tag.tag} 145 style={interactiveStyles} 146 selectable={selectable} 147 authorHandle={authorHandle} 148 />, 149 ) 150 } else { 151 els.push(segment.text) 152 } 153 key++ 154 } 155 156 return ( 157 <Text 158 selectable={selectable} 159 testID={testID} 160 style={plainStyles} 161 numberOfLines={numberOfLines} 162 // @ts-ignore web only -prf 163 dataSet={WORD_WRAP}> 164 {els} 165 </Text> 166 ) 167} 168 169function RichTextTag({ 170 text, 171 tag, 172 style, 173 selectable, 174 authorHandle, 175}: { 176 text: string 177 tag: string 178 selectable?: boolean 179 authorHandle?: string 180} & TextStyleProp) { 181 const t = useTheme() 182 const {_} = useLingui() 183 const control = useTagMenuControl() 184 const { 185 state: hovered, 186 onIn: onHoverIn, 187 onOut: onHoverOut, 188 } = useInteractionState() 189 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 190 const { 191 state: pressed, 192 onIn: onPressIn, 193 onOut: onPressOut, 194 } = useInteractionState() 195 const navigation = useNavigation<NavigationProp>() 196 197 const navigateToPage = React.useCallback(() => { 198 navigation.push('Hashtag', { 199 tag: encodeURIComponent(tag), 200 }) 201 }, [navigation, tag]) 202 203 const openDialog = React.useCallback(() => { 204 control.open() 205 }, [control]) 206 207 /* 208 * N.B. On web, this is wrapped in another pressable comopnent with a11y 209 * labels, etc. That's why only some of these props are applied here. 210 */ 211 212 return ( 213 <React.Fragment> 214 <TagMenu control={control} tag={tag} authorHandle={authorHandle}> 215 <Text 216 selectable={selectable} 217 {...native({ 218 accessibilityLabel: _(msg`Hashtag: #${tag}`), 219 accessibilityHint: _(msg`Long press to open tag menu for #${tag}`), 220 accessibilityRole: isNative ? 'button' : undefined, 221 onPress: navigateToPage, 222 onLongPress: openDialog, 223 onPressIn: onPressIn, 224 onPressOut: onPressOut, 225 })} 226 {...web({ 227 onMouseEnter: onHoverIn, 228 onMouseLeave: onHoverOut, 229 })} 230 // @ts-ignore 231 onFocus={onFocus} 232 onBlur={onBlur} 233 style={[ 234 web({ 235 cursor: 'pointer', 236 }), 237 {color: t.palette.primary_500}, 238 (hovered || focused || pressed) && { 239 ...web({outline: 0}), 240 textDecorationLine: 'underline', 241 textDecorationColor: t.palette.primary_500, 242 }, 243 style, 244 ]}> 245 {text} 246 </Text> 247 </TagMenu> 248 </React.Fragment> 249 ) 250} 251 252export function isOnlyEmoji(text: string) { 253 return ( 254 text.length <= 15 && 255 /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text) 256 ) 257}