Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 248 lines 7.5 kB view raw
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})