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/core/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 {useCloseAllActiveElements} from '#/state/util'
13import {Lightbox} from '#/view/com/lightbox/Lightbox'
14import {ModalsContainer} from '#/view/com/modals/Modal'
15import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
16import {Deactivated} from '#/screens/Deactivated'
17import {Takendown} from '#/screens/Takendown'
18import {atoms as a, select, useBreakpoints, useTheme} from '#/alf'
19import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
20import {EmailDialog} from '#/components/dialogs/EmailDialog'
21import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
22import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
23import {NuxDialogs} from '#/components/dialogs/nuxs'
24import {SigninDialog} from '#/components/dialogs/Signin'
25import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
26import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
27import {
28 Outlet as PolicyUpdateOverlayPortalOutlet,
29 usePolicyUpdateContext,
30} from '#/components/PolicyUpdateOverlay'
31import {Outlet as PortalOutlet} from '#/components/Portal'
32import {WelcomeModal} from '#/components/WelcomeModal'
33import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
34import {PassiveAnalytics} from '#/analytics/PassiveAnalytics'
35import {FlatNavigator, RoutesContainer} from '#/Navigation'
36import {Composer} from './Composer'
37import {DrawerContent} from './Drawer'
38
39function ShellInner() {
40 const t = useTheme()
41 const navigator = useNavigation<NavigationProp>()
42 const closeAllActiveElements = useCloseAllActiveElements()
43 const {state: policyUpdateState} = usePolicyUpdateContext()
44 const welcomeModalControl = useWelcomeModal()
45
46 useIntentHandler()
47
48 useLayoutEffect(() => {
49 const rootElement = document.documentElement
50 rootElement.className = `html`
51 rootElement.style.setProperty(
52 'background',
53 `${t.atoms.bg.backgroundColor}`,
54 'important',
55 )
56 }, [t.atoms.bg.backgroundColor, t.name])
57
58 useLayoutEffect(() => {
59 const color = t.palette.primary_500
60
61 const styleId = 'prosemirror-mention-color'
62 let style = document.getElementById(styleId) as HTMLStyleElement | null
63
64 if (!style) {
65 style = document.createElement('style')
66 style.id = styleId
67 document.head.appendChild(style)
68 }
69
70 style.innerHTML = `
71 .ProseMirror .mention {
72 color: ${color} !important;
73 }
74 .ProseMirror a,
75 .ProseMirror .autolink {
76 color: ${color} !important;
77 }
78 `
79 }, [t.palette.primary_500])
80
81 useEffect(() => {
82 const unsubscribe = navigator.addListener('state', () => {
83 closeAllActiveElements()
84 })
85 return unsubscribe
86 }, [navigator, closeAllActiveElements])
87
88 const drawerLayout = useCallback(
89 ({children}: {children: React.ReactNode}) => (
90 <DrawerLayout>{children}</DrawerLayout>
91 ),
92 [],
93 )
94 return (
95 <>
96 <ErrorBoundary>
97 <FlatNavigator layout={drawerLayout} />
98 </ErrorBoundary>
99 <Composer winHeight={0} />
100 <ModalsContainer />
101 <MutedWordsDialog />
102 <SigninDialog />
103 <EmailDialog />
104 <AgeAssuranceRedirectDialog />
105 <LinkWarningDialog />
106 <Lightbox />
107 <NuxDialogs />
108 <GlobalReportDialog />
109
110 {welcomeModalControl.isOpen && (
111 <WelcomeModal control={welcomeModalControl} />
112 )}
113
114 {/* Until policy update has been completed by the user, don't render anything that is portaled */}
115 {policyUpdateState.completed && (
116 <>
117 <PortalOutlet />
118 </>
119 )}
120
121 <PolicyUpdateOverlayPortalOutlet />
122
123 {/* workaround for a WebKit compositing bug. After a
124 dialog (which uses Portal + fixed elements + CSS animations)
125 closes, WebKit can skip painting subsequent view transitions.
126 A persistent zero-size fixed element keeps the compositing
127 tree from entering this broken state. */}
128 <div
129 aria-hidden
130 style={{
131 position: 'fixed',
132 width: 0,
133 height: 0,
134 pointerEvents: 'none',
135 }}
136 />
137 </>
138 )
139}
140
141function DrawerLayout({children}: {children: React.ReactNode}) {
142 const t = useTheme()
143 const isDrawerOpen = useIsDrawerOpen()
144 const setDrawerOpen = useSetDrawerOpen()
145 const {gtTablet} = useBreakpoints()
146 const {_} = useLingui()
147 const showDrawer = !gtTablet && isDrawerOpen
148 const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer)
149
150 useLayoutEffect(() => {
151 if (showDrawer !== showDrawerDelayedExit) {
152 if (showDrawer) {
153 setShowDrawerDelayedExit(true)
154 } else {
155 const timeout = setTimeout(() => {
156 setShowDrawerDelayedExit(false)
157 }, 160)
158 return () => clearTimeout(timeout)
159 }
160 }
161 }, [showDrawer, showDrawerDelayedExit])
162
163 return (
164 <>
165 {children}
166 {showDrawerDelayedExit && (
167 <>
168 <RemoveScrollBar />
169 <TouchableWithoutFeedback
170 onPress={ev => {
171 // Only close if press happens outside of the drawer
172 if (ev.target === ev.currentTarget) {
173 setDrawerOpen(false)
174 }
175 }}
176 accessibilityLabel={_(msg`Close drawer menu`)}
177 accessibilityHint="">
178 <View
179 style={[
180 styles.drawerMask,
181 {
182 backgroundColor: showDrawer
183 ? select(t.name, {
184 light: 'rgba(0, 57, 117, 0.1)',
185 dark: 'rgba(1, 82, 168, 0.1)',
186 dim: 'rgba(10, 13, 16, 0.8)',
187 })
188 : 'transparent',
189 },
190 a.transition_color,
191 ]}>
192 <View
193 style={[
194 styles.drawerContainer,
195 showDrawer ? a.slide_in_left : a.slide_out_left,
196 ]}>
197 <DrawerContent />
198 </View>
199 </View>
200 </TouchableWithoutFeedback>
201 </>
202 )}
203 </>
204 )
205}
206
207export function Shell() {
208 const t = useTheme()
209 const {currentAccount} = useSession()
210 return (
211 <View style={[a.util_screen_outer, t.atoms.bg]}>
212 {currentAccount?.status === 'takendown' ? (
213 <Takendown />
214 ) : currentAccount?.status === 'deactivated' ? (
215 <Deactivated />
216 ) : (
217 <>
218 <RoutesContainer>
219 <ShellInner />
220 </RoutesContainer>
221
222 <RedirectOverlay />
223 </>
224 )}
225
226 <PassiveAnalytics />
227 </View>
228 )
229}
230
231const styles = StyleSheet.create({
232 drawerMask: {
233 ...a.fixed,
234 width: '100%',
235 height: '100%',
236 top: 0,
237 left: 0,
238 },
239 drawerContainer: {
240 display: 'flex',
241 ...a.fixed,
242 top: 0,
243 left: 0,
244 height: '100%',
245 width: 330,
246 maxWidth: '80%',
247 },
248})