import { Box, Button, config, Dialog, Text } from 'folds'; import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk'; import React, { ReactNode, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { clearLocalSessionAndReload, initClient, startClient, } from '../../../client/initMatrix'; import { ServerConfigsLoader } from '../../components/ServerConfigsLoader'; import { CapabilitiesProvider } from '../../hooks/useCapabilities'; import { MediaConfigProvider } from '../../hooks/useMediaConfig'; import { MatrixClientProvider } from '../../hooks/useMatrixClient'; import { SpecVersions } from './SpecVersions'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useSyncState } from '../../hooks/useSyncState'; import { SyncIndicator } from './SyncIndicator'; import { AuthMetadataProvider } from '../../hooks/useAuthMetadata'; import { getFallbackSession } from '../../state/sessions'; import { AutoDiscovery } from './AutoDiscovery'; import { AuthSplashScreen } from '../auth/AuthSplashScreen'; import { clearSessionBridge, writeSessionBridge } from '../../utils/sessionBridge'; function ClientRootLoading() { return ; } const useLogoutListener = (mx?: MatrixClient) => { useEffect(() => { const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => { // Wipe the native session bridge before the reload — otherwise the dead // access_token lingers in shared_prefs and CallDeclineReceiver spends // the next login cycle posting 401s until writeSessionBridge overwrites. await clearSessionBridge(); mx?.stopClient(); await mx?.clearStores(); window.localStorage.clear(); window.location.reload(); }; mx?.on(HttpApiEvent.SessionLoggedOut, handleLogout); return () => { mx?.removeListener(HttpApiEvent.SessionLoggedOut, handleLogout); }; }, [mx]); }; type ClientRootProps = { children: ReactNode; }; export function ClientRoot({ children }: ClientRootProps) { const { t } = useTranslation(); const [loading, setLoading] = useState(true); // Latches when the SDK reports SyncState.Error before the first PREPARED // arrives — at that point matrix-js-sdk has already burned through its 3-try // keep-alive backoff (~15-30s), so it's a definitive "I gave up" signal, not // a transient blip. Cleared on Syncing in case the network recovers and the // SDK pulls itself out before PREPARED. const [syncErrored, setSyncErrored] = useState(false); const { baseUrl, userId } = getFallbackSession() ?? {}; const [loadState, loadMatrix] = useAsyncCallback( useCallback(() => { const session = getFallbackSession(); if (!session) { throw new Error('No session Found!'); } return initClient(session); }, []) ); const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined; const [startState, startMatrix] = useAsyncCallback( useCallback((m) => startClient(m), []) ); useLogoutListener(mx); useEffect(() => { if (loadState.status === AsyncStatus.Idle) { loadMatrix(); } }, [loadState, loadMatrix]); useEffect(() => { if (mx && !mx.clientRunning) { startMatrix(mx); } }, [mx, startMatrix]); // Mirror {accessToken, baseUrl, userId} into native SharedPreferences so // CallDeclineReceiver can send m.call.decline without booting the WebView. // No-op on web. useEffect(() => { if (!mx) return; writeSessionBridge(mx); }, [mx]); // When the OS reports the network is back, prod the sync loop instead of // waiting for matrix-js-sdk's internal keep-alive jitter (5–10s backoff // per `sync.js`). Only acts when the SDK is genuinely paused on a failed // connection — Error or Reconnecting — so a healthy Syncing client just // ignores the event. `retryImmediately` is itself idempotent, so spurious // duplicate `online` events from flaky NICs are harmless. useEffect(() => { if (!mx) return undefined; const onOnline = () => { const state = mx.getSyncState(); if (state === SyncState.Error || state === SyncState.Reconnecting) { mx.retryImmediately(); } }; window.addEventListener('online', onOnline); return () => window.removeEventListener('online', onOnline); }, [mx]); useSyncState( mx, useCallback((state) => { if (state === SyncState.Prepared) { setLoading(false); setSyncErrored(false); } else if (state === SyncState.Error) { setSyncErrored(true); } else if (state === SyncState.Syncing) { setSyncErrored(false); } }, []) ); // Sync-error case: SDK is wired up correctly, just stuck — poke the next // /sync. Init/start failures need to re-run the failed step from scratch. let onRetry: () => void; if (loading && syncErrored && mx) { onRetry = () => mx.retryImmediately(); } else if (mx) { onRetry = () => startMatrix(mx); } else { onRetry = loadMatrix; } return ( {mx && } {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error || (loading && syncErrored)) && ( {loadState.status === AsyncStatus.Error && ( {t('Boot.client_load_failed', { message: loadState.error.message })} )} {startState.status === AsyncStatus.Error && ( {t('Boot.client_start_failed', { message: startState.error.message })} )} {loading && syncErrored && loadState.status !== AsyncStatus.Error && startState.status !== AsyncStatus.Error && ( {t('Boot.sync_failed')} )} {/* Init/start failures (corrupt IndexedDB, dead crypto store) and pre-PREPARED sync errors share the same recovery — wipe local session state and reload. Network calls are skipped: in every case here the network is the suspect or irrelevant, and the homeserver will time out the session naturally. */} )} {loading || !mx ? ( ) : ( {(serverConfigs) => ( {children} )} )} ); }