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 301 lines 13 kB view raw
1import '#/logger/sentry/setup' // must be near top 2import '#/view/icons' 3import './style.css' 4 5import {Fragment, useEffect, useState} from 'react' 6import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller' 7import {SafeAreaProvider} from 'react-native-safe-area-context' 8import {useLingui} from '@lingui/react/macro' 9import * as Sentry from '@sentry/react-native' 10 11import {Provider as HotkeysProvider} from '#/lib/hotkeys' 12import {SafeAreaOverride} from '#/lib/pwa-safe-area' 13import {QueryProvider} from '#/lib/react-query' 14import {ThemeProvider} from '#/lib/ThemeContext' 15import {Provider as TranslateOnDeviceProvider} from '#/lib/translation' 16import I18nProvider from '#/locale/i18nProvider' 17import {logger} from '#/logger' 18import {Provider as A11yProvider} from '#/state/a11y' 19import { 20 prefetchAppConfig, 21 Provider as AppConfigProvider, 22} from '#/state/appConfig' 23import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 24import {Provider as DialogStateProvider} from '#/state/dialogs' 25import {Provider as EmailVerificationProvider} from '#/state/email-verification' 26import {listenSessionDropped} from '#/state/events' 27import {Provider as HomeBadgeProvider} from '#/state/home-badge' 28import {Provider as LightboxStateProvider} from '#/state/lightbox' 29import {MessagesProvider} from '#/state/messages' 30import {Provider as ModalStateProvider} from '#/state/modals' 31import {init as initPersistedState} from '#/state/persisted' 32import {Provider as PrefsStateProvider} from '#/state/preferences' 33import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 34import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' 35import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' 36import {Provider as ServiceConfigProvider} from '#/state/service-config' 37import { 38 Provider as SessionProvider, 39 type SessionAccount, 40 useSession, 41 useSessionApi, 42} from '#/state/session' 43import {getWebOAuthClient} from '#/state/session/oauth-web-client' 44import {readLastActiveAccount} from '#/state/session/util' 45import {Provider as ShellStateProvider} from '#/state/shell' 46import {Provider as ComposerProvider} from '#/state/shell/composer' 47import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' 48import {Provider as OnboardingProvider} from '#/state/shell/onboarding' 49import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' 50import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 51import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 52import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' 53import {Shell} from '#/view/shell/index' 54import {ThemeProvider as Alf} from '#/alf' 55import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 56import {Provider as ContextMenuProvider} from '#/components/ContextMenu' 57import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' 58import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' 59import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay' 60import {Provider as PortalProvider} from '#/components/Portal' 61import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' 62import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' 63import * as Toast from '#/components/Toast' 64import {ToastOutlet} from '#/components/Toast' 65import { 66 prefetchAgeAssuranceConfig, 67 Provider as AgeAssuranceV2Provider, 68} from '#/ageAssurance' 69import { 70 AnalyticsContext, 71 AnalyticsFeaturesContext, 72 features, 73 setupDeviceId, 74} from '#/analytics' 75import { 76 prefetchLiveEvents, 77 Provider as LiveEventsProvider, 78} from '#/features/liveEvents/context' 79import * as Geo from '#/geolocation' 80import {Splash} from '#/Splash' 81import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 82import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' 83 84// For local development: the OAuth loopback spec requires IP-based origins 85// (127.0.0.1), not "localhost". The auth server redirects to 127.0.0.1, but 86// IndexedDB is per-origin, so PKCE state stored on "localhost" is unreachable 87// from "127.0.0.1". Redirect immediately so both signIn() and the callback 88// use the same origin. 89if (typeof window !== 'undefined' && window.location.hostname === 'localhost') { 90 const url = new URL(window.location.href) 91 url.hostname = '127.0.0.1' 92 window.location.replace(url.href) 93} 94 95function hasOAuthCallbackParams(): boolean { 96 // OAuth callback params come in the hash fragment (response_mode=fragment) 97 // or query string. Check both for "state" + ("code" or "error"). 98 const hash = new URLSearchParams(window.location.hash.slice(1)) 99 const query = new URLSearchParams(window.location.search) 100 const params = hash.has('state') ? hash : query 101 return params.has('state') && (params.has('code') || params.has('error')) 102} 103 104/** 105 * Begin geolocation ASAP 106 */ 107void Geo.resolve() 108void prefetchAgeAssuranceConfig() 109void prefetchLiveEvents() 110void prefetchAppConfig() 111 112function InnerApp() { 113 const [isReady, setIsReady] = useState(false) 114 const {currentAccount} = useSession() 115 const {resumeSession, login} = useSessionApi() 116 const theme = useColorModeTheme() 117 const {t: l} = useLingui() 118 const hasCheckedReferrer = useStarterPackEntry() 119 120 // init 121 useEffect(() => { 122 // Safety valve: if onLaunch hangs (e.g. stale IndexedDB blocking an 123 // upgrade, or a never-settling promise), the app will still load after 124 // this timeout fires. 125 const safetyTimeout = setTimeout(() => { 126 logger.warn('session: onLaunch safety timeout fired, forcing ready state') 127 setIsReady(true) 128 }, 15_000) 129 130 async function onLaunch(account?: SessionAccount) { 131 try { 132 // Check for OAuth callback params first (loopback redirects to /) 133 if (hasOAuthCallbackParams()) { 134 const client = getWebOAuthClient() 135 const result = await client.init() 136 if (result?.session) { 137 await login( 138 { 139 service: '', 140 identifier: '', 141 password: '', 142 oauthSession: result.session, 143 }, 144 'LoginForm', 145 ) 146 // Clear hash fragment after processing 147 window.history.replaceState(null, '', window.location.pathname) 148 return 149 } 150 } 151 152 if (account) { 153 await resumeSession(account) 154 } else { 155 await features.init 156 } 157 } catch (e) { 158 logger.error('session: resumeSession failed', {message: e}) 159 } finally { 160 clearTimeout(safetyTimeout) 161 setIsReady(true) 162 } 163 } 164 const account = readLastActiveAccount() 165 void onLaunch(account) 166 }, [resumeSession, login]) 167 168 useEffect(() => { 169 return listenSessionDropped(() => { 170 Toast.show(l`Sorry! Your session expired. Please sign in again.`, { 171 type: 'info', 172 }) 173 }) 174 }, [l]) 175 176 return ( 177 <Alf theme={theme}> 178 <ThemeProvider theme={theme}> 179 <ContextMenuProvider> 180 <Splash isReady={isReady && hasCheckedReferrer}> 181 <VideoVolumeProvider> 182 <ActiveVideoProvider> 183 <Fragment 184 // Resets the entire tree below when it changes: 185 key={currentAccount?.did}> 186 <AnalyticsFeaturesContext> 187 <QueryProvider currentDid={currentAccount?.did}> 188 <PolicyUpdateOverlayProvider> 189 <LiveEventsProvider> 190 <AgeAssuranceV2Provider> 191 <ComposerProvider> 192 <MessagesProvider> 193 {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 194 <LabelDefsProvider> 195 <ModerationOptsProvider> 196 <LoggedOutViewProvider> 197 <SelectedFeedProvider> 198 <HiddenRepliesProvider> 199 <HomeBadgeProvider> 200 <UnreadNotifsProvider> 201 <BackgroundNotificationPreferencesProvider> 202 <MutedThreadsProvider> 203 <SafeAreaProvider> 204 <SafeAreaOverride> 205 <ProgressGuideProvider> 206 <ServiceConfigProvider> 207 <EmailVerificationProvider> 208 <HideBottomBarBorderProvider> 209 <IntentDialogProvider> 210 <TranslateOnDeviceProvider> 211 <HotkeysProvider> 212 <Shell /> 213 <ToastOutlet /> 214 </HotkeysProvider> 215 </TranslateOnDeviceProvider> 216 </IntentDialogProvider> 217 </HideBottomBarBorderProvider> 218 </EmailVerificationProvider> 219 </ServiceConfigProvider> 220 </ProgressGuideProvider> 221 </SafeAreaOverride> 222 </SafeAreaProvider> 223 </MutedThreadsProvider> 224 </BackgroundNotificationPreferencesProvider> 225 </UnreadNotifsProvider> 226 </HomeBadgeProvider> 227 </HiddenRepliesProvider> 228 </SelectedFeedProvider> 229 </LoggedOutViewProvider> 230 </ModerationOptsProvider> 231 </LabelDefsProvider> 232 </MessagesProvider> 233 </ComposerProvider> 234 </AgeAssuranceV2Provider> 235 </LiveEventsProvider> 236 </PolicyUpdateOverlayProvider> 237 </QueryProvider> 238 </AnalyticsFeaturesContext> 239 </Fragment> 240 </ActiveVideoProvider> 241 </VideoVolumeProvider> 242 </Splash> 243 </ContextMenuProvider> 244 </ThemeProvider> 245 </Alf> 246 ) 247} 248 249function App() { 250 const [isReady, setIsReady] = useState(false) 251 252 useEffect(() => { 253 void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then( 254 () => setIsReady(true), 255 ) 256 }, []) 257 258 if (!isReady) { 259 return null 260 } 261 262 /* 263 * NOTE: only nothing here can depend on other data or session state, since 264 * that is set up in the InnerApp component above. 265 */ 266 return ( 267 <Geo.Provider> 268 <AppConfigProvider> 269 <A11yProvider> 270 <KeyboardControllerProvider> 271 <OnboardingProvider> 272 <AnalyticsContext> 273 <SessionProvider> 274 <PrefsStateProvider> 275 <I18nProvider> 276 <ShellStateProvider> 277 <ModalStateProvider> 278 <DialogStateProvider> 279 <LightboxStateProvider> 280 <PortalProvider> 281 <StarterPackProvider> 282 <InnerApp /> 283 </StarterPackProvider> 284 </PortalProvider> 285 </LightboxStateProvider> 286 </DialogStateProvider> 287 </ModalStateProvider> 288 </ShellStateProvider> 289 </I18nProvider> 290 </PrefsStateProvider> 291 </SessionProvider> 292 </AnalyticsContext> 293 </OnboardingProvider> 294 </KeyboardControllerProvider> 295 </A11yProvider> 296 </AppConfigProvider> 297 </Geo.Provider> 298 ) 299} 300 301export default Sentry.wrap(App)