Bluesky app fork with some witchin' additions 馃挮
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 {SettingsSyncGate} from '#/features/settingsSync'
76import {
77 prefetchLiveEvents,
78 Provider as LiveEventsProvider,
79} from '#/features/liveEvents/context'
80import * as Geo from '#/geolocation'
81import {Splash} from '#/Splash'
82import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
83import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
84
85// For local development: the OAuth loopback spec requires IP-based origins
86// (127.0.0.1), not "localhost". The auth server redirects to 127.0.0.1, but
87// IndexedDB is per-origin, so PKCE state stored on "localhost" is unreachable
88// from "127.0.0.1". Redirect immediately so both signIn() and the callback
89// use the same origin.
90if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
91 const url = new URL(window.location.href)
92 url.hostname = '127.0.0.1'
93 window.location.replace(url.href)
94}
95
96function hasOAuthCallbackParams(): boolean {
97 // OAuth callback params come in the hash fragment (response_mode=fragment)
98 // or query string. Check both for "state" + ("code" or "error").
99 const hash = new URLSearchParams(window.location.hash.slice(1))
100 const query = new URLSearchParams(window.location.search)
101 const params = hash.has('state') ? hash : query
102 return params.has('state') && (params.has('code') || params.has('error'))
103}
104
105/**
106 * Begin geolocation ASAP
107 */
108void Geo.resolve()
109void prefetchAgeAssuranceConfig()
110void prefetchLiveEvents()
111void prefetchAppConfig()
112
113function InnerApp() {
114 const [isReady, setIsReady] = useState(false)
115 const {currentAccount} = useSession()
116 const {resumeSession, login} = useSessionApi()
117 const theme = useColorModeTheme()
118 const {t: l} = useLingui()
119 const hasCheckedReferrer = useStarterPackEntry()
120
121 // init
122 useEffect(() => {
123 // Safety valve: if onLaunch hangs (e.g. stale IndexedDB blocking an
124 // upgrade, or a never-settling promise), the app will still load after
125 // this timeout fires.
126 const safetyTimeout = setTimeout(() => {
127 logger.warn('session: onLaunch safety timeout fired, forcing ready state')
128 setIsReady(true)
129 }, 15_000)
130
131 async function onLaunch(account?: SessionAccount) {
132 try {
133 // Check for OAuth callback params first (loopback redirects to /)
134 if (hasOAuthCallbackParams()) {
135 const client = getWebOAuthClient()
136 const result = await client.init()
137 if (result?.session) {
138 await login(
139 {
140 service: '',
141 identifier: '',
142 password: '',
143 oauthSession: result.session,
144 },
145 'LoginForm',
146 )
147 // Clear hash fragment after processing
148 window.history.replaceState(null, '', window.location.pathname)
149 return
150 }
151 }
152
153 if (account) {
154 await resumeSession(account)
155 } else {
156 await features.init
157 }
158 } catch (e) {
159 logger.error('session: resumeSession failed', {message: e})
160 } finally {
161 clearTimeout(safetyTimeout)
162 setIsReady(true)
163 }
164 }
165 const account = readLastActiveAccount()
166 void onLaunch(account)
167 }, [resumeSession, login])
168
169 useEffect(() => {
170 return listenSessionDropped(() => {
171 Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
172 type: 'info',
173 })
174 })
175 }, [l])
176
177 return (
178 <Alf theme={theme}>
179 <ThemeProvider theme={theme}>
180 <ContextMenuProvider>
181 <Splash isReady={isReady && hasCheckedReferrer}>
182 <VideoVolumeProvider>
183 <ActiveVideoProvider>
184 <Fragment
185 // Resets the entire tree below when it changes:
186 key={currentAccount?.did}>
187 <AnalyticsFeaturesContext>
188 <QueryProvider currentDid={currentAccount?.did}>
189 <SettingsSyncGate>
190 <PolicyUpdateOverlayProvider>
191 <LiveEventsProvider>
192 <AgeAssuranceV2Provider>
193 <ComposerProvider>
194 <MessagesProvider>
195 {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
196 <LabelDefsProvider>
197 <ModerationOptsProvider>
198 <LoggedOutViewProvider>
199 <SelectedFeedProvider>
200 <HiddenRepliesProvider>
201 <HomeBadgeProvider>
202 <UnreadNotifsProvider>
203 <BackgroundNotificationPreferencesProvider>
204 <MutedThreadsProvider>
205 <SafeAreaProvider>
206 <SafeAreaOverride>
207 <ProgressGuideProvider>
208 <ServiceConfigProvider>
209 <EmailVerificationProvider>
210 <HideBottomBarBorderProvider>
211 <IntentDialogProvider>
212 <TranslateOnDeviceProvider>
213 <HotkeysProvider>
214 <Shell />
215 <ToastOutlet />
216 </HotkeysProvider>
217 </TranslateOnDeviceProvider>
218 </IntentDialogProvider>
219 </HideBottomBarBorderProvider>
220 </EmailVerificationProvider>
221 </ServiceConfigProvider>
222 </ProgressGuideProvider>
223 </SafeAreaOverride>
224 </SafeAreaProvider>
225 </MutedThreadsProvider>
226 </BackgroundNotificationPreferencesProvider>
227 </UnreadNotifsProvider>
228 </HomeBadgeProvider>
229 </HiddenRepliesProvider>
230 </SelectedFeedProvider>
231 </LoggedOutViewProvider>
232 </ModerationOptsProvider>
233 </LabelDefsProvider>
234 </MessagesProvider>
235 </ComposerProvider>
236 </AgeAssuranceV2Provider>
237 </LiveEventsProvider>
238 </PolicyUpdateOverlayProvider>
239 </SettingsSyncGate>
240 </QueryProvider>
241 </AnalyticsFeaturesContext>
242 </Fragment>
243 </ActiveVideoProvider>
244 </VideoVolumeProvider>
245 </Splash>
246 </ContextMenuProvider>
247 </ThemeProvider>
248 </Alf>
249 )
250}
251
252function App() {
253 const [isReady, setIsReady] = useState(false)
254
255 useEffect(() => {
256 void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
257 () => setIsReady(true),
258 )
259 }, [])
260
261 if (!isReady) {
262 return null
263 }
264
265 /*
266 * NOTE: only nothing here can depend on other data or session state, since
267 * that is set up in the InnerApp component above.
268 */
269 return (
270 <Geo.Provider>
271 <AppConfigProvider>
272 <A11yProvider>
273 <KeyboardControllerProvider>
274 <OnboardingProvider>
275 <AnalyticsContext>
276 <SessionProvider>
277 <PrefsStateProvider>
278 <I18nProvider>
279 <ShellStateProvider>
280 <ModalStateProvider>
281 <DialogStateProvider>
282 <LightboxStateProvider>
283 <PortalProvider>
284 <StarterPackProvider>
285 <InnerApp />
286 </StarterPackProvider>
287 </PortalProvider>
288 </LightboxStateProvider>
289 </DialogStateProvider>
290 </ModalStateProvider>
291 </ShellStateProvider>
292 </I18nProvider>
293 </PrefsStateProvider>
294 </SessionProvider>
295 </AnalyticsContext>
296 </OnboardingProvider>
297 </KeyboardControllerProvider>
298 </A11yProvider>
299 </AppConfigProvider>
300 </Geo.Provider>
301 )
302}
303
304export default Sentry.wrap(App)