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

[๐Ÿด] send record via link in text (Record DMs - base PR) (#4227)

* send record via link in text

* re-trim text after removing link

authored by samuel.fm and committed by

GitHub 8eb3cebb 455937dd

+99 -51
+2 -13
src/lib/api/index.ts
··· 4 4 AppBskyEmbedRecord, 5 5 AppBskyEmbedRecordWithMedia, 6 6 AppBskyFeedThreadgate, 7 - AppBskyRichtextFacet, 8 7 BskyAgent, 9 8 ComAtprotoLabelDefs, 10 9 ComAtprotoRepoUploadBlob, ··· 15 14 import {logger} from '#/logger' 16 15 import {ThreadgateSetting} from '#/state/queries/threadgate' 17 16 import {isNetworkError} from 'lib/strings/errors' 18 - import {shortenLinks} from 'lib/strings/rich-text-manip' 17 + import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip' 19 18 import {isNative, isWeb} from 'platform/detection' 20 19 import {ImageModel} from 'state/models/media/image' 21 20 import {LinkMeta} from '../link-meta/link-meta' ··· 81 80 opts.onStateChange?.('Processing...') 82 81 await rt.detectFacets(agent) 83 82 rt = shortenLinks(rt) 84 - 85 - // filter out any mention facets that didn't map to a user 86 - rt.facets = rt.facets?.filter(facet => { 87 - const mention = facet.features.find(feature => 88 - AppBskyRichtextFacet.isMention(feature), 89 - ) 90 - if (mention && !mention.did) { 91 - return false 92 - } 93 - return true 94 - }) 83 + rt = stripInvalidMentions(rt) 95 84 96 85 // add quote embed if present 97 86 if (opts.quote) {
+20 -4
src/lib/strings/rich-text-manip.ts
··· 1 - import {RichText, UnicodeString} from '@atproto/api' 1 + import {AppBskyRichtextFacet, RichText, UnicodeString} from '@atproto/api' 2 2 3 3 import {toShortUrl} from './url-helpers' 4 4 ··· 10 10 // enumerate the link facets 11 11 if (rt.facets) { 12 12 for (const facet of rt.facets) { 13 - const isLink = !!facet.features.find( 14 - f => f.$type === 'app.bsky.richtext.facet#link', 15 - ) 13 + const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink) 16 14 if (!isLink) { 17 15 continue 18 16 } ··· 33 31 } 34 32 return rt 35 33 } 34 + 35 + // filter out any mention facets that didn't map to a user 36 + export function stripInvalidMentions(rt: RichText): RichText { 37 + if (!rt.facets?.length) { 38 + return rt 39 + } 40 + rt = rt.clone() 41 + if (rt.facets) { 42 + rt.facets = rt.facets?.filter(facet => { 43 + const mention = facet.features.find(AppBskyRichtextFacet.isMention) 44 + if (mention && !mention.did) { 45 + return false 46 + } 47 + return true 48 + }) 49 + } 50 + return rt 51 + }
+1 -1
src/screens/Messages/Conversation/MessageInput.tsx
··· 63 63 return 64 64 } 65 65 clearDraft() 66 - onSendMessage(message.trimEnd()) 66 + onSendMessage(message) 67 67 playHaptic() 68 68 setMessage('') 69 69
+1 -1
src/screens/Messages/Conversation/MessageInput.web.tsx
··· 43 43 return 44 44 } 45 45 clearDraft() 46 - onSendMessage(message.trimEnd()) 46 + onSendMessage(message) 47 47 setMessage('') 48 48 }, [message, onSendMessage, _, clearDraft]) 49 49
+71 -14
src/screens/Messages/Conversation/MessagesList.tsx
··· 13 13 } from 'react-native-reanimated' 14 14 import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' 15 15 import {useSafeAreaInsets} from 'react-native-safe-area-context' 16 - import {AppBskyRichtextFacet, RichText} from '@atproto/api' 16 + import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api' 17 17 18 - import {shortenLinks} from '#/lib/strings/rich-text-manip' 18 + import {getPostAsQuote} from '#/lib/link-meta/bsky' 19 + import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 20 + import {isBskyPostUrl} from '#/lib/strings/url-helpers' 21 + import {logger} from '#/logger' 19 22 import {isNative} from '#/platform/detection' 20 23 import {isConvoActive, useConvoActive} from '#/state/messages/convo' 21 24 import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' 25 + import {useGetPost} from '#/state/queries/post' 22 26 import {useAgent} from '#/state/session' 23 27 import {clamp} from 'lib/numbers' 24 28 import {ScrollProvider} from 'lib/ScrollContext' ··· 80 84 }) { 81 85 const convoState = useConvoActive() 82 86 const agent = useAgent() 87 + const getPost = useGetPost() 83 88 84 89 const flatListRef = useAnimatedRef<FlatList>() 85 90 ··· 264 269 // -- Message sending 265 270 const onSendMessage = useCallback( 266 271 async (text: string) => { 267 - let rt = new RichText({text}, {cleanNewlines: true}) 268 - await rt.detectFacets(agent) 269 - rt = shortenLinks(rt) 272 + let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true}) 270 273 271 - // filter out any mention facets that didn't map to a user 272 - rt.facets = rt.facets?.filter(facet => { 273 - const mention = facet.features.find(feature => 274 - AppBskyRichtextFacet.isMention(feature), 275 - ) 276 - if (mention && !mention.did) { 274 + // detect facets without resolution first - this is used to see if there's 275 + // any post links in the text that we can embed. We do this first because 276 + // we want to remove the post link from the text, re-trim, then detect facets 277 + rt.detectFacetsWithoutResolution() 278 + 279 + let embed: AppBskyEmbedRecord.Main | undefined 280 + // find the first link facet that is a link to a post 281 + const postLinkFacet = rt.facets?.find(facet => { 282 + return facet.features.find(feature => { 283 + if (AppBskyRichtextFacet.isLink(feature)) { 284 + return isBskyPostUrl(feature.uri) 285 + } 277 286 return false 278 - } 279 - return true 287 + }) 280 288 }) 281 289 290 + // if we found a post link, get the post and embed it 291 + if (postLinkFacet) { 292 + const postLink = postLinkFacet.features.find( 293 + AppBskyRichtextFacet.isLink, 294 + ) 295 + if (!postLink) return 296 + 297 + try { 298 + const post = await getPostAsQuote(getPost, postLink.uri) 299 + if (post) { 300 + embed = { 301 + $type: 'app.bsky.embed.record', 302 + record: { 303 + uri: post.uri, 304 + cid: post.cid, 305 + }, 306 + } 307 + 308 + // remove the post link from the text 309 + rt.delete( 310 + postLinkFacet.index.byteStart, 311 + postLinkFacet.index.byteEnd, 312 + ) 313 + 314 + // re-trim the text, now that we've removed the post link 315 + // 316 + // if the post link is at the start of the text, we don't want to leave a leading space 317 + // so trim on both sides 318 + if (postLinkFacet.index.byteStart === 0) { 319 + rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true}) 320 + } else { 321 + // otherwise just trim the end 322 + rt = new RichText( 323 + {text: rt.text.trimEnd()}, 324 + {cleanNewlines: true}, 325 + ) 326 + } 327 + } 328 + } catch (error) { 329 + logger.error('Failed to get post as quote for DM', {error}) 330 + } 331 + } 332 + 333 + await rt.detectFacets(agent) 334 + 335 + rt = shortenLinks(rt) 336 + rt = stripInvalidMentions(rt) 337 + 282 338 if (!hasScrolled) { 283 339 setHasScrolled(true) 284 340 } ··· 286 342 convoState.sendMessage({ 287 343 text: rt.text, 288 344 facets: rt.facets, 345 + embed, 289 346 }) 290 347 }, 291 - [convoState, agent, hasScrolled, setHasScrolled], 348 + [agent, convoState, getPost, hasScrolled, setHasScrolled], 292 349 ) 293 350 294 351 // -- List layout changes (opening emoji keyboard, etc.)
+1 -1
src/state/messages/convo/agent.ts
··· 753 753 754 754 sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) { 755 755 // Ignore empty messages for now since they have no other purpose atm 756 - if (!message.text.trim()) return 756 + if (!message.text.trim() && !message.embed) return 757 757 758 758 logger.debug('Convo: send message', {}, logger.DebugContext.convo) 759 759
+3 -17
src/view/com/modals/CreateOrEditList.tsx
··· 10 10 } from 'react-native' 11 11 import {Image as RNImage} from 'react-native-image-crop-picker' 12 12 import {LinearGradient} from 'expo-linear-gradient' 13 - import { 14 - AppBskyGraphDefs, 15 - AppBskyRichtextFacet, 16 - RichText as RichTextAPI, 17 - } from '@atproto/api' 13 + import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' 18 14 import {msg, Trans} from '@lingui/macro' 19 15 import {useLingui} from '@lingui/react' 20 16 21 17 import {richTextToString} from '#/lib/strings/rich-text-helpers' 22 - import {shortenLinks} from '#/lib/strings/rich-text-manip' 18 + import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 23 19 import {useModalControls} from '#/state/modals' 24 20 import { 25 21 useListCreateMutation, ··· 159 155 160 156 await richText.detectFacets(agent) 161 157 richText = shortenLinks(richText) 162 - 163 - // filter out any mention facets that didn't map to a user 164 - richText.facets = richText.facets?.filter(facet => { 165 - const mention = facet.features.find(feature => 166 - AppBskyRichtextFacet.isMention(feature), 167 - ) 168 - if (mention && !mention.did) { 169 - return false 170 - } 171 - return true 172 - }) 158 + richText = stripInvalidMentions(richText) 173 159 174 160 if (list) { 175 161 await listMetadataMutation.mutateAsync({