android carousel swiping
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user