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