import { Box, Button, config, Dialog, Icon, IconButton, Icons, Menu, MenuItem, PopOut, RectCords, Text, } from 'folds'; import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } 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 { useSyncState } from '../../hooks/useSyncState'; 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 ClientRootLoading() { return ; } 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 [loading, setLoading] = useState(true); 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 === 'PREPARED') { setLoading(false); } }, []) ); return ( {mx && } {loading && } {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && ( {loadState.status === AsyncStatus.Error && ( {`Failed to load. ${loadState.error.message}`} )} {startState.status === AsyncStatus.Error && ( {`Failed to start. ${startState.error.message}`} )} )} {loading || !mx ? ( ) : ( {(serverConfigs) => ( {children} )} )} ); }