Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

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