mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at rn-bug 255 lines 6.7 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 function RichText({ 21 testID, 22 value, 23 style, 24 numberOfLines, 25 disableLinks, 26 selectable, 27 enableTags = false, 28 authorHandle, 29 onLinkPress, 30 interactiveStyle, 31 emojiMultiplier = 1.85, 32}: TextStyleProp & 33 Pick<TextProps, 'selectable'> & { 34 value: RichTextAPI | string 35 testID?: string 36 numberOfLines?: number 37 disableLinks?: boolean 38 enableTags?: boolean 39 authorHandle?: string 40 onLinkPress?: LinkProps['onPress'] 41 interactiveStyle?: TextStyle 42 emojiMultiplier?: number 43 }) { 44 const richText = React.useMemo( 45 () => 46 value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), 47 [value], 48 ) 49 50 const flattenedStyle = flatten(style) 51 const plainStyles = [a.leading_snug, flattenedStyle] 52 const interactiveStyles = [ 53 a.leading_snug, 54 a.pointer_events_auto, 55 flatten(interactiveStyle), 56 flattenedStyle, 57 ] 58 59 const {text, facets} = richText 60 61 if (!facets?.length) { 62 if (isOnlyEmoji(text)) { 63 const fontSize = 64 (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier 65 return ( 66 <Text 67 selectable={selectable} 68 testID={testID} 69 style={[plainStyles, {fontSize}]} 70 // @ts-ignore web only -prf 71 dataSet={WORD_WRAP}> 72 {text} 73 </Text> 74 ) 75 } 76 return ( 77 <Text 78 selectable={selectable} 79 testID={testID} 80 style={plainStyles} 81 numberOfLines={numberOfLines} 82 // @ts-ignore web only -prf 83 dataSet={WORD_WRAP}> 84 {text} 85 </Text> 86 ) 87 } 88 89 const els = [] 90 let key = 0 91 // N.B. must access segments via `richText.segments`, not via destructuring 92 for (const segment of richText.segments()) { 93 const link = segment.link 94 const mention = segment.mention 95 const tag = segment.tag 96 if ( 97 mention && 98 AppBskyRichtextFacet.validateMention(mention).success && 99 !disableLinks 100 ) { 101 els.push( 102 <ProfileHoverCard key={key} inline did={mention.did}> 103 <InlineLinkText 104 selectable={selectable} 105 to={`/profile/${mention.did}`} 106 style={interactiveStyles} 107 // @ts-ignore TODO 108 dataSet={WORD_WRAP} 109 onPress={onLinkPress}> 110 {segment.text} 111 </InlineLinkText> 112 </ProfileHoverCard>, 113 ) 114 } else if (link && AppBskyRichtextFacet.validateLink(link).success) { 115 if (disableLinks) { 116 els.push(toShortUrl(segment.text)) 117 } else { 118 els.push( 119 <InlineLinkText 120 selectable={selectable} 121 key={key} 122 to={link.uri} 123 style={interactiveStyles} 124 // @ts-ignore TODO 125 dataSet={WORD_WRAP} 126 shareOnLongPress 127 onPress={onLinkPress}> 128 {toShortUrl(segment.text)} 129 </InlineLinkText>, 130 ) 131 } 132 } else if ( 133 !disableLinks && 134 enableTags && 135 tag && 136 AppBskyRichtextFacet.validateTag(tag).success 137 ) { 138 els.push( 139 <RichTextTag 140 key={key} 141 text={segment.text} 142 tag={tag.tag} 143 style={interactiveStyles} 144 selectable={selectable} 145 authorHandle={authorHandle} 146 />, 147 ) 148 } else { 149 els.push(segment.text) 150 } 151 key++ 152 } 153 154 return ( 155 <Text 156 selectable={selectable} 157 testID={testID} 158 style={plainStyles} 159 numberOfLines={numberOfLines} 160 // @ts-ignore web only -prf 161 dataSet={WORD_WRAP}> 162 {els} 163 </Text> 164 ) 165} 166 167function RichTextTag({ 168 text, 169 tag, 170 style, 171 selectable, 172 authorHandle, 173}: { 174 text: string 175 tag: string 176 selectable?: boolean 177 authorHandle?: string 178} & TextStyleProp) { 179 const t = useTheme() 180 const {_} = useLingui() 181 const control = useTagMenuControl() 182 const { 183 state: hovered, 184 onIn: onHoverIn, 185 onOut: onHoverOut, 186 } = useInteractionState() 187 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 188 const { 189 state: pressed, 190 onIn: onPressIn, 191 onOut: onPressOut, 192 } = useInteractionState() 193 const navigation = useNavigation<NavigationProp>() 194 195 const navigateToPage = React.useCallback(() => { 196 navigation.push('Hashtag', { 197 tag: encodeURIComponent(tag), 198 }) 199 }, [navigation, tag]) 200 201 const openDialog = React.useCallback(() => { 202 control.open() 203 }, [control]) 204 205 /* 206 * N.B. On web, this is wrapped in another pressable comopnent with a11y 207 * labels, etc. That's why only some of these props are applied here. 208 */ 209 210 return ( 211 <React.Fragment> 212 <TagMenu control={control} tag={tag} authorHandle={authorHandle}> 213 <Text 214 selectable={selectable} 215 {...native({ 216 accessibilityLabel: _(msg`Hashtag: #${tag}`), 217 accessibilityHint: _(msg`Long press to open tag menu for #${tag}`), 218 accessibilityRole: isNative ? 'button' : undefined, 219 onPress: navigateToPage, 220 onLongPress: openDialog, 221 onPressIn: onPressIn, 222 onPressOut: onPressOut, 223 })} 224 {...web({ 225 onMouseEnter: onHoverIn, 226 onMouseLeave: onHoverOut, 227 })} 228 // @ts-ignore 229 onFocus={onFocus} 230 onBlur={onBlur} 231 style={[ 232 web({ 233 cursor: 'pointer', 234 }), 235 {color: t.palette.primary_500}, 236 (hovered || focused || pressed) && { 237 ...web({outline: 0}), 238 textDecorationLine: 'underline', 239 textDecorationColor: t.palette.primary_500, 240 }, 241 style, 242 ]}> 243 {text} 244 </Text> 245 </TagMenu> 246 </React.Fragment> 247 ) 248} 249 250export function isOnlyEmoji(text: string) { 251 return ( 252 text.length <= 15 && 253 /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text) 254 ) 255}