mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 14:30:29 +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 {
 | 
			
		||||
  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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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(
 | 
			
		||||
  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]);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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