Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

[Perf] Drawer gesture perf fix + related cleanup (#8953)

* split drawer layout into own component

* don't put props in dep array

* memoize pager view

authored by samuel.fm and committed by

GitHub daed047b ee3e0839

+107 -85
+6 -6
src/state/shell/drawer-open.tsx
··· 1 - import React from 'react' 1 + import {createContext, useContext, useState} from 'react' 2 2 3 3 type StateContext = boolean 4 4 type SetContext = (v: boolean) => void 5 5 6 - const stateContext = React.createContext<StateContext>(false) 6 + const stateContext = createContext<StateContext>(false) 7 7 stateContext.displayName = 'DrawerOpenStateContext' 8 - const setContext = React.createContext<SetContext>((_: boolean) => {}) 8 + const setContext = createContext<SetContext>((_: boolean) => {}) 9 9 setContext.displayName = 'DrawerOpenSetContext' 10 10 11 11 export function Provider({children}: React.PropsWithChildren<{}>) { 12 - const [state, setState] = React.useState(false) 12 + const [state, setState] = useState(false) 13 13 14 14 return ( 15 15 <stateContext.Provider value={state}> ··· 19 19 } 20 20 21 21 export function useIsDrawerOpen() { 22 - return React.useContext(stateContext) 22 + return useContext(stateContext) 23 23 } 24 24 25 25 export function useSetDrawerOpen() { 26 - return React.useContext(setContext) 26 + return useContext(setContext) 27 27 }
+7 -7
src/view/com/home/HomeHeader.tsx
··· 1 1 import React from 'react' 2 2 import {useNavigation} from '@react-navigation/native' 3 3 4 - import {NavigationProp} from '#/lib/routes/types' 5 - import {FeedSourceInfo} from '#/state/queries/feed' 4 + import {type NavigationProp} from '#/lib/routes/types' 5 + import {type FeedSourceInfo} from '#/state/queries/feed' 6 6 import {useSession} from '#/state/session' 7 - import {RenderTabBarFnProps} from '#/view/com/pager/Pager' 7 + import {type RenderTabBarFnProps} from '#/view/com/pager/Pager' 8 8 import {TabBar} from '../pager/TabBar' 9 9 import {HomeHeaderLayout} from './HomeHeaderLayout' 10 10 ··· 15 15 feeds: FeedSourceInfo[] 16 16 }, 17 17 ) { 18 - const {feeds} = props 18 + const {feeds, onSelect: onSelectProp} = props 19 19 const {hasSession} = useSession() 20 20 const navigation = useNavigation<NavigationProp>() 21 21 ··· 43 43 (index: number) => { 44 44 if (!hasPinnedCustom && index === items.length - 1) { 45 45 onPressFeedsLink() 46 - } else if (props.onSelect) { 47 - props.onSelect(index) 46 + } else if (onSelectProp) { 47 + onSelectProp(index) 48 48 } 49 49 }, 50 - [items.length, onPressFeedsLink, props, hasPinnedCustom], 50 + [items.length, onPressFeedsLink, onSelectProp, hasPinnedCustom], 51 51 ) 52 52 53 53 return (
+22 -9
src/view/com/pager/Pager.tsx
··· 1 1 import { 2 + memo, 2 3 useCallback, 3 4 useContext, 4 5 useImperativeHandle, 6 + useMemo, 5 7 useRef, 6 8 useState, 7 9 } from 'react' ··· 56 58 } 57 59 58 60 const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) 61 + const MemoizedAnimatedPagerView = memo(AnimatedPagerView) 59 62 60 63 export function Pager({ 61 64 ref, ··· 139 142 [parentOnPageScrollStateChanged], 140 143 ) 141 144 142 - const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web 143 - const nativeGesture = 144 - Gesture.Native().requireExternalGestureToFail(drawerGesture) 145 - 146 145 return ( 147 146 <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}> 148 147 {renderTabBar({ ··· 151 150 dragProgress, 152 151 dragState, 153 152 })} 154 - <GestureDetector gesture={nativeGesture}> 155 - <AnimatedPagerView 153 + <DrawerGestureRequireFail> 154 + <MemoizedAnimatedPagerView 156 155 ref={pagerView} 157 - style={[a.flex_1]} 156 + style={a.flex_1} 158 157 initialPage={initialPage} 159 158 onPageScroll={handlePageScroll}> 160 159 {children} 161 - </AnimatedPagerView> 162 - </GestureDetector> 160 + </MemoizedAnimatedPagerView> 161 + </DrawerGestureRequireFail> 163 162 </View> 164 163 ) 164 + } 165 + 166 + function DrawerGestureRequireFail({children}: {children: React.ReactNode}) { 167 + const drawerGesture = useContext(DrawerGestureContext) 168 + 169 + const nativeGesture = useMemo(() => { 170 + const gesture = Gesture.Native() 171 + if (drawerGesture) { 172 + gesture.requireExternalGestureToFail(drawerGesture) 173 + } 174 + return gesture 175 + }, [drawerGesture]) 176 + 177 + return <GestureDetector gesture={nativeGesture}>{children}</GestureDetector> 165 178 } 166 179 167 180 function usePagerHandlers(
+72 -63
src/view/shell/index.tsx
··· 45 45 import {DrawerContent} from './Drawer' 46 46 47 47 function ShellInner() { 48 - const t = useTheme() 49 - const isDrawerOpen = useIsDrawerOpen() 50 - const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() 51 - const setIsDrawerOpen = useSetDrawerOpen() 52 48 const winDim = useWindowDimensions() 53 49 const insets = useSafeAreaInsets() 54 50 const {state: policyUpdateState} = usePolicyUpdateContext() 55 51 56 - const renderDrawerContent = useCallback(() => <DrawerContent />, []) 57 - const onOpenDrawer = useCallback( 58 - () => setIsDrawerOpen(true), 59 - [setIsDrawerOpen], 60 - ) 61 - const onCloseDrawer = useCallback( 62 - () => setIsDrawerOpen(false), 63 - [setIsDrawerOpen], 64 - ) 65 - const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) 66 - const {hasSession} = useSession() 67 52 const closeAnyActiveElement = useCloseAnyActiveElement() 68 53 69 54 useNotificationsRegistration() ··· 102 87 } 103 88 }, [dedupe, navigation]) 104 89 105 - const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled 106 - const [trendingScrollGesture] = useState(() => Gesture.Native()) 107 90 return ( 108 91 <> 109 92 <View style={[a.h_full]}> 110 93 <ErrorBoundary 111 94 style={{paddingTop: insets.top, paddingBottom: insets.bottom}}> 112 - <Drawer 113 - renderDrawerContent={renderDrawerContent} 114 - drawerStyle={{width: Math.min(400, winDim.width * 0.8)}} 115 - configureGestureHandler={handler => { 116 - handler = handler.requireExternalGestureToFail( 117 - trendingScrollGesture, 118 - ) 119 - 120 - if (swipeEnabled) { 121 - if (isDrawerOpen) { 122 - return handler.activeOffsetX([-1, 1]) 123 - } else { 124 - return ( 125 - handler 126 - // Any movement to the left is a pager swipe 127 - // so fail the drawer gesture immediately. 128 - .failOffsetX(-1) 129 - // Don't rush declaring that a movement to the right 130 - // is a drawer swipe. It could be a vertical scroll. 131 - .activeOffsetX(5) 132 - ) 133 - } 134 - } else { 135 - // Fail the gesture immediately. 136 - // This seems more reliable than the `swipeEnabled` prop. 137 - // With `swipeEnabled` alone, the gesture may freeze after toggling off/on. 138 - return handler.failOffsetX([0, 0]).failOffsetY([0, 0]) 139 - } 140 - }} 141 - open={isDrawerOpen} 142 - onOpen={onOpenDrawer} 143 - onClose={onCloseDrawer} 144 - swipeEdgeWidth={winDim.width} 145 - swipeMinVelocity={100} 146 - swipeMinDistance={10} 147 - drawerType={isIOS ? 'slide' : 'front'} 148 - overlayStyle={{ 149 - backgroundColor: select(t.name, { 150 - light: 'rgba(0, 57, 117, 0.1)', 151 - dark: isAndroid 152 - ? 'rgba(16, 133, 254, 0.1)' 153 - : 'rgba(1, 82, 168, 0.1)', 154 - dim: 'rgba(10, 13, 16, 0.8)', 155 - }), 156 - }}> 95 + <DrawerLayout> 157 96 <TabsNavigator /> 158 - </Drawer> 97 + </DrawerLayout> 159 98 </ErrorBoundary> 160 99 </View> 161 100 ··· 179 118 180 119 <PolicyUpdateOverlayPortalOutlet /> 181 120 </> 121 + ) 122 + } 123 + 124 + function DrawerLayout({children}: {children: React.ReactNode}) { 125 + const t = useTheme() 126 + const isDrawerOpen = useIsDrawerOpen() 127 + const setIsDrawerOpen = useSetDrawerOpen() 128 + const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() 129 + const winDim = useWindowDimensions() 130 + 131 + const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) 132 + const {hasSession} = useSession() 133 + 134 + const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled 135 + const [trendingScrollGesture] = useState(() => Gesture.Native()) 136 + 137 + const renderDrawerContent = useCallback(() => <DrawerContent />, []) 138 + const onOpenDrawer = useCallback( 139 + () => setIsDrawerOpen(true), 140 + [setIsDrawerOpen], 141 + ) 142 + const onCloseDrawer = useCallback( 143 + () => setIsDrawerOpen(false), 144 + [setIsDrawerOpen], 145 + ) 146 + 147 + return ( 148 + <Drawer 149 + renderDrawerContent={renderDrawerContent} 150 + drawerStyle={{width: Math.min(400, winDim.width * 0.8)}} 151 + configureGestureHandler={handler => { 152 + handler = handler.requireExternalGestureToFail(trendingScrollGesture) 153 + 154 + if (swipeEnabled) { 155 + if (isDrawerOpen) { 156 + return handler.activeOffsetX([-1, 1]) 157 + } else { 158 + return ( 159 + handler 160 + // Any movement to the left is a pager swipe 161 + // so fail the drawer gesture immediately. 162 + .failOffsetX(-1) 163 + // Don't rush declaring that a movement to the right 164 + // is a drawer swipe. It could be a vertical scroll. 165 + .activeOffsetX(5) 166 + ) 167 + } 168 + } else { 169 + // Fail the gesture immediately. 170 + // This seems more reliable than the `swipeEnabled` prop. 171 + // With `swipeEnabled` alone, the gesture may freeze after toggling off/on. 172 + return handler.failOffsetX([0, 0]).failOffsetY([0, 0]) 173 + } 174 + }} 175 + open={isDrawerOpen} 176 + onOpen={onOpenDrawer} 177 + onClose={onCloseDrawer} 178 + swipeEdgeWidth={winDim.width} 179 + swipeMinVelocity={100} 180 + swipeMinDistance={10} 181 + drawerType={isIOS ? 'slide' : 'front'} 182 + overlayStyle={{ 183 + backgroundColor: select(t.name, { 184 + light: 'rgba(0, 57, 117, 0.1)', 185 + dark: isAndroid ? 'rgba(16, 133, 254, 0.1)' : 'rgba(1, 82, 168, 0.1)', 186 + dim: 'rgba(10, 13, 16, 0.8)', 187 + }), 188 + }}> 189 + {children} 190 + </Drawer> 182 191 ) 183 192 } 184 193