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 239 lines 8.2 kB view raw
1import {useCallback, useEffect, useState} from 'react' 2import {BackHandler, useWindowDimensions, View} from 'react-native' 3import {Drawer} from 'react-native-drawer-layout' 4import {SystemBars} from 'react-native-edge-to-edge' 5import {Gesture} from 'react-native-gesture-handler' 6import {useSafeAreaInsets} from 'react-native-safe-area-context' 7import {useNavigation, useNavigationState} from '@react-navigation/native' 8 9import {useDedupe} from '#/lib/hooks/useDedupe' 10import {useIntentHandler} from '#/lib/hooks/useIntentHandler' 11import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler' 12import {useNotificationsRegistration} from '#/lib/notifications/notifications' 13import {isStateAtTabRoot} from '#/lib/routes/helpers' 14import {isAndroid, isIOS} from '#/platform/detection' 15import {useDialogFullyExpandedCountContext} from '#/state/dialogs' 16import {useSession} from '#/state/session' 17import { 18 useIsDrawerOpen, 19 useIsDrawerSwipeDisabled, 20 useSetDrawerOpen, 21} from '#/state/shell' 22import {useCloseAnyActiveElement} from '#/state/util' 23import {Lightbox} from '#/view/com/lightbox/Lightbox' 24import {ModalsContainer} from '#/view/com/modals/Modal' 25import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 26import {Deactivated} from '#/screens/Deactivated' 27import {Takendown} from '#/screens/Takendown' 28import {atoms as a, select, useTheme} from '#/alf' 29import {setSystemUITheme} from '#/alf/util/systemUI' 30import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 31import {EmailDialog} from '#/components/dialogs/EmailDialog' 32import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' 33import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' 34import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 35import {SigninDialog} from '#/components/dialogs/Signin' 36import { 37 Outlet as PolicyUpdateOverlayPortalOutlet, 38 usePolicyUpdateContext, 39} from '#/components/PolicyUpdateOverlay' 40import {Outlet as PortalOutlet} from '#/components/Portal' 41import {useAgeAssurance} from '#/ageAssurance' 42import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' 43import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' 44import {RoutesContainer, TabsNavigator} from '#/Navigation' 45import {BottomSheetOutlet} from '../../../modules/bottom-sheet' 46import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView' 47import {Composer} from './Composer' 48import {DrawerContent} from './Drawer' 49 50function ShellInner() { 51 const winDim = useWindowDimensions() 52 const insets = useSafeAreaInsets() 53 const {state: policyUpdateState} = usePolicyUpdateContext() 54 55 const closeAnyActiveElement = useCloseAnyActiveElement() 56 57 useNotificationsRegistration() 58 useNotificationsHandler() 59 60 useEffect(() => { 61 if (isAndroid) { 62 const listener = BackHandler.addEventListener('hardwareBackPress', () => { 63 return closeAnyActiveElement() 64 }) 65 66 return () => { 67 listener.remove() 68 } 69 } 70 }, [closeAnyActiveElement]) 71 72 // HACK 73 // expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually 74 // unregister that player to let the new screen register it. This is only a problem on Android, so we only need to 75 // apply it there. 76 // The `state` event should only fire whenever we push or pop to a screen, and should not fire consecutively quickly. 77 // To be certain though, we will also dedupe these calls. 78 const navigation = useNavigation() 79 const dedupe = useDedupe(1000) 80 useEffect(() => { 81 if (!isAndroid) return 82 const onFocusOrBlur = () => { 83 setTimeout(() => { 84 dedupe(updateActiveViewAsync) 85 }, 500) 86 } 87 navigation.addListener('state', onFocusOrBlur) 88 return () => { 89 navigation.removeListener('state', onFocusOrBlur) 90 } 91 }, [dedupe, navigation]) 92 93 return ( 94 <> 95 <View style={[a.h_full]}> 96 <ErrorBoundary 97 style={{paddingTop: insets.top, paddingBottom: insets.bottom}}> 98 <DrawerLayout> 99 <TabsNavigator /> 100 </DrawerLayout> 101 </ErrorBoundary> 102 </View> 103 104 <Composer winHeight={winDim.height} /> 105 <ModalsContainer /> 106 <MutedWordsDialog /> 107 <SigninDialog /> 108 <EmailDialog /> 109 <AgeAssuranceRedirectDialog /> 110 <InAppBrowserConsentDialog /> 111 <LinkWarningDialog /> 112 <Lightbox /> 113 114 {/* Until policy update has been completed by the user, don't render anything that is portaled */} 115 {policyUpdateState.completed && ( 116 <> 117 <PortalOutlet /> 118 <BottomSheetOutlet /> 119 </> 120 )} 121 122 <PolicyUpdateOverlayPortalOutlet /> 123 </> 124 ) 125} 126 127function DrawerLayout({children}: {children: React.ReactNode}) { 128 const t = useTheme() 129 const isDrawerOpen = useIsDrawerOpen() 130 const setIsDrawerOpen = useSetDrawerOpen() 131 const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() 132 const winDim = useWindowDimensions() 133 134 const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) 135 const {hasSession} = useSession() 136 137 const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled 138 const [trendingScrollGesture] = useState(() => Gesture.Native()) 139 140 const renderDrawerContent = useCallback(() => <DrawerContent />, []) 141 const onOpenDrawer = useCallback( 142 () => setIsDrawerOpen(true), 143 [setIsDrawerOpen], 144 ) 145 const onCloseDrawer = useCallback( 146 () => setIsDrawerOpen(false), 147 [setIsDrawerOpen], 148 ) 149 150 return ( 151 <Drawer 152 renderDrawerContent={renderDrawerContent} 153 drawerStyle={{width: Math.min(400, winDim.width * 0.8)}} 154 configureGestureHandler={handler => { 155 handler = handler.requireExternalGestureToFail(trendingScrollGesture) 156 157 if (swipeEnabled) { 158 if (isDrawerOpen) { 159 return handler.activeOffsetX([-1, 1]) 160 } else { 161 return ( 162 handler 163 // Any movement to the left is a pager swipe 164 // so fail the drawer gesture immediately. 165 .failOffsetX(-1) 166 // Don't rush declaring that a movement to the right 167 // is a drawer swipe. It could be a vertical scroll. 168 .activeOffsetX(5) 169 ) 170 } 171 } else { 172 // Fail the gesture immediately. 173 // This seems more reliable than the `swipeEnabled` prop. 174 // With `swipeEnabled` alone, the gesture may freeze after toggling off/on. 175 return handler.failOffsetX([0, 0]).failOffsetY([0, 0]) 176 } 177 }} 178 open={isDrawerOpen} 179 onOpen={onOpenDrawer} 180 onClose={onCloseDrawer} 181 swipeEdgeWidth={winDim.width} 182 swipeMinVelocity={100} 183 swipeMinDistance={10} 184 drawerType={isIOS ? 'slide' : 'front'} 185 overlayStyle={{ 186 backgroundColor: select(t.name, { 187 light: 'rgba(0, 57, 117, 0.1)', 188 dark: isAndroid ? 'rgba(16, 133, 254, 0.1)' : 'rgba(1, 82, 168, 0.1)', 189 dim: 'rgba(10, 13, 16, 0.8)', 190 }), 191 }}> 192 {children} 193 </Drawer> 194 ) 195} 196 197export function Shell() { 198 const t = useTheme() 199 const aa = useAgeAssurance() 200 const {currentAccount} = useSession() 201 const fullyExpandedCount = useDialogFullyExpandedCountContext() 202 203 useIntentHandler() 204 205 useEffect(() => { 206 setSystemUITheme('theme', t) 207 }, [t]) 208 209 return ( 210 <View testID="mobileShellView" style={[a.h_full, t.atoms.bg]}> 211 <SystemBars 212 style={{ 213 statusBar: 214 t.name !== 'light' || (isIOS && fullyExpandedCount > 0) 215 ? 'light' 216 : 'dark', 217 navigationBar: t.name !== 'light' ? 'light' : 'dark', 218 }} 219 /> 220 {currentAccount?.status === 'takendown' ? ( 221 <Takendown /> 222 ) : currentAccount?.status === 'deactivated' ? ( 223 <Deactivated /> 224 ) : ( 225 <> 226 {aa.state.access === aa.Access.None ? ( 227 <NoAccessScreen /> 228 ) : ( 229 <RoutesContainer> 230 <ShellInner /> 231 </RoutesContainer> 232 )} 233 234 <RedirectOverlay /> 235 </> 236 )} 237 </View> 238 ) 239}