From cd3b502b343e5e79d9a6df77d08935829b655f55 Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Fri, 31 May 2024 19:10:00 +0300
Subject: [🐴] Option to share via chat in post dropdown (#4231)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* add send via chat button to post dropdown

(cherry picked from commit d8458c0bc344f993266f7bc7e325d47e40619648)

* let usePostQuery take uris with DIDs

(cherry picked from commit 16b577ce749fd07e1d5f8461e8ca71c5b874a936)

* add embed preview in composer

(cherry picked from commit 795ceb98d55b6a3ab5b83187a582f9656d71db69)

* rm log

(cherry picked from commit 374d6b8869459f08d8442a3a47d67149e8d9ddd4)

* remove params properly, or at least as close to

(cherry picked from commit c20e0062c2ca4d9c2b28324eee5e713a1a3ab251)

* show images in preview

(cherry picked from commit 5bb617a3ce00f67bfc79784b2f81ef8dcb5bfc25)

* Register embed immediately

(cherry picked from commit ee120d5438a2c91c8980288665576d6a29b4c7e7)

* Add hover to match embeds

(cherry picked from commit 5297a5b06e499f46a9f6da510124610005db2448)

* Update post dropdown copy

(cherry picked from commit bc7e9f6a4303926a53c5c889f1f1b136faf20491)

* Embed preview style tweaks

(cherry picked from commit 9e3ccb0f25ac2f3ce6af538bb29112a3e96e01b1)

* use hydrated posts from API and just use postembed component

(cherry picked from commit cc0b84db87ca812d76cc69f46170ae84cfdde4ef)

* fix type error

(cherry picked from commit 9c49b940e1248e8a7c3b64190c5cb20750043619)

* undo needless export

(cherry picked from commit 1186701c997c50c0b29a809637cb9bc061b8c0a0)

* fix overflow

(cherry picked from commit 8868d5075062d0199c8ef6946fabde27e46ea378)

---------

Co-authored-by: Eric Bailey <git@esb.lol>
---
 .../Messages/Conversation/MessageInputEmbed.tsx    | 231 +++++++++++++++++++++
 1 file changed, 231 insertions(+)
 create mode 100644 src/screens/Messages/Conversation/MessageInputEmbed.tsx

(limited to 'src/screens/Messages/Conversation/MessageInputEmbed.tsx')

diff --git a/src/screens/Messages/Conversation/MessageInputEmbed.tsx b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
new file mode 100644
index 000000000..4fdd31bcf
--- /dev/null
+++ b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
@@ -0,0 +1,231 @@
+import React, {useCallback, useEffect, useMemo, useState} from 'react'
+import {LayoutAnimation, View} from 'react-native'
+import {
+  AppBskyEmbedImages,
+  AppBskyEmbedRecordWithMedia,
+  AppBskyFeedPost,
+  AppBskyRichtextFacet,
+  AtUri,
+  RichText as RichTextAPI,
+} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'
+
+import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
+import {makeProfileLink} from '#/lib/routes/links'
+import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
+import {
+  convertBskyAppUrlIfNeeded,
+  isBskyPostUrl,
+  makeRecordUri,
+} from '#/lib/strings/url-helpers'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {usePostQuery} from '#/state/queries/post'
+import {ImageHorzList} from '#/view/com/util/images/ImageHorzList'
+import {PostMeta} from '#/view/com/util/PostMeta'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {Loader} from '#/components/Loader'
+import {ContentHider} from '#/components/moderation/ContentHider'
+import {PostAlerts} from '#/components/moderation/PostAlerts'
+import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
+
+export function useMessageEmbed() {
+  const route =
+    useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>()
+  const navigation = useNavigation<NavigationProp>()
+  const embedFromParams = route.params.embed
+
+  const [embedUri, setEmbed] = useState(embedFromParams)
+
+  if (embedFromParams && embedUri !== embedFromParams) {
+    setEmbed(embedFromParams)
+  }
+
+  return {
+    embedUri,
+    setEmbed: useCallback(
+      (embedUrl: string | undefined) => {
+        if (!embedUrl) {
+          navigation.setParams({embed: ''})
+          setEmbed(undefined)
+          return
+        }
+
+        if (embedFromParams) return
+
+        const url = convertBskyAppUrlIfNeeded(embedUrl)
+        const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
+        const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
+
+        setEmbed(uri)
+      },
+      [embedFromParams, navigation],
+    ),
+  }
+}
+
+export function useExtractEmbedFromFacets(
+  message: string,
+  setEmbed: (embedUrl: string | undefined) => void,
+) {
+  const rt = new RichTextAPI({text: message})
+  rt.detectFacetsWithoutResolution()
+
+  let uriFromFacet: string | undefined
+
+  for (const facet of rt.facets ?? []) {
+    for (const feature of facet.features) {
+      if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) {
+        uriFromFacet = feature.uri
+        break
+      }
+    }
+  }
+
+  useEffect(() => {
+    if (uriFromFacet) {
+      setEmbed(uriFromFacet)
+    }
+  }, [uriFromFacet, setEmbed])
+}
+
+export function MessageInputEmbed({
+  embedUri,
+  setEmbed,
+}: {
+  embedUri: string | undefined
+  setEmbed: (embedUrl: string | undefined) => void
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const {data: post, status} = usePostQuery(embedUri)
+
+  const moderationOpts = useModerationOpts()
+  const moderation = useMemo(
+    () =>
+      moderationOpts && post ? moderatePost(post, moderationOpts) : undefined,
+    [moderationOpts, post],
+  )
+
+  const {rt, record} = useMemo(() => {
+    if (
+      post &&
+      AppBskyFeedPost.isRecord(post.record) &&
+      AppBskyFeedPost.validateRecord(post.record).success
+    ) {
+      return {
+        rt: new RichTextAPI({
+          text: post.record.text,
+          facets: post.record.facets,
+        }),
+        record: post.record,
+      }
+    }
+
+    return {rt: undefined, record: undefined}
+  }, [post])
+
+  if (!embedUri) {
+    return null
+  }
+
+  let content = null
+  switch (status) {
+    case 'pending':
+      content = (
+        <View
+          style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
+          <Loader />
+        </View>
+      )
+      break
+    case 'error':
+      content = (
+        <View
+          style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
+          <Text style={a.text_center}>Could not fetch post</Text>
+        </View>
+      )
+      break
+    case 'success':
+      const itemUrip = new AtUri(post.uri)
+      const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
+
+      if (!post || !moderation || !rt || !record) {
+        return null
+      }
+
+      const images = AppBskyEmbedImages.isView(post.embed)
+        ? post.embed.images
+        : AppBskyEmbedRecordWithMedia.isView(post.embed) &&
+          AppBskyEmbedImages.isView(post.embed.media)
+        ? post.embed.media.images
+        : undefined
+
+      content = (
+        <View
+          style={[
+            a.flex_1,
+            t.atoms.bg,
+            t.atoms.border_contrast_low,
+            a.rounded_md,
+            a.border,
+            a.p_sm,
+            a.mb_sm,
+          ]}
+          pointerEvents="none">
+          <PostMeta
+            showAvatar
+            author={post.author}
+            moderation={moderation}
+            authorHasWarning={!!post.author.labels?.length}
+            timestamp={post.indexedAt}
+            postHref={itemHref}
+            style={a.flex_0}
+          />
+          <ContentHider modui={moderation.ui('contentView')}>
+            <PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} />
+            {rt.text && (
+              <View style={a.mt_xs}>
+                <RichText
+                  enableTags
+                  testID="postText"
+                  value={rt}
+                  style={[a.text_sm, t.atoms.text_contrast_high]}
+                  authorHandle={post.author.handle}
+                  numberOfLines={3}
+                />
+              </View>
+            )}
+            {images && images?.length > 0 && (
+              <ImageHorzList images={images} style={a.mt_xs} />
+            )}
+          </ContentHider>
+        </View>
+      )
+      break
+  }
+
+  return (
+    <View style={[a.flex_row, a.gap_sm]}>
+      {content}
+      <Button
+        label={_(msg`Remove embed`)}
+        onPress={() => {
+          LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+          setEmbed(undefined)
+        }}
+        size="tiny"
+        variant="solid"
+        color="secondary"
+        shape="round">
+        <ButtonIcon icon={X} />
+      </Button>
+    </View>
+  )
+}
-- 
cgit 1.4.1

