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';
|
} from 'folds';
|
||||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient } from 'matrix-js-sdk';
|
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient } from 'matrix-js-sdk';
|
||||||
import FocusTrap from 'focus-trap-react';
|
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 {
|
import {
|
||||||
clearCacheAndReload,
|
clearCacheAndReload,
|
||||||
clearLoginData,
|
clearLoginData,
|
||||||
@@ -35,7 +42,7 @@ import { useSyncState } from '../../hooks/useSyncState';
|
|||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { SyncStatus } from './SyncStatus';
|
import { SyncStatus } from './SyncStatus';
|
||||||
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
|
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
|
||||||
import { getFallbackSession } from '../../state/sessions';
|
import { getFallbackSession, removeFallbackSession } from '../../state/sessions';
|
||||||
import { AutoDiscovery } from './AutoDiscovery';
|
import { AutoDiscovery } from './AutoDiscovery';
|
||||||
|
|
||||||
function ClientRootLoading() {
|
function ClientRootLoading() {
|
||||||
@@ -130,7 +137,10 @@ const useLogoutListener = (mx?: MatrixClient) => {
|
|||||||
const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => {
|
const handleLogout: HttpApiEventHandlerMap[HttpApiEvent.SessionLoggedOut] = async () => {
|
||||||
mx?.stopClient();
|
mx?.stopClient();
|
||||||
await mx?.clearStores();
|
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();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +156,11 @@ type ClientRootProps = {
|
|||||||
};
|
};
|
||||||
export function ClientRoot({ children }: ClientRootProps) {
|
export function ClientRoot({ children }: ClientRootProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
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 { baseUrl, userId } = getFallbackSession() ?? {};
|
||||||
|
|
||||||
const [loadState, loadMatrix] = useAsyncCallback<MatrixClient, Error, []>(
|
const [loadState, loadMatrix] = useAsyncCallback<MatrixClient, Error, []>(
|
||||||
@@ -180,7 +195,14 @@ export function ClientRoot({ children }: ClientRootProps) {
|
|||||||
mx,
|
mx,
|
||||||
useCallback((state) => {
|
useCallback((state) => {
|
||||||
if (state === 'PREPARED') {
|
if (state === 'PREPARED') {
|
||||||
|
hasPreparedRef.current = true;
|
||||||
|
setSyncError(false);
|
||||||
setLoading(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 (
|
return (
|
||||||
<AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
|
<AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
|
||||||
<SpecVersions baseUrl={baseUrl!}>
|
<SpecVersions baseUrl={baseUrl!}>
|
||||||
{mx && <SyncStatus mx={mx} />}
|
{mx && !syncError && <SyncStatus mx={mx} />}
|
||||||
{loading && <ClientRootOptions mx={mx} />}
|
{loading && <ClientRootOptions mx={mx} />}
|
||||||
{(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
|
{(loadState.status === AsyncStatus.Error ||
|
||||||
|
startState.status === AsyncStatus.Error ||
|
||||||
|
syncError) && (
|
||||||
<SplashScreen>
|
<SplashScreen>
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
@@ -223,6 +247,13 @@ export function ClientRoot({ children }: ClientRootProps) {
|
|||||||
{startState.status === AsyncStatus.Error && (
|
{startState.status === AsyncStatus.Error && (
|
||||||
<Text>{`Failed to start. ${startState.error.message}`}</Text>
|
<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) !==
|
{('error' in loadState ? (loadState as any).error?.message : undefined) !==
|
||||||
IDB_VERSION_CONFLICT && (
|
IDB_VERSION_CONFLICT && (
|
||||||
<Button variant="Critical" onClick={mx ? () => startMatrix(mx) : loadMatrix}>
|
<Button variant="Critical" onClick={mx ? () => startMatrix(mx) : loadMatrix}>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from
|
|||||||
|
|
||||||
import { cryptoCallbacks } from './secretStorageKeys';
|
import { cryptoCallbacks } from './secretStorageKeys';
|
||||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||||
|
import { removeFallbackSession } from '../app/state/sessions';
|
||||||
import { pushSessionToSW } from '../sw-session';
|
import { pushSessionToSW } from '../sw-session';
|
||||||
|
|
||||||
type Session = {
|
type Session = {
|
||||||
@@ -75,7 +76,9 @@ export const logoutClient = async (mx: MatrixClient) => {
|
|||||||
// ignore if failed to logout
|
// ignore if failed to logout
|
||||||
}
|
}
|
||||||
await mx.clearStores();
|
await mx.clearStores();
|
||||||
window.localStorage.clear();
|
// Remove only the session credential keys, preserving user preferences and
|
||||||
|
// unsent drafts (N98). The factory-reset path is clearLoginData() below.
|
||||||
|
removeFallbackSession();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user