fix(client): preserve prefs on logout; recover from initial-sync failure (N98/N99)
- N98: logoutClient and handleLogout now call removeFallbackSession() (removes only the 4 session credential keys) instead of window.localStorage.clear(), so settings, unsent drafts, PiP position, and status are preserved across a normal logout. localStorage.clear() stays reserved for clearLoginData() (the explicit factory-reset path). - N99: the useSyncState callback now handles ERROR/STOPPED. A sync failure before the first PREPARED (offline at startup, homeserver unreachable) shows a dedicated error splash with a Retry button (startMatrix) instead of an endless "Heating up" spinner alongside a contradictory "Connection Lost!" banner. Guarded by a hasPreparedRef so post-PREPARED transient errors still go through <SyncStatus>; PREPARED self-heals the splash on recovery, and the redundant banner is suppressed while the splash is shown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <SyncStatus>'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<MatrixClient, Error, []>(
|
||||
@@ -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, <SyncStatus> owns reconnection UX.
|
||||
if (!hasPreparedRef.current) setSyncError(true);
|
||||
}
|
||||
}, []),
|
||||
);
|
||||
@@ -188,9 +210,11 @@ export function ClientRoot({ children }: ClientRootProps) {
|
||||
return (
|
||||
<AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
|
||||
<SpecVersions baseUrl={baseUrl!}>
|
||||
{mx && <SyncStatus mx={mx} />}
|
||||
{mx && !syncError && <SyncStatus mx={mx} />}
|
||||
{loading && <ClientRootOptions mx={mx} />}
|
||||
{(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
|
||||
{(loadState.status === AsyncStatus.Error ||
|
||||
startState.status === AsyncStatus.Error ||
|
||||
syncError) && (
|
||||
<SplashScreen>
|
||||
<Box
|
||||
direction="Column"
|
||||
@@ -223,6 +247,13 @@ export function ClientRoot({ children }: ClientRootProps) {
|
||||
{startState.status === AsyncStatus.Error && (
|
||||
<Text>{`Failed to start. ${startState.error.message}`}</Text>
|
||||
)}
|
||||
{syncError &&
|
||||
loadState.status !== AsyncStatus.Error &&
|
||||
startState.status !== AsyncStatus.Error && (
|
||||
<Text>
|
||||
Failed to sync with your homeserver. Check your connection and try again.
|
||||
</Text>
|
||||
)}
|
||||
{('error' in loadState ? (loadState as any).error?.message : undefined) !==
|
||||
IDB_VERSION_CONFLICT && (
|
||||
<Button variant="Critical" onClick={mx ? () => startMatrix(mx) : loadMatrix}>
|
||||
|
||||
Reference in New Issue
Block a user