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

Configure Feed

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

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