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 {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
35import {PassiveAnalytics} from '#/analytics/PassiveAnalytics'
36import {FlatNavigator, RoutesContainer} from '#/Navigation'
37import {Composer} from './Composer.web'
38import {DrawerContent} from './Drawer'
39
40function ShellInner() {
41 const t = useTheme()
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 useLayoutEffect(() => {
51 const rootElement = document.documentElement
52 rootElement.className = `html`
53 rootElement.style.setProperty(
54 'background',
55 `${t.atoms.bg.backgroundColor}`,
56 'important',
57 )
58 }, [t.atoms.bg.backgroundColor, t.name])
59
60 useLayoutEffect(() => {
61 const color = t.palette.primary_500
62
63 const styleId = 'prosemirror-mention-color'
64 let style = document.getElementById(styleId) as HTMLStyleElement | null
65
66 if (!style) {
67 style = document.createElement('style')
68 style.id = styleId
69 document.head.appendChild(style)
70 }
71
72 style.innerHTML = `
73 .ProseMirror .mention {
74 color: ${color} !important;
75 }
76 .ProseMirror a,
77 .ProseMirror .autolink {
78 color: ${color} !important;
79 }
80 `
81 }, [t.palette.primary_500])
82
83 useEffect(() => {
84 const unsubscribe = navigator.addListener('state', () => {
85 closeAllActiveElements()
86 })
87 return unsubscribe
88 }, [navigator, closeAllActiveElements])
89
90 const drawerLayout = useCallback(
91 ({children}: {children: React.ReactNode}) => (
92 <DrawerLayout>{children}</DrawerLayout>
93 ),
94 [],
95 )
96 return (
97 <>
98 <ErrorBoundary>
99 <FlatNavigator layout={drawerLayout} />
100 </ErrorBoundary>
101 <Composer winHeight={0} />
102 <ModalsContainer />
103 <MutedWordsDialog />
104 <SigninDialog />
105 <EmailDialog />
106 <AgeAssuranceRedirectDialog />
107 <LinkWarningDialog />
108 <Lightbox />
109 <NuxDialogs />
110 <GlobalReportDialog />
111
112 {welcomeModalControl.isOpen && (
113 <WelcomeModal control={welcomeModalControl} />
114 )}
115
116 {/* Until policy update has been completed by the user, don't render anything that is portaled */}
117 {policyUpdateState.completed && (
118 <>
119 <PortalOutlet />
120 </>
121 )}
122
123 <PolicyUpdateOverlayPortalOutlet />
124 </>
125 )
126}
127
128function DrawerLayout({children}: {children: React.ReactNode}) {
129 const t = useTheme()
130 const isDrawerOpen = useIsDrawerOpen()
131 const setDrawerOpen = useSetDrawerOpen()
132 const {gtTablet} = useBreakpoints()
133 const {_} = useLingui()
134 const showDrawer = !gtTablet && isDrawerOpen
135 const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer)
136
137 useLayoutEffect(() => {
138 if (showDrawer !== showDrawerDelayedExit) {
139 if (showDrawer) {
140 setShowDrawerDelayedExit(true)
141 } else {
142 const timeout = setTimeout(() => {
143 setShowDrawerDelayedExit(false)
144 }, 160)
145 return () => clearTimeout(timeout)
146 }
147 }
148 }, [showDrawer, showDrawerDelayedExit])
149
150 return (
151 <>
152 {children}
153 {showDrawerDelayedExit && (
154 <>
155 <RemoveScrollBar />
156 <TouchableWithoutFeedback
157 onPress={ev => {
158 // Only close if press happens outside of the drawer
159 if (ev.target === ev.currentTarget) {
160 setDrawerOpen(false)
161 }
162 }}
163 accessibilityLabel={_(msg`Close drawer menu`)}
164 accessibilityHint="">
165 <View
166 style={[
167 styles.drawerMask,
168 {
169 backgroundColor: showDrawer
170 ? select(t.name, {
171 light: 'rgba(0, 57, 117, 0.1)',
172 dark: 'rgba(1, 82, 168, 0.1)',
173 dim: 'rgba(10, 13, 16, 0.8)',
174 })
175 : 'transparent',
176 },
177 a.transition_color,
178 ]}>
179 <View
180 style={[
181 styles.drawerContainer,
182 showDrawer ? a.slide_in_left : a.slide_out_left,
183 ]}>
184 <DrawerContent />
185 </View>
186 </View>
187 </TouchableWithoutFeedback>
188 </>
189 )}
190 </>
191 )
192}
193
194export function Shell() {
195 const t = useTheme()
196 const {currentAccount} = useSession()
197 return (
198 <View style={[a.util_screen_outer, t.atoms.bg]}>
199 {currentAccount?.status === 'takendown' ? (
200 <Takendown />
201 ) : currentAccount?.status === 'deactivated' ? (
202 <Deactivated />
203 ) : (
204 <>
205 <RoutesContainer>
206 <ShellInner />
207 </RoutesContainer>
208
209 <RedirectOverlay />
210 </>
211 )}
212
213 <PassiveAnalytics />
214 </View>
215 )
216}
217
218const styles = StyleSheet.create({
219 drawerMask: {
220 ...a.fixed,
221 width: '100%',
222 height: '100%',
223 top: 0,
224 left: 0,
225 },
226 drawerContainer: {
227 display: 'flex',
228 ...a.fixed,
229 top: 0,
230 left: 0,
231 height: '100%',
232 width: 330,
233 maxWidth: '80%',
234 },
235})