import { Box, Button, config, Dialog, Icon, IconButton, Icons, Menu, MenuItem, PopOut, RectCords, Spinner, Text, } from 'folds'; import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient } from 'matrix-js-sdk'; import FocusTrap from 'focus-trap-react'; import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useRef, useState, } from 'react'; import { clearCacheAndReload, clearLoginData, IDB_VERSION_CONFLICT, initClient, logoutClient, startClient, } from '../../../client/initMatrix'; import { SplashScreen } from '../../components/splash-screen'; 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 { stopPropagation } from '../../utils/keyboard'; import { SyncStatus } from './SyncStatus'; import { AuthMetadataProvider } from '../../hooks/useAuthMetadata'; import { getFallbackSession, removeFallbackSession } from '../../state/sessions'; import { useSessionSync } from '../../hooks/useSessionSync'; import { installCryptoDiagLog } from '../../utils/cryptoDiagLog'; import { AutoDiscovery } from './AutoDiscovery'; // Capture-only E2EE diagnostics ring buffer (KE-1→4 signatures) — installed at // module load so it sees crypto warnings from the very first sync. Idempotent; // report download lives in Settings → Developer Tools → Crypto Diagnostics. installCryptoDiagLog(); function ClientRootLoading() { return ( Heating up ); } function ClientRootOptions({ mx }: { mx?: MatrixClient }) { const [menuAnchor, setMenuAnchor] = useState(); const handleToggle: MouseEventHandler = (evt) => { const cords = evt.currentTarget.getBoundingClientRect(); setMenuAnchor((currentState) => { if (currentState) return undefined; return cords; }); }; return ( setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > {mx && ( clearCacheAndReload(mx)} size="300" radii="300"> Clear Cache and Reload )} { if (mx) { logoutClient(mx); return; } clearLoginData(); }} size="300" radii="300" variant="Critical" fill="None" > Logout } /> ); } const useLogoutListener = (mx?: MatrixClient) => { useEffect(() => { const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => { mx?.stopClient(); await mx?.clearStores(); // Remove only the session credential keys — NOT settings, drafts, and // other preferences (N98). The SDK's IndexedDB stores are cleared above; // window.localStorage.clear() is reserved for the explicit reset path. removeFallbackSession(); 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 [loading, setLoading] = useState(true); const [syncError, setSyncError] = useState(false); // Tracks whether the initial sync has ever reached PREPARED. After that, // transient sync errors are handled by 's reconnection banner, // so we must NOT pop the blocking error splash for them. const hasPreparedRef = useRef(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); // Cross-tab session sync: another tab logging out / in (access token changed // in localStorage) reloads this tab so it never runs with stale credentials. useSessionSync(); useEffect(() => { if (loadState.status === AsyncStatus.Idle) { loadMatrix(); } }, [loadState, loadMatrix]); useEffect(() => { if (mx && !mx.clientRunning) { startMatrix(mx); } }, [mx, startMatrix]); useSyncState( mx, useCallback((state) => { if (state === 'PREPARED') { hasPreparedRef.current = true; setSyncError(false); setLoading(false); } else if (state === 'ERROR' || state === 'STOPPED') { // Only surface the blocking error splash when the INITIAL sync fails // (offline at startup, homeserver unreachable, non-retryable /sync // error). After the first PREPARED, owns reconnection UX. if (!hasPreparedRef.current) setSyncError(true); } }, []), ); return ( {mx && !syncError && } {loading && } {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error || syncError) && ( {loadState.status === AsyncStatus.Error && ( <> {loadState.error.message === IDB_VERSION_CONFLICT ? ( <> Local data is from a newer app version and cannot be read. Clear local data to continue (you will need to log in again). ) : ( {`Failed to load. ${loadState.error.message}`} )} )} {startState.status === AsyncStatus.Error && ( {`Failed to start. ${startState.error.message}`} )} {syncError && loadState.status !== AsyncStatus.Error && startState.status !== AsyncStatus.Error && ( Failed to sync with your homeserver. Check your connection and try again. )} {('error' in loadState ? (loadState as any).error?.message : undefined) !== IDB_VERSION_CONFLICT && ( )} )} {loading || !mx ? ( ) : ( {(serverConfigs) => ( {children} )} )} ); }