android carousel swiping

This commit is contained in:
vineyardbovines
2026-05-06 12:37:33 -04:00
parent 0b6ae1cc21
commit ba436ae1e9
7 changed files with 107 additions and 9 deletions
+3 -1
View File
@@ -3,7 +3,9 @@
"allow": [
"Bash(yarn typecheck *)",
"Bash(yarn lint *)",
"Bash(yarn test *)"
"Bash(yarn test *)",
"mcp__plugin_claude-mem_mcp-search__search",
"mcp__plugin_claude-mem_mcp-search__get_observations"
]
}
}
+3 -3
View File
@@ -23,7 +23,7 @@ import {type Dimensions} from '#/lib/media/types'
import {mergeRefs} from '#/lib/merge-refs'
import {useA11y} from '#/state/a11y'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {DrawerWaitGesture} from '#/view/shell/DrawerWaitGesture'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
@@ -248,7 +248,7 @@ export function Gallery({
},
]}
onLayout={measure}>
<BlockDrawerGesture>
<DrawerWaitGesture>
<FlatList
ref={flatListRef}
role="group"
@@ -339,7 +339,7 @@ export function Gallery({
paddingRight: insetRight,
}}
/>
</BlockDrawerGesture>
</DrawerWaitGesture>
</View>
)
}
+6
View File
@@ -0,0 +1,6 @@
/*
* iOS keeps the previously-shipping BlockDrawerGesture wrapper, which works
* correctly here. Only Android needs the new drawer-side wait mechanism.
* See APP-2119.
*/
export {BlockDrawerGesture as DrawerWaitGesture} from './BlockDrawerGesture'
+14
View File
@@ -0,0 +1,14 @@
import {GestureDetector} from 'react-native-gesture-handler'
import {useRegisterDrawerWaitGesture} from './DrawerWaitGestureContext'
/*
* Registers a Gesture.Native() with the shell drawer so the drawer's pan
* gesture must wait for it to fail before activating. Wrap a horizontal-swipe
* surface (like the image carousel) where the drawer would otherwise race
* the inner scroll on Android. See APP-2119.
*/
export function DrawerWaitGesture({children}: {children: React.ReactNode}) {
const gesture = useRegisterDrawerWaitGesture()
return <GestureDetector gesture={gesture}>{children}</GestureDetector>
}
+3
View File
@@ -0,0 +1,3 @@
export function DrawerWaitGesture({children}: {children: React.ReactNode}) {
return children
}
@@ -0,0 +1,66 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import {Gesture, type NativeGesture} from 'react-native-gesture-handler'
/*
* Lets descendant components register a Gesture.Native() that the shell
* drawer's pan handler must wait to fail before it can activate. Used to
* prevent the drawer from intercepting horizontal swipes inside surfaces
* like the image carousel where the child-side blocksExternalGesture pattern
* is unreliable on Android. See APP-2119.
*
* Two contexts so consumers re-render only when they need to:
* - RegisterContext is referentially stable, only used by the inner hook
* that registers a single gesture. Galleries don't re-render when their
* peers mount/unmount.
* - GesturesContext is reactive; only DrawerLayout reads it.
*/
type Register = (gesture: NativeGesture) => () => void
const RegisterContext = createContext<Register | null>(null)
const GesturesContext = createContext<readonly NativeGesture[]>([])
export function DrawerWaitGestureProvider({
children,
}: {
children: React.ReactNode
}) {
const [gestures, setGestures] = useState<readonly NativeGesture[]>([])
const register = useCallback<Register>(gesture => {
setGestures(prev => [...prev, gesture])
return () => {
setGestures(prev => prev.filter(g => g !== gesture))
}
}, [])
return (
<RegisterContext.Provider value={register}>
<GesturesContext.Provider value={gestures}>
{children}
</GesturesContext.Provider>
</RegisterContext.Provider>
)
}
export function useDrawerWaitGestures(): readonly NativeGesture[] {
return useContext(GesturesContext)
}
export function useRegisterDrawerWaitGesture(): NativeGesture {
const register = useContext(RegisterContext)
const [gesture] = useState(() => Gesture.Native())
useEffect(() => {
if (!register) return
return register(gesture)
}, [register, gesture])
return gesture
}
+12 -5
View File
@@ -1,8 +1,7 @@
import {useCallback, useEffect, useState} from 'react'
import {useCallback, useEffect} from 'react'
import {BackHandler, useWindowDimensions, View} from 'react-native'
import {Drawer} from 'react-native-drawer-layout'
import {SystemBars} from 'react-native-edge-to-edge'
import {Gesture} from 'react-native-gesture-handler'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useNavigation, useNavigationState} from '@react-navigation/native'
@@ -49,6 +48,10 @@ import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
import {Composer} from './Composer'
import {DrawerContent} from './Drawer'
import {
DrawerWaitGestureProvider,
useDrawerWaitGestures,
} from './DrawerWaitGestureContext'
function ShellInner() {
const winDim = useWindowDimensions()
@@ -105,7 +108,9 @@ function ShellInner() {
<View style={[a.h_full]}>
<ErrorBoundary
style={{paddingTop: insets.top, paddingBottom: insets.bottom}}>
<TabsNavigator layout={drawerLayout} />
<DrawerWaitGestureProvider>
<TabsNavigator layout={drawerLayout} />
</DrawerWaitGestureProvider>
</ErrorBoundary>
</View>
@@ -145,7 +150,7 @@ function DrawerLayout({children}: {children: React.ReactNode}) {
const {hasSession} = useSession()
const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
const [trendingScrollGesture] = useState(() => Gesture.Native())
const waitGestures = useDrawerWaitGestures()
const renderDrawerContent = useCallback(() => <DrawerContent />, [])
const onOpenDrawer = useCallback(
@@ -162,7 +167,9 @@ function DrawerLayout({children}: {children: React.ReactNode}) {
renderDrawerContent={renderDrawerContent}
drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
configureGestureHandler={handler => {
handler = handler.requireExternalGestureToFail(trendingScrollGesture)
for (const gesture of waitGestures) {
handler = handler.requireExternalGestureToFail(gesture)
}
if (swipeEnabled) {
if (isDrawerOpen) {