forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect, useLayoutEffect, useState} from 'react'
2import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import {useNavigation} from '@react-navigation/native'
6import {RemoveScrollBar} from 'react-remove-scroll-bar'
7
8import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
9import {type NavigationProp} from '#/lib/routes/types'
10import {useSession} from '#/state/session'
11import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
12import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
13import {useCloseAllActiveElements} from '#/state/util'
14import {Lightbox} from '#/view/com/lightbox/Lightbox'
15import {ModalsContainer} from '#/view/com/modals/Modal'
16import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
17import {Deactivated} from '#/screens/Deactivated'
18import {Takendown} from '#/screens/Takendown'
19import {atoms as a, select, useBreakpoints, useTheme} from '#/alf'
20import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
21import {EmailDialog} from '#/components/dialogs/EmailDialog'
22import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
23import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
24import {NuxDialogs} from '#/components/dialogs/nuxs'
25import {SigninDialog} from '#/components/dialogs/Signin'
26import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
27import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
28import {
29 Outlet as PolicyUpdateOverlayPortalOutlet,
30 usePolicyUpdateContext,
31} from '#/components/PolicyUpdateOverlay'
32import {Outlet as PortalOutlet} from '#/components/Portal'
33import {WelcomeModal} from '#/components/WelcomeModal'
34import {useAgeAssurance} from '#/ageAssurance'
35import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen'
36import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
37import {FlatNavigator, RoutesContainer} from '#/Navigation'
38import {Composer} from './Composer.web'
39import {DrawerContent} from './Drawer'
40
41function ShellInner() {
42 const navigator = useNavigation<NavigationProp>()
43 const closeAllActiveElements = useCloseAllActiveElements()
44 const {state: policyUpdateState} = usePolicyUpdateContext()
45 const welcomeModalControl = useWelcomeModal()
46
47 useComposerKeyboardShortcut()
48 useIntentHandler()
49
50 useEffect(() => {
51 const unsubscribe = navigator.addListener('state', () => {
52 closeAllActiveElements()
53 })
54 return unsubscribe
55 }, [navigator, closeAllActiveElements])
56
57 const drawerLayout = useCallback(
58 ({children}: {children: React.ReactNode}) => (
59 <DrawerLayout>{children}</DrawerLayout>
60 ),
61 [],
62 )
63 return (
64 <>
65 <ErrorBoundary>
66 <FlatNavigator layout={drawerLayout} />
67 </ErrorBoundary>
68 <Composer winHeight={0} />
69 <ModalsContainer />
70 <MutedWordsDialog />
71 <SigninDialog />
72 <EmailDialog />
73 <AgeAssuranceRedirectDialog />
74 <LinkWarningDialog />
75 <Lightbox />
76 <NuxDialogs />
77 <GlobalReportDialog />
78
79 {welcomeModalControl.isOpen && (
80 <WelcomeModal control={welcomeModalControl} />
81 )}
82
83 {/* Until policy update has been completed by the user, don't render anything that is portaled */}
84 {policyUpdateState.completed && (
85 <>
86 <PortalOutlet />
87 </>
88 )}
89
90 <PolicyUpdateOverlayPortalOutlet />
91 </>
92 )
93}
94
95function DrawerLayout({children}: {children: React.ReactNode}) {
96 const t = useTheme()
97 const isDrawerOpen = useIsDrawerOpen()
98 const setDrawerOpen = useSetDrawerOpen()
99 const {gtTablet} = useBreakpoints()
100 const {_} = useLingui()
101 const showDrawer = !gtTablet && isDrawerOpen
102 const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer)
103
104 useLayoutEffect(() => {
105 if (showDrawer !== showDrawerDelayedExit) {
106 if (showDrawer) {
107 setShowDrawerDelayedExit(true)
108 } else {
109 const timeout = setTimeout(() => {
110 setShowDrawerDelayedExit(false)
111 }, 160)
112 return () => clearTimeout(timeout)
113 }
114 }
115 }, [showDrawer, showDrawerDelayedExit])
116
117 return (
118 <>
119 {children}
120 {showDrawerDelayedExit && (
121 <>
122 <RemoveScrollBar />
123 <TouchableWithoutFeedback
124 onPress={ev => {
125 // Only close if press happens outside of the drawer
126 if (ev.target === ev.currentTarget) {
127 setDrawerOpen(false)
128 }
129 }}
130 accessibilityLabel={_(msg`Close drawer menu`)}
131 accessibilityHint="">
132 <View
133 style={[
134 styles.drawerMask,
135 {
136 backgroundColor: showDrawer
137 ? select(t.name, {
138 light: 'rgba(0, 57, 117, 0.1)',
139 dark: 'rgba(1, 82, 168, 0.1)',
140 dim: 'rgba(10, 13, 16, 0.8)',
141 })
142 : 'transparent',
143 },
144 a.transition_color,
145 ]}>
146 <View
147 style={[
148 styles.drawerContainer,
149 showDrawer ? a.slide_in_left : a.slide_out_left,
150 ]}>
151 <DrawerContent />
152 </View>
153 </View>
154 </TouchableWithoutFeedback>
155 </>
156 )}
157 </>
158 )
159}
160
161export function Shell() {
162 const t = useTheme()
163 const aa = useAgeAssurance()
164 const {currentAccount} = useSession()
165 return (
166 <View style={[a.util_screen_outer, t.atoms.bg]}>
167 {currentAccount?.status === 'takendown' ? (
168 <Takendown />
169 ) : currentAccount?.status === 'deactivated' ? (
170 <Deactivated />
171 ) : (
172 <>
173 {aa.state.access === aa.Access.None ? (
174 <NoAccessScreen />
175 ) : (
176 <RoutesContainer>
177 <ShellInner />
178 </RoutesContainer>
179 )}
180
181 <RedirectOverlay />
182 </>
183 )}
184 </View>
185 )
186}
187
188const styles = StyleSheet.create({
189 drawerMask: {
190 ...a.fixed,
191 width: '100%',
192 height: '100%',
193 top: 0,
194 left: 0,
195 },
196 drawerContainer: {
197 display: 'flex',
198 ...a.fixed,
199 top: 0,
200 left: 0,
201 height: '100%',
202 width: 330,
203 maxWidth: '80%',
204 },
205})