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