Bluesky app fork with some witchin' additions 馃挮
at main 232 lines 7.0 kB view raw
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})