about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-05-31 18:43:04 +0300
committerGitHub <noreply@github.com>2024-05-31 10:43:04 -0500
commit22e1eb18c81b6f41927bc86d4726223c2634e19e (patch)
tree1fbd17678b2922667affa895270dff1f634216e0 /src/components
parent8eb3cebb362cc438e368a9fbb78e2f85403ffeed (diff)
downloadvoidsky-22e1eb18c81b6f41927bc86d4726223c2634e19e.tar.zst
[🐴] Record message (#4230)
* send record via link in text

* re-trim text after removing link

* record message

* only show copy text if message + add translate

* reduce padding

* adjust padding

* Tweak spacing

* Stop clickthrough for hidden content

* Update bg to show labels

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src/components')
-rw-r--r--src/components/dms/MessageItem.tsx74
-rw-r--r--src/components/dms/MessageItemEmbed.tsx109
-rw-r--r--src/components/dms/MessageMenu.tsx47
-rw-r--r--src/components/moderation/ContentHider.tsx17
4 files changed, 198 insertions, 49 deletions
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index c5ff81091..b498ddf1c 100644
--- a/src/components/dms/MessageItem.tsx
+++ b/src/components/dms/MessageItem.tsx
@@ -6,7 +6,11 @@ import {
   TextStyle,
   View,
 } from 'react-native'
-import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
+import {
+  AppBskyEmbedRecord,
+  ChatBskyConvoDefs,
+  RichText as RichTextAPI,
+} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -18,6 +22,7 @@ import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
 import {InlineLinkText} from '#/components/Link'
 import {Text} from '#/components/Typography'
 import {RichText} from '../RichText'
+import {MessageItemEmbed} from './MessageItemEmbed'
 
 let MessageItem = ({
   item,
@@ -77,37 +82,44 @@ let MessageItem = ({
   return (
     <View style={[isFromSelf ? a.mr_md : a.ml_md]}>
       <ActionsWrapper isFromSelf={isFromSelf} message={message}>
-        <View
-          style={[
-            a.py_sm,
-            a.my_2xs,
-            a.rounded_md,
-            {
-              paddingLeft: 14,
-              paddingRight: 14,
-              backgroundColor: isFromSelf
-                ? isPending
-                  ? pendingColor
-                  : t.palette.primary_500
-                : t.palette.contrast_50,
-              borderRadius: 17,
-            },
-            isFromSelf
-              ? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
-              : {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
-          ]}>
-          <RichText
-            value={rt}
+        {AppBskyEmbedRecord.isMain(message.embed) && (
+          <MessageItemEmbed embed={message.embed} />
+        )}
+        {rt.text.length > 0 && (
+          <View
             style={[
-              a.text_md,
-              a.leading_snug,
-              isFromSelf && {color: t.palette.white},
-              isPending && t.name !== 'light' && {color: t.palette.primary_300},
-            ]}
-            interactiveStyle={a.underline}
-            enableTags
-          />
-        </View>
+              a.py_sm,
+              a.my_2xs,
+              a.rounded_md,
+              {
+                paddingLeft: 14,
+                paddingRight: 14,
+                backgroundColor: isFromSelf
+                  ? isPending
+                    ? pendingColor
+                    : t.palette.primary_500
+                  : t.palette.contrast_50,
+                borderRadius: 17,
+              },
+              isFromSelf ? a.self_end : a.self_start,
+              isFromSelf
+                ? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
+                : {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
+            ]}>
+            <RichText
+              value={rt}
+              style={[
+                a.text_md,
+                a.leading_snug,
+                isFromSelf && {color: t.palette.white},
+                isPending &&
+                  t.name !== 'light' && {color: t.palette.primary_300},
+              ]}
+              interactiveStyle={a.underline}
+              enableTags
+            />
+          </View>
+        )}
       </ActionsWrapper>
 
       {isLastInGroup && (
diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx
new file mode 100644
index 000000000..d64563b91
--- /dev/null
+++ b/src/components/dms/MessageItemEmbed.tsx
@@ -0,0 +1,109 @@
+import React, {useMemo} from 'react'
+import {View} from 'react-native'
+import {
+  AppBskyEmbedRecord,
+  AppBskyFeedPost,
+  AtUri,
+  RichText as RichTextAPI,
+} from '@atproto/api'
+
+import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
+import {makeProfileLink} from '#/lib/routes/links'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {usePostQuery} from '#/state/queries/post'
+import {PostEmbeds} from '#/view/com/util/post-embeds'
+import {PostMeta} from '#/view/com/util/PostMeta'
+import {atoms as a, useTheme} from '#/alf'
+import {Link} from '#/components/Link'
+import {ContentHider} from '#/components/moderation/ContentHider'
+import {PostAlerts} from '#/components/moderation/PostAlerts'
+import {RichText} from '#/components/RichText'
+
+let MessageItemEmbed = ({
+  embed,
+}: {
+  embed: AppBskyEmbedRecord.Main
+}): React.ReactNode => {
+  const t = useTheme()
+  const {data: post} = usePostQuery(embed.record.uri)
+
+  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 (!post || !moderation || !rt || !record) {
+    return null
+  }
+
+  const itemUrip = new AtUri(post.uri)
+  const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
+
+  return (
+    <Link to={itemHref}>
+      <View
+        style={[
+          a.w_full,
+          t.atoms.bg,
+          t.atoms.border_contrast_low,
+          a.rounded_md,
+          a.border,
+          a.p_md,
+          a.my_xs,
+        ]}>
+        <PostMeta
+          showAvatar
+          author={post.author}
+          moderation={moderation}
+          authorHasWarning={!!post.author.labels?.length}
+          timestamp={post.indexedAt}
+          postHref={itemHref}
+        />
+        <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}
+              />
+            </View>
+          )}
+          {post.embed && (
+            <PostEmbeds
+              embed={post.embed}
+              moderation={moderation}
+              style={a.mt_xs}
+              quoteTextStyle={[a.text_sm, t.atoms.text_contrast_high]}
+            />
+          )}
+        </ContentHider>
+      </View>
+    </Link>
+  )
+}
+MessageItemEmbed = React.memo(MessageItemEmbed)
+export {MessageItemEmbed}
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx
index 21812d268..92913d1cb 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageMenu.tsx
@@ -6,12 +6,16 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {richTextToString} from '#/lib/strings/rich-text-helpers'
+import {getTranslatorLink} from '#/locale/helpers'
+import {useLanguagePrefs} from '#/state/preferences'
+import {useOpenLink} from '#/state/preferences/in-app-browser'
 import {isWeb} from 'platform/detection'
 import {useConvoActive} from 'state/messages/convo'
 import {useSession} from 'state/session'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
 import {ReportDialog} from '#/components/dms/ReportDialog'
+import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
 import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
 import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
@@ -35,10 +39,12 @@ export let MessageMenu = ({
   const convo = useConvoActive()
   const deleteControl = usePromptControl()
   const reportControl = usePromptControl()
+  const langPrefs = useLanguagePrefs()
+  const openLink = useOpenLink()
 
   const isFromSelf = message.sender?.did === currentAccount?.did
 
-  const onCopyPostText = React.useCallback(() => {
+  const onCopyMessage = React.useCallback(() => {
     const str = richTextToString(
       new RichText({
         text: message.text,
@@ -51,6 +57,14 @@ export let MessageMenu = ({
     Toast.show(_(msg`Copied to clipboard`))
   }, [_, message.text, message.facets])
 
+  const onPressTranslateMessage = React.useCallback(() => {
+    const translatorUrl = getTranslatorLink(
+      message.text,
+      langPrefs.primaryLanguage,
+    )
+    openLink(translatorUrl)
+  }, [langPrefs.primaryLanguage, message.text, openLink])
+
   const onDelete = React.useCallback(() => {
     LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
     convo
@@ -81,16 +95,27 @@ export let MessageMenu = ({
         )}
 
         <Menu.Outer>
-          <Menu.Group>
-            <Menu.Item
-              testID="messageDropdownCopyBtn"
-              label={_(msg`Copy message text`)}
-              onPress={onCopyPostText}>
-              <Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText>
-              <Menu.ItemIcon icon={ClipboardIcon} position="right" />
-            </Menu.Item>
-          </Menu.Group>
-          <Menu.Divider />
+          {message.text.length > 0 && (
+            <>
+              <Menu.Group>
+                <Menu.Item
+                  testID="messageDropdownTranslateBtn"
+                  label={_(msg`Translate`)}
+                  onPress={onPressTranslateMessage}>
+                  <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
+                  <Menu.ItemIcon icon={Translate} position="right" />
+                </Menu.Item>
+                <Menu.Item
+                  testID="messageDropdownCopyBtn"
+                  label={_(msg`Copy message text`)}
+                  onPress={onCopyMessage}>
+                  <Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText>
+                  <Menu.ItemIcon icon={ClipboardIcon} position="right" />
+                </Menu.Item>
+              </Menu.Group>
+              <Menu.Divider />
+            </>
+          )}
           <Menu.Group>
             <Menu.Item
               testID="messageDropdownDeleteBtn"
diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx
index 1e8f36d31..fd71ec838 100644
--- a/src/components/moderation/ContentHider.tsx
+++ b/src/components/moderation/ContentHider.tsx
@@ -1,20 +1,19 @@
 import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {ModerationUI} from '@atproto/api'
-import {useLingui} from '@lingui/react'
 import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
-import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
 import {isJustAMute} from '#/lib/moderation'
+import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
-
-import {atoms as a, useTheme, useBreakpoints, web} from '#/alf'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
 import {Button} from '#/components/Button'
-import {Text} from '#/components/Typography'
 import {
   ModerationDetailsDialog,
   useModerationDetailsDialogControl,
 } from '#/components/moderation/ModerationDetailsDialog'
+import {Text} from '#/components/Typography'
 
 export function ContentHider({
   testID,
@@ -52,7 +51,9 @@ export function ContentHider({
       <ModerationDetailsDialog control={control} modcause={blur} />
 
       <Button
-        onPress={() => {
+        onPress={e => {
+          e.preventDefault()
+          e.stopPropagation()
           if (!modui.noOverride) {
             setOverride(v => !v)
           } else {
@@ -121,7 +122,9 @@ export function ContentHider({
 
       {desc.source && blur.type === 'label' && !override && (
         <Button
-          onPress={() => {
+          onPress={e => {
+            e.preventDefault()
+            e.stopPropagation()
             control.open()
           }}
           label={_(