diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/dms/ActionsWrapper.tsx | 74 | ||||
-rw-r--r-- | src/components/dms/MessagesListHeader.tsx | 3 | ||||
-rw-r--r-- | src/components/dms/NewMessagesPill.tsx | 100 |
3 files changed, 114 insertions, 63 deletions
diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx index 3b9a56bdc..a349c3cfa 100644 --- a/src/components/dms/ActionsWrapper.tsx +++ b/src/components/dms/ActionsWrapper.tsx @@ -1,5 +1,6 @@ import React from 'react' -import {Keyboard, Pressable, View} from 'react-native' +import {Keyboard} from 'react-native' +import {Gesture, GestureDetector} from 'react-native-gesture-handler' import Animated, { cancelAnimation, runOnJS, @@ -15,8 +16,6 @@ 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, @@ -30,56 +29,59 @@ export function ActionsWrapper({ 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 = React.useCallback(() => { + playHaptic() Keyboard.dismiss() menuControl.open() - }, [menuControl]) + }, [menuControl, playHaptic]) const shrink = React.useCallback(() => { 'worklet' cancelAnimation(scale) - scale.value = withTiming(1, {duration: 200}, () => { - animationDidComplete.value = false - }) - }, [animationDidComplete, scale]) + scale.value = withTiming(1, {duration: 200}) + }, [scale]) - const grow = React.useCallback(() => { - 'worklet' - scale.value = withTiming(1.05, {duration: 450}, finished => { - if (!finished) return - animationDidComplete.value = true - runOnJS(playHaptic)() - runOnJS(open)() + const doubleTapGesture = Gesture.Tap() + .numberOfTaps(2) + .hitSlop(HITSLOP_10) + .onEnd(open) - shrink() + const pressAndHoldGesture = Gesture.LongPress() + .onStart(() => { + scale.value = withTiming(1.05, {duration: 200}, finished => { + if (!finished) return + runOnJS(open)() + shrink() + }) }) - }, [scale, animationDidComplete, playHaptic, shrink, open]) + .onTouchesUp(shrink) + .onTouchesMove(shrink) + .cancelsTouchesInView(false) + .runOnJS(true) + + const composedGestures = Gesture.Exclusive( + doubleTapGesture, + pressAndHoldGesture, + ) return ( - <View - style={[ - { - maxWidth: '80%', - }, - isFromSelf ? a.self_end : a.self_start, - ]}> - <AnimatedPressable - style={animatedStyle} - unstable_pressDelay={200} - onPressIn={grow} - onTouchEnd={shrink} - hitSlop={HITSLOP_10}> + <GestureDetector gesture={composedGestures}> + <Animated.View + style={[ + { + maxWidth: '80%', + }, + isFromSelf ? a.self_end : a.self_start, + animatedStyle, + ]}> {children} - </AnimatedPressable> - <MessageMenu message={message} control={menuControl} /> - </View> + <MessageMenu message={message} control={menuControl} /> + </Animated.View> + </GestureDetector> ) } diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 1e6fd3609..a6dff4032 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -1,5 +1,5 @@ import React, {useCallback} from 'react' -import {Keyboard, TouchableOpacity, View} from 'react-native' +import {TouchableOpacity, View} from 'react-native' import { AppBskyActorDefs, ModerationCause, @@ -46,7 +46,6 @@ export let MessagesListHeader = ({ if (isWeb) { navigation.replace('Messages', {}) } else { - Keyboard.dismiss() navigation.goBack() } }, [navigation]) diff --git a/src/components/dms/NewMessagesPill.tsx b/src/components/dms/NewMessagesPill.tsx index 4a0ba22c9..924f7c455 100644 --- a/src/components/dms/NewMessagesPill.tsx +++ b/src/components/dms/NewMessagesPill.tsx @@ -1,47 +1,97 @@ import React from 'react' -import {View} from 'react-native' -import Animated from 'react-native-reanimated' +import {Pressable, View} from 'react-native' +import Animated, { + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Trans} from '@lingui/macro' import { ScaleAndFadeIn, ScaleAndFadeOut, } from 'lib/custom-animations/ScaleAndFade' +import {useHaptics} from 'lib/haptics' +import {isAndroid, isIOS, isWeb} from 'platform/detection' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' -export function NewMessagesPill() { +const AnimatedPressable = Animated.createAnimatedComponent(Pressable) + +export function NewMessagesPill({ + onPress: onPressInner, +}: { + onPress: () => void +}) { const t = useTheme() + const playHaptic = useHaptics() + const {bottom: bottomInset} = useSafeAreaInsets() + const bottomBarHeight = isIOS ? 42 : isAndroid ? 60 : 0 + const bottomOffset = isWeb ? 0 : bottomInset + bottomBarHeight + + const scale = useSharedValue(1) + + const onPressIn = React.useCallback(() => { + if (isWeb) return + scale.value = withTiming(1.075, {duration: 100}) + }, [scale]) + + const onPressOut = React.useCallback(() => { + if (isWeb) return + scale.value = withTiming(1, {duration: 100}) + }, [scale]) + + const onPress = React.useCallback(() => { + runOnJS(playHaptic)() + onPressInner?.() + }, [onPressInner, playHaptic]) - React.useEffect(() => {}, []) + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{scale: scale.value}], + })) return ( - <Animated.View + <View style={[ - a.py_sm, - a.rounded_full, - a.shadow_sm, - a.border, - t.atoms.bg_contrast_50, - t.atoms.border_contrast_medium, + a.absolute, + a.w_full, + a.z_10, + a.align_center, { - position: 'absolute', - bottom: 70, - width: '40%', - left: '30%', - alignItems: 'center', - shadowOpacity: 0.125, - shadowRadius: 12, - shadowOffset: {width: 0, height: 5}, + bottom: bottomOffset + 70, + // Don't prevent scrolling in this area _except_ for in the pill itself + pointerEvents: 'box-none', }, - ]} - entering={ScaleAndFadeIn} - exiting={ScaleAndFadeOut}> - <View style={{flex: 1}}> + ]}> + <AnimatedPressable + style={[ + a.py_sm, + a.rounded_full, + a.shadow_sm, + a.border, + t.atoms.bg_contrast_50, + t.atoms.border_contrast_medium, + { + width: 160, + alignItems: 'center', + shadowOpacity: 0.125, + shadowRadius: 12, + shadowOffset: {width: 0, height: 5}, + pointerEvents: 'box-only', + }, + animatedStyle, + ]} + entering={ScaleAndFadeIn} + exiting={ScaleAndFadeOut} + onPress={onPress} + onPressIn={onPressIn} + onPressOut={onPressOut}> <Text style={[a.font_bold]}> <Trans>New messages</Trans> </Text> - </View> - </Animated.View> + </AnimatedPressable> + </View> ) } |