import { Box, Button, config, Dialog, Icon, IconButton, Icons, Menu, MenuItem, PopOut, RectCords, Text, } from 'folds'; import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient } from 'matrix-js-sdk'; import FocusTrap from 'focus-trap-react'; import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useState } from 'react'; import { clearCacheAndReload, clearLoginData, initClient, logoutClient, 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 { stopPropagation } from '../../utils/keyboard'; 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 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 () => { // 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 { 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]); const hasError = loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error; return ( {mx && !hasError && } {(hasError || !mx) && } {hasError ? ( {loadState.status === AsyncStatus.Error && ( {`Failed to load. ${loadState.error.message}`} )} {startState.status === AsyncStatus.Error && ( {`Failed to start. ${startState.error.message}`} )} ) : !mx ? ( // Brief gap (~300–700ms) while initClient runs IndexedDB open + // crypto WASM init. Render nothing — the dark body background // shows through instead of the mascot splash. SyncIndicator // covers the post-mx phase visually. null ) : ( {(serverConfigs) => ( {children} )} )} ); }