diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 53998ac58c..6e0aa643d7 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -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"
]
}
}
diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx
index 1566f09736..0334608c3f 100644
--- a/src/components/images/Gallery/index.tsx
+++ b/src/components/images/Gallery/index.tsx
@@ -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}>
-
+
-
+
)
}
diff --git a/src/view/shell/DrawerWaitGesture.ios.tsx b/src/view/shell/DrawerWaitGesture.ios.tsx
new file mode 100644
index 0000000000..85b9775bac
--- /dev/null
+++ b/src/view/shell/DrawerWaitGesture.ios.tsx
@@ -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'
diff --git a/src/view/shell/DrawerWaitGesture.tsx b/src/view/shell/DrawerWaitGesture.tsx
new file mode 100644
index 0000000000..53210fbe38
--- /dev/null
+++ b/src/view/shell/DrawerWaitGesture.tsx
@@ -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 {children}
+}
diff --git a/src/view/shell/DrawerWaitGesture.web.tsx b/src/view/shell/DrawerWaitGesture.web.tsx
new file mode 100644
index 0000000000..f145199260
--- /dev/null
+++ b/src/view/shell/DrawerWaitGesture.web.tsx
@@ -0,0 +1,3 @@
+export function DrawerWaitGesture({children}: {children: React.ReactNode}) {
+ return children
+}
diff --git a/src/view/shell/DrawerWaitGestureContext.tsx b/src/view/shell/DrawerWaitGestureContext.tsx
new file mode 100644
index 0000000000..3012a44c6e
--- /dev/null
+++ b/src/view/shell/DrawerWaitGestureContext.tsx
@@ -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(null)
+const GesturesContext = createContext([])
+
+export function DrawerWaitGestureProvider({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const [gestures, setGestures] = useState([])
+
+ const register = useCallback(gesture => {
+ setGestures(prev => [...prev, gesture])
+ return () => {
+ setGestures(prev => prev.filter(g => g !== gesture))
+ }
+ }, [])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+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
+}
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 53c716d222..3fae7d12a4 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -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() {
-
+
+
+
@@ -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(() => , [])
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) {