Skip to content
43 changes: 7 additions & 36 deletions src/app/state/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { atom } from 'jotai';
import {
atomWithLocalStorage,
getLocalStorageItem,
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
);
48 changes: 48 additions & 0 deletions src/app/state/utils/atomWithIndexedDB.ts
Original file line number Diff line number Diff line change
@@ -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;
};
16 changes: 11 additions & 5 deletions src/client/action/auth.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import cons from '../state/cons';
import { setLocalStorageItem } from '../../app/state/utils/atomWithLocalStorage';
import { Session } from '../../app/state/sessions';

export function updateLocalStore(
accessToken: string,
deviceId: string,
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]);
}
17 changes: 6 additions & 11 deletions src/client/initMatrix.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
40 changes: 32 additions & 8 deletions src/client/state/auth.ts
Original file line number Diff line number Diff line change
@@ -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 };
30 changes: 4 additions & 26 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,16 @@ import { enableMapSet } from 'immer';
import '@fontsource/inter/variable.css';
import 'folds/dist/style.css';
import { configClass, varsClass } from 'folds';

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';
import { readyServiceWorker } from './serviceWorkerBridge';

document.body.classList.add(configClass, varsClass);
enableMapSet();

// 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`;
document.body.classList.add(configClass, varsClass);

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');
Expand Down
85 changes: 85 additions & 0 deletions src/serviceWorkerBridge.ts
Original file line number Diff line number Diff line change
@@ -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?,
});
*/
}
});
}
};
Loading