New ContextMenu menu type for DM messages (#8014)

* get context menu somewhat working ish

* take screenshot rather than double rendering

* get animations somewhat working

* get transform animation working

* rm log

* upwards safe area

* get working on android

* get android working once and for all

* fix positioning on both platforms

* use dark blur on ios always, fix dark mode

* allow closing with hardware back press

* try and fix type error

* add note about ts-ignore

* round post

* add image capture error handling

* extract magic numbers

* set explicit embed width, rm top margin

* Message embed width tweaks

* Format

* fix position of embeds

* same as above for web

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Samuel Newman
2025-03-21 18:29:14 +02:00
committed by GitHub
parent f6f253b4c9
commit c4785ef96e
15 changed files with 1090 additions and 295 deletions
+47 -78
View File
@@ -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>
)
}