about summary refs log tree commit diff
path: root/src/components/dms
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/dms')
-rw-r--r--src/components/dms/ActionsWrapper.tsx125
-rw-r--r--src/components/dms/ActionsWrapper.web.tsx68
-rw-r--r--src/components/dms/MessageContextMenu.tsx (renamed from src/components/dms/MessageMenu.tsx)128
-rw-r--r--src/components/dms/MessageItemEmbed.tsx33
4 files changed, 165 insertions, 189 deletions
diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx
index a087fed3f..385086d7c 100644
--- a/src/components/dms/ActionsWrapper.tsx
+++ b/src/components/dms/ActionsWrapper.tsx
@@ -1,22 +1,10 @@
-import React from 'react'
-import {Keyboard} from 'react-native'
-import {Gesture, GestureDetector} from 'react-native-gesture-handler'
-import Animated, {
-  cancelAnimation,
-  runOnJS,
-  useAnimatedStyle,
-  useSharedValue,
-  withTiming,
-} from 'react-native-reanimated'
+import {View} from 'react-native'
 import {ChatBskyConvoDefs} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {HITSLOP_10} from '#/lib/constants'
-import {useHaptics} from '#/lib/haptics'
 import {atoms as a} from '#/alf'
-import {MessageMenu} from '#/components/dms/MessageMenu'
-import {useMenuControl} from '#/components/Menu'
+import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
 
 export function ActionsWrapper({
   message,
@@ -28,71 +16,52 @@ export function ActionsWrapper({
   children: React.ReactNode
 }) {
   const {_} = useLingui()
-  const playHaptic = useHaptics()
-  const menuControl = useMenuControl()
-
-  const scale = useSharedValue(1)
-
-  const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.get()}],
-  }))
-
-  const open = React.useCallback(() => {
-    playHaptic()
-    Keyboard.dismiss()
-    menuControl.open()
-  }, [menuControl, playHaptic])
-
-  const shrink = React.useCallback(() => {
-    'worklet'
-    cancelAnimation(scale)
-    scale.set(() => withTiming(1, {duration: 200}))
-  }, [scale])
-
-  const doubleTapGesture = Gesture.Tap()
-    .numberOfTaps(2)
-    .hitSlop(HITSLOP_10)
-    .onEnd(open)
-    .runOnJS(true)
-
-  const pressAndHoldGesture = Gesture.LongPress()
-    .onStart(() => {
-      'worklet'
-      scale.set(() =>
-        withTiming(1.05, {duration: 200}, finished => {
-          if (!finished) return
-          runOnJS(open)()
-          shrink()
-        }),
-      )
-    })
-    .onTouchesUp(shrink)
-    .onTouchesMove(shrink)
-    .cancelsTouchesInView(false)
-
-  const composedGestures = Gesture.Exclusive(
-    doubleTapGesture,
-    pressAndHoldGesture,
-  )
 
   return (
-    <GestureDetector gesture={composedGestures}>
-      <Animated.View
-        style={[
-          {
-            maxWidth: '80%',
-          },
-          isFromSelf ? a.self_end : a.self_start,
-          animatedStyle,
-        ]}
-        accessible={true}
-        accessibilityActions={[
-          {name: 'activate', label: _(msg`Open message options`)},
-        ]}
-        onAccessibilityAction={open}>
-        {children}
-        <MessageMenu message={message} control={menuControl} />
-      </Animated.View>
-    </GestureDetector>
+    <MessageContextMenu message={message}>
+      {trigger =>
+        // will always be true, since this file is platform split
+        trigger.isNative && (
+          <View style={[a.flex_1, a.relative]}>
+            {/* {isNative && (
+              <View
+                style={[
+                  a.rounded_full,
+                  a.absolute,
+                  {bottom: '100%'},
+                  isFromSelf ? a.right_0 : a.left_0,
+                  t.atoms.bg,
+                  a.flex_row,
+                  a.shadow_lg,
+                  a.py_xs,
+                  a.px_md,
+                  a.gap_md,
+                  a.mb_xs,
+                ]}>
+                {['👍', '😆', '❤️', '👀', '😢'].map(emoji => (
+                  <Text key={emoji} style={[a.text_center, {fontSize: 32}]}>
+                    {emoji}
+                  </Text>
+                ))}
+              </View>
+            )} */}
+            <View
+              style={[
+                {maxWidth: '80%'},
+                isFromSelf
+                  ? [a.self_end, a.align_end]
+                  : [a.self_start, a.align_start],
+              ]}
+              accessible={true}
+              accessibilityActions={[
+                {name: 'activate', label: _(msg`Open message options`)},
+              ]}
+              onAccessibilityAction={trigger.control.open}>
+              {children}
+            </View>
+          </View>
+        )
+      }
+    </MessageContextMenu>
   )
 }
diff --git a/src/components/dms/ActionsWrapper.web.tsx b/src/components/dms/ActionsWrapper.web.tsx
index 29cc89dd1..188d18eb7 100644
--- a/src/components/dms/ActionsWrapper.web.tsx
+++ b/src/components/dms/ActionsWrapper.web.tsx
@@ -1,10 +1,10 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {Pressable, View} from 'react-native'
 import {ChatBskyConvoDefs} from '@atproto/api'
 
-import {atoms as a} from '#/alf'
-import {MessageMenu} from '#/components/dms/MessageMenu'
-import {useMenuControl} from '#/components/Menu'
+import {atoms as a, useTheme} from '#/alf'
+import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
+import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '../icons/DotGrid'
 
 export function ActionsWrapper({
   message,
@@ -15,8 +15,8 @@ export function ActionsWrapper({
   isFromSelf: boolean
   children: React.ReactNode
 }) {
-  const menuControl = useMenuControl()
   const viewRef = React.useRef(null)
+  const t = useTheme()
 
   const [showActions, setShowActions] = React.useState(false)
 
@@ -42,39 +42,39 @@ export function ActionsWrapper({
       onMouseLeave={onMouseLeave}
       onFocus={onFocus}
       onBlur={onMouseLeave}
-      style={StyleSheet.flatten([a.flex_1, a.flex_row])}
+      style={[a.flex_1, isFromSelf ? a.flex_row : a.flex_row_reverse]}
       ref={viewRef}>
-      {isFromSelf && (
-        <View
-          style={[
-            a.mr_xl,
-            a.justify_center,
-            {
-              marginLeft: 'auto',
-            },
-          ]}>
-          <MessageMenu
-            message={message}
-            control={menuControl}
-            triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
-          />
-        </View>
-      )}
       <View
-        style={{
-          maxWidth: '80%',
-        }}>
+        style={[
+          a.justify_center,
+          isFromSelf
+            ? [a.mr_xl, {marginLeft: 'auto'}]
+            : [a.ml_xl, {marginRight: 'auto'}],
+        ]}>
+        <MessageContextMenu message={message}>
+          {({props, state, isNative, control}) => {
+            // always false, file is platform split
+            if (isNative) return null
+            const showMenuTrigger = showActions || control.isOpen ? 1 : 0
+            return (
+              <Pressable
+                {...props}
+                style={[
+                  {opacity: showMenuTrigger},
+                  a.p_sm,
+                  a.rounded_full,
+                  (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
+                ]}>
+                <DotsHorizontalIcon size="md" style={t.atoms.text} />
+              </Pressable>
+            )
+          }}
+        </MessageContextMenu>
+      </View>
+      <View
+        style={[{maxWidth: '80%'}, isFromSelf ? a.align_end : a.align_start]}>
         {children}
       </View>
-      {!isFromSelf && (
-        <View style={[a.flex_row, a.align_center, a.ml_xl]}>
-          <MessageMenu
-            message={message}
-            control={menuControl}
-            triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
-          />
-        </View>
-      )}
     </View>
   )
 }
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageContextMenu.tsx
index cff5f9dd4..b5542690f 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {LayoutAnimation, Pressable, View} from 'react-native'
+import {LayoutAnimation} from 'react-native'
 import * as Clipboard from 'expo-clipboard'
 import {ChatBskyConvoDefs, RichText} from '@atproto/api'
 import {msg} from '@lingui/macro'
@@ -8,33 +8,28 @@ 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 {isWeb} from '#/platform/detection'
 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 {atoms as a, useTheme} from '#/alf'
+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 {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
+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 Menu from '#/components/Menu'
 import * as Prompt from '#/components/Prompt'
 import {usePromptControl} from '#/components/Prompt'
-import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipboard'
 
-export let MessageMenu = ({
+export let MessageContextMenu = ({
   message,
-  control,
-  triggerOpacity,
+  children,
 }: {
-  triggerOpacity?: number
   message: ChatBskyConvoDefs.MessageView
-  control: Menu.MenuControlProps
+  children: TriggerProps['children']
 }): React.ReactNode => {
   const {_} = useLingui()
-  const t = useTheme()
   const {currentAccount} = useSession()
   const convo = useConvoActive()
   const deleteControl = usePromptControl()
@@ -75,69 +70,64 @@ export let MessageMenu = ({
       .catch(() => Toast.show(_(msg`Failed to delete message`)))
   }, [_, convo, message.id])
 
+  const sender = convo.convo.members.find(
+    member => member.did === message.sender.did,
+  )
+
   return (
     <>
-      <Menu.Root control={control}>
-        {isWeb && (
-          <View style={{opacity: triggerOpacity}}>
-            <Menu.Trigger label={_(msg`Chat settings`)}>
-              {({props, state}) => (
-                <Pressable
-                  {...props}
-                  style={[
-                    a.p_sm,
-                    a.rounded_full,
-                    (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
-                  ]}>
-                  <DotsHorizontal size="md" style={t.atoms.text} />
-                </Pressable>
-              )}
-            </Menu.Trigger>
-          </View>
-        )}
+      <ContextMenu.Root>
+        <ContextMenu.Trigger
+          label={_(msg`Message options`)}
+          contentLabel={_(
+            msg`Message from @${
+              sender?.handle ?? // should always be defined
+              'unknown'
+            }: ${message.text}`,
+          )}>
+          {children}
+        </ContextMenu.Trigger>
 
-        <Menu.Outer>
+        <ContextMenu.Outer align={isFromSelf ? 'right' : 'left'}>
           {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 />
+              <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 />
             </>
           )}
-          <Menu.Group>
-            <Menu.Item
-              testID="messageDropdownDeleteBtn"
-              label={_(msg`Delete message for me`)}
-              onPress={() => deleteControl.open()}>
-              <Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
-              <Menu.ItemIcon icon={Trash} position="right" />
-            </Menu.Item>
-            {!isFromSelf && (
-              <Menu.Item
-                testID="messageDropdownReportBtn"
-                label={_(msg`Report message`)}
-                onPress={() => reportControl.open()}>
-                <Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
-                <Menu.ItemIcon icon={Warning} position="right" />
-              </Menu.Item>
-            )}
-          </Menu.Group>
-        </Menu.Outer>
-      </Menu.Root>
+          <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"
@@ -158,4 +148,4 @@ export let MessageMenu = ({
     </>
   )
 }
-MessageMenu = React.memo(MessageMenu)
+MessageContextMenu = React.memo(MessageContextMenu)
diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx
index f9eb4d3af..f1c6189d0 100644
--- a/src/components/dms/MessageItemEmbed.tsx
+++ b/src/components/dms/MessageItemEmbed.tsx
@@ -1,9 +1,9 @@
 import React from 'react'
-import {View} from 'react-native'
+import {useWindowDimensions, View} from 'react-native'
 import {AppBskyEmbedRecord} from '@atproto/api'
 
 import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
-import {atoms as a, native, useTheme} from '#/alf'
+import {atoms as a, native, tokens, useTheme, web} from '#/alf'
 import {MessageContextProvider} from './MessageContext'
 
 let MessageItemEmbed = ({
@@ -12,15 +12,32 @@ let MessageItemEmbed = ({
   embed: AppBskyEmbedRecord.View
 }): React.ReactNode => {
   const t = useTheme()
+  const screen = useWindowDimensions()
 
   return (
     <MessageContextProvider>
-      <View style={[a.my_xs, t.atoms.bg, native({flexBasis: 0})]}>
-        <PostEmbeds
-          embed={embed}
-          allowNestedQuotes
-          viewContext={PostEmbedViewContext.Feed}
-        />
+      <View
+        style={[
+          a.my_xs,
+          t.atoms.bg,
+          a.rounded_md,
+          native({
+            flexBasis: 0,
+            width: Math.min(screen.width, 600) / 1.4,
+          }),
+          web({
+            width: '100%',
+            minWidth: 280,
+            maxWidth: 360,
+          }),
+        ]}>
+        <View style={{marginTop: tokens.space.sm * -1}}>
+          <PostEmbeds
+            embed={embed}
+            allowNestedQuotes
+            viewContext={PostEmbedViewContext.Feed}
+          />
+        </View>
       </View>
     </MessageContextProvider>
   )