Bluesky app fork with some witchin' additions ๐Ÿ’ซ
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[๐Ÿด] 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

* โ“

authored by

Hailey and committed by
GitHub
2fe76333 55ac287d

+71 -43
-6
src/components/dms/ActionsWrapper.web.tsx
··· 57 57 message={message} 58 58 control={menuControl} 59 59 triggerOpacity={showActions || menuControl.isOpen ? 1 : 0} 60 - onTriggerPress={onMouseEnter} 61 - // @ts-expect-error web only 62 - onMouseLeave={onMouseLeave} 63 60 /> 64 61 </View> 65 62 )} ··· 75 72 message={message} 76 73 control={menuControl} 77 74 triggerOpacity={showActions || menuControl.isOpen ? 1 : 0} 78 - onTriggerPress={onMouseEnter} 79 - // @ts-expect-error web only 80 - onMouseLeave={onMouseLeave} 81 75 /> 82 76 </View> 83 77 )}
+27 -23
src/components/dms/ConvoMenu.tsx
··· 1 1 import React, {useCallback} from 'react' 2 - import {Keyboard, Pressable} from 'react-native' 2 + import {Keyboard, Pressable, View} from 'react-native' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 4 import {ChatBskyConvoDefs} from '@atproto-labs/api' 5 5 import {msg, Trans} from '@lingui/macro' ··· 32 32 profile, 33 33 onUpdateConvo, 34 34 control, 35 - hideTrigger, 36 35 currentScreen, 37 36 showMarkAsRead, 37 + hideTrigger, 38 + triggerOpacity, 38 39 }: { 39 40 convo: ChatBskyConvoDefs.ConvoView 40 41 profile: AppBskyActorDefs.ProfileViewBasic 41 42 onUpdateConvo?: (convo: ChatBskyConvoDefs.ConvoView) => void 42 43 control?: Menu.MenuControlProps 43 - hideTrigger?: boolean 44 44 currentScreen: 'list' | 'conversation' 45 45 showMarkAsRead?: boolean 46 + hideTrigger?: boolean 47 + triggerOpacity?: number 46 48 }): React.ReactNode => { 47 49 const navigation = useNavigation<NavigationProp>() 48 50 const {_} = useLingui() ··· 89 91 <> 90 92 <Menu.Root control={control}> 91 93 {!hideTrigger && ( 92 - <Menu.Trigger label={_(msg`Chat settings`)}> 93 - {({props, state}) => ( 94 - <Pressable 95 - {...props} 96 - onPress={() => { 97 - Keyboard.dismiss() 98 - // eslint-disable-next-line react/prop-types -- eslint is confused by the name `props` 99 - props.onPress() 100 - }} 101 - style={[ 102 - a.p_sm, 103 - a.rounded_sm, 104 - (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 105 - // make sure pfp is in the middle 106 - {marginLeft: -10}, 107 - ]}> 108 - <DotsHorizontal size="lg" style={t.atoms.text} /> 109 - </Pressable> 110 - )} 111 - </Menu.Trigger> 94 + <View style={{opacity: triggerOpacity}}> 95 + <Menu.Trigger label={_(msg`Chat settings`)}> 96 + {({props, state}) => ( 97 + <Pressable 98 + {...props} 99 + onPress={() => { 100 + Keyboard.dismiss() 101 + // eslint-disable-next-line react/prop-types -- eslint is confused by the name `props` 102 + props.onPress() 103 + }} 104 + style={[ 105 + a.p_sm, 106 + a.rounded_full, 107 + (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 108 + // make sure pfp is in the middle 109 + {marginLeft: -10}, 110 + ]}> 111 + <DotsHorizontal size="md" style={t.atoms.text} /> 112 + </Pressable> 113 + )} 114 + </Menu.Trigger> 115 + </View> 112 116 )} 113 117 <Menu.Outer> 114 118 <Menu.Group>
+3 -4
src/components/dms/MessageMenu.tsx
··· 5 5 import {msg} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 8 + import {isWeb} from 'platform/detection' 8 9 import {useConvo} from 'state/messages/convo' 9 10 import {ConvoStatus} from 'state/messages/convo/types' 10 11 import {useSession} from 'state/session' ··· 21 22 export let MessageMenu = ({ 22 23 message, 23 24 control, 24 - hideTrigger, 25 25 triggerOpacity, 26 26 }: { 27 27 hideTrigger?: boolean 28 28 triggerOpacity?: number 29 - onTriggerPress?: () => void 30 29 message: ChatBskyConvoDefs.MessageView 31 30 control: Menu.MenuControlProps 32 31 }): React.ReactNode => { ··· 64 63 return ( 65 64 <> 66 65 <Menu.Root control={control}> 67 - {!hideTrigger && ( 66 + {isWeb && ( 68 67 <View style={{opacity: triggerOpacity}}> 69 68 <Menu.Trigger label={_(msg`Chat settings`)}> 70 69 {({props, state}) => ( ··· 75 74 a.rounded_full, 76 75 (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 77 76 ]}> 78 - <DotsHorizontal size="sm" style={t.atoms.text} /> 77 + <DotsHorizontal size="md" style={t.atoms.text} /> 79 78 </Pressable> 80 79 )} 81 80 </Menu.Trigger>
+41 -10
src/screens/Messages/List/index.tsx
··· 5 5 import {ChatBskyConvoDefs} from '@atproto-labs/api' 6 6 import {msg, Trans} from '@lingui/macro' 7 7 import {useLingui} from '@lingui/react' 8 + import {useNavigation} from '@react-navigation/native' 8 9 import {NativeStackScreenProps} from '@react-navigation/native-stack' 9 10 import {sha256} from 'js-sha256' 10 11 11 12 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 - import {MessagesTabNavigatorParams} from '#/lib/routes/types' 13 + import {MessagesTabNavigatorParams, NavigationProp} from '#/lib/routes/types' 13 14 import {useGate} from '#/lib/statsig/statsig' 14 15 import {cleanError} from '#/lib/strings/errors' 15 16 import {logger} from '#/logger' ··· 18 19 import {useSession} from '#/state/session' 19 20 import {List} from '#/view/com/util/List' 20 21 import {TimeElapsed} from '#/view/com/util/TimeElapsed' 21 - import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 22 + import {UserAvatar} from '#/view/com/util/UserAvatar' 22 23 import {ViewHeader} from '#/view/com/util/ViewHeader' 23 24 import {CenteredView} from '#/view/com/util/Views' 24 25 import {ScrollView} from '#/view/com/util/Views' ··· 239 240 const {_} = useLingui() 240 241 const {currentAccount} = useSession() 241 242 const menuControl = useMenuControl() 243 + const {gtMobile} = useBreakpoints() 242 244 243 245 let lastMessage = _(msg`No messages yet`) 244 246 let lastMessageSentAt: string | null = null ··· 257 259 const otherUser = convo.members.find( 258 260 member => member.did !== currentAccount?.did, 259 261 ) 262 + 263 + const navigation = useNavigation<NavigationProp>() 264 + const [showActions, setShowActions] = React.useState(false) 265 + 266 + const onMouseEnter = React.useCallback(() => { 267 + setShowActions(true) 268 + }, []) 269 + 270 + const onMouseLeave = React.useCallback(() => { 271 + setShowActions(false) 272 + }, []) 273 + 274 + const onFocus = React.useCallback<React.FocusEventHandler>(e => { 275 + if (e.nativeEvent.relatedTarget == null) return 276 + setShowActions(true) 277 + }, []) 278 + 279 + const onPress = React.useCallback(() => { 280 + navigation.push('MessagesConversation', { 281 + conversation: convo.id, 282 + }) 283 + }, [convo.id, navigation]) 260 284 261 285 if (!otherUser) { 262 286 return null 263 287 } 264 288 265 289 return ( 266 - <Link 267 - to={`/messages/${convo.id}`} 290 + <Button 291 + label={otherUser.displayName || otherUser.handle} 292 + onPress={onPress} 268 293 style={a.flex_1} 269 - onLongPress={isNative ? menuControl.open : undefined}> 294 + onLongPress={isNative ? menuControl.open : undefined} 295 + // @ts-expect-error web only 296 + onMouseEnter={onMouseEnter} 297 + onMouseLeave={onMouseLeave} 298 + onFocus={onFocus} 299 + onBlur={onMouseLeave}> 270 300 {({hovered, pressed}) => ( 271 301 <View 272 302 style={[ ··· 279 309 (hovered || pressed) && t.atoms.bg_contrast_25, 280 310 ]}> 281 311 <View pointerEvents="none"> 282 - <PreviewableUserAvatar profile={otherUser} size={42} /> 312 + <UserAvatar avatar={otherUser?.avatar} size={42} /> 283 313 </View> 284 314 <View style={[a.flex_1]}> 285 315 <Text ··· 336 366 convo={convo} 337 367 profile={otherUser} 338 368 control={menuControl} 339 - // TODO(sam) show on hover on web 340 - // tricky because it captures the mouse event 341 - hideTrigger 342 369 currentScreen="list" 343 370 showMarkAsRead={convo.unreadCount > 0} 371 + hideTrigger={isNative} 372 + triggerOpacity={ 373 + !gtMobile || showActions || menuControl.isOpen ? 1 : 0 374 + } 344 375 /> 345 376 </View> 346 377 )} 347 - </Link> 378 + </Button> 348 379 ) 349 380 } 350 381