android carousel swiping
This commit is contained in:
@@ -3,7 +3,9 @@
|
|||||||
"allow": [
|
"allow": [
|
||||||
"Bash(yarn typecheck *)",
|
"Bash(yarn typecheck *)",
|
||||||
"Bash(yarn lint *)",
|
"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 {mergeRefs} from '#/lib/merge-refs'
|
||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
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 {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
|
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
|
||||||
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
||||||
@@ -248,7 +248,7 @@ export function Gallery({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onLayout={measure}>
|
onLayout={measure}>
|
||||||
<BlockDrawerGesture>
|
<DrawerWaitGesture>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
role="group"
|
role="group"
|
||||||
@@ -339,7 +339,7 @@ export function Gallery({
|
|||||||
paddingRight: insetRight,
|
paddingRight: insetRight,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</BlockDrawerGesture>
|
</DrawerWaitGesture>
|
||||||
</View>
|
</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 {BackHandler, useWindowDimensions, View} from 'react-native'
|
||||||
import {Drawer} from 'react-native-drawer-layout'
|
import {Drawer} from 'react-native-drawer-layout'
|
||||||
import {SystemBars} from 'react-native-edge-to-edge'
|
import {SystemBars} from 'react-native-edge-to-edge'
|
||||||
import {Gesture} from 'react-native-gesture-handler'
|
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
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 {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
|
||||||
import {Composer} from './Composer'
|
import {Composer} from './Composer'
|
||||||
import {DrawerContent} from './Drawer'
|
import {DrawerContent} from './Drawer'
|
||||||
|
import {
|
||||||
|
DrawerWaitGestureProvider,
|
||||||
|
useDrawerWaitGestures,
|
||||||
|
} from './DrawerWaitGestureContext'
|
||||||
|
|
||||||
function ShellInner() {
|
function ShellInner() {
|
||||||
const winDim = useWindowDimensions()
|
const winDim = useWindowDimensions()
|
||||||
@@ -105,7 +108,9 @@ function ShellInner() {
|
|||||||
<View style={[a.h_full]}>
|
<View style={[a.h_full]}>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
style={{paddingTop: insets.top, paddingBottom: insets.bottom}}>
|
style={{paddingTop: insets.top, paddingBottom: insets.bottom}}>
|
||||||
|
<DrawerWaitGestureProvider>
|
||||||
<TabsNavigator layout={drawerLayout} />
|
<TabsNavigator layout={drawerLayout} />
|
||||||
|
</DrawerWaitGestureProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -145,7 +150,7 @@ function DrawerLayout({children}: {children: React.ReactNode}) {
|
|||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
|
||||||
const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
|
const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
|
||||||
const [trendingScrollGesture] = useState(() => Gesture.Native())
|
const waitGestures = useDrawerWaitGestures()
|
||||||
|
|
||||||
const renderDrawerContent = useCallback(() => <DrawerContent />, [])
|
const renderDrawerContent = useCallback(() => <DrawerContent />, [])
|
||||||
const onOpenDrawer = useCallback(
|
const onOpenDrawer = useCallback(
|
||||||
@@ -162,7 +167,9 @@ function DrawerLayout({children}: {children: React.ReactNode}) {
|
|||||||
renderDrawerContent={renderDrawerContent}
|
renderDrawerContent={renderDrawerContent}
|
||||||
drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
|
drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
|
||||||
configureGestureHandler={handler => {
|
configureGestureHandler={handler => {
|
||||||
handler = handler.requireExternalGestureToFail(trendingScrollGesture)
|
for (const gesture of waitGestures) {
|
||||||
|
handler = handler.requireExternalGestureToFail(gesture)
|
||||||
|
}
|
||||||
|
|
||||||
if (swipeEnabled) {
|
if (swipeEnabled) {
|
||||||
if (isDrawerOpen) {
|
if (isDrawerOpen) {
|
||||||
|
|||||||
Reference in New Issue
Block a user