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

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 294 lines 8.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 {LOGO_PATH, LOGO_VIEW_BOX} from '#/view/icons/Logo' 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' 38import {DrawerContent} from './Drawer' 39 40function createFaviconDataUrl(color: string) { 41 const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${LOGO_VIEW_BOX}"><path fill="${color}" d="${LOGO_PATH}"/></svg>` 42 return `data:image/svg+xml,${encodeURIComponent(svg)}` 43} 44 45function upsertHeadLink({ 46 rel, 47 href, 48 type, 49 sizes, 50}: { 51 rel: string 52 href: string 53 type?: string 54 sizes?: string 55}) { 56 let link = document.head.querySelector<HTMLLinkElement>(`link[rel="${rel}"]`) 57 58 if (!link) { 59 link = document.createElement('link') 60 link.rel = rel 61 document.head.appendChild(link) 62 } 63 64 if (type) link.type = type 65 if (sizes) link.sizes = sizes 66 link.href = href 67} 68 69function ShellInner() { 70 const t = useTheme() 71 const navigator = useNavigation<NavigationProp>() 72 const closeAllActiveElements = useCloseAllActiveElements() 73 const {state: policyUpdateState} = usePolicyUpdateContext() 74 const welcomeModalControl = useWelcomeModal() 75 76 useIntentHandler() 77 78 useLayoutEffect(() => { 79 const rootElement = document.documentElement 80 rootElement.className = `html` 81 rootElement.style.setProperty( 82 'background', 83 `${t.atoms.bg.backgroundColor}`, 84 'important', 85 ) 86 }, [t.atoms.bg.backgroundColor, t.name]) 87 88 useLayoutEffect(() => { 89 const color = t.palette.primary_500 90 91 const styleId = 'prosemirror-mention-color' 92 let style = document.getElementById(styleId) as HTMLStyleElement | null 93 94 if (!style) { 95 style = document.createElement('style') 96 style.id = styleId 97 document.head.appendChild(style) 98 } 99 100 style.innerHTML = ` 101 .ProseMirror .mention { 102 color: ${color} !important; 103 } 104 .ProseMirror a, 105 .ProseMirror .autolink { 106 color: ${color} !important; 107 } 108 ` 109 }, [t.palette.primary_500]) 110 111 useLayoutEffect(() => { 112 const faviconHref = createFaviconDataUrl(t.palette.primary_500) 113 114 upsertHeadLink({ 115 rel: 'icon', 116 href: faviconHref, 117 type: 'image/svg+xml', 118 sizes: 'any', 119 }) 120 upsertHeadLink({ 121 rel: 'shortcut icon', 122 href: faviconHref, 123 type: 'image/svg+xml', 124 }) 125 }, [t.palette.primary_500]) 126 127 useEffect(() => { 128 const unsubscribe = navigator.addListener('state', () => { 129 closeAllActiveElements() 130 }) 131 return unsubscribe 132 }, [navigator, closeAllActiveElements]) 133 134 const drawerLayout = useCallback( 135 ({children}: {children: React.ReactNode}) => ( 136 <DrawerLayout>{children}</DrawerLayout> 137 ), 138 [], 139 ) 140 return ( 141 <> 142 <ErrorBoundary> 143 <FlatNavigator layout={drawerLayout} /> 144 </ErrorBoundary> 145 <Composer winHeight={0} /> 146 <ModalsContainer /> 147 <MutedWordsDialog /> 148 <SigninDialog /> 149 <EmailDialog /> 150 <AgeAssuranceRedirectDialog /> 151 <LinkWarningDialog /> 152 <Lightbox /> 153 <NuxDialogs /> 154 <GlobalReportDialog /> 155 156 {welcomeModalControl.isOpen && ( 157 <WelcomeModal control={welcomeModalControl} /> 158 )} 159 160 {/* Until policy update has been completed by the user, don't render anything that is portaled */} 161 {policyUpdateState.completed && ( 162 <> 163 <PortalOutlet /> 164 </> 165 )} 166 167 <PolicyUpdateOverlayPortalOutlet /> 168 169 {/* workaround for a WebKit compositing bug. After a 170 dialog (which uses Portal + fixed elements + CSS animations) 171 closes, WebKit can skip painting subsequent view transitions. 172 A persistent zero-size fixed element keeps the compositing 173 tree from entering this broken state. */} 174 <div 175 aria-hidden 176 style={{ 177 position: 'fixed', 178 width: 0, 179 height: 0, 180 pointerEvents: 'none', 181 }} 182 /> 183 </> 184 ) 185} 186 187function DrawerLayout({children}: {children: React.ReactNode}) { 188 const t = useTheme() 189 const isDrawerOpen = useIsDrawerOpen() 190 const setDrawerOpen = useSetDrawerOpen() 191 const {gtTablet} = useBreakpoints() 192 const {_} = useLingui() 193 const showDrawer = !gtTablet && isDrawerOpen 194 const [showDrawerDelayedExit, setShowDrawerDelayedExit] = useState(showDrawer) 195 196 useLayoutEffect(() => { 197 if (showDrawer !== showDrawerDelayedExit) { 198 if (showDrawer) { 199 setShowDrawerDelayedExit(true) 200 } else { 201 const timeout = setTimeout(() => { 202 setShowDrawerDelayedExit(false) 203 }, 160) 204 return () => clearTimeout(timeout) 205 } 206 } 207 }, [showDrawer, showDrawerDelayedExit]) 208 209 return ( 210 <> 211 {children} 212 {showDrawerDelayedExit && ( 213 <> 214 <RemoveScrollBar /> 215 <TouchableWithoutFeedback 216 onPress={ev => { 217 // Only close if press happens outside of the drawer 218 if (ev.target === ev.currentTarget) { 219 setDrawerOpen(false) 220 } 221 }} 222 accessibilityLabel={_(msg`Close drawer menu`)} 223 accessibilityHint=""> 224 <View 225 style={[ 226 styles.drawerMask, 227 { 228 backgroundColor: showDrawer 229 ? select(t.name, { 230 light: 'rgba(0, 57, 117, 0.1)', 231 dark: 'rgba(1, 82, 168, 0.1)', 232 dim: 'rgba(10, 13, 16, 0.8)', 233 }) 234 : 'transparent', 235 }, 236 a.transition_color, 237 ]}> 238 <View 239 style={[ 240 styles.drawerContainer, 241 showDrawer ? a.slide_in_left : a.slide_out_left, 242 ]}> 243 <DrawerContent /> 244 </View> 245 </View> 246 </TouchableWithoutFeedback> 247 </> 248 )} 249 </> 250 ) 251} 252 253export function Shell() { 254 const t = useTheme() 255 const {currentAccount} = useSession() 256 return ( 257 <View style={[a.util_screen_outer, t.atoms.bg]}> 258 {currentAccount?.status === 'takendown' ? ( 259 <Takendown /> 260 ) : currentAccount?.status === 'deactivated' ? ( 261 <Deactivated /> 262 ) : ( 263 <> 264 <RoutesContainer> 265 <ShellInner /> 266 </RoutesContainer> 267 268 <RedirectOverlay /> 269 </> 270 )} 271 272 <PassiveAnalytics /> 273 </View> 274 ) 275} 276 277const styles = StyleSheet.create({ 278 drawerMask: { 279 ...a.fixed, 280 width: '100%', 281 height: '100%', 282 top: 0, 283 left: 0, 284 }, 285 drawerContainer: { 286 display: 'flex', 287 ...a.fixed, 288 top: 0, 289 left: 0, 290 height: '100%', 291 width: 330, 292 maxWidth: '80%', 293 }, 294})