mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 22:32:26 +03:00
Merge ffa6a9c6aa
into 399b1a373e
This commit is contained in:
commit
63a5e6e983
7 changed files with 194 additions and 87 deletions
|
@ -1,4 +1,3 @@
|
||||||
import { atom } from 'jotai';
|
|
||||||
import {
|
import {
|
||||||
atomWithLocalStorage,
|
atomWithLocalStorage,
|
||||||
getLocalStorageItem,
|
getLocalStorageItem,
|
||||||
|
@ -71,22 +70,19 @@ export const getSessionStoreName = (session: Session): SessionStoreName => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MATRIX_SESSIONS_KEY = 'matrixSessions';
|
export const MATRIX_SESSIONS_KEY = 'matrixSessions';
|
||||||
const baseSessionsAtom = atomWithLocalStorage<Sessions>(
|
export const sessionsAtom = atomWithLocalStorage<Sessions>(
|
||||||
MATRIX_SESSIONS_KEY,
|
MATRIX_SESSIONS_KEY,
|
||||||
(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();
|
const fallbackSession = getFallbackSession();
|
||||||
if (fallbackSession) {
|
if (fallbackSession) {
|
||||||
|
console.warn('Migrating from a fallback session...');
|
||||||
|
const newSessions: Sessions = [fallbackSession];
|
||||||
|
setLocalStorageItem(key, newSessions);
|
||||||
removeFallbackSession();
|
removeFallbackSession();
|
||||||
sessions.push(fallbackSession);
|
return newSessions;
|
||||||
setLocalStorageItem(key, sessions);
|
|
||||||
}
|
}
|
||||||
return sessions;
|
|
||||||
|
return getLocalStorageItem(key, []);
|
||||||
},
|
},
|
||||||
(key, value) => {
|
(key, value) => {
|
||||||
setLocalStorageItem(key, value);
|
setLocalStorageItem(key, value);
|
||||||
|
@ -102,28 +98,3 @@ export type SessionsAction =
|
||||||
type: 'DELETE';
|
type: 'DELETE';
|
||||||
session: Session;
|
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
src/app/state/utils/atomWithIndexedDB.ts
Normal file
48
src/app/state/utils/atomWithIndexedDB.ts
Normal 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;
|
||||||
|
};
|
|
@ -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(
|
export function updateLocalStore(
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
|
@ -6,8 +7,13 @@ export function updateLocalStore(
|
||||||
userId: string,
|
userId: string,
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
) {
|
) {
|
||||||
localStorage.setItem(cons.secretKey.ACCESS_TOKEN, accessToken);
|
const newSession: Session = {
|
||||||
localStorage.setItem(cons.secretKey.DEVICE_ID, deviceId);
|
accessToken,
|
||||||
localStorage.setItem(cons.secretKey.USER_ID, userId);
|
deviceId,
|
||||||
localStorage.setItem(cons.secretKey.BASE_URL, baseUrl);
|
userId,
|
||||||
|
baseUrl,
|
||||||
|
fallbackSdkStores: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
setLocalStorageItem('matrixSessions', [newSession]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,25 @@
|
||||||
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
|
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||||
|
import { Session, getSessionStoreName } from '../app/state/sessions';
|
||||||
type Session = {
|
|
||||||
baseUrl: string;
|
|
||||||
accessToken: string;
|
|
||||||
userId: string;
|
|
||||||
deviceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||||
|
const storeName = getSessionStoreName(session);
|
||||||
|
|
||||||
const indexedDBStore = new IndexedDBStore({
|
const indexedDBStore = new IndexedDBStore({
|
||||||
indexedDB: global.indexedDB,
|
indexedDB: global.indexedDB,
|
||||||
localStorage: global.localStorage,
|
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({
|
const mx = createClient({
|
||||||
baseUrl: session.baseUrl,
|
baseUrl: session.baseUrl,
|
||||||
accessToken: session.accessToken,
|
accessToken: session.accessToken,
|
||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
store: indexedDBStore,
|
store: indexedDBStore,
|
||||||
cryptoStore: legacyCryptoStore,
|
cryptoStore,
|
||||||
deviceId: session.deviceId,
|
deviceId: session.deviceId,
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
cryptoCallbacks: cryptoCallbacks as any,
|
cryptoCallbacks: cryptoCallbacks as any,
|
||||||
|
|
|
@ -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 = () => ({
|
const getActiveSession = (): Session | null => {
|
||||||
accessToken: localStorage.getItem(cons.secretKey.ACCESS_TOKEN),
|
const sessionsJSON = localStorage.getItem('matrixSessions');
|
||||||
deviceId: localStorage.getItem(cons.secretKey.DEVICE_ID),
|
if (!sessionsJSON) {
|
||||||
userId: localStorage.getItem(cons.secretKey.USER_ID),
|
return null;
|
||||||
baseUrl: localStorage.getItem(cons.secretKey.BASE_URL),
|
}
|
||||||
});
|
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 };
|
export { isAuthenticated, getSecret };
|
||||||
|
|
|
@ -5,38 +5,16 @@ import { enableMapSet } from 'immer';
|
||||||
import '@fontsource/inter/variable.css';
|
import '@fontsource/inter/variable.css';
|
||||||
import 'folds/dist/style.css';
|
import 'folds/dist/style.css';
|
||||||
import { configClass, varsClass } from 'folds';
|
import { configClass, varsClass } from 'folds';
|
||||||
|
import './index.scss';
|
||||||
|
import App from './app/pages/App';
|
||||||
|
import './app/i18n';
|
||||||
|
import { readyServiceWorker } from './serviceWorkerBridge';
|
||||||
|
|
||||||
enableMapSet();
|
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);
|
document.body.classList.add(configClass, varsClass);
|
||||||
|
|
||||||
// Register Service Worker
|
readyServiceWorker();
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const mountApp = () => {
|
const mountApp = () => {
|
||||||
const rootContainer = document.getElementById('root');
|
const rootContainer = document.getElementById('root');
|
||||||
|
|
85
src/serviceWorkerBridge.ts
Normal file
85
src/serviceWorkerBridge.ts
Normal 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?,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue