Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
117
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})