about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-09-05 18:39:28 +0300
committerGitHub <noreply@github.com>2025-09-05 08:39:28 -0700
commitdaed047bb41bcdac374398b06f87895511ea34a8 (patch)
tree559fa4d9c0d65eb4fd0c8269ee53a73e5a0d7934
parentee3e08393882a9d72ae9cab5f765ed2885c5a98d (diff)
downloadvoidsky-daed047bb41bcdac374398b06f87895511ea34a8.tar.zst
[Perf] Drawer gesture perf fix + related cleanup (#8953)
* split drawer layout into own component

* don't put props in dep array

* memoize pager view
-rw-r--r--src/state/shell/drawer-open.tsx12
-rw-r--r--src/view/com/home/HomeHeader.tsx14
-rw-r--r--src/view/com/pager/Pager.tsx31
-rw-r--r--src/view/shell/index.tsx135
4 files changed, 107 insertions, 85 deletions
diff --git a/src/state/shell/drawer-open.tsx b/src/state/shell/drawer-open.tsx
index 87650a09c..7ce4189c3 100644
--- a/src/state/shell/drawer-open.tsx
+++ b/src/state/shell/drawer-open.tsx
@@ -1,15 +1,15 @@
-import React from 'react'
+import {createContext, useContext, useState} from 'react'
 
 type StateContext = boolean
 type SetContext = (v: boolean) => void
 
-const stateContext = React.createContext<StateContext>(false)
+const stateContext = createContext<StateContext>(false)
 stateContext.displayName = 'DrawerOpenStateContext'
-const setContext = React.createContext<SetContext>((_: boolean) => {})
+const setContext = createContext<SetContext>((_: boolean) => {})
 setContext.displayName = 'DrawerOpenSetContext'
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
-  const [state, setState] = React.useState(false)
+  const [state, setState] = useState(false)
 
   return (
     <stateContext.Provider value={state}>
@@ -19,9 +19,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 }
 
 export function useIsDrawerOpen() {
-  return React.useContext(stateContext)
+  return useContext(stateContext)
 }
 
 export function useSetDrawerOpen() {
-  return React.useContext(setContext)
+  return useContext(setContext)
 }
diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx
index 0ec9ac753..4ae344549 100644
--- a/src/view/com/home/HomeHeader.tsx
+++ b/src/view/com/home/HomeHeader.tsx
@@ -1,10 +1,10 @@
 import React from 'react'
 import {useNavigation} from '@react-navigation/native'
 
-import {NavigationProp} from '#/lib/routes/types'
-import {FeedSourceInfo} from '#/state/queries/feed'
+import {type NavigationProp} from '#/lib/routes/types'
+import {type FeedSourceInfo} from '#/state/queries/feed'
 import {useSession} from '#/state/session'
-import {RenderTabBarFnProps} from '#/view/com/pager/Pager'
+import {type RenderTabBarFnProps} from '#/view/com/pager/Pager'
 import {TabBar} from '../pager/TabBar'
 import {HomeHeaderLayout} from './HomeHeaderLayout'
 
@@ -15,7 +15,7 @@ export function HomeHeader(
     feeds: FeedSourceInfo[]
   },
 ) {
-  const {feeds} = props
+  const {feeds, onSelect: onSelectProp} = props
   const {hasSession} = useSession()
   const navigation = useNavigation<NavigationProp>()
 
@@ -43,11 +43,11 @@ export function HomeHeader(
     (index: number) => {
       if (!hasPinnedCustom && index === items.length - 1) {
         onPressFeedsLink()
-      } else if (props.onSelect) {
-        props.onSelect(index)
+      } else if (onSelectProp) {
+        onSelectProp(index)
       }
     },
-    [items.length, onPressFeedsLink, props, hasPinnedCustom],
+    [items.length, onPressFeedsLink, onSelectProp, hasPinnedCustom],
   )
 
   return (
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index 8cc346903..5eb7d7608 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -1,7 +1,9 @@
 import {
+  memo,
   useCallback,
   useContext,
   useImperativeHandle,
+  useMemo,
   useRef,
   useState,
 } from 'react'
@@ -56,6 +58,7 @@ interface Props {
 }
 
 const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
+const MemoizedAnimatedPagerView = memo(AnimatedPagerView)
 
 export function Pager({
   ref,
@@ -139,10 +142,6 @@ export function Pager({
     [parentOnPageScrollStateChanged],
   )
 
-  const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
-  const nativeGesture =
-    Gesture.Native().requireExternalGestureToFail(drawerGesture)
-
   return (
     <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
       {renderTabBar({
@@ -151,19 +150,33 @@ export function Pager({
         dragProgress,
         dragState,
       })}
-      <GestureDetector gesture={nativeGesture}>
-        <AnimatedPagerView
+      <DrawerGestureRequireFail>
+        <MemoizedAnimatedPagerView
           ref={pagerView}
-          style={[a.flex_1]}
+          style={a.flex_1}
           initialPage={initialPage}
           onPageScroll={handlePageScroll}>
           {children}
-        </AnimatedPagerView>
-      </GestureDetector>
+        </MemoizedAnimatedPagerView>
+      </DrawerGestureRequireFail>
     </View>
   )
 }
 
+function DrawerGestureRequireFail({children}: {children: React.ReactNode}) {
+  const drawerGesture = useContext(DrawerGestureContext)
+
+  const nativeGesture = useMemo(() => {
+    const gesture = Gesture.Native()
+    if (drawerGesture) {
+      gesture.requireExternalGestureToFail(drawerGesture)
+    }
+    return gesture
+  }, [drawerGesture])
+
+  return <GestureDetector gesture={nativeGesture}>{children}</GestureDetector>
+}
+
 function usePagerHandlers(
   handlers: {
     onPageScroll: (e: PagerViewOnPageScrollEventData) => void
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 277e5c523..5075f05cb 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -45,25 +45,10 @@ import {Composer} from './Composer'
 import {DrawerContent} from './Drawer'
 
 function ShellInner() {
-  const t = useTheme()
-  const isDrawerOpen = useIsDrawerOpen()
-  const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled()
-  const setIsDrawerOpen = useSetDrawerOpen()
   const winDim = useWindowDimensions()
   const insets = useSafeAreaInsets()
   const {state: policyUpdateState} = usePolicyUpdateContext()
 
-  const renderDrawerContent = useCallback(() => <DrawerContent />, [])
-  const onOpenDrawer = useCallback(
-    () => setIsDrawerOpen(true),
-    [setIsDrawerOpen],
-  )
-  const onCloseDrawer = useCallback(
-    () => setIsDrawerOpen(false),
-    [setIsDrawerOpen],
-  )
-  const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
-  const {hasSession} = useSession()
   const closeAnyActiveElement = useCloseAnyActiveElement()
 
   useNotificationsRegistration()
@@ -102,60 +87,14 @@ function ShellInner() {
     }
   }, [dedupe, navigation])
 
-  const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
-  const [trendingScrollGesture] = useState(() => Gesture.Native())
   return (
     <>
       <View style={[a.h_full]}>
         <ErrorBoundary
           style={{paddingTop: insets.top, paddingBottom: insets.bottom}}>
-          <Drawer
-            renderDrawerContent={renderDrawerContent}
-            drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
-            configureGestureHandler={handler => {
-              handler = handler.requireExternalGestureToFail(
-                trendingScrollGesture,
-              )
-
-              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}
-            swipeMinVelocity={100}
-            swipeMinDistance={10}
-            drawerType={isIOS ? 'slide' : 'front'}
-            overlayStyle={{
-              backgroundColor: select(t.name, {
-                light: 'rgba(0, 57, 117, 0.1)',
-                dark: isAndroid
-                  ? 'rgba(16, 133, 254, 0.1)'
-                  : 'rgba(1, 82, 168, 0.1)',
-                dim: 'rgba(10, 13, 16, 0.8)',
-              }),
-            }}>
+          <DrawerLayout>
             <TabsNavigator />
-          </Drawer>
+          </DrawerLayout>
         </ErrorBoundary>
       </View>
 
@@ -182,6 +121,76 @@ function ShellInner() {
   )
 }
 
+function DrawerLayout({children}: {children: React.ReactNode}) {
+  const t = useTheme()
+  const isDrawerOpen = useIsDrawerOpen()
+  const setIsDrawerOpen = useSetDrawerOpen()
+  const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled()
+  const winDim = useWindowDimensions()
+
+  const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
+  const {hasSession} = useSession()
+
+  const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
+  const [trendingScrollGesture] = useState(() => Gesture.Native())
+
+  const renderDrawerContent = useCallback(() => <DrawerContent />, [])
+  const onOpenDrawer = useCallback(
+    () => setIsDrawerOpen(true),
+    [setIsDrawerOpen],
+  )
+  const onCloseDrawer = useCallback(
+    () => setIsDrawerOpen(false),
+    [setIsDrawerOpen],
+  )
+
+  return (
+    <Drawer
+      renderDrawerContent={renderDrawerContent}
+      drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
+      configureGestureHandler={handler => {
+        handler = handler.requireExternalGestureToFail(trendingScrollGesture)
+
+        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}
+      swipeMinVelocity={100}
+      swipeMinDistance={10}
+      drawerType={isIOS ? 'slide' : 'front'}
+      overlayStyle={{
+        backgroundColor: select(t.name, {
+          light: 'rgba(0, 57, 117, 0.1)',
+          dark: isAndroid ? 'rgba(16, 133, 254, 0.1)' : 'rgba(1, 82, 168, 0.1)',
+          dim: 'rgba(10, 13, 16, 0.8)',
+        }),
+      }}>
+      {children}
+    </Drawer>
+  )
+}
+
 export function Shell() {
   const t = useTheme()
   const {status: geolocation} = useGeolocationStatus()