From 46e1e5cee6f0670444da4e1c64a26d8247cf49ec Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 10 Dec 2024 04:40:40 +0000 Subject: 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 --- patches/react-native-drawer-layout+4.0.4.patch | 876 +++++++++++++++++++++++++ src/screens/Hashtag.tsx | 6 +- src/view/com/pager/Pager.tsx | 39 +- src/view/screens/Home.tsx | 12 +- src/view/screens/Profile.tsx | 12 +- src/view/screens/Search/Search.tsx | 6 +- src/view/shell/index.tsx | 28 +- 7 files changed, 941 insertions(+), 38 deletions(-) create mode 100644 patches/react-native-drawer-layout+4.0.4.patch diff --git a/patches/react-native-drawer-layout+4.0.4.patch b/patches/react-native-drawer-layout+4.0.4.patch new file mode 100644 index 000000000..004fb4a00 --- /dev/null +++ b/patches/react-native-drawer-layout+4.0.4.patch @@ -0,0 +1,876 @@ +diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/index.js b/node_modules/react-native-drawer-layout/lib/commonjs/index.js +index 3dce76e..6c4b3e5 100644 +--- a/node_modules/react-native-drawer-layout/lib/commonjs/index.js ++++ b/node_modules/react-native-drawer-layout/lib/commonjs/index.js +@@ -9,6 +9,12 @@ Object.defineProperty(exports, "Drawer", { + return _Drawer.Drawer; + } + }); ++Object.defineProperty(exports, "DrawerGestureContext", { ++ enumerable: true, ++ get: function () { ++ return _DrawerGestureContext.DrawerGestureContext; ++ } ++}); + Object.defineProperty(exports, "DrawerProgressContext", { + enumerable: true, + get: function () { +@@ -21,6 +27,7 @@ Object.defineProperty(exports, "useDrawerProgress", { + return _useDrawerProgress.useDrawerProgress; + } + }); ++var _DrawerGestureContext = require("./utils/DrawerGestureContext.js"); + var _DrawerProgressContext = require("./utils/DrawerProgressContext.js"); + var _useDrawerProgress = require("./utils/useDrawerProgress.js"); + var _Drawer = require("./views/Drawer"); +diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js +new file mode 100644 +index 0000000..de6d793 +--- /dev/null ++++ b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js +@@ -0,0 +1,11 @@ ++"use strict"; ++ ++Object.defineProperty(exports, "__esModule", { ++ value: true ++}); ++exports.DrawerGestureContext = void 0; ++var React = _interopRequireWildcard(require("react")); ++function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } ++function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } ++const DrawerGestureContext = exports.DrawerGestureContext = /*#__PURE__*/React.createContext(undefined); ++//# sourceMappingURL=DrawerGestureContext.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js +index fd65ab8..86a9a6a 100644 +--- a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js ++++ b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js +@@ -8,6 +8,7 @@ var React = _interopRequireWildcard(require("react")); + var _reactNative = require("react-native"); + var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated")); + var _useLatestCallback = _interopRequireDefault(require("use-latest-callback")); ++var _DrawerGestureContext = require("../utils/DrawerGestureContext.js"); + var _DrawerProgressContext = require("../utils/DrawerProgressContext.js"); + var _getDrawerWidth = require("../utils/getDrawerWidth.js"); + var _GestureHandler = require("./GestureHandler"); +@@ -79,38 +80,38 @@ function Drawer({ + return () => hideStatusBar(false); + }, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]); + const interactionHandleRef = React.useRef(null); +- const startInteraction = () => { ++ const startInteraction = React.useCallback(() => { + interactionHandleRef.current = _reactNative.InteractionManager.createInteractionHandle(); +- }; +- const endInteraction = () => { ++ }, []); ++ const endInteraction = React.useCallback(() => { + if (interactionHandleRef.current != null) { + _reactNative.InteractionManager.clearInteractionHandle(interactionHandleRef.current); + interactionHandleRef.current = null; + } +- }; +- const hideKeyboard = () => { ++ }, []); ++ const hideKeyboard = React.useCallback(() => { + if (keyboardDismissMode === 'on-drag') { + _reactNative.Keyboard.dismiss(); + } +- }; +- const onGestureBegin = () => { ++ }, [keyboardDismissMode]); ++ const onGestureBegin = React.useCallback(() => { + onGestureStart?.(); + startInteraction(); + hideKeyboard(); + hideStatusBar(true); +- }; +- const onGestureFinish = () => { ++ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]); ++ const onGestureFinish = React.useCallback(() => { + onGestureEnd?.(); + endInteraction(); +- }; +- const onGestureAbort = () => { ++ }, [onGestureEnd, endInteraction]); ++ const onGestureAbort = React.useCallback(() => { + onGestureCancel?.(); + endInteraction(); +- }; ++ }, [onGestureCancel, endInteraction]); + + // FIXME: Currently hitSlop is broken when on Android when drawer is on right + // https://github.com/software-mansion/react-native-gesture-handler/issues/569 +- const hitSlop = isRight ? ++ const hitSlop = React.useMemo(() => isRight ? + // Extend hitSlop to the side of the screen when drawer is closed + // This lets the user drag the drawer from the side of the screen + { +@@ -119,7 +120,7 @@ function Drawer({ + } : { + left: 0, + width: isOpen ? undefined : swipeEdgeWidth +- }; ++ }, [isRight, isOpen, swipeEdgeWidth]); + const touchStartX = (0, _reactNativeReanimated.useSharedValue)(0); + const touchX = (0, _reactNativeReanimated.useSharedValue)(0); + const translationX = (0, _reactNativeReanimated.useSharedValue)(getDrawerTranslationX(open)); +@@ -158,40 +159,43 @@ function Drawer({ + }, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]); + React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]); + const startX = (0, _reactNativeReanimated.useSharedValue)(0); +- let pan = _GestureHandler.Gesture?.Pan().onBegin(event => { +- 'worklet'; ++ const pan = React.useMemo(() => { ++ let panGesture = _GestureHandler.Gesture?.Pan().onBegin(event => { ++ 'worklet'; + +- startX.value = translationX.value; +- gestureState.value = event.state; +- touchStartX.value = event.x; +- }).onStart(() => { +- 'worklet'; ++ startX.value = translationX.value; ++ gestureState.value = event.state; ++ touchStartX.value = event.x; ++ }).onStart(() => { ++ 'worklet'; + +- (0, _reactNativeReanimated.runOnJS)(onGestureBegin)(); +- }).onChange(event => { +- 'worklet'; ++ (0, _reactNativeReanimated.runOnJS)(onGestureBegin)(); ++ }).onChange(event => { ++ 'worklet'; + +- touchX.value = event.x; +- translationX.value = startX.value + event.translationX; +- gestureState.value = event.state; +- }).onEnd((event, success) => { +- 'worklet'; ++ touchX.value = event.x; ++ translationX.value = startX.value + event.translationX; ++ gestureState.value = event.state; ++ }).onEnd((event, success) => { ++ 'worklet'; + +- gestureState.value = event.state; +- if (!success) { +- (0, _reactNativeReanimated.runOnJS)(onGestureAbort)(); ++ gestureState.value = event.state; ++ if (!success) { ++ (0, _reactNativeReanimated.runOnJS)(onGestureAbort)(); ++ } ++ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ? ++ // If swiped to right, open the drawer, otherwise close it ++ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 : ++ // If swiped to left, open the drawer, otherwise close it ++ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open; ++ toggleDrawer(nextOpen, event.velocityX); ++ (0, _reactNativeReanimated.runOnJS)(onGestureFinish)(); ++ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled); ++ if (panGesture && configureGestureHandler) { ++ panGesture = configureGestureHandler(panGesture); + } +- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ? +- // If swiped to right, open the drawer, otherwise close it +- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 : +- // If swiped to left, open the drawer, otherwise close it +- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open; +- toggleDrawer(nextOpen, event.velocityX); +- (0, _reactNativeReanimated.runOnJS)(onGestureFinish)(); +- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled); +- if (pan && configureGestureHandler) { +- pan = configureGestureHandler(pan); +- } ++ return panGesture; ++ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]); + const translateX = (0, _reactNativeReanimated.useDerivedValue)(() => { + // Comment stolen from react-native-gesture-handler/DrawerLayout + // +@@ -254,35 +258,38 @@ function Drawer({ + style: [styles.container, style], + children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerProgressContext.DrawerProgressContext.Provider, { + value: progress, +- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, { +- gesture: pan, +- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, { +- style: [styles.main, { +- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row' +- }], +- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, { +- style: [styles.content, contentAnimatedStyle], +- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { +- accessibilityElementsHidden: isOpen && drawerType !== 'permanent', +- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto', +- style: styles.content, +- children: children +- }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, { +- open: open, +- progress: progress, +- onPress: () => toggleDrawer(false), +- style: overlayStyle, +- accessibilityLabel: overlayAccessibilityLabel +- }) : null] +- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, { +- removeClippedSubviews: _reactNative.Platform.OS !== 'ios', +- style: [styles.drawer, { +- width: drawerWidth, +- position: drawerType === 'permanent' ? 'relative' : 'absolute', +- zIndex: drawerType === 'back' ? -1 : 0 +- }, drawerAnimatedStyle, drawerStyle], +- children: renderDrawerContent() +- })] ++ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerGestureContext.DrawerGestureContext.Provider, { ++ value: pan, ++ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, { ++ gesture: pan, ++ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, { ++ style: [styles.main, { ++ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row' ++ }], ++ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, { ++ style: [styles.content, contentAnimatedStyle], ++ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { ++ accessibilityElementsHidden: isOpen && drawerType !== 'permanent', ++ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto', ++ style: styles.content, ++ children: children ++ }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, { ++ open: open, ++ progress: progress, ++ onPress: () => toggleDrawer(false), ++ style: overlayStyle, ++ accessibilityLabel: overlayAccessibilityLabel ++ }) : null] ++ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, { ++ removeClippedSubviews: _reactNative.Platform.OS !== 'ios', ++ style: [styles.drawer, { ++ width: drawerWidth, ++ position: drawerType === 'permanent' ? 'relative' : 'absolute', ++ zIndex: drawerType === 'back' ? -1 : 0 ++ }, drawerAnimatedStyle, drawerStyle], ++ children: renderDrawerContent() ++ })] ++ }) + }) + }) + }) +diff --git a/node_modules/react-native-drawer-layout/lib/module/index.js b/node_modules/react-native-drawer-layout/lib/module/index.js +index a600e51..f08275c 100644 +--- a/node_modules/react-native-drawer-layout/lib/module/index.js ++++ b/node_modules/react-native-drawer-layout/lib/module/index.js +@@ -1,5 +1,6 @@ + "use strict"; + ++export { DrawerGestureContext } from "./utils/DrawerGestureContext.js"; + export { DrawerProgressContext } from "./utils/DrawerProgressContext.js"; + export { useDrawerProgress } from "./utils/useDrawerProgress.js"; + export { Drawer } from './views/Drawer'; +diff --git a/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js +new file mode 100644 +index 0000000..1adaa9c +--- /dev/null ++++ b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js +@@ -0,0 +1,5 @@ ++"use strict"; ++ ++import * as React from 'react'; ++export const DrawerGestureContext = /*#__PURE__*/React.createContext(undefined); ++//# sourceMappingURL=DrawerGestureContext.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js +index 6d07126..981d9f8 100644 +--- a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js ++++ b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js +@@ -4,6 +4,7 @@ import * as React from 'react'; + import { I18nManager, InteractionManager, Keyboard, Platform, StatusBar, StyleSheet, useWindowDimensions, View } from 'react-native'; + import Animated, { interpolate, ReduceMotion, runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated'; + import useLatestCallback from 'use-latest-callback'; ++import { DrawerGestureContext } from "../utils/DrawerGestureContext.js"; + import { DrawerProgressContext } from "../utils/DrawerProgressContext.js"; + import { getDrawerWidth } from "../utils/getDrawerWidth.js"; + import { Gesture, GestureDetector, GestureHandlerRootView, GestureState } from './GestureHandler'; +@@ -72,38 +73,38 @@ export function Drawer({ + return () => hideStatusBar(false); + }, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]); + const interactionHandleRef = React.useRef(null); +- const startInteraction = () => { ++ const startInteraction = React.useCallback(() => { + interactionHandleRef.current = InteractionManager.createInteractionHandle(); +- }; +- const endInteraction = () => { ++ }, []); ++ const endInteraction = React.useCallback(() => { + if (interactionHandleRef.current != null) { + InteractionManager.clearInteractionHandle(interactionHandleRef.current); + interactionHandleRef.current = null; + } +- }; +- const hideKeyboard = () => { ++ }, []); ++ const hideKeyboard = React.useCallback(() => { + if (keyboardDismissMode === 'on-drag') { + Keyboard.dismiss(); + } +- }; +- const onGestureBegin = () => { ++ }, [keyboardDismissMode]); ++ const onGestureBegin = React.useCallback(() => { + onGestureStart?.(); + startInteraction(); + hideKeyboard(); + hideStatusBar(true); +- }; +- const onGestureFinish = () => { ++ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]); ++ const onGestureFinish = React.useCallback(() => { + onGestureEnd?.(); + endInteraction(); +- }; +- const onGestureAbort = () => { ++ }, [onGestureEnd, endInteraction]); ++ const onGestureAbort = React.useCallback(() => { + onGestureCancel?.(); + endInteraction(); +- }; ++ }, [onGestureCancel, endInteraction]); + + // FIXME: Currently hitSlop is broken when on Android when drawer is on right + // https://github.com/software-mansion/react-native-gesture-handler/issues/569 +- const hitSlop = isRight ? ++ const hitSlop = React.useMemo(() => isRight ? + // Extend hitSlop to the side of the screen when drawer is closed + // This lets the user drag the drawer from the side of the screen + { +@@ -112,7 +113,7 @@ export function Drawer({ + } : { + left: 0, + width: isOpen ? undefined : swipeEdgeWidth +- }; ++ }, [isRight, isOpen, swipeEdgeWidth]); + const touchStartX = useSharedValue(0); + const touchX = useSharedValue(0); + const translationX = useSharedValue(getDrawerTranslationX(open)); +@@ -151,40 +152,43 @@ export function Drawer({ + }, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]); + React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]); + const startX = useSharedValue(0); +- let pan = Gesture?.Pan().onBegin(event => { +- 'worklet'; ++ const pan = React.useMemo(() => { ++ let panGesture = Gesture?.Pan().onBegin(event => { ++ 'worklet'; + +- startX.value = translationX.value; +- gestureState.value = event.state; +- touchStartX.value = event.x; +- }).onStart(() => { +- 'worklet'; ++ startX.value = translationX.value; ++ gestureState.value = event.state; ++ touchStartX.value = event.x; ++ }).onStart(() => { ++ 'worklet'; + +- runOnJS(onGestureBegin)(); +- }).onChange(event => { +- 'worklet'; ++ runOnJS(onGestureBegin)(); ++ }).onChange(event => { ++ 'worklet'; + +- touchX.value = event.x; +- translationX.value = startX.value + event.translationX; +- gestureState.value = event.state; +- }).onEnd((event, success) => { +- 'worklet'; ++ touchX.value = event.x; ++ translationX.value = startX.value + event.translationX; ++ gestureState.value = event.state; ++ }).onEnd((event, success) => { ++ 'worklet'; + +- gestureState.value = event.state; +- if (!success) { +- runOnJS(onGestureAbort)(); ++ gestureState.value = event.state; ++ if (!success) { ++ runOnJS(onGestureAbort)(); ++ } ++ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ? ++ // If swiped to right, open the drawer, otherwise close it ++ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 : ++ // If swiped to left, open the drawer, otherwise close it ++ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open; ++ toggleDrawer(nextOpen, event.velocityX); ++ runOnJS(onGestureFinish)(); ++ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled); ++ if (panGesture && configureGestureHandler) { ++ panGesture = configureGestureHandler(panGesture); + } +- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ? +- // If swiped to right, open the drawer, otherwise close it +- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 : +- // If swiped to left, open the drawer, otherwise close it +- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open; +- toggleDrawer(nextOpen, event.velocityX); +- runOnJS(onGestureFinish)(); +- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled); +- if (pan && configureGestureHandler) { +- pan = configureGestureHandler(pan); +- } ++ return panGesture; ++ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]); + const translateX = useDerivedValue(() => { + // Comment stolen from react-native-gesture-handler/DrawerLayout + // +@@ -247,35 +251,38 @@ export function Drawer({ + style: [styles.container, style], + children: /*#__PURE__*/_jsx(DrawerProgressContext.Provider, { + value: progress, +- children: /*#__PURE__*/_jsx(GestureDetector, { +- gesture: pan, +- children: /*#__PURE__*/_jsxs(Animated.View, { +- style: [styles.main, { +- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row' +- }], +- children: [/*#__PURE__*/_jsxs(Animated.View, { +- style: [styles.content, contentAnimatedStyle], +- children: [/*#__PURE__*/_jsx(View, { +- accessibilityElementsHidden: isOpen && drawerType !== 'permanent', +- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto', +- style: styles.content, +- children: children +- }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, { +- open: open, +- progress: progress, +- onPress: () => toggleDrawer(false), +- style: overlayStyle, +- accessibilityLabel: overlayAccessibilityLabel +- }) : null] +- }), /*#__PURE__*/_jsx(Animated.View, { +- removeClippedSubviews: Platform.OS !== 'ios', +- style: [styles.drawer, { +- width: drawerWidth, +- position: drawerType === 'permanent' ? 'relative' : 'absolute', +- zIndex: drawerType === 'back' ? -1 : 0 +- }, drawerAnimatedStyle, drawerStyle], +- children: renderDrawerContent() +- })] ++ children: /*#__PURE__*/_jsx(DrawerGestureContext.Provider, { ++ value: pan, ++ children: /*#__PURE__*/_jsx(GestureDetector, { ++ gesture: pan, ++ children: /*#__PURE__*/_jsxs(Animated.View, { ++ style: [styles.main, { ++ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row' ++ }], ++ children: [/*#__PURE__*/_jsxs(Animated.View, { ++ style: [styles.content, contentAnimatedStyle], ++ children: [/*#__PURE__*/_jsx(View, { ++ accessibilityElementsHidden: isOpen && drawerType !== 'permanent', ++ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto', ++ style: styles.content, ++ children: children ++ }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, { ++ open: open, ++ progress: progress, ++ onPress: () => toggleDrawer(false), ++ style: overlayStyle, ++ accessibilityLabel: overlayAccessibilityLabel ++ }) : null] ++ }), /*#__PURE__*/_jsx(Animated.View, { ++ removeClippedSubviews: Platform.OS !== 'ios', ++ style: [styles.drawer, { ++ width: drawerWidth, ++ position: drawerType === 'permanent' ? 'relative' : 'absolute', ++ zIndex: drawerType === 'back' ? -1 : 0 ++ }, drawerAnimatedStyle, drawerStyle], ++ children: renderDrawerContent() ++ })] ++ }) + }) + }) + }) +diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts +index 7e978f0..a8bce18 100644 +--- a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts ++++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts +@@ -1,3 +1,4 @@ ++export { DrawerGestureContext } from './utils/DrawerGestureContext'; + export { DrawerProgressContext } from './utils/DrawerProgressContext'; + export { useDrawerProgress } from './utils/useDrawerProgress'; + export { Drawer } from './views/Drawer'; +diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts +new file mode 100644 +index 0000000..33ffbeb +--- /dev/null ++++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts +@@ -0,0 +1,3 @@ ++import * as React from 'react'; ++export declare const DrawerGestureContext: React.Context; ++//# sourceMappingURL=DrawerGestureContext.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts +index 7e978f0..a8bce18 100644 +--- a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts ++++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts +@@ -1,3 +1,4 @@ ++export { DrawerGestureContext } from './utils/DrawerGestureContext'; + export { DrawerProgressContext } from './utils/DrawerProgressContext'; + export { useDrawerProgress } from './utils/useDrawerProgress'; + export { Drawer } from './views/Drawer'; +diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts +new file mode 100644 +index 0000000..33ffbeb +--- /dev/null ++++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts +@@ -0,0 +1,3 @@ ++import * as React from 'react'; ++export declare const DrawerGestureContext: React.Context; ++//# sourceMappingURL=DrawerGestureContext.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-drawer-layout/src/index.tsx b/node_modules/react-native-drawer-layout/src/index.tsx +index 0aa6f53..61a37cf 100644 +--- a/node_modules/react-native-drawer-layout/src/index.tsx ++++ b/node_modules/react-native-drawer-layout/src/index.tsx +@@ -1,3 +1,4 @@ ++export { DrawerGestureContext } from './utils/DrawerGestureContext'; + export { DrawerProgressContext } from './utils/DrawerProgressContext'; + export { useDrawerProgress } from './utils/useDrawerProgress'; + export { Drawer } from './views/Drawer'; +diff --git a/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx +new file mode 100644 +index 0000000..3aac957 +--- /dev/null ++++ b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx +@@ -0,0 +1,6 @@ ++import * as React from 'react'; ++import type { PanGesture } from 'react-native-gesture-handler'; ++ ++export const DrawerGestureContext = React.createContext( ++ undefined ++); +diff --git a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx +index 9c40f41..ee5d075 100644 +--- a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx ++++ b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx +@@ -21,6 +21,7 @@ import Animated, { + import useLatestCallback from 'use-latest-callback'; + + import type { DrawerProps } from '../types'; ++import { DrawerGestureContext } from '../utils/DrawerGestureContext'; + import { DrawerProgressContext } from '../utils/DrawerProgressContext'; + import { getDrawerWidth } from '../utils/getDrawerWidth'; + import { +@@ -110,47 +111,51 @@ export function Drawer({ + + const interactionHandleRef = React.useRef(null); + +- const startInteraction = () => { ++ const startInteraction = React.useCallback(() => { + interactionHandleRef.current = InteractionManager.createInteractionHandle(); +- }; ++ }, []); + +- const endInteraction = () => { ++ const endInteraction = React.useCallback(() => { + if (interactionHandleRef.current != null) { + InteractionManager.clearInteractionHandle(interactionHandleRef.current); + interactionHandleRef.current = null; + } +- }; ++ }, []); + +- const hideKeyboard = () => { ++ const hideKeyboard = React.useCallback(() => { + if (keyboardDismissMode === 'on-drag') { + Keyboard.dismiss(); + } +- }; ++ }, [keyboardDismissMode]); + +- const onGestureBegin = () => { ++ const onGestureBegin = React.useCallback(() => { + onGestureStart?.(); + startInteraction(); + hideKeyboard(); + hideStatusBar(true); +- }; ++ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]); + +- const onGestureFinish = () => { ++ const onGestureFinish = React.useCallback(() => { + onGestureEnd?.(); + endInteraction(); +- }; ++ }, [onGestureEnd, endInteraction]); + +- const onGestureAbort = () => { ++ const onGestureAbort = React.useCallback(() => { + onGestureCancel?.(); + endInteraction(); +- }; ++ }, [onGestureCancel, endInteraction]); + + // FIXME: Currently hitSlop is broken when on Android when drawer is on right + // https://github.com/software-mansion/react-native-gesture-handler/issues/569 +- const hitSlop = isRight +- ? // Extend hitSlop to the side of the screen when drawer is closed +- // This lets the user drag the drawer from the side of the screen +- { right: 0, width: isOpen ? undefined : swipeEdgeWidth } +- : { left: 0, width: isOpen ? undefined : swipeEdgeWidth }; ++ const hitSlop = React.useMemo( ++ () => ++ isRight ++ ? // Extend hitSlop to the side of the screen when drawer is closed ++ // This lets the user drag the drawer from the side of the screen ++ { right: 0, width: isOpen ? undefined : swipeEdgeWidth } ++ : { left: 0, width: isOpen ? undefined : swipeEdgeWidth }, ++ [isRight, isOpen, swipeEdgeWidth] ++ ); + + const touchStartX = useSharedValue(0); + const touchX = useSharedValue(0); +@@ -217,53 +222,76 @@ export function Drawer({ + + const startX = useSharedValue(0); + +- let pan = Gesture?.Pan() +- .onBegin((event) => { +- 'worklet'; +- startX.value = translationX.value; +- gestureState.value = event.state; +- touchStartX.value = event.x; +- }) +- .onStart(() => { +- 'worklet'; +- runOnJS(onGestureBegin)(); +- }) +- .onChange((event) => { +- 'worklet'; +- touchX.value = event.x; +- translationX.value = startX.value + event.translationX; +- gestureState.value = event.state; +- }) +- .onEnd((event, success) => { +- 'worklet'; +- gestureState.value = event.state; +- +- if (!success) { +- runOnJS(onGestureAbort)(); +- } +- +- const nextOpen = +- (Math.abs(event.translationX) > SWIPE_MIN_OFFSET && +- Math.abs(event.translationX) > swipeMinVelocity) || +- Math.abs(event.translationX) > swipeMinDistance +- ? drawerPosition === 'left' +- ? // If swiped to right, open the drawer, otherwise close it +- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 +- : // If swiped to left, open the drawer, otherwise close it +- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 +- : open; +- +- toggleDrawer(nextOpen, event.velocityX); +- runOnJS(onGestureFinish)(); +- }) +- .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]) +- .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]) +- .hitSlop(hitSlop) +- .enabled(drawerType !== 'permanent' && swipeEnabled); +- +- if (pan && configureGestureHandler) { +- pan = configureGestureHandler(pan); +- } ++ const pan = React.useMemo(() => { ++ let panGesture = Gesture?.Pan() ++ .onBegin((event) => { ++ 'worklet'; ++ startX.value = translationX.value; ++ gestureState.value = event.state; ++ touchStartX.value = event.x; ++ }) ++ .onStart(() => { ++ 'worklet'; ++ runOnJS(onGestureBegin)(); ++ }) ++ .onChange((event) => { ++ 'worklet'; ++ touchX.value = event.x; ++ translationX.value = startX.value + event.translationX; ++ gestureState.value = event.state; ++ }) ++ .onEnd((event, success) => { ++ 'worklet'; ++ gestureState.value = event.state; ++ ++ if (!success) { ++ runOnJS(onGestureAbort)(); ++ } ++ ++ const nextOpen = ++ (Math.abs(event.translationX) > SWIPE_MIN_OFFSET && ++ Math.abs(event.translationX) > swipeMinVelocity) || ++ Math.abs(event.translationX) > swipeMinDistance ++ ? drawerPosition === 'left' ++ ? // If swiped to right, open the drawer, otherwise close it ++ (event.velocityX === 0 ? event.translationX : event.velocityX) > ++ 0 ++ : // If swiped to left, open the drawer, otherwise close it ++ (event.velocityX === 0 ? event.translationX : event.velocityX) < ++ 0 ++ : open; ++ ++ toggleDrawer(nextOpen, event.velocityX); ++ runOnJS(onGestureFinish)(); ++ }) ++ .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]) ++ .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]) ++ .hitSlop(hitSlop) ++ .enabled(drawerType !== 'permanent' && swipeEnabled); ++ ++ if (panGesture && configureGestureHandler) { ++ panGesture = configureGestureHandler(panGesture); ++ } ++ return panGesture; ++ }, [ ++ configureGestureHandler, ++ drawerPosition, ++ drawerType, ++ gestureState, ++ hitSlop, ++ onGestureBegin, ++ onGestureAbort, ++ onGestureFinish, ++ open, ++ startX, ++ swipeEnabled, ++ swipeMinDistance, ++ swipeMinVelocity, ++ toggleDrawer, ++ touchStartX, ++ touchX, ++ translationX, ++ ]); + + const translateX = useDerivedValue(() => { + // Comment stolen from react-native-gesture-handler/DrawerLayout +@@ -376,64 +404,66 @@ export function Drawer({ + return ( + + +- +- {/* Immediate child of gesture handler needs to be an Animated.View */} +- +- +- +- {children} +- +- {drawerType !== 'permanent' ? ( +- toggleDrawer(false)} +- style={overlayStyle} +- accessibilityLabel={overlayAccessibilityLabel} +- /> +- ) : null} +- ++ ++ ++ {/* Immediate child of gesture handler needs to be an Animated.View */} + +- {renderDrawerContent()} ++ ++ ++ {children} ++ ++ {drawerType !== 'permanent' ? ( ++ toggleDrawer(false)} ++ style={overlayStyle} ++ accessibilityLabel={overlayAccessibilityLabel} ++ /> ++ ) : null} ++ ++ ++ {renderDrawerContent()} ++ + +- +- ++ ++ + + + ); 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>( const [selectedPage, setSelectedPage] = React.useState(initialPage) const pagerView = React.useRef(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>( }, 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>( [parentOnPageScrollStateChanged], ) + const drawerGesture = useContext(DrawerGestureContext)! + const nativeGesture = + Gesture.Native().requireExternalGestureToFail(drawerGesture) + return ( {renderTabBar({ @@ -121,13 +142,15 @@ export const Pager = forwardRef>( dragProgress, dragState, })} - - {children} - + + + {children} + + ) }, 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(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 ( <> @@ -98,12 +99,35 @@ function ShellInner() { { + 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)', -- cgit 1.4.1