[🐴] 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

* 
zio/stable
Hailey 2024-05-09 09:07:57 -07:00 committed by GitHub
parent 55ac287d5e
commit 2fe76333bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 71 additions and 43 deletions

View File

@ -57,9 +57,6 @@ export function ActionsWrapper({
message={message} message={message}
control={menuControl} control={menuControl}
triggerOpacity={showActions || menuControl.isOpen ? 1 : 0} triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
onTriggerPress={onMouseEnter}
// @ts-expect-error web only
onMouseLeave={onMouseLeave}
/> />
</View> </View>
)} )}
@ -75,9 +72,6 @@ export function ActionsWrapper({
message={message} message={message}
control={menuControl} control={menuControl}
triggerOpacity={showActions || menuControl.isOpen ? 1 : 0} triggerOpacity={showActions || menuControl.isOpen ? 1 : 0}
onTriggerPress={onMouseEnter}
// @ts-expect-error web only
onMouseLeave={onMouseLeave}
/> />
</View> </View>
)} )}

View File

@ -1,5 +1,5 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {Keyboard, Pressable} from 'react-native' import {Keyboard, Pressable, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {ChatBskyConvoDefs} from '@atproto-labs/api' import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
@ -32,17 +32,19 @@ let ConvoMenu = ({
profile, profile,
onUpdateConvo, onUpdateConvo,
control, control,
hideTrigger,
currentScreen, currentScreen,
showMarkAsRead, showMarkAsRead,
hideTrigger,
triggerOpacity,
}: { }: {
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
onUpdateConvo?: (convo: ChatBskyConvoDefs.ConvoView) => void onUpdateConvo?: (convo: ChatBskyConvoDefs.ConvoView) => void
control?: Menu.MenuControlProps control?: Menu.MenuControlProps
hideTrigger?: boolean
currentScreen: 'list' | 'conversation' currentScreen: 'list' | 'conversation'
showMarkAsRead?: boolean showMarkAsRead?: boolean
hideTrigger?: boolean
triggerOpacity?: number
}): React.ReactNode => { }): React.ReactNode => {
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {_} = useLingui() const {_} = useLingui()
@ -89,26 +91,28 @@ let ConvoMenu = ({
<> <>
<Menu.Root control={control}> <Menu.Root control={control}>
{!hideTrigger && ( {!hideTrigger && (
<Menu.Trigger label={_(msg`Chat settings`)}> <View style={{opacity: triggerOpacity}}>
{({props, state}) => ( <Menu.Trigger label={_(msg`Chat settings`)}>
<Pressable {({props, state}) => (
{...props} <Pressable
onPress={() => { {...props}
Keyboard.dismiss() onPress={() => {
// eslint-disable-next-line react/prop-types -- eslint is confused by the name `props` Keyboard.dismiss()
props.onPress() // eslint-disable-next-line react/prop-types -- eslint is confused by the name `props`
}} props.onPress()
style={[ }}
a.p_sm, style={[
a.rounded_sm, a.p_sm,
(state.hovered || state.pressed) && t.atoms.bg_contrast_25, a.rounded_full,
// make sure pfp is in the middle (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
{marginLeft: -10}, // make sure pfp is in the middle
]}> {marginLeft: -10},
<DotsHorizontal size="lg" style={t.atoms.text} /> ]}>
</Pressable> <DotsHorizontal size="md" style={t.atoms.text} />
)} </Pressable>
</Menu.Trigger> )}
</Menu.Trigger>
</View>
)} )}
<Menu.Outer> <Menu.Outer>
<Menu.Group> <Menu.Group>

View File

@ -5,6 +5,7 @@ import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isWeb} from 'platform/detection'
import {useConvo} from 'state/messages/convo' import {useConvo} from 'state/messages/convo'
import {ConvoStatus} from 'state/messages/convo/types' import {ConvoStatus} from 'state/messages/convo/types'
import {useSession} from 'state/session' import {useSession} from 'state/session'
@ -21,12 +22,10 @@ import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipb
export let MessageMenu = ({ export let MessageMenu = ({
message, message,
control, control,
hideTrigger,
triggerOpacity, triggerOpacity,
}: { }: {
hideTrigger?: boolean hideTrigger?: boolean
triggerOpacity?: number triggerOpacity?: number
onTriggerPress?: () => void
message: ChatBskyConvoDefs.MessageView message: ChatBskyConvoDefs.MessageView
control: Menu.MenuControlProps control: Menu.MenuControlProps
}): React.ReactNode => { }): React.ReactNode => {
@ -64,7 +63,7 @@ export let MessageMenu = ({
return ( return (
<> <>
<Menu.Root control={control}> <Menu.Root control={control}>
{!hideTrigger && ( {isWeb && (
<View style={{opacity: triggerOpacity}}> <View style={{opacity: triggerOpacity}}>
<Menu.Trigger label={_(msg`Chat settings`)}> <Menu.Trigger label={_(msg`Chat settings`)}>
{({props, state}) => ( {({props, state}) => (
@ -75,7 +74,7 @@ export let MessageMenu = ({
a.rounded_full, a.rounded_full,
(state.hovered || state.pressed) && t.atoms.bg_contrast_25, (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
]}> ]}>
<DotsHorizontal size="sm" style={t.atoms.text} /> <DotsHorizontal size="md" style={t.atoms.text} />
</Pressable> </Pressable>
)} )}
</Menu.Trigger> </Menu.Trigger>

View File

@ -5,11 +5,12 @@ import {View} from 'react-native'
import {ChatBskyConvoDefs} from '@atproto-labs/api' import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 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 {useGate} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
@ -18,7 +19,7 @@ import {useListConvos} from '#/state/queries/messages/list-converations'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {List} from '#/view/com/util/List' import {List} from '#/view/com/util/List'
import {TimeElapsed} from '#/view/com/util/TimeElapsed' 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 {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views' import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} 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 {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const menuControl = useMenuControl() const menuControl = useMenuControl()
const {gtMobile} = useBreakpoints()
let lastMessage = _(msg`No messages yet`) let lastMessage = _(msg`No messages yet`)
let lastMessageSentAt: string | null = null let lastMessageSentAt: string | null = null
@ -258,15 +260,43 @@ function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
member => member.did !== currentAccount?.did, 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) { if (!otherUser) {
return null return null
} }
return ( return (
<Link <Button
to={`/messages/${convo.id}`} label={otherUser.displayName || otherUser.handle}
onPress={onPress}
style={a.flex_1} 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}) => ( {({hovered, pressed}) => (
<View <View
style={[ style={[
@ -279,7 +309,7 @@ function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
(hovered || pressed) && t.atoms.bg_contrast_25, (hovered || pressed) && t.atoms.bg_contrast_25,
]}> ]}>
<View pointerEvents="none"> <View pointerEvents="none">
<PreviewableUserAvatar profile={otherUser} size={42} /> <UserAvatar avatar={otherUser?.avatar} size={42} />
</View> </View>
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<Text <Text
@ -336,15 +366,16 @@ function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
convo={convo} convo={convo}
profile={otherUser} profile={otherUser}
control={menuControl} control={menuControl}
// TODO(sam) show on hover on web
// tricky because it captures the mouse event
hideTrigger
currentScreen="list" currentScreen="list"
showMarkAsRead={convo.unreadCount > 0} showMarkAsRead={convo.unreadCount > 0}
hideTrigger={isNative}
triggerOpacity={
!gtMobile || showActions || menuControl.isOpen ? 1 : 0
}
/> />
</View> </View>
)} )}
</Link> </Button>
) )
} }