-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Clipclops] Message actions for native and web (#3807)
* haptic on long press * add animation to press and hold * eslint disable for now * adjust styles * dont trigger if animation is cancelled * organize * add a delete menu * reset scale automatically * message actions dialog cleanup center the trigger handle focus/unfocus better make triggers accessible weg dropdown menu add a wep specific wrapper decrease press delay add report button improve shrink logic use `self_end` instead of `margin: auto` rm extra `?` move `MessageItem` to `components` add delete button * rm some padding * update after merge * fix merge * web only types * fix crash * add an explanation * fix web types --------- Co-authored-by: Samuel Newman <[email protected]>
- Loading branch information
Showing
5 changed files
with
297 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import React, {useCallback} from 'react' | ||
import {Pressable, View} from 'react-native' | ||
import Animated, { | ||
cancelAnimation, | ||
runOnJS, | ||
useAnimatedStyle, | ||
useSharedValue, | ||
withTiming, | ||
} from 'react-native-reanimated' | ||
import {ChatBskyConvoDefs} from '@atproto-labs/api' | ||
|
||
import {useHaptics} from 'lib/haptics' | ||
import {atoms as a} from '#/alf' | ||
import {MessageMenu} from '#/components/dms/MessageMenu' | ||
import {useMenuControl} from '#/components/Menu' | ||
|
||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable) | ||
|
||
export function ActionsWrapper({ | ||
message, | ||
isFromSelf, | ||
children, | ||
}: { | ||
message: ChatBskyConvoDefs.MessageView | ||
isFromSelf: boolean | ||
children: React.ReactNode | ||
}) { | ||
const playHaptic = useHaptics() | ||
const menuControl = useMenuControl() | ||
|
||
const scale = useSharedValue(1) | ||
const animationDidComplete = useSharedValue(false) | ||
|
||
const animatedStyle = useAnimatedStyle(() => ({ | ||
transform: [{scale: scale.value}], | ||
})) | ||
|
||
// Reanimated's `runOnJS` doesn't like refs, so we can't use `runOnJS(menuControl.open)()`. Instead, we'll use this | ||
// function | ||
const open = useCallback(() => { | ||
menuControl.open() | ||
}, [menuControl]) | ||
|
||
const shrink = useCallback(() => { | ||
'worklet' | ||
cancelAnimation(scale) | ||
scale.value = withTiming(1, {duration: 200}, () => { | ||
animationDidComplete.value = false | ||
}) | ||
}, [animationDidComplete, scale]) | ||
|
||
const grow = React.useCallback(() => { | ||
'worklet' | ||
scale.value = withTiming(1.05, {duration: 750}, finished => { | ||
if (!finished) return | ||
animationDidComplete.value = true | ||
runOnJS(playHaptic)() | ||
runOnJS(open)() | ||
|
||
shrink() | ||
}) | ||
}, [scale, animationDidComplete, playHaptic, shrink, open]) | ||
|
||
return ( | ||
<View | ||
style={[ | ||
{ | ||
maxWidth: '65%', | ||
}, | ||
isFromSelf ? a.self_end : a.self_start, | ||
]}> | ||
<AnimatedPressable | ||
style={animatedStyle} | ||
unstable_pressDelay={200} | ||
onPressIn={grow} | ||
onTouchEnd={shrink}> | ||
{children} | ||
</AnimatedPressable> | ||
<MessageMenu message={message} control={menuControl} hideTrigger={true} /> | ||
</View> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import React from 'react' | ||
import {StyleSheet, View} from 'react-native' | ||
import {ChatBskyConvoDefs} from '@atproto-labs/api' | ||
|
||
import {atoms as a} from '#/alf' | ||
import {MessageMenu} from '#/components/dms/MessageMenu' | ||
import {useMenuControl} from '#/components/Menu' | ||
|
||
export function ActionsWrapper({ | ||
message, | ||
isFromSelf, | ||
children, | ||
}: { | ||
message: ChatBskyConvoDefs.MessageView | ||
isFromSelf: boolean | ||
children: React.ReactNode | ||
}) { | ||
const menuControl = useMenuControl() | ||
const viewRef = React.useRef(null) | ||
|
||
const [showActions, setShowActions] = React.useState(false) | ||
|
||
const onMouseEnter = React.useCallback(() => { | ||
setShowActions(true) | ||
}, []) | ||
|
||
const onMouseLeave = React.useCallback(() => { | ||
setShowActions(false) | ||
}, []) | ||
|
||
// We need to handle the `onFocus` separately because we want to know if there is a related target (the element | ||
// that is losing focus). If there isn't that means the focus is coming from a dropdown that is now closed. | ||
const onFocus = React.useCallback<React.FocusEventHandler>(e => { | ||
if (e.nativeEvent.relatedTarget == null) return | ||
setShowActions(true) | ||
}, []) | ||
|
||
return ( | ||
<View | ||
// @ts-expect-error web only | ||
onMouseEnter={onMouseEnter} | ||
onMouseLeave={onMouseLeave} | ||
onFocus={onFocus} | ||
onBlur={onMouseLeave} | ||
style={StyleSheet.flatten([a.flex_1, a.flex_row])} | ||
ref={viewRef}> | ||
{isFromSelf && ( | ||
<View | ||
style={[ | ||
a.mr_xl, | ||
a.justify_center, | ||
{ | ||
marginLeft: 'auto', | ||
}, | ||
]}> | ||
<MessageMenu | ||
message={message} | ||
control={menuControl} | ||
triggerOpacity={showActions || menuControl.isOpen ? 1 : 0} | ||
onTriggerPress={onMouseEnter} | ||
// @ts-expect-error web only | ||
onMouseLeave={onMouseLeave} | ||
/> | ||
</View> | ||
)} | ||
<View | ||
style={{ | ||
maxWidth: '65%', | ||
}}> | ||
{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} | ||
onTriggerPress={onMouseEnter} | ||
// @ts-expect-error web only | ||
onMouseLeave={onMouseLeave} | ||
/> | ||
</View> | ||
)} | ||
</View> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import React from 'react' | ||
import {Pressable, View} from 'react-native' | ||
import {ChatBskyConvoDefs} from '@atproto-labs/api' | ||
import {msg} from '@lingui/macro' | ||
import {useLingui} from '@lingui/react' | ||
|
||
import {useSession} from 'state/session' | ||
import {atoms as a, useTheme} from '#/alf' | ||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' | ||
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' | ||
|
||
export let MessageMenu = ({ | ||
message, | ||
control, | ||
hideTrigger, | ||
triggerOpacity, | ||
}: { | ||
hideTrigger?: boolean | ||
triggerOpacity?: number | ||
onTriggerPress?: () => void | ||
message: ChatBskyConvoDefs.MessageView | ||
control: Menu.MenuControlProps | ||
}): React.ReactNode => { | ||
const {_} = useLingui() | ||
const t = useTheme() | ||
const {currentAccount} = useSession() | ||
const deleteControl = usePromptControl() | ||
|
||
const isFromSelf = message.sender?.did === currentAccount?.did | ||
|
||
const onDelete = React.useCallback(() => { | ||
// TODO delete the message | ||
}, []) | ||
|
||
const onReport = React.useCallback(() => { | ||
// TODO report the message | ||
}, []) | ||
|
||
return ( | ||
<> | ||
<Menu.Root control={control}> | ||
{!hideTrigger && ( | ||
<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="sm" style={t.atoms.text} /> | ||
</Pressable> | ||
)} | ||
</Menu.Trigger> | ||
</View> | ||
)} | ||
|
||
<Menu.Outer> | ||
<Menu.Group> | ||
<Menu.Item | ||
testID="messageDropdownDeleteBtn" | ||
label={_(msg`Delete message`)} | ||
onPress={deleteControl.open}> | ||
<Menu.ItemText>{_(msg`Delete`)}</Menu.ItemText> | ||
<Menu.ItemIcon icon={Trash} position="right" /> | ||
</Menu.Item> | ||
{!isFromSelf && ( | ||
<Menu.Item | ||
testID="messageDropdownReportBtn" | ||
label={_(msg`Report message`)} | ||
onPress={onReport}> | ||
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText> | ||
<Menu.ItemIcon icon={Warning} position="right" /> | ||
</Menu.Item> | ||
)} | ||
</Menu.Group> | ||
</Menu.Outer> | ||
</Menu.Root> | ||
|
||
<Prompt.Basic | ||
control={deleteControl} | ||
title={_(msg`Delete message`)} | ||
description={_( | ||
msg`Are you sure you want to delete this message? The message will be deleted for you, but not for other participants.`, | ||
)} | ||
confirmButtonCta={_(msg`Delete`)} | ||
confirmButtonColor="negative" | ||
onConfirm={onDelete} | ||
/> | ||
</> | ||
) | ||
} | ||
MessageMenu = React.memo(MessageMenu) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters