Bluesky app fork with some witchin' additions ๐Ÿ’ซ
0
fork

Configure Feed

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

[๐Ÿด] Rich text in messages (#3926)

* add facets to message

* richtext messages

* undo richtexttag changes

* whoops, don't redetect facets

* dont set color directly

* shorten links and filter invalid facets

* fix link shortening

* pass in underline style

authored by

Samuel Newman and committed by
GitHub
becc708c 03b27969

+75 -20
+18
src/alf/atoms.ts
··· 840 840 mr_auto: { 841 841 marginRight: 'auto', 842 842 }, 843 + /* 844 + * Pointer events 845 + */ 846 + pointer_events_none: { 847 + pointerEvents: 'none', 848 + }, 849 + pointer_events_auto: { 850 + pointerEvents: 'auto', 851 + }, 852 + /* 853 + * Text decoration 854 + */ 855 + underline: { 856 + textDecorationLine: 'underline', 857 + }, 858 + strike_through: { 859 + textDecorationLine: 'line-through', 860 + }, 843 861 } as const
+19 -11
src/components/RichText.tsx
··· 1 1 import React from 'react' 2 + import {TextStyle} from 'react-native' 2 3 import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' 3 4 import {msg} from '@lingui/macro' 4 5 import {useLingui} from '@lingui/react' ··· 26 27 enableTags = false, 27 28 authorHandle, 28 29 onLinkPress, 30 + interactiveStyle, 29 31 }: TextStyleProp & 30 32 Pick<TextProps, 'selectable'> & { 31 33 value: RichTextAPI | string ··· 35 37 enableTags?: boolean 36 38 authorHandle?: string 37 39 onLinkPress?: LinkProps['onPress'] 40 + interactiveStyle?: TextStyle 38 41 }) { 39 42 const richText = React.useMemo( 40 43 () => 41 44 value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), 42 45 [value], 43 46 ) 44 - const styles = [a.leading_snug, flatten(style)] 47 + 48 + const flattenedStyle = flatten(style) 49 + const plainStyles = [a.leading_snug, flattenedStyle] 50 + const interactiveStyles = [ 51 + a.leading_snug, 52 + a.pointer_events_auto, 53 + flatten(interactiveStyle), 54 + flattenedStyle, 55 + ] 45 56 46 57 const {text, facets} = richText 47 58 ··· 67 78 <Text 68 79 selectable={selectable} 69 80 testID={testID} 70 - style={styles} 81 + style={plainStyles} 71 82 numberOfLines={numberOfLines} 72 83 // @ts-ignore web only -prf 73 84 dataSet={WORD_WRAP}> ··· 93 104 <InlineLinkText 94 105 selectable={selectable} 95 106 to={`/profile/${mention.did}`} 96 - style={[...styles, {pointerEvents: 'auto'}]} 107 + style={interactiveStyles} 97 108 // @ts-ignore TODO 98 109 dataSet={WORD_WRAP} 99 110 onPress={onLinkPress}> ··· 110 121 selectable={selectable} 111 122 key={key} 112 123 to={link.uri} 113 - style={[...styles, {pointerEvents: 'auto'}]} 124 + style={interactiveStyles} 114 125 // @ts-ignore TODO 115 126 dataSet={WORD_WRAP} 116 127 shareOnLongPress ··· 130 141 key={key} 131 142 text={segment.text} 132 143 tag={tag.tag} 133 - style={styles} 144 + style={interactiveStyles} 134 145 selectable={selectable} 135 146 authorHandle={authorHandle} 136 147 />, ··· 145 156 <Text 146 157 selectable={selectable} 147 158 testID={testID} 148 - style={styles} 159 + style={plainStyles} 149 160 numberOfLines={numberOfLines} 150 161 // @ts-ignore web only -prf 151 162 dataSet={WORD_WRAP}> ··· 219 230 onFocus={onFocus} 220 231 onBlur={onBlur} 221 232 style={[ 222 - style, 223 - { 224 - pointerEvents: 'auto', 225 - color: t.palette.primary_500, 226 - }, 227 233 web({ 228 234 cursor: 'pointer', 229 235 }), 236 + {color: t.palette.primary_500}, 230 237 (hovered || focused || pressed) && { 231 238 ...web({outline: 0}), 232 239 textDecorationLine: 'underline', 233 240 textDecorationColor: t.palette.primary_500, 234 241 }, 242 + style, 235 243 ]}> 236 244 {text} 237 245 </Text>
+14 -6
src/components/dms/MessageItem.tsx
··· 1 1 import React, {useCallback, useMemo, useRef} from 'react' 2 2 import {LayoutAnimation, StyleProp, TextStyle, View} from 'react-native' 3 + import {RichText as RichTextAPI} from '@atproto/api' 3 4 import {ChatBskyConvoDefs} from '@atproto-labs/api' 4 5 import {msg} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' ··· 9 10 import {atoms as a, useTheme} from '#/alf' 10 11 import {ActionsWrapper} from '#/components/dms/ActionsWrapper' 11 12 import {Text} from '#/components/Typography' 13 + import {RichText} from '../RichText' 12 14 13 - export let MessageItem = ({ 15 + let MessageItem = ({ 14 16 item, 15 17 next, 16 18 pending, ··· 65 67 const pendingColor = 66 68 t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800 67 69 70 + const rt = useMemo(() => { 71 + return new RichTextAPI({text: item.text, facets: item.facets}) 72 + }, [item.text, item.facets]) 73 + 68 74 return ( 69 75 <View> 70 76 <ActionsWrapper isFromSelf={isFromSelf} message={item}> ··· 87 93 ? {borderBottomRightRadius: isLastInGroup ? 2 : 17} 88 94 : {borderBottomLeftRadius: isLastInGroup ? 2 : 17}, 89 95 ]}> 90 - <Text 96 + <RichText 97 + value={rt} 91 98 style={[ 92 99 a.text_md, 93 100 a.leading_snug, 94 101 isFromSelf && {color: t.palette.white}, 95 102 pending && t.name !== 'light' && {color: t.palette.primary_300}, 96 - ]}> 97 - {item.text} 98 - </Text> 103 + ]} 104 + interactiveStyle={a.underline} 105 + enableTags 106 + /> 99 107 </View> 100 108 </ActionsWrapper> 101 109 <MessageItemMetadata ··· 106 114 </View> 107 115 ) 108 116 } 109 - 110 117 MessageItem = React.memo(MessageItem) 118 + export {MessageItem} 111 119 112 120 let MessageItemMetadata = ({ 113 121 message,
+1
src/lib/strings/rich-text-manip.ts
··· 1 1 import {RichText, UnicodeString} from '@atproto/api' 2 + 2 3 import {toShortUrl} from './url-helpers' 3 4 4 5 export function shortenLinks(rt: RichText): RichText {
+23 -3
src/screens/Messages/Conversation/MessagesList.tsx
··· 7 7 import {runOnJS, useSharedValue} from 'react-native-reanimated' 8 8 import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' 9 9 import {useSafeAreaInsets} from 'react-native-safe-area-context' 10 + import {AppBskyRichtextFacet, RichText} from '@atproto/api' 10 11 import {msg, Trans} from '@lingui/macro' 11 12 import {useLingui} from '@lingui/react' 12 13 14 + import {shortenLinks} from '#/lib/strings/rich-text-manip' 13 15 import {isIOS} from '#/platform/detection' 14 16 import {useConvo} from '#/state/messages/convo' 15 17 import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' 18 + import {useAgent} from '#/state/session' 16 19 import {ScrollProvider} from 'lib/ScrollContext' 17 20 import {isWeb} from 'platform/detection' 18 21 import {List} from 'view/com/util/List' ··· 87 90 88 91 export function MessagesList() { 89 92 const convo = useConvo() 93 + const {getAgent} = useAgent() 90 94 const flatListRef = useRef<FlatList>(null) 91 95 92 96 // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items ··· 159 163 }, [convo, hasInitiallyScrolled]) 160 164 161 165 const onSendMessage = useCallback( 162 - (text: string) => { 166 + async (text: string) => { 167 + let rt = new RichText({text}, {cleanNewlines: true}) 168 + await rt.detectFacets(getAgent()) 169 + rt = shortenLinks(rt) 170 + 171 + // filter out any mention facets that didn't map to a user 172 + rt.facets = rt.facets?.filter(facet => { 173 + const mention = facet.features.find(feature => 174 + AppBskyRichtextFacet.isMention(feature), 175 + ) 176 + if (mention && !mention.did) { 177 + return false 178 + } 179 + return true 180 + }) 181 + 163 182 if (convo.status === ConvoStatus.Ready) { 164 183 convo.sendMessage({ 165 - text, 184 + text: rt.text, 185 + facets: rt.facets, 166 186 }) 167 187 } 168 188 }, 169 - [convo], 189 + [convo, getAgent], 170 190 ) 171 191 172 192 const onScroll = React.useCallback(