import {
Box,
Button,
config,
Dialog,
Icon,
IconButton,
Icons,
Menu,
MenuItem,
PopOut,
RectCords,
Spinner,
Text,
} from 'folds';
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import {
clearCacheAndReload,
clearLoginData,
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 { PluginLoader } from '../../features/settings/plugins';
import { SpecVersions } from './SpecVersions';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useSyncState } from '../../hooks/useSyncState';
import { stopPropagation } from '../../utils/keyboard';
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
import { getCurrentSession } from '../../state/sessions';
import { CallProviderWrapper } from '../../features/call/CallProviderWrapper';
import { isTauriMobile, startBackgroundSync, stopBackgroundSync } from '../../utils/tauri';
import { TitleBar } from '../../components/title-bar';
import { AccountSwitcher } from '../../components/account-switcher';
import { getLoginPath, withSearchParam } from '../pathUtils';
import { useViewTransitions } from '../../hooks/useViewTransitions';
function ClientRootLoading() {
return (
Heating up
);
}
const useLogoutListener = (mx?: MatrixClient) => {
useEffect(() => {
const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => {
mx?.stopClient();
await mx?.clearStores();
window.localStorage.clear();
window.location.reload();
};
mx?.on(HttpApiEvent.SessionLoggedOut, handleLogout);
return () => {
mx?.removeListener(HttpApiEvent.SessionLoggedOut, handleLogout);
};
}, [mx]);
};
/**
* Hook to handle visibility changes and restart sync on mobile
* When the app goes to background and comes back, sync may be stale/stopped
*/
const useVisibilitySync = (mx?: MatrixClient) => {
useEffect(() => {
if (!mx || !isTauriMobile()) return;
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
console.log('[useVisibilitySync] App became visible, checking sync state');
const syncState = mx.getSyncState();
console.log('[useVisibilitySync] Current sync state:', syncState);
// If sync is not running or errored, restart it
if (!mx.clientRunning || syncState === SyncState.Error || syncState === SyncState.Stopped || syncState === null) {
console.log('[useVisibilitySync] Restarting sync...');
startClient(mx).catch((err) => {
console.error('[useVisibilitySync] Failed to restart sync:', err);
});
} else {
// Force a sync even if client is running (to catch up quickly)
console.log('[useVisibilitySync] Triggering immediate sync');
mx.retryImmediately();
}
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
// Also handle resume event for mobile (if available)
window.addEventListener('resume', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('resume', handleVisibilityChange);
};
}, [mx]);
};
/**
* Hook to start/stop native Rust background sync on mobile
* This allows notifications to work even when the app is backgrounded
*/
const useBackgroundSync = (mx?: MatrixClient) => {
useEffect(() => {
if (!mx || !isTauriMobile()) return;
// Start background sync with credentials from the Matrix client
const startSync = async () => {
try {
const homeserverUrl = mx.getHomeserverUrl();
const userId = mx.getUserId();
const accessToken = mx.getAccessToken();
const deviceId = mx.getDeviceId();
if (!homeserverUrl || !userId || !accessToken || !deviceId) {
console.warn('[useBackgroundSync] Missing credentials, cannot start background sync');
return;
}
await startBackgroundSync({
homeserverUrl,
userId,
accessToken,
deviceId,
});
console.log('[useBackgroundSync] Background sync started');
} catch (err) {
console.error('[useBackgroundSync] Failed to start background sync:', err);
}
};
startSync();
// Stop background sync on cleanup
return () => {
stopBackgroundSync().catch((err) => {
console.error('[useBackgroundSync] Failed to stop background sync:', err);
});
};
}, [mx]);
};
type ClientRootProps = {
children: ReactNode;
};
const MAX_SYNC_RETRIES = 3;
export function ClientRoot({ children }: ClientRootProps) {
const [loading, setLoading] = useState(true);
const [syncError, setSyncError] = useState(null);
const syncRetryCount = useRef(0);
const mxRef = useRef(undefined);
const { baseUrl } = getCurrentSession() ?? {};
// Enable view transitions for smooth navigation
useViewTransitions();
const [loadState, loadMatrix] = useAsyncCallback(
useCallback(() => {
const session = getCurrentSession();
if (!session) {
throw new Error('No session Found!');
}
return initClient(session);
}, [])
);
const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined;
useEffect(() => {
mxRef.current = mx;
}, [mx]);
const [startState, startMatrix] = useAsyncCallback(
useCallback((m) => startClient(m), [])
);
useLogoutListener(mx);
useVisibilitySync(mx);
useBackgroundSync(mx);
useEffect(() => {
if (loadState.status === AsyncStatus.Idle) {
loadMatrix();
}
}, [loadState, loadMatrix]);
useEffect(() => {
if (mx && !mx.clientRunning) {
startMatrix(mx);
}
}, [mx, startMatrix]);
// Timeout if sync takes too long - nudge the client rather than immediately erroring
useEffect(() => {
if (!mx || !loading) return;
const timeout = setTimeout(() => {
console.warn('[ClientRoot] Sync taking too long, nudging client...');
mx.retryImmediately();
}, 30000);
return () => clearTimeout(timeout);
}, [mx, loading]);
useSyncState(
mx,
useCallback((state, prevState, data) => {
const client = mxRef.current;
if (state === 'PREPARED') {
setLoading(false);
setSyncError(null);
syncRetryCount.current = 0;
} else if (state === 'ERROR') {
if (syncRetryCount.current < MAX_SYNC_RETRIES) {
syncRetryCount.current += 1;
const attempt = syncRetryCount.current;
console.warn(`[ClientRoot] Sync error, auto-retrying (${attempt}/${MAX_SYNC_RETRIES})...`);
setTimeout(() => {
if (client) startClient(client).catch(() => {});
}, 2000 * attempt);
} else {
const error = data instanceof Error ? data : new Error('Sync failed');
console.error('[ClientRoot] Sync error after max retries:', error);
setSyncError(error);
}
}
}, [])
);
return (
{(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
)}
{syncError && (
)}
{loading || !mx ? (
) : (
{(serverConfigs) => (
{children}
)}
)}
);
}