Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

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