about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-12-10 04:40:40 +0000
committerGitHub <noreply@github.com>2024-12-10 04:40:40 +0000
commit46e1e5cee6f0670444da4e1c64a26d8247cf49ec (patch)
tree6b74644ea81733c11794796b712b5fe7ab077db5 /src
parentfec3352b68473f1e1d9b2c038a783b7e2c8650e6 (diff)
downloadvoidsky-46e1e5cee6f0670444da4e1c64a26d8247cf49ec.tar.zst
Fix drawer swipe (#7007)
* Fix drawer swipe

* Remove existing setDrawerSwipeDisabled management

This is already pretty error-prone. And with tracking whether we're idle it's going to get more complicated. Let's pause and think.

* Move setDrawerSwipeDisabled logic into Pager

* Remove win/2 threshold

It feels super arbitrary and breaks muscle memory. If the gesture is reliable, we shouldn't need it.

* Maybe work around iOS freeze

* Tweak gestures, add comments

* Tune gestures
Diffstat (limited to 'src')
-rw-r--r--src/screens/Hashtag.tsx6
-rw-r--r--src/view/com/pager/Pager.tsx39
-rw-r--r--src/view/screens/Home.tsx12
-rw-r--r--src/view/screens/Profile.tsx12
-rw-r--r--src/view/screens/Search/Search.tsx6
-rw-r--r--src/view/shell/index.tsx28
6 files changed, 65 insertions, 38 deletions
diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx
index a0fc3707c..a87487150 100644
--- a/src/screens/Hashtag.tsx
+++ b/src/screens/Hashtag.tsx
@@ -14,7 +14,7 @@ import {cleanError} from '#/lib/strings/errors'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {enforceLen} from '#/lib/strings/helpers'
 import {useSearchPostsQuery} from '#/state/queries/search-posts'
-import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
+import {useSetMinimalShellMode} from '#/state/shell'
 import {Pager} from '#/view/com/pager/Pager'
 import {TabBar} from '#/view/com/pager/TabBar'
 import {Post} from '#/view/com/post/Post'
@@ -63,7 +63,6 @@ export default function HashtagScreen({
 
   const [activeTab, setActiveTab] = React.useState(0)
   const setMinimalShellMode = useSetMinimalShellMode()
-  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -74,10 +73,9 @@ export default function HashtagScreen({
   const onPageSelected = React.useCallback(
     (index: number) => {
       setMinimalShellMode(false)
-      setDrawerSwipeDisabled(index > 0)
       setActiveTab(index)
     },
-    [setDrawerSwipeDisabled, setMinimalShellMode],
+    [setMinimalShellMode],
   )
 
   const sections = React.useMemo(() => {
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index da7fd1e93..2c0bbee52 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -1,5 +1,7 @@
-import React, {forwardRef} from 'react'
+import React, {forwardRef, useCallback, useContext} from 'react'
 import {View} from 'react-native'
+import {DrawerGestureContext} from 'react-native-drawer-layout'
+import {Gesture, GestureDetector} from 'react-native-gesture-handler'
 import PagerView, {
   PagerViewOnPageScrollEventData,
   PagerViewOnPageSelectedEvent,
@@ -13,7 +15,9 @@ import Animated, {
   useHandler,
   useSharedValue,
 } from 'react-native-reanimated'
+import {useFocusEffect} from '@react-navigation/native'
 
+import {useSetDrawerSwipeDisabled} from '#/state/shell'
 import {atoms as a, native} from '#/alf'
 
 export type PageSelectedEvent = PagerViewOnPageSelectedEvent
@@ -58,6 +62,18 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
     const [selectedPage, setSelectedPage] = React.useState(initialPage)
     const pagerView = React.useRef<PagerView>(null)
 
+    const [isIdle, setIsIdle] = React.useState(true)
+    const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+    useFocusEffect(
+      useCallback(() => {
+        const canSwipeDrawer = selectedPage === 0 && isIdle
+        setDrawerSwipeDisabled(!canSwipeDrawer)
+        return () => {
+          setDrawerSwipeDisabled(false)
+        }
+      }, [setDrawerSwipeDisabled, selectedPage, isIdle]),
+    )
+
     React.useImperativeHandle(ref, () => ({
       setPage: (index: number) => {
         pagerView.current?.setPage(index)
@@ -96,6 +112,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
         },
         onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
           'worklet'
+          runOnJS(setIsIdle)(e.pageScrollState === 'idle')
           if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
             // This is a programmatic scroll on Android.
             // Stay "idle" to match iOS and avoid confusing downstream code.
@@ -113,6 +130,10 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
       [parentOnPageScrollStateChanged],
     )
 
+    const drawerGesture = useContext(DrawerGestureContext)!
+    const nativeGesture =
+      Gesture.Native().requireExternalGestureToFail(drawerGesture)
+
     return (
       <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
         {renderTabBar({
@@ -121,13 +142,15 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
           dragProgress,
           dragState,
         })}
-        <AnimatedPagerView
-          ref={pagerView}
-          style={[a.flex_1]}
-          initialPage={initialPage}
-          onPageScroll={handlePageScroll}>
-          {children}
-        </AnimatedPagerView>
+        <GestureDetector gesture={nativeGesture}>
+          <AnimatedPagerView
+            ref={pagerView}
+            style={[a.flex_1]}
+            initialPage={initialPage}
+            onPageScroll={handlePageScroll}>
+            {children}
+          </AnimatedPagerView>
+        </GestureDetector>
       </View>
     )
   },
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 1218a5ba0..59b296730 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -19,7 +19,7 @@ import {FeedParams} from '#/state/queries/post-feed'
 import {usePreferencesQuery} from '#/state/queries/preferences'
 import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
 import {useSession} from '#/state/session'
-import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
+import {useSetMinimalShellMode} from '#/state/shell'
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
 import {FeedPage} from '#/view/com/feeds/FeedPage'
@@ -127,15 +127,10 @@ function HomeScreenReady({
 
   const {hasSession} = useSession()
   const setMinimalShellMode = useSetMinimalShellMode()
-  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
   useFocusEffect(
     React.useCallback(() => {
       setMinimalShellMode(false)
-      setDrawerSwipeDisabled(selectedIndex > 0)
-      return () => {
-        setDrawerSwipeDisabled(false)
-      }
-    }, [setDrawerSwipeDisabled, selectedIndex, setMinimalShellMode]),
+    }, [setMinimalShellMode]),
   )
 
   useFocusEffect(
@@ -154,7 +149,6 @@ function HomeScreenReady({
   const onPageSelected = React.useCallback(
     (index: number) => {
       setMinimalShellMode(false)
-      setDrawerSwipeDisabled(index > 0)
       const feed = allFeeds[index]
       // Mutate the ref before setting state to avoid the imperative syncing effect
       // above from starting a loop on Android when swiping back and forth.
@@ -166,7 +160,7 @@ function HomeScreenReady({
         feedUrl: feed,
       })
     },
-    [setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds],
+    [setSelectedFeed, setMinimalShellMode, allFeeds],
   )
 
   const onPressSelected = React.useCallback(() => {
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 6a9b6f7f2..782e9b9c8 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -32,7 +32,7 @@ import {resetProfilePostsQueries} from '#/state/queries/post-feed'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useAgent, useSession} from '#/state/session'
-import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
+import {useSetMinimalShellMode} from '#/state/shell'
 import {useComposerControls} from '#/state/shell/composer'
 import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
 import {ProfileLists} from '#/view/com/lists/ProfileLists'
@@ -183,7 +183,6 @@ function ProfileScreenLoaded({
   })
   const [currentPage, setCurrentPage] = React.useState(0)
   const {_} = useLingui()
-  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
 
   const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
 
@@ -307,15 +306,6 @@ function ProfileScreenLoaded({
     }, [setMinimalShellMode, currentPage, scrollSectionToTop]),
   )
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setDrawerSwipeDisabled(currentPage > 0)
-      return () => {
-        setDrawerSwipeDisabled(false)
-      }
-    }, [setDrawerSwipeDisabled, currentPage]),
-  )
-
   // events
   // =
 
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 0871458c9..ed62c5a51 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -47,7 +47,7 @@ import {usePopularFeedsSearch} from '#/state/queries/feed'
 import {useSearchPostsQuery} from '#/state/queries/search-posts'
 import {useSession} from '#/state/session'
 import {useSetDrawerOpen} from '#/state/shell'
-import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
+import {useSetMinimalShellMode} from '#/state/shell'
 import {Pager} from '#/view/com/pager/Pager'
 import {TabBar} from '#/view/com/pager/TabBar'
 import {Post} from '#/view/com/post/Post'
@@ -471,7 +471,6 @@ let SearchScreenInner = ({
 }): React.ReactNode => {
   const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
-  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
   const {hasSession} = useSession()
   const {isDesktop} = useWebMediaQueries()
   const [activeTab, setActiveTab] = React.useState(0)
@@ -480,10 +479,9 @@ let SearchScreenInner = ({
   const onPageSelected = React.useCallback(
     (index: number) => {
       setMinimalShellMode(false)
-      setDrawerSwipeDisabled(index > 0)
       setActiveTab(index)
     },
-    [setDrawerSwipeDisabled, setMinimalShellMode],
+    [setMinimalShellMode],
   )
 
   const sections = React.useMemo(() => {
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 8dbbbea6f..179e8858e 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -90,6 +90,7 @@ function ShellInner() {
     }
   }, [dedupe, navigation])
 
+  const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
   return (
     <>
       <View style={[a.h_full]}>
@@ -98,12 +99,35 @@ function ShellInner() {
           <Drawer
             renderDrawerContent={renderDrawerContent}
             drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
+            configureGestureHandler={handler => {
+              if (swipeEnabled) {
+                if (isDrawerOpen) {
+                  return handler.activeOffsetX([-1, 1])
+                } else {
+                  return (
+                    handler
+                      // Any movement to the left is a pager swipe
+                      // so fail the drawer gesture immediately.
+                      .failOffsetX(-1)
+                      // Don't rush declaring that a movement to the right
+                      // is a drawer swipe. It could be a vertical scroll.
+                      .activeOffsetX(5)
+                  )
+                }
+              } else {
+                // Fail the gesture immediately.
+                // This seems more reliable than the `swipeEnabled` prop.
+                // With `swipeEnabled` alone, the gesture may freeze after toggling off/on.
+                return handler.failOffsetX([0, 0]).failOffsetY([0, 0])
+              }
+            }}
             open={isDrawerOpen}
             onOpen={onOpenDrawer}
             onClose={onCloseDrawer}
-            swipeEdgeWidth={winDim.width / 2}
+            swipeEdgeWidth={winDim.width}
+            swipeMinVelocity={100}
+            swipeMinDistance={10}
             drawerType={isIOS ? 'slide' : 'front'}
-            swipeEnabled={!canGoBack && hasSession && !isDrawerSwipeDisabled}
             overlayStyle={{
               backgroundColor: select(t.name, {
                 light: 'rgba(0, 57, 117, 0.1)',