diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index b50c9926..c9b720d1 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -5,12 +5,14 @@ import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import settings from '../../../client/state/settings'; import navigation from '../../../client/state/navigation'; +import { openReusableDialog } from '../../../client/action/navigation' import { toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents, toggleNotifications, toggleNotificationSounds, } from '../../../client/action/settings'; import logout from '../../../client/action/logout'; import { usePermission } from '../../hooks/usePermission'; +import colorMXID from '../../../util/colorMXID'; import Text from '../../atoms/text/Text'; import IconButton from '../../atoms/button/IconButton'; @@ -25,6 +27,7 @@ import SettingTile from '../../molecules/setting-tile/SettingTile'; import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys'; import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys'; import { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack'; +import PeopleSelector from '../../molecules/people-selector/PeopleSelector'; import ProfileEditor from '../profile-editor/ProfileEditor'; import CrossSigning from './CrossSigning'; @@ -38,6 +41,7 @@ import BellIC from '../../../../public/res/ic/outlined/bell.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg'; import PowerIC from '../../../../public/res/ic/outlined/power.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; +import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg'; import CinnySVG from '../../../../public/res/svg/cinny.svg'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; @@ -322,6 +326,50 @@ function Settings() { logout(); } }; + const handleSwitchUsers = async () => { + const loggedInUsers = JSON.parse(window.localStorage.getItem("loggedInUsers")); + + const renderUserSwitcher = () => ( +
+ { + loggedInUsers.map((userId) => { + let mx = initMatrix.matrixClient; + let user = mx.getUser(userId); + let avatarUrl = user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null; + return ( + { + if (userId != window.localStorage.getItem("currentUser")) { + window.localStorage.setItem("currentUser", userId); + window.location.reload(); + } + }} + name={user.displayName} + avatarSrc={avatarUrl} + color={colorMXID(userId)} + /> + ); + }) + } + +
+ ); + + await openReusableDialog( + Switch users, + renderUserSwitcher, + ) + } return ( Settings} contentOptions={( <> + diff --git a/src/app/templates/auth/Auth.jsx b/src/app/templates/auth/Auth.jsx index 7c211736..d8553133 100644 --- a/src/app/templates/auth/Auth.jsx +++ b/src/app/templates/auth/Auth.jsx @@ -9,6 +9,7 @@ import * as auth from '../../../client/action/auth'; import cons from '../../../client/state/cons'; import { Debounce, getUrlPrams } from '../../../util/common'; import { getBaseUrl } from '../../../util/matrixUtil'; +import colorMXID from '../../../util/colorMXID'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; @@ -25,6 +26,7 @@ import EyeIC from '../../../../public/res/ic/outlined/eye.svg'; import EyeBlindIC from '../../../../public/res/ic/outlined/eye-blind.svg'; import CinnySvg from '../../../../public/res/svg/cinny.svg'; import SSOButtons from '../../molecules/sso-buttons/SSOButtons'; +import PeopleSelector from '../../molecules/people-selector/PeopleSelector'; const LOCALPART_SIGNUP_REGEX = /^[a-z0-9_\-.=/]+$/; const BAD_LOCALPART_ERROR = 'Username can only contain characters a-z, 0-9, or \'=_-./\''; @@ -539,11 +541,11 @@ function Auth() { useEffect(async () => { if (!loginToken) return; - if (localStorage.getItem(cons.secretKey.BASE_URL) === undefined) { + if (window.userLocalStorage.getItem(cons.secretKey.BASE_URL) === undefined) { setLoginToken(null); return; } - const baseUrl = localStorage.getItem(cons.secretKey.BASE_URL); + const baseUrl = window.userLocalStorage.getItem(cons.secretKey.BASE_URL); try { await auth.loginWithToken(baseUrl, loginToken); @@ -567,6 +569,24 @@ function Auth() { Cinny + {(() => { + const loggedInUsers = JSON.parse(window.localStorage.getItem("loggedInUsers")); + return ( + loggedInUsers.map((userId) => { + return ( + { + window.localStorage.setItem("currentUser", userId); + window.location.reload(); + }} + name={userId} + color={colorMXID(userId)} + /> + ); + }) + ); + })()}
diff --git a/src/client/action/auth.js b/src/client/action/auth.js index f9be13bc..19caa0ae 100644 --- a/src/client/action/auth.js +++ b/src/client/action/auth.js @@ -1,11 +1,19 @@ import * as sdk from 'matrix-js-sdk'; import cons from '../state/cons'; +import NamespacedStorage from '../state/NamespacedStorage'; function updateLocalStore(accessToken, deviceId, userId, baseUrl) { - 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); + window.localStorage.setItem("currentUser", userId); + let loggedInUsers = new Set(JSON.parse(window.localStorage.getItem("loggedInUsers"))); + loggedInUsers.add(userId); + window.localStorage.setItem("loggedInUsers", JSON.stringify(Array.from(loggedInUsers))); + + window.userLocalStorage = new NamespacedStorage(userId, window.localStorage); + + window.userLocalStorage.setItem(cons.secretKey.ACCESS_TOKEN, accessToken); + window.userLocalStorage.setItem(cons.secretKey.DEVICE_ID, deviceId); + window.userLocalStorage.setItem(cons.secretKey.USER_ID, userId); + window.userLocalStorage.setItem(cons.secretKey.BASE_URL, baseUrl); } function createTemporaryClient(baseUrl) { @@ -14,7 +22,7 @@ function createTemporaryClient(baseUrl) { async function startSsoLogin(baseUrl, type, idpId) { const client = createTemporaryClient(baseUrl); - localStorage.setItem(cons.secretKey.BASE_URL, client.baseUrl); + window.userLocalStorage.setItem(cons.secretKey.BASE_URL, client.baseUrl); window.location.href = client.getSsoLoginUrl(window.location.href, type, idpId); } diff --git a/src/client/action/logout.js b/src/client/action/logout.js index c4047bb7..caf9554f 100644 --- a/src/client/action/logout.js +++ b/src/client/action/logout.js @@ -9,7 +9,14 @@ async function logout() { // ignore if failed to logout } mx.clearStores(); - window.localStorage.clear(); + + window.userLocalStorage.clear(); + let currentUser = window.localStorage.getItem("currentUser"); + let loggedInUsers = new Set(JSON.parse(window.localStorage.getItem("loggedInUsers"))); + loggedInUsers.delete(currentUser); + window.localStorage.setItem("loggedInUsers", JSON.stringify(loggedInUsers)); + window.localStorage.removeItem("currentUser"); + window.location.reload(); } diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js index e219a777..2cfbbcfd 100644 --- a/src/client/initMatrix.js +++ b/src/client/initMatrix.js @@ -9,6 +9,7 @@ import RoomsInput from './state/RoomsInput'; import Notifications from './state/Notifications'; import { cryptoCallbacks } from './state/secretStorageKeys'; import navigation from './state/navigation'; +import NamespacedStorage from './state/NamespacedStorage'; global.Olm = require('@matrix-org/olm'); @@ -22,16 +23,19 @@ class InitMatrix extends EventEmitter { } async init() { - await this.startClient(); + const user = window.localStorage.getItem("currentUser"); + window.userLocalStorage = new NamespacedStorage(user, window.localStorage); + + await this.startClient(user, window.userLocalStorage); this.setupSync(); this.listenEvents(); } - async startClient() { + async startClient(user, storage) { const indexedDBStore = new sdk.IndexedDBStore({ indexedDB: global.indexedDB, - localStorage: global.localStorage, - dbName: 'web-sync-store', + localStorage: storage, + dbName: `${user}.web-sync-store`, }); await indexedDBStore.startup(); @@ -40,7 +44,7 @@ class InitMatrix extends EventEmitter { accessToken: secret.accessToken, userId: secret.userId, store: indexedDBStore, - cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'), + cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, `${user}.crypto-store`), deviceId: secret.deviceId, timelineSupport: true, cryptoCallbacks, @@ -98,7 +102,7 @@ class InitMatrix extends EventEmitter { this.matrixClient.on('Session.logged_out', () => { this.matrixClient.stopClient(); this.matrixClient.clearStores(); - window.localStorage.clear(); + window.userLocalStorage.clear(); window.location.reload(); }); } diff --git a/src/client/state/NamespacedStorage.js b/src/client/state/NamespacedStorage.js new file mode 100644 index 00000000..1d2c220b --- /dev/null +++ b/src/client/state/NamespacedStorage.js @@ -0,0 +1,44 @@ +class NamespacedStorage { + #namespace; + #storage; + #cache; + + constructor(namespace, storage) { + this.#namespace = namespace; + this.#storage = storage; + + this.#cache = new Set(); + for (let i = 0; i < this.#storage.length; i++) { + let key = this.#storage.key(i); + if (key.startsWith(`${this.#namespace}.`)) { + this.#cache.add(key.replace(`${this.#namespace}.`, "")); + } + } + } + + key(n) { + return this.#cache[n]; + } + + getItem(key) { + return this.#storage.getItem(`${this.#namespace}.${key}`); + } + + setItem(key, value) { + this.#storage.setItem(`${this.#namespace}.${key}`, value); + this.#cache.add(key); + } + + removeItem(key) { + this.#storage.removeItem(`${this.#namespace}.${key}`); + this.#cache.delete(key); + } + + clear() { + for (let key of this.#cache) { + this.removeItem(key); + } + } +} + +export default NamespacedStorage; diff --git a/src/client/state/auth.js b/src/client/state/auth.js index fbc23f6f..9fc4142a 100644 --- a/src/client/state/auth.js +++ b/src/client/state/auth.js @@ -1,7 +1,10 @@ import cons from './cons'; +import NamespacedStorage from './NamespacedStorage'; function getSecret(key) { - return localStorage.getItem(key); + const user = global.localStorage.getItem("currentUser"); + const storage = new NamespacedStorage(user, global.localStorage); + return storage.getItem(key); } const isAuthenticated = () => getSecret(cons.secretKey.ACCESS_TOKEN) !== null; diff --git a/src/client/state/settings.js b/src/client/state/settings.js index 32f55fcc..0c442d26 100644 --- a/src/client/state/settings.js +++ b/src/client/state/settings.js @@ -4,7 +4,7 @@ import appDispatcher from '../dispatcher'; import cons from './cons'; function getSettings() { - const settings = localStorage.getItem('settings'); + const settings = window.localStorage.getItem('settings'); if (settings === null) return null; return JSON.parse(settings); } @@ -13,7 +13,7 @@ function setSettings(key, value) { let settings = getSettings(); if (settings === null) settings = {}; settings[key] = value; - localStorage.setItem('settings', JSON.stringify(settings)); + window.localStorage.setItem('settings', JSON.stringify(settings)); } class Settings extends EventEmitter {