forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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 {
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 {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 (isAndroid) {
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 (!isAndroid) 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
121 {/* Until policy update has been completed by the user, don't render anything that is portaled */}
122 {policyUpdateState.completed && (
123 <>
124 <PortalOutlet />
125 <BottomSheetOutlet />
126 </>
127 )}
128
129 <PolicyUpdateOverlayPortalOutlet />
130 </>
131 )
132}
133
134function DrawerLayout({children}: {children: React.ReactNode}) {
135 const t = useTheme()
136 const isDrawerOpen = useIsDrawerOpen()
137 const setIsDrawerOpen = useSetDrawerOpen()
138 const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled()
139 const winDim = useWindowDimensions()
140
141 const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
142 const {hasSession} = useSession()
143
144 const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
145 const [trendingScrollGesture] = useState(() => Gesture.Native())
146
147 const renderDrawerContent = useCallback(() => <DrawerContent />, [])
148 const onOpenDrawer = useCallback(
149 () => setIsDrawerOpen(true),
150 [setIsDrawerOpen],
151 )
152 const onCloseDrawer = useCallback(
153 () => setIsDrawerOpen(false),
154 [setIsDrawerOpen],
155 )
156
157 return (
158 <Drawer
159 renderDrawerContent={renderDrawerContent}
160 drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
161 configureGestureHandler={handler => {
162 handler = handler.requireExternalGestureToFail(trendingScrollGesture)
163
164 if (swipeEnabled) {
165 if (isDrawerOpen) {
166 return handler.activeOffsetX([-1, 1])
167 } else {
168 return (
169 handler
170 // Any movement to the left is a pager swipe
171 // so fail the drawer gesture immediately.
172 .failOffsetX(-1)
173 // Don't rush declaring that a movement to the right
174 // is a drawer swipe. It could be a vertical scroll.
175 .activeOffsetX(5)
176 )
177 }
178 } else {
179 // Fail the gesture immediately.
180 // This seems more reliable than the `swipeEnabled` prop.
181 // With `swipeEnabled` alone, the gesture may freeze after toggling off/on.
182 return handler.failOffsetX([0, 0]).failOffsetY([0, 0])
183 }
184 }}
185 open={isDrawerOpen}
186 onOpen={onOpenDrawer}
187 onClose={onCloseDrawer}
188 swipeEdgeWidth={winDim.width}
189 swipeMinVelocity={100}
190 swipeMinDistance={10}
191 drawerType={isIOS ? 'slide' : 'front'}
192 overlayStyle={{
193 backgroundColor: select(t.name, {
194 light: 'rgba(0, 57, 117, 0.1)',
195 dark: isAndroid ? 'rgba(16, 133, 254, 0.1)' : 'rgba(1, 82, 168, 0.1)',
196 dim: 'rgba(10, 13, 16, 0.8)',
197 }),
198 }}>
199 {children}
200 </Drawer>
201 )
202}
203
204export function Shell() {
205 const t = useTheme()
206 const aa = useAgeAssurance()
207 const {currentAccount} = useSession()
208 const fullyExpandedCount = useDialogFullyExpandedCountContext()
209
210 useIntentHandler()
211
212 useEffect(() => {
213 setSystemUITheme('theme', t)
214 }, [t])
215
216 return (
217 <View testID="mobileShellView" style={[a.h_full, t.atoms.bg]}>
218 <SystemBars
219 style={{
220 statusBar:
221 t.name !== 'light' || (isIOS && fullyExpandedCount > 0)
222 ? 'light'
223 : 'dark',
224 navigationBar: t.name !== 'light' ? 'light' : 'dark',
225 }}
226 />
227 {currentAccount?.status === 'takendown' ? (
228 <Takendown />
229 ) : currentAccount?.status === 'deactivated' ? (
230 <Deactivated />
231 ) : (
232 <>
233 {aa.state.access === aa.Access.None ? (
234 <NoAccessScreen />
235 ) : (
236 <RoutesContainer>
237 <ShellInner />
238 </RoutesContainer>
239 )}
240
241 <RedirectOverlay />
242 </>
243 )}
244 </View>
245 )
246}