···1313} from 'react-native-reanimated'
1414import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
1515import {useSafeAreaInsets} from 'react-native-safe-area-context'
1616-import {AppBskyRichtextFacet, RichText} from '@atproto/api'
1616+import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
17171818-import {shortenLinks} from '#/lib/strings/rich-text-manip'
1818+import {getPostAsQuote} from '#/lib/link-meta/bsky'
1919+import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
2020+import {isBskyPostUrl} from '#/lib/strings/url-helpers'
2121+import {logger} from '#/logger'
1922import {isNative} from '#/platform/detection'
2023import {isConvoActive, useConvoActive} from '#/state/messages/convo'
2124import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
2525+import {useGetPost} from '#/state/queries/post'
2226import {useAgent} from '#/state/session'
2327import {clamp} from 'lib/numbers'
2428import {ScrollProvider} from 'lib/ScrollContext'
···8084}) {
8185 const convoState = useConvoActive()
8286 const agent = useAgent()
8787+ const getPost = useGetPost()
83888489 const flatListRef = useAnimatedRef<FlatList>()
8590···264269 // -- Message sending
265270 const onSendMessage = useCallback(
266271 async (text: string) => {
267267- let rt = new RichText({text}, {cleanNewlines: true})
268268- await rt.detectFacets(agent)
269269- rt = shortenLinks(rt)
272272+ let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
270273271271- // filter out any mention facets that didn't map to a user
272272- rt.facets = rt.facets?.filter(facet => {
273273- const mention = facet.features.find(feature =>
274274- AppBskyRichtextFacet.isMention(feature),
275275- )
276276- if (mention && !mention.did) {
274274+ // detect facets without resolution first - this is used to see if there's
275275+ // any post links in the text that we can embed. We do this first because
276276+ // we want to remove the post link from the text, re-trim, then detect facets
277277+ rt.detectFacetsWithoutResolution()
278278+279279+ let embed: AppBskyEmbedRecord.Main | undefined
280280+ // find the first link facet that is a link to a post
281281+ const postLinkFacet = rt.facets?.find(facet => {
282282+ return facet.features.find(feature => {
283283+ if (AppBskyRichtextFacet.isLink(feature)) {
284284+ return isBskyPostUrl(feature.uri)
285285+ }
277286 return false
278278- }
279279- return true
287287+ })
280288 })
281289290290+ // if we found a post link, get the post and embed it
291291+ if (postLinkFacet) {
292292+ const postLink = postLinkFacet.features.find(
293293+ AppBskyRichtextFacet.isLink,
294294+ )
295295+ if (!postLink) return
296296+297297+ try {
298298+ const post = await getPostAsQuote(getPost, postLink.uri)
299299+ if (post) {
300300+ embed = {
301301+ $type: 'app.bsky.embed.record',
302302+ record: {
303303+ uri: post.uri,
304304+ cid: post.cid,
305305+ },
306306+ }
307307+308308+ // remove the post link from the text
309309+ rt.delete(
310310+ postLinkFacet.index.byteStart,
311311+ postLinkFacet.index.byteEnd,
312312+ )
313313+314314+ // re-trim the text, now that we've removed the post link
315315+ //
316316+ // if the post link is at the start of the text, we don't want to leave a leading space
317317+ // so trim on both sides
318318+ if (postLinkFacet.index.byteStart === 0) {
319319+ rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true})
320320+ } else {
321321+ // otherwise just trim the end
322322+ rt = new RichText(
323323+ {text: rt.text.trimEnd()},
324324+ {cleanNewlines: true},
325325+ )
326326+ }
327327+ }
328328+ } catch (error) {
329329+ logger.error('Failed to get post as quote for DM', {error})
330330+ }
331331+ }
332332+333333+ await rt.detectFacets(agent)
334334+335335+ rt = shortenLinks(rt)
336336+ rt = stripInvalidMentions(rt)
337337+282338 if (!hasScrolled) {
283339 setHasScrolled(true)
284340 }
···286342 convoState.sendMessage({
287343 text: rt.text,
288344 facets: rt.facets,
345345+ embed,
289346 })
290347 },
291291- [convoState, agent, hasScrolled, setHasScrolled],
348348+ [agent, convoState, getPost, hasScrolled, setHasScrolled],
292349 )
293350294351 // -- List layout changes (opening emoji keyboard, etc.)
+1-1
src/state/messages/convo/agent.ts
···753753754754 sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
755755 // Ignore empty messages for now since they have no other purpose atm
756756- if (!message.text.trim()) return
756756+ if (!message.text.trim() && !message.embed) return
757757758758 logger.debug('Convo: send message', {}, logger.DebugContext.convo)
759759
+3-17
src/view/com/modals/CreateOrEditList.tsx
···1010} from 'react-native'
1111import {Image as RNImage} from 'react-native-image-crop-picker'
1212import {LinearGradient} from 'expo-linear-gradient'
1313-import {
1414- AppBskyGraphDefs,
1515- AppBskyRichtextFacet,
1616- RichText as RichTextAPI,
1717-} from '@atproto/api'
1313+import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
1814import {msg, Trans} from '@lingui/macro'
1915import {useLingui} from '@lingui/react'
20162117import {richTextToString} from '#/lib/strings/rich-text-helpers'
2222-import {shortenLinks} from '#/lib/strings/rich-text-manip'
1818+import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
2319import {useModalControls} from '#/state/modals'
2420import {
2521 useListCreateMutation,
···159155160156 await richText.detectFacets(agent)
161157 richText = shortenLinks(richText)
162162-163163- // filter out any mention facets that didn't map to a user
164164- richText.facets = richText.facets?.filter(facet => {
165165- const mention = facet.features.find(feature =>
166166- AppBskyRichtextFacet.isMention(feature),
167167- )
168168- if (mention && !mention.did) {
169169- return false
170170- }
171171- return true
172172- })
158158+ richText = stripInvalidMentions(richText)
173159174160 if (list) {
175161 await listMetadataMutation.mutateAsync({