Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
117
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 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}