about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-05-09 09:07:57 -0700
committerGitHub <noreply@github.com>2024-05-09 09:07:57 -0700
commit2fe76333bc6161d78c9f9b43f4855ff6919de507 (patch)
treec33e1df9f7b2829df62173fd956e45a45c05cdc8
parent55ac287d5e822a24c201d6f756b2d0ab76df242c (diff)
downloadvoidsky-2fe76333bc6161d78c9f9b43f4855ff6919de507.tar.zst
[🐴] Add hover context menu for convo list on web (#3923)
* remove some unnecessary props

* add hover trigger on web for convo list

* lint

* use `UserAvatar` to not affect accessibility

* remove extra wrapper

* add `label`

* always show on mobile

* adjust size of dots

* make the message trigger dots the same size

* ❓
-rw-r--r--src/components/dms/ActionsWrapper.web.tsx6
-rw-r--r--src/components/dms/ConvoMenu.tsx50
-rw-r--r--src/components/dms/MessageMenu.tsx7
-rw-r--r--src/screens/Messages/List/index.tsx51
4 files changed, 71 insertions, 43 deletions
diff --git a/src/components/dms/ActionsWrapper.web.tsx b/src/components/dms/ActionsWrapper.web.tsx
index f4c85ab94..5cb30d3da 100644
--- a/src/components/dms/ActionsWrapper.web.tsx
+++ b/src/components/dms/ActionsWrapper.web.tsx
@@ -57,9 +57,6 @@ export function ActionsWrapper({
             message={message}
             control={menuControl}
             triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
-            onTriggerPress={onMouseEnter}
-            // @ts-expect-error web only
-            onMouseLeave={onMouseLeave}
           />
         </View>
       )}
@@ -75,9 +72,6 @@ export function ActionsWrapper({
             message={message}
             control={menuControl}
             triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
-            onTriggerPress={onMouseEnter}
-            // @ts-expect-error web only
-            onMouseLeave={onMouseLeave}
           />
         </View>
       )}
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index 263befd56..cac4eb4d9 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -1,5 +1,5 @@
 import React, {useCallback} from 'react'
-import {Keyboard, Pressable} from 'react-native'
+import {Keyboard, Pressable, View} from 'react-native'
 import {AppBskyActorDefs} from '@atproto/api'
 import {ChatBskyConvoDefs} from '@atproto-labs/api'
 import {msg, Trans} from '@lingui/macro'
@@ -32,17 +32,19 @@ let ConvoMenu = ({
   profile,
   onUpdateConvo,
   control,
-  hideTrigger,
   currentScreen,
   showMarkAsRead,
+  hideTrigger,
+  triggerOpacity,
 }: {
   convo: ChatBskyConvoDefs.ConvoView
   profile: AppBskyActorDefs.ProfileViewBasic
   onUpdateConvo?: (convo: ChatBskyConvoDefs.ConvoView) => void
   control?: Menu.MenuControlProps
-  hideTrigger?: boolean
   currentScreen: 'list' | 'conversation'
   showMarkAsRead?: boolean
+  hideTrigger?: boolean
+  triggerOpacity?: number
 }): React.ReactNode => {
   const navigation = useNavigation<NavigationProp>()
   const {_} = useLingui()
@@ -89,26 +91,28 @@ let ConvoMenu = ({
     <>
       <Menu.Root control={control}>
         {!hideTrigger && (
-          <Menu.Trigger label={_(msg`Chat settings`)}>
-            {({props, state}) => (
-              <Pressable
-                {...props}
-                onPress={() => {
-                  Keyboard.dismiss()
-                  // eslint-disable-next-line react/prop-types -- eslint is confused by the name `props`
-                  props.onPress()
-                }}
-                style={[
-                  a.p_sm,
-                  a.rounded_sm,
-                  (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
-                  // make sure pfp is in the middle
-                  {marginLeft: -10},
-                ]}>
-                <DotsHorizontal size="lg" style={t.atoms.text} />
-              </Pressable>
-            )}
-          </Menu.Trigger>
+          <View style={{opacity: triggerOpacity}}>
+            <Menu.Trigger label={_(msg`Chat settings`)}>
+              {({props, state}) => (
+                <Pressable
+                  {...props}
+                  onPress={() => {
+                    Keyboard.dismiss()
+                    // eslint-disable-next-line react/prop-types -- eslint is confused by the name `props`
+                    props.onPress()
+                  }}
+                  style={[
+                    a.p_sm,
+                    a.rounded_full,
+                    (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
+                    // make sure pfp is in the middle
+                    {marginLeft: -10},
+                  ]}>
+                  <DotsHorizontal size="md" style={t.atoms.text} />
+                </Pressable>
+              )}
+            </Menu.Trigger>
+          </View>
         )}
         <Menu.Outer>
           <Menu.Group>
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx
index 3a5fa54d8..75807f818 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageMenu.tsx
@@ -5,6 +5,7 @@ import {ChatBskyConvoDefs} from '@atproto-labs/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {isWeb} from 'platform/detection'
 import {useConvo} from 'state/messages/convo'
 import {ConvoStatus} from 'state/messages/convo/types'
 import {useSession} from 'state/session'
@@ -21,12 +22,10 @@ import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipb
 export let MessageMenu = ({
   message,
   control,
-  hideTrigger,
   triggerOpacity,
 }: {
   hideTrigger?: boolean
   triggerOpacity?: number
-  onTriggerPress?: () => void
   message: ChatBskyConvoDefs.MessageView
   control: Menu.MenuControlProps
 }): React.ReactNode => {
@@ -64,7 +63,7 @@ export let MessageMenu = ({
   return (
     <>
       <Menu.Root control={control}>
-        {!hideTrigger && (
+        {isWeb && (
           <View style={{opacity: triggerOpacity}}>
             <Menu.Trigger label={_(msg`Chat settings`)}>
               {({props, state}) => (
@@ -75,7 +74,7 @@ export let MessageMenu = ({
                     a.rounded_full,
                     (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
                   ]}>
-                  <DotsHorizontal size="sm" style={t.atoms.text} />
+                  <DotsHorizontal size="md" style={t.atoms.text} />
                 </Pressable>
               )}
             </Menu.Trigger>
diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx
index a82f2f00a..4d218bda8 100644
--- a/src/screens/Messages/List/index.tsx
+++ b/src/screens/Messages/List/index.tsx
@@ -5,11 +5,12 @@ import {View} from 'react-native'
 import {ChatBskyConvoDefs} from '@atproto-labs/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {sha256} from 'js-sha256'
 
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
-import {MessagesTabNavigatorParams} from '#/lib/routes/types'
+import {MessagesTabNavigatorParams, NavigationProp} from '#/lib/routes/types'
 import {useGate} from '#/lib/statsig/statsig'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
@@ -18,7 +19,7 @@ import {useListConvos} from '#/state/queries/messages/list-converations'
 import {useSession} from '#/state/session'
 import {List} from '#/view/com/util/List'
 import {TimeElapsed} from '#/view/com/util/TimeElapsed'
-import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
 import {CenteredView} from '#/view/com/util/Views'
 import {ScrollView} from '#/view/com/util/Views'
@@ -239,6 +240,7 @@ function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
   const {_} = useLingui()
   const {currentAccount} = useSession()
   const menuControl = useMenuControl()
+  const {gtMobile} = useBreakpoints()
 
   let lastMessage = _(msg`No messages yet`)
   let lastMessageSentAt: string | null = null
@@ -258,15 +260,43 @@ function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
     member => member.did !== currentAccount?.did,
   )
 
+  const navigation = useNavigation<NavigationProp>()
+  const [showActions, setShowActions] = React.useState(false)
+
+  const onMouseEnter = React.useCallback(() => {
+    setShowActions(true)
+  }, [])
+
+  const onMouseLeave = React.useCallback(() => {
+    setShowActions(false)
+  }, [])
+
+  const onFocus = React.useCallback<React.FocusEventHandler>(e => {
+    if (e.nativeEvent.relatedTarget == null) return
+    setShowActions(true)
+  }, [])
+
+  const onPress = React.useCallback(() => {
+    navigation.push('MessagesConversation', {
+      conversation: convo.id,
+    })
+  }, [convo.id, navigation])
+
   if (!otherUser) {
     return null
   }
 
   return (
-    <Link
-      to={`/messages/${convo.id}`}
+    <Button
+      label={otherUser.displayName || otherUser.handle}
+      onPress={onPress}
       style={a.flex_1}
-      onLongPress={isNative ? menuControl.open : undefined}>
+      onLongPress={isNative ? menuControl.open : undefined}
+      // @ts-expect-error web only
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
+      onFocus={onFocus}
+      onBlur={onMouseLeave}>
       {({hovered, pressed}) => (
         <View
           style={[
@@ -279,7 +309,7 @@ function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
             (hovered || pressed) && t.atoms.bg_contrast_25,
           ]}>
           <View pointerEvents="none">
-            <PreviewableUserAvatar profile={otherUser} size={42} />
+            <UserAvatar avatar={otherUser?.avatar} size={42} />
           </View>
           <View style={[a.flex_1]}>
             <Text
@@ -336,15 +366,16 @@ function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
             convo={convo}
             profile={otherUser}
             control={menuControl}
-            // TODO(sam) show on hover on web
-            // tricky because it captures the mouse event
-            hideTrigger
             currentScreen="list"
             showMarkAsRead={convo.unreadCount > 0}
+            hideTrigger={isNative}
+            triggerOpacity={
+              !gtMobile || showActions || menuControl.isOpen ? 1 : 0
+            }
           />
         </View>
       )}
-    </Link>
+    </Button>
   )
 }