177 lines
6.2 KiB
TypeScript
177 lines
6.2 KiB
TypeScript
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
|
|
|
|
import { cryptoCallbacks } from './secretStorageKeys';
|
|
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
|
import { pushSessionToSW } from '../sw-session';
|
|
import { clearPusherIds, loadPusherIds, setPushEnabled, unregisterPusher } from '../app/utils/push';
|
|
import { isNativePlatform } from '../app/utils/capacitor';
|
|
import { clearSessionBridge } from '../app/utils/sessionBridge';
|
|
import { polling } from '../app/plugins/polling';
|
|
|
|
type Session = {
|
|
baseUrl: string;
|
|
accessToken: string;
|
|
userId: string;
|
|
deviceId: string;
|
|
};
|
|
|
|
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
|
const indexedDBStore = new IndexedDBStore({
|
|
indexedDB: global.indexedDB,
|
|
localStorage: global.localStorage,
|
|
dbName: 'web-sync-store',
|
|
});
|
|
|
|
const legacyCryptoStore = new IndexedDBCryptoStore(global.indexedDB, 'crypto-store');
|
|
|
|
const mx = createClient({
|
|
baseUrl: session.baseUrl,
|
|
accessToken: session.accessToken,
|
|
userId: session.userId,
|
|
store: indexedDBStore,
|
|
cryptoStore: legacyCryptoStore,
|
|
deviceId: session.deviceId,
|
|
timelineSupport: true,
|
|
// cryptoCallbacks ships from a .js module with a duck-typed shape; matrix-js-sdk's exported type is too narrow.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
cryptoCallbacks: cryptoCallbacks as any,
|
|
verificationMethods: ['m.sas.v1'],
|
|
});
|
|
|
|
await indexedDBStore.startup();
|
|
await mx.initRustCrypto();
|
|
|
|
mx.setMaxListeners(50);
|
|
|
|
return mx;
|
|
};
|
|
|
|
export const startClient = async (mx: MatrixClient) => {
|
|
// threadSupport partitions m.thread relations into Thread objects so
|
|
// /channels thread drawer can read room.getThread(rootId), receive
|
|
// ThreadEvent.New / Update from the room emitter, and let
|
|
// sendReadReceipt auto-route thread_id. SDK 38.2+ default is false:
|
|
// without this flag drawer reads silently NO-OP. Flag is global —
|
|
// also affects DM/Bots receipt shape (SDK now adds thread_id: 'main'
|
|
// to main-timeline receipts and per-thread unread shape changes
|
|
// arrive in `unread_thread_notifications`). M4 will consume the
|
|
// shape; M2 just enables it so the drawer surface works.
|
|
await mx.startClient({
|
|
lazyLoadMembers: true,
|
|
threadSupport: true,
|
|
});
|
|
};
|
|
|
|
export const clearCacheAndReload = async (mx: MatrixClient) => {
|
|
mx.stopClient();
|
|
clearNavToActivePathStore(mx.getSafeUserId());
|
|
await mx.store.deleteAllData();
|
|
window.location.reload();
|
|
};
|
|
|
|
export const logoutClient = async (mx: MatrixClient) => {
|
|
// 1. Deactivate pusher on the homeserver while we still have a valid token.
|
|
// Run before pushSessionToSW() so that if a push arrives mid-logout, the
|
|
// SW still has a working session to resolve event details with.
|
|
const pusherIds = loadPusherIds();
|
|
if (pusherIds) {
|
|
try {
|
|
await unregisterPusher(mx, pusherIds.pushkey, pusherIds.appId);
|
|
} catch {
|
|
// ignore — logout must proceed
|
|
}
|
|
}
|
|
|
|
pushSessionToSW();
|
|
|
|
// 2. Unsubscribe locally so the browser/FCM stops delivering to this device.
|
|
// Critical: if step 1 failed (bad network, server 5xx), this is the only
|
|
// thing that actually stops pushes from arriving at this installation.
|
|
try {
|
|
if (isNativePlatform()) {
|
|
const { PushNotifications } = await import('@capacitor/push-notifications');
|
|
await PushNotifications.unregister();
|
|
} else if ('serviceWorker' in navigator) {
|
|
const reg = await navigator.serviceWorker.ready;
|
|
const sub = await reg.pushManager.getSubscription();
|
|
if (sub) await sub.unsubscribe();
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
clearPusherIds();
|
|
setPushEnabled(false);
|
|
|
|
// Tear down the WorkManager polling fallback synchronously here — the
|
|
// React lifecycle cleanup that does the same thing runs async and we
|
|
// don't get a chance to await it before window.location.replace below.
|
|
// Without this explicit await, the Worker can fire one more time with
|
|
// the old access_token and surface notifications belonging to the
|
|
// logged-out account.
|
|
await polling.cancel();
|
|
await polling.clearSession();
|
|
|
|
// Wipe the native session bridge so a re-login with a different user
|
|
// can't resurrect the old access_token via CallDeclineReceiver.
|
|
await clearSessionBridge();
|
|
|
|
mx.stopClient();
|
|
try {
|
|
await mx.logout();
|
|
} catch {
|
|
// ignore if failed to logout
|
|
}
|
|
await mx.clearStores();
|
|
window.localStorage.clear();
|
|
// Navigate to root instead of reload(): on Android Capacitor the
|
|
// WebViewLocalServer fails to SPA-fallback on URLs with `%3A` in path
|
|
// segments (Matrix room/space ids), so reload of `/<spaceId>/<roomId>/`
|
|
// or `/channels/<spaceId>/...` returns ERR_HTTP_RESPONSE_CODE_FAILURE.
|
|
// Replacing to `/` always resolves to index.html; the Router index
|
|
// loader then redirects to the login page since the session is gone.
|
|
window.location.replace('/');
|
|
};
|
|
|
|
export const clearLoginData = async () => {
|
|
const dbs = await window.indexedDB.databases();
|
|
|
|
dbs.forEach((idbInfo) => {
|
|
const { name } = idbInfo;
|
|
if (name) {
|
|
window.indexedDB.deleteDatabase(name);
|
|
}
|
|
});
|
|
|
|
window.localStorage.clear();
|
|
window.location.reload();
|
|
};
|
|
|
|
// Boot-error logout: full LOCAL cleanup with no network calls — useful when the
|
|
// network is the reason we're stuck (sync error, init/start failure). Wipes
|
|
// the native session bridge so CallDeclineReceiver can't resurrect a dead
|
|
// token, clears push state so a re-login doesn't double-register, and nukes
|
|
// IndexedDB + localStorage. Server-side logout is skipped — the homeserver
|
|
// will time out the session naturally.
|
|
export const clearLocalSessionAndReload = async () => {
|
|
await clearSessionBridge();
|
|
// Same reasoning as the normal logoutClient: kill the WorkManager
|
|
// polling fallback before the reload so it can't fire one more time
|
|
// with the old access_token and surface notifications from the
|
|
// logged-out account.
|
|
await polling.cancel();
|
|
await polling.clearSession();
|
|
clearPusherIds();
|
|
setPushEnabled(false);
|
|
|
|
const dbs = await window.indexedDB.databases();
|
|
dbs.forEach((idbInfo) => {
|
|
const { name } = idbInfo;
|
|
if (name) {
|
|
window.indexedDB.deleteDatabase(name);
|
|
}
|
|
});
|
|
|
|
window.localStorage.clear();
|
|
window.location.reload();
|
|
};
|