about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/view/com/util/ViewSelector.tsx1
-rw-r--r--src/view/com/util/gestures/HorzSwipe.tsx46
-rw-r--r--src/view/shell/mobile/index.tsx78
3 files changed, 68 insertions, 57 deletions
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index f7652334f..f39254977 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -76,6 +76,7 @@ export function ViewSelector({
   const data = [HEADER_ITEM, SELECTOR_ITEM, ...items]
   return (
     <HorzSwipe
+      hasPriority
       panX={panX}
       swipeEnabled={swipeEnabled || false}
       canSwipeLeft={selectedIndex > 0}
diff --git a/src/view/com/util/gestures/HorzSwipe.tsx b/src/view/com/util/gestures/HorzSwipe.tsx
index 7ae1ee759..0176bd4c8 100644
--- a/src/view/com/util/gestures/HorzSwipe.tsx
+++ b/src/view/com/util/gestures/HorzSwipe.tsx
@@ -12,9 +12,12 @@ import {clamp} from 'lodash'
 
 interface Props {
   panX: Animated.Value
-  canSwipeLeft: boolean
-  canSwipeRight: boolean
-  swipeEnabled: boolean
+  canSwipeLeft?: boolean
+  canSwipeRight?: boolean
+  swipeEnabled?: boolean
+  hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
+  distThresholdDivisor?: number
+  useNativeDriver?: boolean
   onSwipeStart?: () => void
   onSwipeEnd?: (dx: number) => void
   children: React.ReactNode
@@ -22,9 +25,12 @@ interface Props {
 
 export function HorzSwipe({
   panX,
-  canSwipeLeft,
-  canSwipeRight,
+  canSwipeLeft = false,
+  canSwipeRight = false,
   swipeEnabled = true,
+  hasPriority = false,
+  distThresholdDivisor = 1.75,
+  useNativeDriver = false,
   onSwipeStart,
   onSwipeEnd,
   children,
@@ -32,7 +38,7 @@ export function HorzSwipe({
   const winDim = useWindowDimensions()
 
   const swipeVelocityThreshold = 35
-  const swipeDistanceThreshold = winDim.width / 1.75
+  const swipeDistanceThreshold = winDim.width / distThresholdDivisor
 
   const isMovingHorizontally = (
     _: GestureResponderEvent,
@@ -53,10 +59,10 @@ export function HorzSwipe({
     }
 
     const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
-    return (
+    const willHandle =
       isMovingHorizontally(event, gestureState) &&
       ((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight))
-    )
+    return willHandle
   }
 
   const startGesture = () => {
@@ -94,28 +100,42 @@ export function HorzSwipe({
     _: GestureResponderEvent,
     gestureState: PanResponderGestureState,
   ) => {
-    panX.flattenOffset()
-    panX.setValue(0)
     if (
       Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
       Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
       (Math.abs(gestureState.dx) > swipeDistanceThreshold / 3 ||
         Math.abs(gestureState.vx) > swipeVelocityThreshold)
     ) {
-      onSwipeEnd?.(((gestureState.dx / Math.abs(gestureState.dx)) * -1) | 0)
+      const final = ((gestureState.dx / Math.abs(gestureState.dx)) * -1) | 0
+      Animated.timing(panX, {
+        toValue: final,
+        duration: 100,
+        useNativeDriver,
+      }).start(() => {
+        onSwipeEnd?.(final)
+        panX.flattenOffset()
+        panX.setValue(0)
+      })
     } else {
       onSwipeEnd?.(0)
+      Animated.timing(panX, {
+        toValue: 0,
+        duration: 100,
+        useNativeDriver,
+      }).start(() => {
+        panX.flattenOffset()
+        panX.setValue(0)
+      })
     }
   }
 
   const panResponder = PanResponder.create({
     onMoveShouldSetPanResponder: canMoveScreen,
-    onMoveShouldSetPanResponderCapture: canMoveScreen,
     onPanResponderGrant: startGesture,
     onPanResponderMove: respondToGesture,
     onPanResponderTerminate: finishGesture,
     onPanResponderRelease: finishGesture,
-    onPanResponderTerminationRequest: () => true,
+    onPanResponderTerminationRequest: () => !hasPriority,
   })
 
   return (
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index 6bb111877..ee6c9985b 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -1,7 +1,7 @@
 import React, {useState, useEffect, useRef} from 'react'
 import {observer} from 'mobx-react-lite'
 import {
-  useWindowDimensions,
+  Animated as RNAnimated,
   FlatList,
   GestureResponderEvent,
   SafeAreaView,
@@ -9,12 +9,12 @@ import {
   Text,
   TouchableOpacity,
   useColorScheme,
+  useWindowDimensions,
   View,
   ViewStyle,
 } from 'react-native'
 import {ScreenContainer, Screen} from 'react-native-screens'
 import LinearGradient from 'react-native-linear-gradient'
-import {GestureDetector, Gesture} from 'react-native-gesture-handler'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import Animated, {
   Easing,
@@ -32,6 +32,7 @@ import {NavigationModel} from '../../../state/models/navigation'
 import {match, MatchResult} from '../../routes'
 import {Login} from '../../screens/Login'
 import {Onboard} from '../../screens/Onboard'
+import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
 import {Modal} from '../../com/modals/Modal'
 import {TabsSelector} from './TabsSelector'
 import {Composer} from './Composer'
@@ -45,9 +46,7 @@ import {
   BellIcon,
   BellIconSolid,
 } from '../../lib/icons'
-
-const SWIPE_GESTURE_DIST_TRIGGER = 0.3
-const SWIPE_GESTURE_VEL_TRIGGER = 2000
+import {useAnimatedValue} from '../../lib/useAnimatedValue'
 
 const Btn = ({
   icon,
@@ -120,7 +119,7 @@ export const MobileShell: React.FC = observer(() => {
   const [isTabsSelectorActive, setTabsSelectorActive] = useState(false)
   const scrollElRef = useRef<FlatList | undefined>()
   const winDim = useWindowDimensions()
-  const swipeGestureInterp = useSharedValue<number>(0)
+  const swipeGestureInterp = useAnimatedValue(0)
   const tabMenuInterp = useSharedValue<number>(0)
   const newTabInterp = useSharedValue<number>(0)
   const [isRunningNewTabAnim, setIsRunningNewTabAnim] = useState(false)
@@ -185,37 +184,22 @@ export const MobileShell: React.FC = observer(() => {
 
   // navigation swipes
   // =
-  const goBack = () => store.nav.tab.goBack()
-  const swipeGesture = Gesture.Pan()
-    .enabled(store.nav.tab.canGoBack)
-    .onUpdate(e => {
-      if (store.nav.tab.canGoBack) {
-        swipeGestureInterp.value = Math.max(e.translationX / winDim.width, 0)
-      }
-    })
-    .onEnd(e => {
-      if (
-        swipeGestureInterp.value >= SWIPE_GESTURE_DIST_TRIGGER ||
-        e.velocityX > SWIPE_GESTURE_VEL_TRIGGER
-      ) {
-        swipeGestureInterp.value = withTiming(1, {duration: 100}, () => {
-          runOnJS(goBack)()
-        })
-      } else {
-        swipeGestureInterp.value = withTiming(0, {duration: 100})
-      }
-    })
-  useEffect(() => {
-    // reset the swipe interopolation when the page changes
-    swipeGestureInterp.value = 0
-  }, [swipeGestureInterp, store.nav.tab.current])
-
-  const swipeTransform = useAnimatedStyle(() => ({
-    transform: [{translateX: swipeGestureInterp.value * winDim.width}],
-  }))
-  const swipeOpacity = useAnimatedStyle(() => ({
-    opacity: interpolate(swipeGestureInterp.value, [0, 1.0], [0.6, 0.0]),
-  }))
+  const onNavSwipeEnd = (dx: number) => {
+    if (dx < 0 && store.nav.tab.canGoBack) {
+      store.nav.tab.goBack()
+    }
+  }
+  const swipeTransform = {
+    transform: [
+      {translateX: RNAnimated.multiply(swipeGestureInterp, winDim.width * -1)},
+    ],
+  }
+  const swipeOpacity = {
+    opacity: swipeGestureInterp.interpolate({
+      inputRange: [-1, 0, 1],
+      outputRange: [0, 0.6, 0],
+    }),
+  }
   const tabMenuTransform = useAnimatedStyle(() => ({
     transform: [{translateY: tabMenuInterp.value * -320}],
   }))
@@ -252,7 +236,13 @@ export const MobileShell: React.FC = observer(() => {
   return (
     <View style={styles.outerContainer}>
       <SafeAreaView style={styles.innerContainer}>
-        <GestureDetector gesture={swipeGesture}>
+        <HorzSwipe
+          distThresholdDivisor={1.5}
+          useNativeDriver
+          panX={swipeGestureInterp}
+          swipeEnabled
+          canSwipeLeft={store.nav.tab.canGoBack}
+          onSwipeEnd={onNavSwipeEnd}>
           <ScreenContainer style={styles.screenContainer}>
             {screenRenderDesc.screens.map(
               ({Com, navIdx, params, key, current, previous}) => {
@@ -261,20 +251,20 @@ export const MobileShell: React.FC = observer(() => {
                     key={key}
                     style={[StyleSheet.absoluteFill]}
                     activityState={current ? 2 : previous ? 1 : 0}>
-                    <Animated.View
+                    <RNAnimated.View
                       style={
                         current ? [styles.screenMask, swipeOpacity] : undefined
                       }
                     />
-                    <Animated.View
+                    <RNAnimated.View
                       style={[
                         s.flex1,
                         styles.screen,
                         current
                           ? [
                               swipeTransform,
-                              tabMenuTransform,
-                              isRunningNewTabAnim ? newTabTransform : undefined,
+                              // tabMenuTransform, TODO
+                              // isRunningNewTabAnim ? newTabTransform : undefined, TODO
                             ]
                           : undefined,
                       ]}>
@@ -284,13 +274,13 @@ export const MobileShell: React.FC = observer(() => {
                         visible={current}
                         scrollElRef={current ? scrollElRef : undefined}
                       />
-                    </Animated.View>
+                    </RNAnimated.View>
                   </Screen>
                 )
               },
             )}
           </ScreenContainer>
-        </GestureDetector>
+        </HorzSwipe>
       </SafeAreaView>
       {isTabsSelectorActive ? (
         <View