215 lines
8.1 KiB
TypeScript
215 lines
8.1 KiB
TypeScript
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 <AuthSplashScreen />;
|
||
}
|
||
|
||
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<MatrixClient, Error, []>(
|
||
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<void, Error, [MatrixClient]>(
|
||
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 (
|
||
<AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
|
||
<SpecVersions baseUrl={baseUrl!}>
|
||
{mx && <SyncIndicator mx={mx} />}
|
||
{(loadState.status === AsyncStatus.Error ||
|
||
startState.status === AsyncStatus.Error ||
|
||
(loading && syncErrored)) && (
|
||
<AuthSplashScreen>
|
||
<Box
|
||
direction="Column"
|
||
grow="Yes"
|
||
alignItems="Center"
|
||
justifyContent="Center"
|
||
gap="400"
|
||
>
|
||
<Dialog>
|
||
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
||
{loadState.status === AsyncStatus.Error && (
|
||
<Text>
|
||
{t('Boot.client_load_failed', { message: loadState.error.message })}
|
||
</Text>
|
||
)}
|
||
{startState.status === AsyncStatus.Error && (
|
||
<Text>
|
||
{t('Boot.client_start_failed', { message: startState.error.message })}
|
||
</Text>
|
||
)}
|
||
{loading &&
|
||
syncErrored &&
|
||
loadState.status !== AsyncStatus.Error &&
|
||
startState.status !== AsyncStatus.Error && (
|
||
<Text>{t('Boot.sync_failed')}</Text>
|
||
)}
|
||
<Button variant="Critical" onClick={onRetry}>
|
||
<Text as="span" size="B400">
|
||
{t('Boot.retry')}
|
||
</Text>
|
||
</Button>
|
||
{/* 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. */}
|
||
<Button variant="Secondary" fill="Soft" onClick={clearLocalSessionAndReload}>
|
||
<Text as="span" size="B400">
|
||
{t('Boot.logout')}
|
||
</Text>
|
||
</Button>
|
||
</Box>
|
||
</Dialog>
|
||
</Box>
|
||
</AuthSplashScreen>
|
||
)}
|
||
{loading || !mx ? (
|
||
<ClientRootLoading />
|
||
) : (
|
||
<MatrixClientProvider value={mx}>
|
||
<ServerConfigsLoader>
|
||
{(serverConfigs) => (
|
||
<CapabilitiesProvider value={serverConfigs.capabilities ?? {}}>
|
||
<MediaConfigProvider value={serverConfigs.mediaConfig ?? {}}>
|
||
<AuthMetadataProvider value={serverConfigs.authMetadata}>
|
||
{children}
|
||
</AuthMetadataProvider>
|
||
</MediaConfigProvider>
|
||
</CapabilitiesProvider>
|
||
)}
|
||
</ServerConfigsLoader>
|
||
</MatrixClientProvider>
|
||
)}
|
||
</SpecVersions>
|
||
</AutoDiscovery>
|
||
);
|
||
}
|