From c4785ef96e13d02b217dce4e777269c0e895507d Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Fri, 21 Mar 2025 18:29:14 +0200
Subject: New `ContextMenu` menu type for DM messages (#8014)

* get context menu somewhat working ish

* take screenshot rather than double rendering

* get animations somewhat working

* get transform animation working

* rm log

* upwards safe area

* get working on android

* get android working once and for all

* fix positioning on both platforms

* use dark blur on ios always, fix dark mode

* allow closing with hardware back press

* try and fix type error

* add note about ts-ignore

* round post

* add image capture error handling

* extract magic numbers

* set explicit embed width, rm top margin

* Message embed width tweaks

* Format

* fix position of embeds

* same as above for web

---------

Co-authored-by: Eric Bailey <git@esb.lol>
---
 src/components/dms/MessageContextMenu.tsx | 151 ++++++++++++++++++++++++++++++
 1 file changed, 151 insertions(+)
 create mode 100644 src/components/dms/MessageContextMenu.tsx

(limited to 'src/components/dms/MessageContextMenu.tsx')

diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx
new file mode 100644
index 000000000..b5542690f
--- /dev/null
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -0,0 +1,151 @@
+import React from 'react'
+import {LayoutAnimation} from 'react-native'
+import * as Clipboard from 'expo-clipboard'
+import {ChatBskyConvoDefs, RichText} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useOpenLink} from '#/lib/hooks/useOpenLink'
+import {richTextToString} from '#/lib/strings/rich-text-helpers'
+import {getTranslatorLink} from '#/locale/helpers'
+import {useConvoActive} from '#/state/messages/convo'
+import {useLanguagePrefs} from '#/state/preferences'
+import {useSession} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+import * as ContextMenu from '#/components/ContextMenu'
+import {TriggerProps} from '#/components/ContextMenu/types'
+import {ReportDialog} from '#/components/dms/ReportDialog'
+import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
+import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
+import * as Prompt from '#/components/Prompt'
+import {usePromptControl} from '#/components/Prompt'
+
+export let MessageContextMenu = ({
+  message,
+  children,
+}: {
+  message: ChatBskyConvoDefs.MessageView
+  children: TriggerProps['children']
+}): React.ReactNode => {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const convo = useConvoActive()
+  const deleteControl = usePromptControl()
+  const reportControl = usePromptControl()
+  const langPrefs = useLanguagePrefs()
+  const openLink = useOpenLink()
+
+  const isFromSelf = message.sender?.did === currentAccount?.did
+
+  const onCopyMessage = React.useCallback(() => {
+    const str = richTextToString(
+      new RichText({
+        text: message.text,
+        facets: message.facets,
+      }),
+      true,
+    )
+
+    Clipboard.setStringAsync(str)
+    Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
+  }, [_, message.text, message.facets])
+
+  const onPressTranslateMessage = React.useCallback(() => {
+    const translatorUrl = getTranslatorLink(
+      message.text,
+      langPrefs.primaryLanguage,
+    )
+    openLink(translatorUrl, true)
+  }, [langPrefs.primaryLanguage, message.text, openLink])
+
+  const onDelete = React.useCallback(() => {
+    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+    convo
+      .deleteMessage(message.id)
+      .then(() =>
+        Toast.show(_(msg({message: 'Message deleted', context: 'toast'}))),
+      )
+      .catch(() => Toast.show(_(msg`Failed to delete message`)))
+  }, [_, convo, message.id])
+
+  const sender = convo.convo.members.find(
+    member => member.did === message.sender.did,
+  )
+
+  return (
+    <>
+      <ContextMenu.Root>
+        <ContextMenu.Trigger
+          label={_(msg`Message options`)}
+          contentLabel={_(
+            msg`Message from @${
+              sender?.handle ?? // should always be defined
+              'unknown'
+            }: ${message.text}`,
+          )}>
+          {children}
+        </ContextMenu.Trigger>
+
+        <ContextMenu.Outer align={isFromSelf ? 'right' : 'left'}>
+          {message.text.length > 0 && (
+            <>
+              <ContextMenu.Item
+                testID="messageDropdownTranslateBtn"
+                label={_(msg`Translate`)}
+                onPress={onPressTranslateMessage}>
+                <ContextMenu.ItemText>{_(msg`Translate`)}</ContextMenu.ItemText>
+                <ContextMenu.ItemIcon icon={Translate} position="right" />
+              </ContextMenu.Item>
+              <ContextMenu.Item
+                testID="messageDropdownCopyBtn"
+                label={_(msg`Copy message text`)}
+                onPress={onCopyMessage}>
+                <ContextMenu.ItemText>
+                  {_(msg`Copy message text`)}
+                </ContextMenu.ItemText>
+                <ContextMenu.ItemIcon icon={ClipboardIcon} position="right" />
+              </ContextMenu.Item>
+              <ContextMenu.Divider />
+            </>
+          )}
+          <ContextMenu.Item
+            testID="messageDropdownDeleteBtn"
+            label={_(msg`Delete message for me`)}
+            onPress={() => deleteControl.open()}>
+            <ContextMenu.ItemText>{_(msg`Delete for me`)}</ContextMenu.ItemText>
+            <ContextMenu.ItemIcon icon={Trash} position="right" />
+          </ContextMenu.Item>
+          {!isFromSelf && (
+            <ContextMenu.Item
+              testID="messageDropdownReportBtn"
+              label={_(msg`Report message`)}
+              onPress={() => reportControl.open()}>
+              <ContextMenu.ItemText>{_(msg`Report`)}</ContextMenu.ItemText>
+              <ContextMenu.ItemIcon icon={Warning} position="right" />
+            </ContextMenu.Item>
+          )}
+        </ContextMenu.Outer>
+      </ContextMenu.Root>
+
+      <ReportDialog
+        currentScreen="conversation"
+        params={{type: 'convoMessage', convoId: convo.convo.id, message}}
+        control={reportControl}
+      />
+
+      <Prompt.Basic
+        control={deleteControl}
+        title={_(msg`Delete message`)}
+        description={_(
+          msg`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant.`,
+        )}
+        confirmButtonCta={_(msg`Delete`)}
+        confirmButtonColor="negative"
+        onConfirm={onDelete}
+      />
+    </>
+  )
+}
+MessageContextMenu = React.memo(MessageContextMenu)
-- 
cgit 1.4.1

