From 3770cd9fce5041575886d61d5eff7d19e70ce332 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 21 Sep 2026 14:20:34 -0700 Subject: [PATCH 1/2] fix(mobile): stop horizontal recognizers from hijacking diagonal vertical scrolls Three full-screen horizontal recognizers competed with list scrolling, so a vertical scroll with some sideways drift either did nothing or peeked a screen/drawer and snapped back: - Nested screens: react-native-screens' RNSPanGestureRecognizer (in use since #14205) has no direction test. Commit the patch that requires a committed horizontal drag (>= 5pt toward dismissal, >= 2x the cross-axis travel). Native change: ships with the next binary, not OTA. - Root tabs: the drawer navigator's pan spanned the full screen with fixed 5pt thresholds. Keep it to a 40pt edge and open from mid-screen with a new angle-gated pan (useOpenDrawerGesture) around each tab's stack. - Feed: the For You drawer pan had no failOffsetY, so 20pt of rightward drift anywhere in a long vertical drag opened the drawer. It now uses the shared angle-gated gesture. Co-Authored-By: Claude Fable 5.1 --- .../app-drawer-screen/AppDrawerScreen.tsx | 15 +- .../app-drawer-screen/useOpenDrawerGesture.ts | 40 +++ .../src/screens/app-screen/AppTabScreen.tsx | 286 ++++++++++-------- .../src/screens/feed-screen/FeedScreen.tsx | 22 +- patches/react-native-screens+4.18.0.patch | 86 ++++++ 5 files changed, 302 insertions(+), 147 deletions(-) create mode 100644 packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts create mode 100644 patches/react-native-screens+4.18.0.patch diff --git a/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx b/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx index 14eb88ff312..c5c04d622d0 100644 --- a/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx +++ b/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx @@ -3,7 +3,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { DrawerContentComponentProps } from '@react-navigation/drawer' import { createDrawerNavigator } from '@react-navigation/drawer' import { useNavigation } from '@react-navigation/native' -import { Dimensions, Platform, View, StyleSheet } from 'react-native' +import { Platform, View, StyleSheet } from 'react-native' import { IconAudiusLogoHorizontal } from '@audius/harmony-native' import { AudioPlayer } from 'app/components/audio/AudioPlayer' @@ -15,7 +15,7 @@ import { AppScreen } from '../app-screen' import { AppDrawerContextProvider } from './AppDrawerContext' import { LeftNavDrawer } from './left-nav-drawer' -const SCREEN_WIDTH = Dimensions.get('window').width +const DRAWER_SWIPE_EDGE_WIDTH = 40 const Drawer = createDrawerNavigator() @@ -66,13 +66,16 @@ export const AppDrawerScreen = memo(() => { // Drawer swipe-to-open is enabled only when at the tab stack's root, so // the right-swipe gesture inside a nested stack falls through to the native - // stack's fullScreenSwipe back behavior. swipeEdgeWidth stays at the full - // screen width so opening the drawer from the root doesn't require a swipe - // from the screen edge. + // stack's fullScreenSwipe back behavior. The navigator's own pan is kept to + // the left edge: its activation thresholds are a fixed 5pt on both axes, so + // stretched across the full screen it claimed any touch that drifted + // sideways first and killed the vertical scroll underneath. Opening from + // mid-screen is handled by `useOpenDrawerGesture`, which is angle-gated. + // (swipeEdgeWidth only applies while closed; swipe-to-close stays full-width.) const drawerScreenOptions = useMemo( () => ({ headerShown: false, - swipeEdgeWidth: SCREEN_WIDTH, + swipeEdgeWidth: DRAWER_SWIPE_EDGE_WIDTH, drawerType: 'slide' as const, drawerStyle: { width: '75%' as const }, swipeEnabled: diff --git a/packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts b/packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts new file mode 100644 index 00000000000..f55bb177bb3 --- /dev/null +++ b/packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts @@ -0,0 +1,40 @@ +import { useContext, useMemo } from 'react' + +import { Gesture } from 'react-native-gesture-handler' + +import { AppDrawerContext } from './AppDrawerContext' + +// Rightward travel needed before the pan claims the touch. +const OPEN_DRAWER_ACTIVE_OFFSET_X = 20 +// Vertical travel that rules the touch out as a drawer swipe. Together with +// the active offset this is an angle gate: the finger has to get 20pt right +// before it gets 12pt up or down (within ~30° of horizontal), so a vertical +// scroll that drifts sideways — however far, over however long a drag — fails +// the pan early and the list keeps the touch for good. +const OPEN_DRAWER_FAIL_OFFSET_Y = 12 + +/** + * Direction-locked pan that opens the left nav drawer: rightward only, and + * only when the drag is clearly horizontal. This is the app's drawer-opener + * everywhere away from the screen edge. The drawer navigator's own pan can't + * do this job full-screen — its thresholds are a hardcoded 5pt on both axes, + * so it wins any touch that happens to move sideways first and cancels the + * scroll underneath it. + */ +export const useOpenDrawerGesture = (enabled: boolean) => { + const { drawerHelpers } = useContext(AppDrawerContext) + + return useMemo( + () => + Gesture.Pan() + .enabled(enabled) + .activeOffsetX(OPEN_DRAWER_ACTIVE_OFFSET_X) + .failOffsetX(-OPEN_DRAWER_ACTIVE_OFFSET_X) + .failOffsetY([-OPEN_DRAWER_FAIL_OFFSET_Y, OPEN_DRAWER_FAIL_OFFSET_Y]) + .runOnJS(true) + .onStart(() => { + drawerHelpers?.openDrawer() + }), + [enabled, drawerHelpers] + ) +} diff --git a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx index 98cb1fc3174..5d12baf1326 100644 --- a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useRef } from 'react' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' import type { FavoriteType, @@ -19,12 +19,15 @@ import type { import type { EventArg, NavigationState } from '@react-navigation/native' import { useIsFocused } from '@react-navigation/native' import type { createNativeStackNavigator } from '@react-navigation/native-stack' +import { StyleSheet, View } from 'react-native' +import { GestureDetector } from 'react-native-gesture-handler' import { FilterButtonScreen } from '@audius/harmony-native' import type { FilterButtonScreenParams } from '@audius/harmony-native' import { useDrawer } from 'app/hooks/useDrawer' import { setLastNavAction } from 'app/hooks/useNavigation' import { AppDrawerContext } from 'app/screens/app-drawer-screen' +import { useOpenDrawerGesture } from 'app/screens/app-drawer-screen/useOpenDrawerGesture' import { SetAppTabNavigationContext } from 'app/screens/app-screen/AppTabNavigationProvider' import type { AppTabNavigation } from 'app/screens/app-screen/AppTabNavigationProvider' import { AudioScreen } from 'app/screens/audio-screen' @@ -187,11 +190,13 @@ type AppTabScreenProps = { */ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { const screenOptions = useAppScreenOptions() - const { drawerNavigation, setIsAtStackRoot } = useContext(AppDrawerContext) + const { drawerNavigation, setIsAtStackRoot, gesturesDisabled } = + useContext(AppDrawerContext) const { isOpen: isNowPlayingDrawerOpen } = useDrawer('NowPlaying') const { setNavigation } = useContext(SetAppTabNavigationContext) const isFocused = useIsFocused() const isAtStackRootRef = useRef(true) + const [isAtStackRoot, setIsAtStackRootLocal] = useState(true) const applyDrawerSwipe = useCallback( (isAtRoot: boolean) => { @@ -212,6 +217,7 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { if (event?.data?.state?.type !== 'stack') return const isAtRoot = event.data.state.routes.length === 1 isAtStackRootRef.current = isAtRoot + setIsAtStackRootLocal(isAtRoot) if (isFocused) applyDrawerSwipe(isAtRoot) }, [isFocused, applyDrawerSwipe] @@ -244,6 +250,14 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { [handleChangeState, handleTransitionEnd, setNavigation] ) + // Mid-screen swipe-to-open for this tab's root screen. Off whenever a screen + // has claimed horizontal gestures for itself (`gesturesDisabled` — e.g. the + // feed, which composes its own opener with its pager) and whenever a + // right-swipe should mean "back" instead. + const openDrawerGesture = useOpenDrawerGesture( + isFocused && isAtStackRoot && !gesturesDisabled && !isNowPlayingDrawerOpen + ) + return ( // Publishes the floating root header's height to the screens below it, so // they can pad their scrollable content and let it slide behind the glass. @@ -253,133 +267,153 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => { scroll signal directly — this hands it out while the tab is focused. */} - - {baseScreen(Stack)} - - - - - - - - - - - - - - + + + + {baseScreen(Stack)} + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - // @ts-ignore hard to correctly type navigation params (PAY-1141) - params?.chatId - } - options={{ ...screenOptions, fullScreenGestureEnabled: false }} - /> - - + + + + + + + // @ts-ignore hard to correctly type navigation params (PAY-1141) + params?.chatId + } + options={{ ...screenOptions, fullScreenGestureEnabled: false }} + /> + + + + ) } + +const styles = StyleSheet.create({ + root: { flex: 1 } +}) diff --git a/packages/mobile/src/screens/feed-screen/FeedScreen.tsx b/packages/mobile/src/screens/feed-screen/FeedScreen.tsx index 9092bf34e80..0e2a555c4c0 100644 --- a/packages/mobile/src/screens/feed-screen/FeedScreen.tsx +++ b/packages/mobile/src/screens/feed-screen/FeedScreen.tsx @@ -28,6 +28,7 @@ import { TrackLineup } from 'app/components/lineup/TrackLineup' import { SuggestedFollows } from 'app/components/suggested-follows' import { useDrawer } from 'app/hooks/useDrawer' import { AppDrawerContext } from 'app/screens/app-drawer-screen' +import { useOpenDrawerGesture } from 'app/screens/app-drawer-screen/useOpenDrawerGesture' import { FloatingSubHeader } from 'app/screens/app-screen/FloatingSubHeader' import { useGlassHeaderInset, @@ -89,7 +90,7 @@ export const FeedScreen = () => { [feedArgs] ) - const { drawerHelpers, setGesturesDisabled } = useContext(AppDrawerContext) + const { setGesturesDisabled } = useContext(AppDrawerContext) const { isOpen: isNowPlayingDrawerOpen } = useDrawer('NowPlaying') const drawerStatus = useDrawerStatus() @@ -111,21 +112,12 @@ export const FeedScreen = () => { // The For You page is the leftmost pager page, so a right-swipe there has // no page to fall back to — PagerView would otherwise swallow it, leaving // the left nav drawer unreachable by gesture (only the header avatar would - // open it). This gesture re-enables swipe-to-open on For You: it activates - // only on a rightward drag (`activeOffsetX`) and bails on a leftward drag - // (`failOffsetX`) so swiping to Latest still reaches the pager untouched. + // open it). The shared drawer-opener only claims a clearly horizontal + // rightward drag, so swiping to Latest still reaches the pager untouched and + // a vertical scroll with sideways drift stays with the lineup. // Disabled on Latest, where right-swipe legitimately pages back to For You. - const openDrawerGesture = useMemo( - () => - Gesture.Pan() - .enabled(isForYou && !isNowPlayingDrawerOpen) - .activeOffsetX(20) - .failOffsetX(-20) - .runOnJS(true) - .onStart(() => { - drawerHelpers?.openDrawer() - }), - [isForYou, isNowPlayingDrawerOpen, drawerHelpers] + const openDrawerGesture = useOpenDrawerGesture( + isForYou && !isNowPlayingDrawerOpen ) // Run the pan simultaneously with the pager's own native gesture. Without diff --git a/patches/react-native-screens+4.18.0.patch b/patches/react-native-screens+4.18.0.patch new file mode 100644 index 00000000000..dbcb359e09c --- /dev/null +++ b/patches/react-native-screens+4.18.0.patch @@ -0,0 +1,86 @@ +diff --git a/node_modules/react-native-screens/ios/RNSScreenStack.mm b/node_modules/react-native-screens/ios/RNSScreenStack.mm +index 51f0218..263f558 100644 +--- a/node_modules/react-native-screens/ios/RNSScreenStack.mm ++++ b/node_modules/react-native-screens/ios/RNSScreenStack.mm +@@ -915,6 +915,12 @@ RNS_IGNORE_SUPER_CALL_END + // with native interactiveContentPopGestureRecognizer, so we have to fallback to the old implementation. + // In this case, the old one should behave as close as the new native one, having only the difference + // in animation, and without any customization that is exclusive for it (e.g. gestureResponseDistance). ++ // AUDIUS PATCH: full-screen swipe-to-pop must be a committed horizontal drag, ++ // otherwise a slightly-diagonal vertical scroll gets hijacked as a back gesture. ++ if (![self rnsAudius_isCommittedDismissPan:gestureRecognizer topScreen:topScreen]) { ++ return NO; ++ } ++ + if (@available(iOS 26, *)) { + if (customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) { + _isFullWidthSwipingWithPanGesture = YES; +@@ -1117,6 +1123,68 @@ RNS_IGNORE_SUPER_CALL_END + return _controller; + } + ++// AUDIUS PATCH ----------------------------------------------------------------- ++// `RNSPanGestureRecognizer` spans the whole screen when `fullScreenSwipeEnabled` ++// is set, and upstream treats *any* rightward translation as a back gesture (see ++// `isBackGesture` in shouldRecognizeSimultaneously...). With no angle threshold, ++// a vertical scroll carrying a pixel or two of horizontal drift gets hijacked as ++// swipe-to-pop and the scroll view's pan is cancelled mid-scroll. ++// ++// This gate requires the drag to be *committed* along the screen's dismiss axis ++// before the pop gesture may claim it: enough travel in the dismiss direction, ++// and clearly more travel on that axis than across it. A drag that starts ++// vertical fails here, the recognizer never leaves Possible, and the scroll view ++// keeps the touch sequence for good. ++// NOTE: keep the minimum *below* UIPanGestureRecognizer's own activation ++// threshold (~10pt). `gestureRecognizerShouldBegin:` is consulted once, at the ++// moment the recognizer crosses that threshold, so a minimum at or above it ++// would reject legitimate horizontal swipes that simply haven't travelled far ++// enough yet. The dominance ratio does the real work here; the minimum only ++// rejects jitter. Raising the ratio is the safe way to make this stricter. ++static const CGFloat RNSAudiusDismissMinTranslation = 5.0; ++static const CGFloat RNSAudiusDismissDominanceRatio = 2.0; ++ ++- (BOOL)rnsAudius_isCommittedDismissPan:(UIGestureRecognizer *)gestureRecognizer ++ topScreen:(RNSScreenView *)topScreen ++{ ++ if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { ++ return YES; ++ } ++ ++ UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer; ++ CGPoint translation = [pan translationInView:pan.view]; ++ ++ CGFloat alongAxis; ++ CGFloat acrossAxis; ++ ++ if (topScreen.swipeDirection == RNSScreenSwipeDirectionVertical) { ++ // Vertically-dismissed screens (e.g. modals): down is the dismiss direction. ++ alongAxis = translation.y; ++ acrossAxis = translation.x; ++ } else { ++ alongAxis = translation.x; ++ acrossAxis = translation.y; ++ ++ // Mirror the direction handling in `handleSwipe:` so RTL layouts and ++ // slide-from-left screens dismiss toward their own trailing edge. ++ if (_controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft) { ++ alongAxis = -alongAxis; ++ } ++ if (topScreen.stackAnimation == RNSScreenStackAnimationSlideFromLeft) { ++ alongAxis = -alongAxis; ++ } ++ } ++ ++ // Must be travelling toward dismissal, not merely off-axis. ++ if (alongAxis < RNSAudiusDismissMinTranslation) { ++ return NO; ++ } ++ ++ // ...and must be decisively along the axis rather than diagonal. ++ return fabs(alongAxis) >= fabs(acrossAxis) * RNSAudiusDismissDominanceRatio; ++} ++// END AUDIUS PATCH ------------------------------------------------------------- ++ + - (BOOL)isInGestureResponseDistance:(UIGestureRecognizer *)gestureRecognizer topScreen:(RNSScreenView *)topScreen + { + NSDictionary *gestureResponseDistanceValues = topScreen.gestureResponseDistance; From ec49753a2ae6fc62b918f4efe617eddf97e937fd Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Wed, 23 Sep 2026 16:47:15 -0700 Subject: [PATCH 2/2] chore(mobile): simplify drawer gesture comments Shorten the drawer/swipe-back comments and fix the Feed comment that still described the drawer's swipe edge as full screen. Co-Authored-By: Claude Opus 5.5 --- .../app-drawer-screen/AppDrawerScreen.tsx | 10 +++---- .../app-drawer-screen/useOpenDrawerGesture.ts | 16 ++++------- .../src/screens/feed-screen/FeedScreen.tsx | 8 +++--- patches/react-native-screens+4.18.0.patch | 27 +++++-------------- 4 files changed, 20 insertions(+), 41 deletions(-) diff --git a/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx b/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx index c5c04d622d0..724f79c0fe3 100644 --- a/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx +++ b/packages/mobile/src/screens/app-drawer-screen/AppDrawerScreen.tsx @@ -66,12 +66,10 @@ export const AppDrawerScreen = memo(() => { // Drawer swipe-to-open is enabled only when at the tab stack's root, so // the right-swipe gesture inside a nested stack falls through to the native - // stack's fullScreenSwipe back behavior. The navigator's own pan is kept to - // the left edge: its activation thresholds are a fixed 5pt on both axes, so - // stretched across the full screen it claimed any touch that drifted - // sideways first and killed the vertical scroll underneath. Opening from - // mid-screen is handled by `useOpenDrawerGesture`, which is angle-gated. - // (swipeEdgeWidth only applies while closed; swipe-to-close stays full-width.) + // stack's fullScreenSwipe back behavior. The navigator's pan (fixed 5pt + // thresholds) is limited to the left edge so it can't take vertical scrolls; + // mid-screen opening uses `useOpenDrawerGesture`. swipeEdgeWidth only applies + // while closed, so swipe-to-close stays full-width. const drawerScreenOptions = useMemo( () => ({ headerShown: false, diff --git a/packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts b/packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts index f55bb177bb3..cce7ea9a84f 100644 --- a/packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts +++ b/packages/mobile/src/screens/app-drawer-screen/useOpenDrawerGesture.ts @@ -6,20 +6,14 @@ import { AppDrawerContext } from './AppDrawerContext' // Rightward travel needed before the pan claims the touch. const OPEN_DRAWER_ACTIVE_OFFSET_X = 20 -// Vertical travel that rules the touch out as a drawer swipe. Together with -// the active offset this is an angle gate: the finger has to get 20pt right -// before it gets 12pt up or down (within ~30° of horizontal), so a vertical -// scroll that drifts sideways — however far, over however long a drag — fails -// the pan early and the list keeps the touch for good. +// Fail on 12pt of vertical travel, so only drags within ~30 degrees of +// horizontal open the drawer. const OPEN_DRAWER_FAIL_OFFSET_Y = 12 /** - * Direction-locked pan that opens the left nav drawer: rightward only, and - * only when the drag is clearly horizontal. This is the app's drawer-opener - * everywhere away from the screen edge. The drawer navigator's own pan can't - * do this job full-screen — its thresholds are a hardcoded 5pt on both axes, - * so it wins any touch that happens to move sideways first and cancels the - * scroll underneath it. + * Rightward, mostly horizontal pan that opens the left nav drawer from + * mid-screen. The drawer navigator's own pan has fixed 5pt thresholds, so it + * is limited to the screen edge. */ export const useOpenDrawerGesture = (enabled: boolean) => { const { drawerHelpers } = useContext(AppDrawerContext) diff --git a/packages/mobile/src/screens/feed-screen/FeedScreen.tsx b/packages/mobile/src/screens/feed-screen/FeedScreen.tsx index 0e2a555c4c0..b6da0db9aa4 100644 --- a/packages/mobile/src/screens/feed-screen/FeedScreen.tsx +++ b/packages/mobile/src/screens/feed-screen/FeedScreen.tsx @@ -94,10 +94,10 @@ export const FeedScreen = () => { const { isOpen: isNowPlayingDrawerOpen } = useDrawer('NowPlaying') const drawerStatus = useDrawerStatus() - // The drawer's own root-level swipe-to-open (swipeEdgeWidth = full screen) - // competes with the pager here: with both active, a right-swipe on Latest - // opens the drawer instead of paging back to For You, and the two openers - // arbitrate unpredictably with the track tiles' tap gesture. While the feed + // The drawer navigator's edge swipe-to-open competes with the pager here: + // with both active, a right-swipe on Latest opens the drawer instead of + // paging back to For You, and the two openers arbitrate unpredictably with + // the track tiles' tap gesture. While the feed // is focused and the drawer is closed, turn the native drawer swipe off so // the pager owns paging and the gesture below is the single drawer-opener // (For You only). Re-enable once it's open so it can still be swiped closed, diff --git a/patches/react-native-screens+4.18.0.patch b/patches/react-native-screens+4.18.0.patch index dbcb359e09c..2a0055dddb2 100644 --- a/patches/react-native-screens+4.18.0.patch +++ b/patches/react-native-screens+4.18.0.patch @@ -15,28 +15,15 @@ index 51f0218..263f558 100644 if (@available(iOS 26, *)) { if (customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) { _isFullWidthSwipingWithPanGesture = YES; -@@ -1117,6 +1123,68 @@ RNS_IGNORE_SUPER_CALL_END +@@ -1117,6 +1123,55 @@ RNS_IGNORE_SUPER_CALL_END return _controller; } +// AUDIUS PATCH ----------------------------------------------------------------- -+// `RNSPanGestureRecognizer` spans the whole screen when `fullScreenSwipeEnabled` -+// is set, and upstream treats *any* rightward translation as a back gesture (see -+// `isBackGesture` in shouldRecognizeSimultaneously...). With no angle threshold, -+// a vertical scroll carrying a pixel or two of horizontal drift gets hijacked as -+// swipe-to-pop and the scroll view's pan is cancelled mid-scroll. -+// -+// This gate requires the drag to be *committed* along the screen's dismiss axis -+// before the pop gesture may claim it: enough travel in the dismiss direction, -+// and clearly more travel on that axis than across it. A drag that starts -+// vertical fails here, the recognizer never leaves Possible, and the scroll view -+// keeps the touch sequence for good. -+// NOTE: keep the minimum *below* UIPanGestureRecognizer's own activation -+// threshold (~10pt). `gestureRecognizerShouldBegin:` is consulted once, at the -+// moment the recognizer crosses that threshold, so a minimum at or above it -+// would reject legitimate horizontal swipes that simply haven't travelled far -+// enough yet. The dominance ratio does the real work here; the minimum only -+// rejects jitter. Raising the ratio is the safe way to make this stricter. ++// Require the full-screen pop pan to be mostly horizontal (along >= 2x across, ++// >= 5pt) so a diagonal vertical scroll isn't taken as swipe-to-pop. Keep the ++// minimum below UIPanGestureRecognizer's ~10pt activation, since ++// gestureRecognizerShouldBegin: runs once at that point. +static const CGFloat RNSAudiusDismissMinTranslation = 5.0; +static const CGFloat RNSAudiusDismissDominanceRatio = 2.0; + @@ -71,12 +58,12 @@ index 51f0218..263f558 100644 + } + } + -+ // Must be travelling toward dismissal, not merely off-axis. ++ // Must be moving in the dismiss direction. + if (alongAxis < RNSAudiusDismissMinTranslation) { + return NO; + } + -+ // ...and must be decisively along the axis rather than diagonal. ++ // Must be mostly along the dismiss axis. + return fabs(alongAxis) >= fabs(acrossAxis) * RNSAudiusDismissDominanceRatio; +} +// END AUDIUS PATCH -------------------------------------------------------------