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