Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 235 lines 7.9 kB view raw
1import * as React from 'react' 2import {View} from 'react-native' 3// Based on @react-navigation/native-stack/src/navigators/createNativeStackNavigator.ts 4// MIT License 5// Copyright (c) 2017 React Navigation Contributors 6import { 7 createNavigatorFactory, 8 type EventArg, 9 type NavigatorTypeBagBase, 10 type ParamListBase, 11 type StackActionHelpers, 12 StackActions, 13 type StackNavigationState, 14 StackRouter, 15 type StackRouterOptions, 16 type StaticConfig, 17 type TypedNavigator, 18 useNavigationBuilder, 19} from '@react-navigation/native' 20import {NativeStackView} from '@react-navigation/native-stack' 21import { 22 type NativeStackNavigationEventMap, 23 type NativeStackNavigationOptions, 24 type NativeStackNavigationProp, 25 type NativeStackNavigatorProps, 26} from '@react-navigation/native-stack' 27 28import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 29import {useSession} from '#/state/session' 30import {useOnboardingState} from '#/state/shell' 31import { 32 useLoggedOutView, 33 useLoggedOutViewControls, 34} from '#/state/shell/logged-out' 35import {LoggedOut} from '#/view/com/auth/LoggedOut' 36import {Onboarding} from '#/screens/Onboarding' 37import {SignupQueued} from '#/screens/SignupQueued' 38import {atoms as a, useLayoutBreakpoints} from '#/alf' 39import {PolicyUpdateOverlay} from '#/components/PolicyUpdateOverlay' 40import {IS_NATIVE, IS_WEB} from '#/env' 41import {BottomBarWeb} from './bottom-bar/BottomBarWeb' 42import {DesktopLeftNav} from './desktop/LeftNav' 43import {DesktopRightNav} from './desktop/RightNav' 44 45// On web, only this many screens (beyond Home + focused) stay mounted. 46// Older screens are unmounted to prevent memory growth during long sessions. 47const WEB_MAX_CACHED_SCREENS = 5 48 49type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { 50 requireAuth?: boolean 51} 52 53function NativeStackNavigator({ 54 id, 55 initialRouteName, 56 UNSTABLE_routeNamesChangeBehavior, 57 children, 58 layout, 59 screenListeners, 60 screenOptions, 61 screenLayout, 62 UNSTABLE_router, 63 ...rest 64}: NativeStackNavigatorProps) { 65 // --- this is copy and pasted from the original native stack navigator --- 66 const {state, describe, descriptors, navigation, NavigationContent} = 67 useNavigationBuilder< 68 StackNavigationState<ParamListBase>, 69 StackRouterOptions, 70 StackActionHelpers<ParamListBase>, 71 NativeStackNavigationOptionsWithAuth, 72 NativeStackNavigationEventMap 73 >(StackRouter, { 74 id, 75 initialRouteName, 76 UNSTABLE_routeNamesChangeBehavior, 77 children, 78 layout, 79 screenListeners, 80 screenOptions, 81 screenLayout, 82 UNSTABLE_router, 83 }) 84 85 React.useEffect( 86 () => 87 // @ts-expect-error: there may not be a tab navigator in parent 88 navigation?.addListener?.('tabPress', (e: any) => { 89 const isFocused = navigation.isFocused() 90 91 // Run the operation in the next frame so we're sure all listeners have been run 92 // This is necessary to know if preventDefault() has been called 93 requestAnimationFrame(() => { 94 if ( 95 state.index > 0 && 96 isFocused && 97 !(e as EventArg<'tabPress', true>).defaultPrevented 98 ) { 99 // When user taps on already focused tab and we're inside the tab, 100 // reset the stack to replicate native behaviour 101 navigation.dispatch({ 102 ...StackActions.popToTop(), 103 target: state.key, 104 }) 105 } 106 }) 107 }), 108 [navigation, state.index, state.key], 109 ) 110 111 // --- our custom logic starts here --- 112 // Web LRU: tracks route keys in most-recently-focused order 113 const lruKeysRef = React.useRef<string[]>([]) 114 const {hasSession, currentAccount} = useSession() 115 const activeRoute = state.routes[state.index] 116 const activeDescriptor = descriptors[activeRoute.key] 117 const activeRouteRequiresAuth = activeDescriptor.options.requireAuth ?? false 118 const onboardingState = useOnboardingState() 119 const {showLoggedOut} = useLoggedOutView() 120 const {setShowLoggedOut} = useLoggedOutViewControls() 121 const {isMobile} = useWebMediaQueries() 122 const {leftNavMinimal} = useLayoutBreakpoints() 123 if (!hasSession && (activeRouteRequiresAuth || IS_NATIVE)) { 124 return <LoggedOut /> 125 } 126 if (hasSession && currentAccount?.signupQueued) { 127 return <SignupQueued /> 128 } 129 if (showLoggedOut) { 130 return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> 131 } 132 if (onboardingState.isActive) { 133 return <Onboarding /> 134 } 135 // On web, limit how many screens stay mounted to prevent memory growth. 136 // Home is always pinned, the focused screen is always mounted, and the 137 // most recently visited screens are kept up to WEB_MAX_CACHED_SCREENS. 138 // Evicted screens render a lightweight placeholder — the route stays in 139 // state so browser back/forward still works; the component just re-mounts. 140 let finalDescriptors = descriptors 141 if (IS_WEB) { 142 const focusedKey = activeRoute.key 143 144 // Update LRU: move focused key to front 145 const lru = lruKeysRef.current 146 const idx = lru.indexOf(focusedKey) 147 if (idx > 0) { 148 lru.splice(idx, 1) 149 lru.unshift(focusedKey) 150 } else if (idx === -1) { 151 lru.unshift(focusedKey) 152 } 153 154 // Remove keys for routes no longer in the stack 155 const routeKeySet = new Set(state.routes.map(r => r.key)) 156 lruKeysRef.current = lruKeysRef.current.filter(k => routeKeySet.has(k)) 157 158 // Build mount set: Home (pinned) + focused + N most recent 159 const mountSet = new Set<string>() 160 mountSet.add(focusedKey) 161 const homeKey = state.routes.find(r => r.name === 'Home')?.key 162 if (homeKey) mountSet.add(homeKey) 163 let cached = 0 164 for (const key of lruKeysRef.current) { 165 if (cached >= WEB_MAX_CACHED_SCREENS) break 166 if (!mountSet.has(key)) { 167 mountSet.add(key) 168 cached++ 169 } 170 } 171 172 // Evicted screens get a lightweight placeholder instead of their full tree 173 finalDescriptors = {} as typeof descriptors 174 for (const key in descriptors) { 175 if (mountSet.has(key)) { 176 finalDescriptors[key] = descriptors[key] 177 } else { 178 finalDescriptors[key] = { 179 ...descriptors[key], 180 render: () => <View />, 181 } 182 } 183 } 184 } 185 186 // Show the bottom bar if we have a session only on mobile web. If we don't have a session, we want to show it 187 // on both tablet and mobile web so that we see the create account CTA. 188 const showBottomBar = hasSession ? isMobile : leftNavMinimal 189 190 return ( 191 <NavigationContent> 192 <View role="main" style={a.flex_1}> 193 <NativeStackView 194 {...rest} 195 state={state} 196 navigation={navigation} 197 descriptors={finalDescriptors} 198 describe={describe} 199 /> 200 </View> 201 {IS_WEB && ( 202 <> 203 {showBottomBar ? <BottomBarWeb /> : <DesktopLeftNav />} 204 {!isMobile && <DesktopRightNav routeName={activeRoute.name} />} 205 </> 206 )} 207 208 {/* Only shown after logged in and onboaring etc are complete */} 209 {hasSession && <PolicyUpdateOverlay />} 210 </NavigationContent> 211 ) 212} 213 214export function createNativeStackNavigatorWithAuth< 215 const ParamList extends ParamListBase, 216 const NavigatorID extends string | undefined = string | undefined, 217 const TypeBag extends NavigatorTypeBagBase = { 218 ParamList: ParamList 219 NavigatorID: NavigatorID 220 State: StackNavigationState<ParamList> 221 ScreenOptions: NativeStackNavigationOptionsWithAuth 222 EventMap: NativeStackNavigationEventMap 223 NavigationList: { 224 [RouteName in keyof ParamList]: NativeStackNavigationProp< 225 ParamList, 226 RouteName, 227 NavigatorID 228 > 229 } 230 Navigator: typeof NativeStackNavigator 231 }, 232 const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>, 233>(config?: Config): TypedNavigator<TypeBag, Config> { 234 return createNavigatorFactory(NativeStackNavigator)(config) 235}