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