Bluesky app fork with some witchin' additions 馃挮
at main 235 lines 7.1 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 {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})