This commit is contained in:
Jaggar 2025-08-27 14:09:44 -04:00 committed by GitHub
commit 63a5e6e983
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 194 additions and 87 deletions

View file

@ -1,4 +1,3 @@
import { atom } from 'jotai';
import {
atomWithLocalStorage,
getLocalStorageItem,
@ -71,22 +70,19 @@ export const getSessionStoreName = (session: Session): SessionStoreName => {
};
export const MATRIX_SESSIONS_KEY = 'matrixSessions';
const baseSessionsAtom = atomWithLocalStorage<Sessions>(
export const sessionsAtom = atomWithLocalStorage<Sessions>(
MATRIX_SESSIONS_KEY,
(key) => {
const defaultSessions: Sessions = [];
const sessions = getLocalStorageItem(key, defaultSessions);
// Before multi account support session was stored
// as multiple item in local storage.
// So we need these migration code.
const fallbackSession = getFallbackSession();
if (fallbackSession) {
console.warn('Migrating from a fallback session...');
const newSessions: Sessions = [fallbackSession];
setLocalStorageItem(key, newSessions);
removeFallbackSession();
sessions.push(fallbackSession);
setLocalStorageItem(key, sessions);
return newSessions;
}
return sessions;
return getLocalStorageItem(key, []);
},
(key, value) => {
setLocalStorageItem(key, value);
@ -102,28 +98,3 @@ export type SessionsAction =
type: 'DELETE';
session: Session;
};
export const sessionsAtom = atom<Sessions, [SessionsAction], undefined>(
(get) => get(baseSessionsAtom),
(get, set, action) => {
if (action.type === 'PUT') {
const sessions = [...get(baseSessionsAtom)];
const sessionIndex = sessions.findIndex(
(session) => session.userId === action.session.userId
);
if (sessionIndex === -1) {
sessions.push(action.session);
} else {
sessions.splice(sessionIndex, 1, action.session);
}
set(baseSessionsAtom, sessions);
return;
}
if (action.type === 'DELETE') {
const sessions = get(baseSessionsAtom).filter(
(session) => session.userId !== action.session.userId
);
set(baseSessionsAtom, sessions);
}
}
);

View file

@ -0,0 +1,48 @@
import { atom, PrimitiveAtom } from 'jotai';
import type { SetStateAction } from 'jotai';
import { get as getFromDB, set as setInDB } from 'idb-keyval';
export const setIndexedDBItem = async <T>(key: string, value: T) => {
await setInDB(key, value);
};
export const atomWithIndexedDB = <T>(key: string, initialValue: T): PrimitiveAtom<T> => {
const channel = new BroadcastChannel(key);
const baseAtom = atom(initialValue);
let isInitialized = false;
baseAtom.onMount = (setAtom) => {
(async () => {
const storedValue = await getFromDB<T>(key);
if (storedValue !== undefined && !isInitialized) {
setAtom(storedValue);
}
isInitialized = true;
})();
const handleChange = (event: MessageEvent) => {
setAtom(event.data);
};
channel.addEventListener('message', handleChange);
return () => {
channel.removeEventListener('message', handleChange);
};
};
const derivedAtom = atom<T, [SetStateAction<T>], void>(
(get) => get(baseAtom),
(get, set, update: SetStateAction<T>) => {
const currentValue = get(baseAtom);
const newValue =
typeof update === 'function' ? (update as (prev: T) => T)(currentValue) : update;
isInitialized = true;
set(baseAtom, newValue);
setIndexedDBItem(key, newValue);
channel.postMessage(newValue);
}
);
return derivedAtom;
};

View file

@ -1,4 +1,5 @@
import cons from '../state/cons';
import { setLocalStorageItem } from '../../app/state/utils/atomWithLocalStorage';
import { Session } from '../../app/state/sessions';
export function updateLocalStore(
accessToken: string,
@ -6,8 +7,13 @@ export function updateLocalStore(
userId: string,
baseUrl: string
) {
localStorage.setItem(cons.secretKey.ACCESS_TOKEN, accessToken);
localStorage.setItem(cons.secretKey.DEVICE_ID, deviceId);
localStorage.setItem(cons.secretKey.USER_ID, userId);
localStorage.setItem(cons.secretKey.BASE_URL, baseUrl);
const newSession: Session = {
accessToken,
deviceId,
userId,
baseUrl,
fallbackSdkStores: false,
};
setLocalStorageItem('matrixSessions', [newSession]);
}

View file

@ -1,30 +1,25 @@
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
import { cryptoCallbacks } from './state/secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
type Session = {
baseUrl: string;
accessToken: string;
userId: string;
deviceId: string;
};
import { Session, getSessionStoreName } from '../app/state/sessions';
export const initClient = async (session: Session): Promise<MatrixClient> => {
const storeName = getSessionStoreName(session);
const indexedDBStore = new IndexedDBStore({
indexedDB: global.indexedDB,
localStorage: global.localStorage,
dbName: 'web-sync-store',
dbName: storeName.sync,
});
const legacyCryptoStore = new IndexedDBCryptoStore(global.indexedDB, 'crypto-store');
const cryptoStore = new IndexedDBCryptoStore(global.indexedDB, storeName.crypto); // 4. USE THE DYNAMIC NAME
const mx = createClient({
baseUrl: session.baseUrl,
accessToken: session.accessToken,
userId: session.userId,
store: indexedDBStore,
cryptoStore: legacyCryptoStore,
cryptoStore,
deviceId: session.deviceId,
timelineSupport: true,
cryptoCallbacks: cryptoCallbacks as any,

View file

@ -1,12 +1,36 @@
import cons from './cons';
import { Session, Sessions } from '../../app/state/sessions';
const isAuthenticated = () => localStorage.getItem(cons.secretKey.ACCESS_TOKEN) !== null;
/*
* Transition code for moving to the multi-account session storage solution
*/
const getSecret = () => ({
accessToken: localStorage.getItem(cons.secretKey.ACCESS_TOKEN),
deviceId: localStorage.getItem(cons.secretKey.DEVICE_ID),
userId: localStorage.getItem(cons.secretKey.USER_ID),
baseUrl: localStorage.getItem(cons.secretKey.BASE_URL),
});
const getActiveSession = (): Session | null => {
const sessionsJSON = localStorage.getItem('matrixSessions');
if (!sessionsJSON) {
return null;
}
try {
const sessions = JSON.parse(sessionsJSON) as Sessions;
return sessions[0] || null;
} catch (e) {
console.error('Failed to parse matrixSessions from localStorage', e);
return null;
}
};
const isAuthenticated = (): boolean => {
const session = getActiveSession();
return !!session?.accessToken;
};
const getSecret = () => {
const session = getActiveSession();
return {
accessToken: session?.accessToken,
deviceId: session?.deviceId,
userId: session?.userId,
baseUrl: session?.baseUrl,
};
};
export { isAuthenticated, getSecret };

View file

@ -5,38 +5,16 @@ import { enableMapSet } from 'immer';
import '@fontsource/inter/variable.css';
import 'folds/dist/style.css';
import { configClass, varsClass } from 'folds';
import './index.scss';
import App from './app/pages/App';
import './app/i18n';
import { readyServiceWorker } from './serviceWorkerBridge';
enableMapSet();
import './index.scss';
import { trimTrailingSlash } from './app/utils/common';
import App from './app/pages/App';
// import i18n (needs to be bundled ;))
import './app/i18n';
document.body.classList.add(configClass, varsClass);
// Register Service Worker
if ('serviceWorker' in navigator) {
const swUrl =
import.meta.env.MODE === 'production'
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
navigator.serviceWorker.register(swUrl);
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data?.type === 'token' && event.data?.responseKey) {
// Get the token for SW.
const token = localStorage.getItem('cinny_access_token') ?? undefined;
event.source!.postMessage({
responseKey: event.data.responseKey,
token,
});
}
});
}
readyServiceWorker();
const mountApp = () => {
const rootContainer = document.getElementById('root');

View file

@ -0,0 +1,85 @@
import { trimTrailingSlash } from './app/utils/common';
const SESSIONS_KEY = 'matrixSessions';
function getActiveSessionFromStorage() {
try {
const sessionsJSON = localStorage.getItem(SESSIONS_KEY);
if (!sessionsJSON) {
return null;
}
const sessions = JSON.parse(sessionsJSON);
return sessions[0] || null;
} catch (e) {
console.error('SW: Error reading or parsing sessions from localStorage', e);
return null;
}
}
export const readyServiceWorker = () => {
if ('serviceWorker' in navigator) {
const isProduction = import.meta.env.MODE === 'production';
const swUrl = isProduction
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
const swRegisterOptions: RegistrationOptions = {};
if (!isProduction) {
swRegisterOptions.type = 'module';
}
const showUpdateAvailablePrompt = (registration: ServiceWorkerRegistration) => {
const DONT_SHOW_PROMPT_KEY = 'cinny_dont_show_sw_update_prompt';
const userPreference = localStorage.getItem(DONT_SHOW_PROMPT_KEY);
if (userPreference === 'true') {
return;
}
if (window.confirm('A new version of the app is available. Refresh to update?')) {
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING_AND_CLAIM' });
} else {
window.location.reload();
}
}
};
navigator.serviceWorker.register(swUrl, swRegisterOptions).then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
showUpdateAvailablePrompt(registration);
}
}
};
}
};
});
navigator.serviceWorker.addEventListener('message', (event) => {
if (!event.data || !event.source) {
return;
}
if (event.data.type === 'token' && event.data.id) {
const token = getActiveSessionFromStorage().accessToken ?? undefined;
event.source.postMessage({
replyTo: event.data.id,
payload: token,
});
} else if (event.data.type === 'openRoom' && event.data.id) {
/* Example:
event.source.postMessage({
replyTo: event.data.id,
payload: success?,
});
*/
}
});
}
};