about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/lib/api/index.ts15
-rw-r--r--src/lib/strings/rich-text-manip.ts24
-rw-r--r--src/screens/Messages/Conversation/MessageInput.tsx2
-rw-r--r--src/screens/Messages/Conversation/MessageInput.web.tsx2
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx85
-rw-r--r--src/state/messages/convo/agent.ts2
-rw-r--r--src/view/com/modals/CreateOrEditList.tsx20
7 files changed, 99 insertions, 51 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index bc50f9cb3..dfaae2e01 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -4,7 +4,6 @@ import {
   AppBskyEmbedRecord,
   AppBskyEmbedRecordWithMedia,
   AppBskyFeedThreadgate,
-  AppBskyRichtextFacet,
   BskyAgent,
   ComAtprotoLabelDefs,
   ComAtprotoRepoUploadBlob,
@@ -15,7 +14,7 @@ import {AtUri} from '@atproto/api'
 import {logger} from '#/logger'
 import {ThreadgateSetting} from '#/state/queries/threadgate'
 import {isNetworkError} from 'lib/strings/errors'
-import {shortenLinks} from 'lib/strings/rich-text-manip'
+import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
 import {isNative, isWeb} from 'platform/detection'
 import {ImageModel} from 'state/models/media/image'
 import {LinkMeta} from '../link-meta/link-meta'
@@ -81,17 +80,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
   opts.onStateChange?.('Processing...')
   await rt.detectFacets(agent)
   rt = shortenLinks(rt)
-
-  // filter out any mention facets that didn't map to a user
-  rt.facets = rt.facets?.filter(facet => {
-    const mention = facet.features.find(feature =>
-      AppBskyRichtextFacet.isMention(feature),
-    )
-    if (mention && !mention.did) {
-      return false
-    }
-    return true
-  })
+  rt = stripInvalidMentions(rt)
 
   // add quote embed if present
   if (opts.quote) {
diff --git a/src/lib/strings/rich-text-manip.ts b/src/lib/strings/rich-text-manip.ts
index 508e0772e..2e84656f2 100644
--- a/src/lib/strings/rich-text-manip.ts
+++ b/src/lib/strings/rich-text-manip.ts
@@ -1,4 +1,4 @@
-import {RichText, UnicodeString} from '@atproto/api'
+import {AppBskyRichtextFacet, RichText, UnicodeString} from '@atproto/api'
 
 import {toShortUrl} from './url-helpers'
 
@@ -10,9 +10,7 @@ export function shortenLinks(rt: RichText): RichText {
   // enumerate the link facets
   if (rt.facets) {
     for (const facet of rt.facets) {
-      const isLink = !!facet.features.find(
-        f => f.$type === 'app.bsky.richtext.facet#link',
-      )
+      const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink)
       if (!isLink) {
         continue
       }
@@ -33,3 +31,21 @@ export function shortenLinks(rt: RichText): RichText {
   }
   return rt
 }
+
+// filter out any mention facets that didn't map to a user
+export function stripInvalidMentions(rt: RichText): RichText {
+  if (!rt.facets?.length) {
+    return rt
+  }
+  rt = rt.clone()
+  if (rt.facets) {
+    rt.facets = rt.facets?.filter(facet => {
+      const mention = facet.features.find(AppBskyRichtextFacet.isMention)
+      if (mention && !mention.did) {
+        return false
+      }
+      return true
+    })
+  }
+  return rt
+}
diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx
index 698fc6b7a..149188684 100644
--- a/src/screens/Messages/Conversation/MessageInput.tsx
+++ b/src/screens/Messages/Conversation/MessageInput.tsx
@@ -63,7 +63,7 @@ export function MessageInput({
       return
     }
     clearDraft()
-    onSendMessage(message.trimEnd())
+    onSendMessage(message)
     playHaptic()
     setMessage('')
 
diff --git a/src/screens/Messages/Conversation/MessageInput.web.tsx b/src/screens/Messages/Conversation/MessageInput.web.tsx
index 5d8d568ff..a61355e55 100644
--- a/src/screens/Messages/Conversation/MessageInput.web.tsx
+++ b/src/screens/Messages/Conversation/MessageInput.web.tsx
@@ -43,7 +43,7 @@ export function MessageInput({
       return
     }
     clearDraft()
-    onSendMessage(message.trimEnd())
+    onSendMessage(message)
     setMessage('')
   }, [message, onSendMessage, _, clearDraft])
 
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index 583c40852..d6aa06a1c 100644
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -13,12 +13,16 @@ import {
 } from 'react-native-reanimated'
 import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {AppBskyRichtextFacet, RichText} from '@atproto/api'
+import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
 
-import {shortenLinks} from '#/lib/strings/rich-text-manip'
+import {getPostAsQuote} from '#/lib/link-meta/bsky'
+import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
+import {isBskyPostUrl} from '#/lib/strings/url-helpers'
+import {logger} from '#/logger'
 import {isNative} from '#/platform/detection'
 import {isConvoActive, useConvoActive} from '#/state/messages/convo'
 import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
+import {useGetPost} from '#/state/queries/post'
 import {useAgent} from '#/state/session'
 import {clamp} from 'lib/numbers'
 import {ScrollProvider} from 'lib/ScrollContext'
@@ -80,6 +84,7 @@ export function MessagesList({
 }) {
   const convoState = useConvoActive()
   const agent = useAgent()
+  const getPost = useGetPost()
 
   const flatListRef = useAnimatedRef<FlatList>()
 
@@ -264,20 +269,71 @@ export function MessagesList({
   // -- Message sending
   const onSendMessage = useCallback(
     async (text: string) => {
-      let rt = new RichText({text}, {cleanNewlines: true})
-      await rt.detectFacets(agent)
-      rt = shortenLinks(rt)
+      let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
+
+      // detect facets without resolution first - this is used to see if there's
+      // any post links in the text that we can embed. We do this first because
+      // we want to remove the post link from the text, re-trim, then detect facets
+      rt.detectFacetsWithoutResolution()
+
+      let embed: AppBskyEmbedRecord.Main | undefined
+      // find the first link facet that is a link to a post
+      const postLinkFacet = rt.facets?.find(facet => {
+        return facet.features.find(feature => {
+          if (AppBskyRichtextFacet.isLink(feature)) {
+            return isBskyPostUrl(feature.uri)
+          }
+          return false
+        })
+      })
 
-      // filter out any mention facets that didn't map to a user
-      rt.facets = rt.facets?.filter(facet => {
-        const mention = facet.features.find(feature =>
-          AppBskyRichtextFacet.isMention(feature),
+      // if we found a post link, get the post and embed it
+      if (postLinkFacet) {
+        const postLink = postLinkFacet.features.find(
+          AppBskyRichtextFacet.isLink,
         )
-        if (mention && !mention.did) {
-          return false
+        if (!postLink) return
+
+        try {
+          const post = await getPostAsQuote(getPost, postLink.uri)
+          if (post) {
+            embed = {
+              $type: 'app.bsky.embed.record',
+              record: {
+                uri: post.uri,
+                cid: post.cid,
+              },
+            }
+
+            // remove the post link from the text
+            rt.delete(
+              postLinkFacet.index.byteStart,
+              postLinkFacet.index.byteEnd,
+            )
+
+            // re-trim the text, now that we've removed the post link
+            //
+            // if the post link is at the start of the text, we don't want to leave a leading space
+            // so trim on both sides
+            if (postLinkFacet.index.byteStart === 0) {
+              rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true})
+            } else {
+              // otherwise just trim the end
+              rt = new RichText(
+                {text: rt.text.trimEnd()},
+                {cleanNewlines: true},
+              )
+            }
+          }
+        } catch (error) {
+          logger.error('Failed to get post as quote for DM', {error})
         }
-        return true
-      })
+      }
+
+      await rt.detectFacets(agent)
+
+      rt = shortenLinks(rt)
+      rt = stripInvalidMentions(rt)
 
       if (!hasScrolled) {
         setHasScrolled(true)
@@ -286,9 +342,10 @@ export function MessagesList({
       convoState.sendMessage({
         text: rt.text,
         facets: rt.facets,
+        embed,
       })
     },
-    [convoState, agent, hasScrolled, setHasScrolled],
+    [agent, convoState, getPost, hasScrolled, setHasScrolled],
   )
 
   // -- List layout changes (opening emoji keyboard, etc.)
diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts
index a0355ab07..9850124c9 100644
--- a/src/state/messages/convo/agent.ts
+++ b/src/state/messages/convo/agent.ts
@@ -753,7 +753,7 @@ export class Convo {
 
   sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
     // Ignore empty messages for now since they have no other purpose atm
-    if (!message.text.trim()) return
+    if (!message.text.trim() && !message.embed) return
 
     logger.debug('Convo: send message', {}, logger.DebugContext.convo)
 
diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx
index 2ea34e808..3088c92a1 100644
--- a/src/view/com/modals/CreateOrEditList.tsx
+++ b/src/view/com/modals/CreateOrEditList.tsx
@@ -10,16 +10,12 @@ import {
 } from 'react-native'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {LinearGradient} from 'expo-linear-gradient'
-import {
-  AppBskyGraphDefs,
-  AppBskyRichtextFacet,
-  RichText as RichTextAPI,
-} from '@atproto/api'
+import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {richTextToString} from '#/lib/strings/rich-text-helpers'
-import {shortenLinks} from '#/lib/strings/rich-text-manip'
+import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
 import {useModalControls} from '#/state/modals'
 import {
   useListCreateMutation,
@@ -159,17 +155,7 @@ export function Component({
 
       await richText.detectFacets(agent)
       richText = shortenLinks(richText)
-
-      // filter out any mention facets that didn't map to a user
-      richText.facets = richText.facets?.filter(facet => {
-        const mention = facet.features.find(feature =>
-          AppBskyRichtextFacet.isMention(feature),
-        )
-        if (mention && !mention.did) {
-          return false
-        }
-        return true
-      })
+      richText = stripInvalidMentions(richText)
 
       if (list) {
         await listMetadataMutation.mutateAsync({