diff options
Diffstat (limited to 'src/components/dms')
-rw-r--r-- | src/components/dms/ActionsWrapper.tsx | 125 | ||||
-rw-r--r-- | src/components/dms/ActionsWrapper.web.tsx | 68 | ||||
-rw-r--r-- | src/components/dms/MessageContextMenu.tsx (renamed from src/components/dms/MessageMenu.tsx) | 128 | ||||
-rw-r--r-- | src/components/dms/MessageItemEmbed.tsx | 33 |
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> ) |