Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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}