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
This commit is contained in:
dan
2024-12-10 04:40:40 +00:00
committed by GitHub
parent fec3352b68
commit 46e1e5cee6
7 changed files with 941 additions and 38 deletions
+2 -4
View File
@@ -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(() => {
+31 -8
View File
@@ -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>
)
},
+3 -9
View File
@@ -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(() => {
+1 -11
View File
@@ -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
// =
+2 -4
View File
@@ -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(() => {
+26 -2
View File
@@ -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)',