about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/dms/ActionsWrapper.tsx74
-rw-r--r--src/components/dms/MessagesListHeader.tsx3
-rw-r--r--src/components/dms/NewMessagesPill.tsx100
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>
   )
 }