diff --git a/fluxer_app/index.html b/fluxer_app/index.html
index 38cb35690..8f301bb09 100644
--- a/fluxer_app/index.html
+++ b/fluxer_app/index.html
@@ -25,7 +25,7 @@
-
+
diff --git a/fluxer_app/rspack.config.mjs b/fluxer_app/rspack.config.mjs
index 47099ab6a..bfc7d1fd4 100644
--- a/fluxer_app/rspack.config.mjs
+++ b/fluxer_app/rspack.config.mjs
@@ -508,6 +508,13 @@ export default () => {
priority: 43,
reuseExistingChunk: true,
},
+ i18n: {
+ test: /[\\/]node_modules[\\/]@lingui[\\/]/,
+ name: 'i18n',
+ priority: 42,
+ reuseExistingChunk: true,
+ enforce: true,
+ },
reactAria: {
test: /[\\/]node_modules[\\/]react-aria-components[\\/]/,
name: 'react-aria',
diff --git a/fluxer_app/src/app/AppBootstrap.tsx b/fluxer_app/src/app/AppBootstrap.tsx
new file mode 100644
index 000000000..3f381a453
--- /dev/null
+++ b/fluxer_app/src/app/AppBootstrap.tsx
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+import 'urlpattern-polyfill';
+import {bootstrapSyntheticHistory} from '@app/app/HistoryBootstrap';
+import reactiveI18n, {initI18n} from '@app/app/I18n';
+import {Routes} from '@app/app/Routes';
+import {AppErrorBoundary} from '@app/features/app/components/AppErrorBoundary';
+import {ErrorFallback} from '@app/features/app/components/ErrorFallback';
+import type {DomainMigrationSide} from '@app/features/app/domain_migration/DomainMigrationCore';
+import {installSelfXssNotice} from '@app/features/devtools/utils/SelfXssNotice';
+import {AppI18nProvider} from '@app/features/i18n/components/AppI18nProvider';
+import {installLocaleSwitchWatchdog} from '@app/features/i18n/utils/LocaleSwitchWatchdog';
+import {installTranslationDomGuard} from '@app/features/i18n/utils/TranslationDomGuard';
+import {installScrollRestoration} from '@app/features/platform/components/router/ScrollRestoration';
+import {Logger} from '@app/features/platform/utils/AppLogger';
+import {
+ getFormattedClientInfo,
+ getFormattedClientInfoSync,
+ installFluxerConfigDebugApi,
+ preloadClientInfo,
+} from '@app/features/platform/utils/ClientInfo';
+import {loadLazyModule} from '@app/features/platform/utils/LazyModuleLoader';
+import {scheduleNonLatinScriptFaces} from '@app/features/theme/fonts/ScriptFontLoader';
+import {installVoiceSubscriptionDebugApi} from '@app/features/voice/diagnostics/VoiceSubscriptionDebugApi';
+import {PASSKEY_BRIDGE_PATH} from '@fluxer/constants/src/PasskeyConstants';
+import {i18n} from '@lingui/core';
+import type {ReactNode} from 'react';
+import ReactDOM from 'react-dom/client';
+
+const logger = new Logger('index');
+
+if (typeof window !== 'undefined' && window.history) {
+ bootstrapSyntheticHistory();
+ installScrollRestoration();
+}
+
+installFluxerConfigDebugApi();
+installVoiceSubscriptionDebugApi();
+
+function createRoot(): ReactDOM.Root {
+ const container = document.getElementById('root');
+ if (!container) {
+ throw new Error('Missing #root element');
+ }
+ return ReactDOM.createRoot(container);
+}
+
+function mountRoot(content: ReactNode, dataFlxScope: string): void {
+ installTranslationDomGuard();
+ createRoot().render(
+ (
+
+
+
+ )}
+ data-flx={`${dataFlxScope}.app-error-boundary`}
+ >
+ {content}
+ ,
+ );
+}
+
+async function logClientInfo(): Promise {
+ try {
+ const info = await getFormattedClientInfo();
+ logger.info(`[CLIENT INFO] ${info}`);
+ } catch (error) {
+ logger.warn('Failed to load full client info:', error);
+ logger.info(`[CLIENT INFO] ${getFormattedClientInfoSync()}`);
+ }
+}
+
+async function preloadMarkdownParser(): Promise {
+ try {
+ const {preloadMarkdownParserWasm} = await loadLazyModule(
+ () => import('@app/features/messaging/utils/markdown/parser/MarkdownParserWasm'),
+ );
+ await preloadMarkdownParserWasm();
+ } catch (error) {
+ logger.warn('Failed to preload markdown parser:', error);
+ }
+}
+
+async function bootstrapThemeStudio(): Promise {
+ const markdownParserReady = preloadMarkdownParser();
+ const [{ThemeStudioStandaloneApp}, {setupHttp}, {default: AccountManager}] = await Promise.all([
+ loadLazyModule(() => import('@app/features/theme_studio/ThemeStudioStandaloneApp')),
+ loadLazyModule(() => import('@app/app/SetupHttp')),
+ loadLazyModule(() => import('@app/features/auth/state/AccountManager')),
+ ]);
+ await AccountManager.bootstrap();
+ setupHttp();
+ await markdownParserReady;
+ mountRoot(
+
+
+ ,
+ 'index.render-theme-studio',
+ );
+}
+
+export async function runPasskeyBridge(side: DomainMigrationSide): Promise {
+ const hash = window.location.hash;
+ const opensInOwnTab = window.history.length === 1;
+ window.history.replaceState(null, '', PASSKEY_BRIDGE_PATH);
+ const [{PasskeyBridgePage}] = await Promise.all([
+ loadLazyModule(() => import('@app/features/auth/passkey_migration/PasskeyBridgePage')),
+ initI18n(),
+ ]);
+ mountRoot(
+
+
+ ,
+ 'index.passkey-bridge',
+ );
+}
+
+async function probeSignedOutDomainMigration(): Promise {
+ try {
+ const {probeSignedOutDeviceEnrollment} = await loadLazyModule(
+ () => import('@app/features/app/domain_migration/DomainMigrationTrigger'),
+ );
+ probeSignedOutDeviceEnrollment();
+ } catch (error) {
+ logger.warn('Failed to start the signed-out domain migration probe:', error);
+ }
+}
+
+async function bootstrapApp(): Promise {
+ const markdownParserReady = preloadMarkdownParser();
+ const [
+ {App},
+ {setupHttp},
+ {default: CaptchaInterceptor},
+ {initializeEmojiParser},
+ {registerServiceWorker},
+ {default: AccountManager},
+ {default: ChannelDisplayName},
+ _channelFrecency,
+ _geoIp,
+ {default: Keybind},
+ {default: NewDeviceMonitoring},
+ {default: Notification},
+ {default: QuickSwitcher},
+ _runtimeConfig,
+ {default: StatusPage},
+ {installMigratedDeviceRemap},
+ ] = await Promise.all([
+ loadLazyModule(() => import('@app/app/App')),
+ loadLazyModule(() => import('@app/app/SetupHttp')),
+ loadLazyModule(() => import('@app/features/auth/components/CaptchaInterceptor')),
+ loadLazyModule(() => import('@app/features/messaging/utils/markdown/EmojiProviderSetup')),
+ loadLazyModule(() => import('@app/features/platform/service_worker/Register')),
+ loadLazyModule(() => import('@app/features/auth/state/AccountManager')),
+ loadLazyModule(() => import('@app/features/channel/state/ChannelDisplayName')),
+ loadLazyModule(() => import('@app/features/channel/state/ChannelFrecency')),
+ loadLazyModule(() => import('@app/features/app/state/GeoIP')),
+ loadLazyModule(() => import('@app/features/input/state/InputKeybind')),
+ loadLazyModule(() => import('@app/features/auth/state/NewDeviceMonitoring')),
+ loadLazyModule(() => import('@app/features/ui/state/Notification')),
+ loadLazyModule(() => import('@app/features/search/state/QuickSwitcher')),
+ loadLazyModule(() => import('@app/features/app/state/RuntimeConfig')),
+ loadLazyModule(() => import('@app/features/user/state/StatusPage')),
+ loadLazyModule(() => import('@app/features/app/domain_migration/DomainMigrationDeviceRemap')),
+ ]);
+ void preloadClientInfo();
+ QuickSwitcher.setI18n(reactiveI18n);
+ ChannelDisplayName.setI18n(reactiveI18n);
+ Keybind.setI18n(reactiveI18n);
+ NewDeviceMonitoring.setI18n(reactiveI18n);
+ Notification.setI18n(reactiveI18n);
+ CaptchaInterceptor.setI18n(reactiveI18n);
+ void StatusPage.checkIncidents();
+ StatusPage.startPolling();
+ await Promise.all([AccountManager.bootstrap(), installMigratedDeviceRemap()]);
+ if (AccountManager.currentUserId === null) {
+ void probeSignedOutDomainMigration();
+ }
+ setupHttp();
+ initializeEmojiParser();
+ await markdownParserReady;
+ mountRoot(, 'index.bootstrap');
+ QuickSwitcher.preloadModal();
+ registerServiceWorker();
+}
+
+export async function runApp(): Promise {
+ scheduleNonLatinScriptFaces();
+ await initI18n();
+ installLocaleSwitchWatchdog();
+ installSelfXssNotice();
+ void logClientInfo();
+ if (window.location.pathname === Routes.THEME_STUDIO) {
+ await bootstrapThemeStudio();
+ } else {
+ await bootstrapApp();
+ }
+}
diff --git a/fluxer_app/src/app/BootstrapErrorMount.tsx b/fluxer_app/src/app/BootstrapErrorMount.tsx
new file mode 100644
index 000000000..7299d4e49
--- /dev/null
+++ b/fluxer_app/src/app/BootstrapErrorMount.tsx
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+import {initI18n} from '@app/app/I18n';
+import {BootstrapErrorScreen} from '@app/features/app/components/BootstrapErrorScreen';
+import {AppI18nProvider} from '@app/features/i18n/components/AppI18nProvider';
+import {Logger} from '@app/features/platform/utils/AppLogger';
+import {i18n} from '@lingui/core';
+import ReactDOM from 'react-dom/client';
+
+const logger = new Logger('index');
+
+export async function reportBootstrapError(error: unknown): Promise {
+ const normalized = error instanceof Error ? error : new Error(String(error));
+ logger.error('Failed to bootstrap app:', normalized);
+ const container = document.getElementById('root');
+ if (!container) {
+ throw new Error('Missing #root element');
+ }
+ await initI18n();
+ ReactDOM.createRoot(container).render(
+
+
+ ,
+ );
+}
diff --git a/fluxer_app/src/features/app/components/BootstrapErrorScreen.tsx b/fluxer_app/src/features/app/components/BootstrapErrorScreen.tsx
index e055df851..ebda61b56 100644
--- a/fluxer_app/src/features/app/components/BootstrapErrorScreen.tsx
+++ b/fluxer_app/src/features/app/components/BootstrapErrorScreen.tsx
@@ -7,9 +7,12 @@ import {
PRODUCT_NAME,
} from '@app/features/app/config/I18nDisplayConstants';
import {TRY_AGAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
-import AppStorage, {PRESERVED_RESET_STORAGE_KEYS} from '@app/features/platform/state/PersistentStorage';
+import AppStorage, {
+ PRESERVED_RESET_STORAGE_KEY_PREFIXES,
+ PRESERVED_RESET_STORAGE_KEYS,
+} from '@app/features/platform/state/PersistentStorage';
import {Button} from '@app/features/ui/button/Button';
-import {FluxerIcon} from '@app/features/ui/components/icons/FluxerIcon';
+import {APPLICATION_ICON_DESCRIPTOR, FluxerIconMark} from '@app/features/ui/components/icons/FluxerIconMark';
import {ExternalUrls} from '@fluxer/constants/src/ExternalUrls';
import {Trans, useLingui} from '@lingui/react/macro';
import type React from 'react';
@@ -25,12 +28,16 @@ export const BootstrapErrorScreen: React.FC = ({error
window.location.reload();
}, []);
const handleReset = useCallback(() => {
- AppStorage.clearExcept(PRESERVED_RESET_STORAGE_KEYS);
+ AppStorage.clearExcept(PRESERVED_RESET_STORAGE_KEYS, PRESERVED_RESET_STORAGE_KEY_PREFIXES);
window.location.reload();
}, []);
return (
-
+
Failed to start
diff --git a/fluxer_app/src/features/app/components/ErrorFallback.tsx b/fluxer_app/src/features/app/components/ErrorFallback.tsx
index f56fbbc5c..0273ec1e7 100644
--- a/fluxer_app/src/features/app/components/ErrorFallback.tsx
+++ b/fluxer_app/src/features/app/components/ErrorFallback.tsx
@@ -3,7 +3,10 @@
import errorFallbackStyles from '@app/features/app/components/ErrorFallback.module.css';
import {NativeTitlebar} from '@app/features/app/components/layout/NativeTitlebar';
import {useNativePlatform} from '@app/features/app/hooks/useNativePlatform';
-import AppStorage, {PRESERVED_RESET_STORAGE_KEYS} from '@app/features/platform/state/PersistentStorage';
+import AppStorage, {
+ PRESERVED_RESET_STORAGE_KEY_PREFIXES,
+ PRESERVED_RESET_STORAGE_KEYS,
+} from '@app/features/platform/state/PersistentStorage';
import {ensureLatestAssets} from '@app/features/platform/types/Versioning';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {Button} from '@app/features/ui/button/Button';
@@ -154,7 +157,7 @@ export const ErrorFallback: React.FC = ({error}) => {
)}