diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index 7d1478909..52cf00973 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -15,7 +15,14 @@ import { } 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 React, { + MouseEventHandler, + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { clearCacheAndReload, clearLoginData, @@ -35,7 +42,7 @@ import { useSyncState } from '../../hooks/useSyncState'; import { stopPropagation } from '../../utils/keyboard'; import { SyncStatus } from './SyncStatus'; import { AuthMetadataProvider } from '../../hooks/useAuthMetadata'; -import { getFallbackSession } from '../../state/sessions'; +import { getFallbackSession, removeFallbackSession } from '../../state/sessions'; import { AutoDiscovery } from './AutoDiscovery'; function ClientRootLoading() { @@ -130,7 +137,10 @@ const useLogoutListener = (mx?: MatrixClient) => { const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => { mx?.stopClient(); await mx?.clearStores(); - window.localStorage.clear(); + // 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(); }; @@ -146,6 +156,11 @@ type ClientRootProps = { }; 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( @@ -180,7 +195,14 @@ export function ClientRoot({ children }: ClientRootProps) { 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); } }, []), ); @@ -188,9 +210,11 @@ export function ClientRoot({ children }: ClientRootProps) { return ( - {mx && } + {mx && !syncError && } {loading && } - {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && ( + {(loadState.status === AsyncStatus.Error || + startState.status === AsyncStatus.Error || + syncError) && ( {`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 && (