add client boot screen options for clear cache and logout

This commit is contained in:
Ajay Bura 2026-07-26 13:18:32 +05:30
commit 5d9c9d588d
6 changed files with 196 additions and 53 deletions

View file

@ -14,6 +14,7 @@ import { ClientContext } from '../../../client/context';
import { ClientContextProvider } from '../../hooks/useClient';
import { InitialSyncSplashScreen } from './InitialSyncSplashScreen';
import { AutoDiscovery } from './AutoDiscovery';
import { ClientRootOptions } from './ClientRootOptions';
type ClientRootProps = {
children: ReactNode;
@ -46,6 +47,7 @@ export function ClientRoot({ children }: ClientRootProps) {
return (
<AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
{initState.status !== AsyncStatus.Success && <ClientRootOptions userId={userId!} />}
<SpecVersions baseUrl={baseUrl!}>
{initState.status === AsyncStatus.Error && (
<SplashScreen>
@ -96,7 +98,7 @@ export function ClientRoot({ children }: ClientRootProps) {
)}
{initState.status === AsyncStatus.Success && (
<ClientContextProvider value={initState.data}>
<InitialSyncSplashScreen sync={initState.data.sync}>
<InitialSyncSplashScreen>
<SyncStatus />
<ServerConfigsLoader>
{(serverConfigs) => (

View file

@ -0,0 +1,106 @@
import {
Box,
config,
Icon,
IconButton,
Icons,
Menu,
MenuItem,
PopOut,
RectCords,
Text,
} from 'folds';
import React, { MouseEventHandler, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import { ClientContext } from '../../../client/context';
import { stopPropagation } from '../../utils/keyboard';
import { deleteClientStorage } from '../../../client/storage';
import { clearNavToActivePathStore } from '../../state/navToActivePath';
import { deleteClientCryptoStorage } from '../../../client/crypto';
export function ClientRootOptions({ userId, ct }: { userId: string; ct?: ClientContext }) {
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const handleToggle: MouseEventHandler<HTMLButtonElement> = (evt) => {
const cords = evt.currentTarget.getBoundingClientRect();
setMenuAnchor((currentState) => {
if (currentState) return undefined;
return cords;
});
};
const handleClearCache = () => {
clearNavToActivePathStore(userId);
if (ct) {
ct.clearCacheAndReload();
return;
}
deleteClientStorage(userId);
};
const handleLogout = async () => {
if (ct) {
ct.logout();
return;
}
localStorage.clear();
await deleteClientCryptoStorage(userId);
await deleteClientStorage(userId);
};
return (
<IconButton
style={{
position: 'absolute',
top: config.space.S100,
right: config.space.S100,
}}
variant="Background"
fill="None"
onClick={handleToggle}
>
<Icon size="200" src={Icons.VerticalDots} />
<PopOut
anchor={menuAnchor}
position="Bottom"
align="End"
offset={6}
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
returnFocusOnDeactivate: false,
onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem onClick={handleClearCache} size="300" radii="300">
<Text as="span" size="T300" truncate>
Clear Cache and Reload
</Text>
</MenuItem>
<MenuItem
onClick={handleLogout}
size="300"
radii="300"
variant="Critical"
fill="None"
>
<Text as="span" size="T300" truncate>
Logout
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
/>
</IconButton>
);
}

View file

@ -7,6 +7,8 @@ import { useObservableValue } from '../../hooks/useObservableValue';
import { createFakeProgress } from '../../../client/responseProgress';
import { bytesToSize } from '../../utils/common';
import { SyncContext, SyncProgress } from '../../../client/sync';
import { ClientRootOptions } from './ClientRootOptions';
import { useClient } from '../../hooks/useClient';
function InitialSyncProgressScreen({
sync,
@ -112,17 +114,17 @@ function InitialSyncProgressScreen({
);
}
export function InitialSyncSplashScreen({
sync,
children,
}: {
sync: SyncContext;
children: ReactNode;
}) {
const initialSync = useObservableValue(sync.controller.initialSync);
export function InitialSyncSplashScreen({ children }: { children: ReactNode }) {
const ct = useClient();
const initialSync = useObservableValue(ct.sync.controller.initialSync);
if (initialSync) {
return <InitialSyncProgressScreen sync={sync} progressObservable={initialSync.progress} />;
return (
<>
<ClientRootOptions userId={ct.session.userId} />
<InitialSyncProgressScreen sync={ct.sync} progressObservable={initialSync.progress} />;
</>
);
}
return children;

55
src/client/crypto.ts Normal file
View file

@ -0,0 +1,55 @@
import { deleteDB } from 'idb';
import { Machine, OutgoingRequestSender } from 'neurone/lib/crypto/machine';
import { MatrixRequest } from 'neurone/lib/matrix-request';
import { MatrixRequestRetrier, sleep } from 'neurone/lib/utils';
import { Session } from '../app/state/sessions';
const getCryptoStoreName = (userId: string): string => `crypto${userId}`;
// the matrix-crypto-sdk add postfix to storeName
const getCryptoDBName = (userId: string): string =>
`${getCryptoStoreName(userId)}::matrix-sdk-crypto`;
let closeMachineRef: (() => void) | undefined;
export const initializeCrypto = async (
session: Session,
request: MatrixRequest
): Promise<{
machine: Machine;
deleteCryptoStorage: () => Promise<void>;
closeMachine: () => void;
}> => {
const storeName = getCryptoStoreName(session.userId);
const { olmMachine, closeMachine: closeMachineInner } = await Machine.init(
session.userId,
session.deviceId,
storeName
);
const closeMachine = () => {
closeMachineInner();
closeMachineRef = undefined;
};
closeMachineRef = closeMachine;
const outgoingSenderAbortCont = new AbortController();
const outgoingRequestRetrier = new MatrixRequestRetrier(sleep);
const outgoingSender = new OutgoingRequestSender(request, outgoingSenderAbortCont.signal);
const machine = new Machine(olmMachine, outgoingSender, outgoingRequestRetrier);
const deleteCryptoStorage = async () => {
closeMachine();
await deleteDB(getCryptoDBName(session.userId));
};
return { machine, deleteCryptoStorage, closeMachine };
};
export const deleteClientCryptoStorage = async (userId: string): Promise<void> => {
closeMachineRef?.();
await deleteDB(getCryptoDBName(userId));
};

View file

@ -1,8 +1,5 @@
import { Machine, OutgoingRequestSender } from 'neurone/lib/crypto/machine';
import { MatrixRequestRetrier, sleep } from 'neurone/lib/utils';
import { MatrixRequest } from 'neurone/lib/matrix-request';
import { Disposer } from 'neurone/lib/utils/disposable';
import { deleteDB } from 'idb';
import { Session } from '../app/state/sessions';
import {
ClientContext,
@ -34,43 +31,12 @@ import { SyncResponseManager } from './SyncResponseManager';
import { LogoutManager } from './LogoutManager';
import { MachineManager } from './MachineManager';
import { DecryptionManager } from './decryption';
const initializeCrypto = async (
session: Session,
request: MatrixRequest
): Promise<{
machine: Machine;
deleteCryptoStorage: () => Promise<void>;
}> => {
const storeName = `crypto${session.userId}`;
const { olmMachine, closeMachine } = await Machine.init(
session.userId,
session.deviceId,
storeName
);
const outgoingSenderAbortCont = new AbortController();
const outgoingRequestRetrier = new MatrixRequestRetrier(sleep);
const outgoingSender = new OutgoingRequestSender(request, outgoingSenderAbortCont.signal);
const machine = new Machine(olmMachine, outgoingSender, outgoingRequestRetrier);
const deleteCryptoStorage = async () => {
closeMachine();
// the matrix-crypto-sdk add postfix to storeName
const realStoreName = `${storeName}::matrix-sdk-crypto`;
await deleteDB(realStoreName);
};
return { machine, deleteCryptoStorage };
};
import { initializeCrypto } from './crypto';
export const initializeClient = async (session: Session): Promise<ClientContext> => {
const disposer = new Disposer();
const { storage, deleteStorage } = await createStorage('client', session.userId);
const { storage, deleteStorage } = await createStorage(session.userId);
const requestAuthJSON: MatrixRequest = getAuthJSONRequest(session.baseUrl, session.accessToken);

View file

@ -78,16 +78,20 @@ export interface Storage {
presence: PresenceCollection;
}
const createDBName = (storageName: string, userId: string): string => `${storageName}${userId}`;
const getDBName = (userId: string): string => `client${userId}`;
// create a reference to opened database close callback.
// used in clearing cache if client failed to initilize.
let disconnectStorageRef: (() => void) | undefined;
export const createStorage = async (
storageName: string,
userId: string
): Promise<{
storage: Storage;
deleteStorage: () => Promise<void>;
disconnectStorage: () => void;
}> => {
const dbName = createDBName(storageName, userId);
const dbName = getDBName(userId);
// TODO: handle all openDB callbacks.
const idb = await openDB(dbName, DATABASE_VERSION, {
@ -118,6 +122,13 @@ export const createStorage = async (
},
});
const disconnectStorage = () => {
idb.close();
disconnectStorageRef = undefined;
};
disconnectStorageRef = disconnectStorage;
const driver = new IndexedDBStorageDriver(unwrap(idb));
const storage: Storage = {
@ -147,14 +158,15 @@ export const createStorage = async (
};
const deleteStorage = async () => {
idb.close();
disconnectStorage();
await deleteDB(dbName);
};
return { storage, deleteStorage };
return { storage, deleteStorage, disconnectStorage };
};
export const deleteClientStorage = async (storageName: string, userId: string) => {
const dbName = createDBName(storageName, userId);
export const deleteClientStorage = async (userId: string) => {
disconnectStorageRef?.();
const dbName = getDBName(userId);
await deleteDB(dbName);
};