diff --git a/CHANGELOG.md b/CHANGELOG.md index 159de1dea4..88dc308f0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Improvements - 依存関係の更新 +- localStorageのaccountsはindexedDBで保持するように ### Bugfixes - チャンネルを作成しているとアカウントを削除できないのを修正 diff --git a/src/client/account.ts b/src/client/account.ts index 2b860b3ddf..cf52c4d828 100644 --- a/src/client/account.ts +++ b/src/client/account.ts @@ -1,7 +1,8 @@ +import { get, set } from '@client/scripts/idb-proxy'; import { reactive } from 'vue'; import { apiUrl } from '@client/config'; import { waiting } from '@client/os'; -import { unisonReload } from '@client/scripts/unison-reload'; +import { unisonReload, reloadChannel } from '@client/scripts/unison-reload'; // TODO: 他のタブと永続化されたstateを同期 @@ -17,22 +18,43 @@ const data = localStorage.getItem('account'); // TODO: 外部からはreadonlyに export const $i = data ? reactive(JSON.parse(data) as Account) : null; -export function signout() { +export async function signout() { + waiting(); localStorage.removeItem('account'); + + //#region Remove account + const accounts = await getAccounts(); + accounts.splice(accounts.findIndex(x => x.id === $i.id), 1); + set('accounts', accounts); + //#endregion + + //#region Remove push notification registration + const registration = await navigator.serviceWorker.ready; + const push = await registration.pushManager.getSubscription(); + if (!push) return; + await fetch(`${apiUrl}/sw/unregister`, { + method: 'POST', + body: JSON.stringify({ + i: $i.token, + endpoint: push.endpoint, + }), + }); + //#endregion + document.cookie = `igi=; path=/`; - location.href = '/'; + + if (accounts.length > 0) login(accounts[0].token); + else unisonReload(); } -export function getAccounts() { - const accountsData = localStorage.getItem('accounts'); - const accounts: { id: Account['id'], token: Account['token'] }[] = accountsData ? JSON.parse(accountsData) : []; - return accounts; +export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> { + return (await get('accounts')) || []; } -export function addAccount(id: Account['id'], token: Account['token']) { - const accounts = getAccounts(); +export async function addAccount(id: Account['id'], token: Account['token']) { + const accounts = await getAccounts(); if (!accounts.some(x => x.id === id)) { - localStorage.setItem('accounts', JSON.stringify(accounts.concat([{ id, token }]))); + await set('accounts', accounts.concat([{ id, token }])); } } @@ -47,7 +69,7 @@ function fetchAccount(token): Promise { }) .then(res => { // When failed to authenticate user - if (res.status >= 400 && res.status < 500) { + if (res.status !== 200 && res.status < 500) { return signout(); } @@ -69,15 +91,22 @@ export function updateAccount(data) { } export function refreshAccount() { - fetchAccount($i.token).then(updateAccount); + return fetchAccount($i.token).then(updateAccount); } -export async function login(token: Account['token']) { +export async function login(token: Account['token'], redirect?: string) { waiting(); if (_DEV_) console.log('logging as token ', token); const me = await fetchAccount(token); localStorage.setItem('account', JSON.stringify(me)); - addAccount(me.id, token); + await addAccount(me.id, token); + + if (redirect) { + reloadChannel.postMessage('reload'); + location.href = redirect; + return; + } + unisonReload(); } diff --git a/src/client/init.ts b/src/client/init.ts index 1580ef3e08..0313af4374 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -4,6 +4,15 @@ import '@client/style.scss'; +//#region account indexedDB migration +import { set } from '@client/scripts/idb-proxy'; + +if (localStorage.getItem('accounts') != null) { + set('accounts', JSON.parse(localStorage.getItem('accounts'))); + localStorage.removeItem('accounts'); +} +//#endregion + import * as Sentry from '@sentry/browser'; import { Integrations } from '@sentry/tracing'; import { computed, createApp, watch, markRaw } from 'vue'; diff --git a/src/client/pages/settings/accounts.vue b/src/client/pages/settings/accounts.vue index 53e28bdf6f..ca6f53776a 100644 --- a/src/client/pages/settings/accounts.vue +++ b/src/client/pages/settings/accounts.vue @@ -48,10 +48,10 @@ export default defineComponent({ title: this.$ts.accounts, icon: 'fas fa-users', }, - storedAccounts: getAccounts().filter(x => x.id !== this.$i.id), + storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)), accounts: null, - init: () => os.api('users/show', { - userIds: this.storedAccounts.map(x => x.id) + init: async () => os.api('users/show', { + userIds: (await this.storedAccounts).map(x => x.id) }).then(accounts => { this.accounts = accounts; }), @@ -104,8 +104,8 @@ export default defineComponent({ }, 'closed'); }, - switchAccount(account: any) { - const storedAccounts = getAccounts(); + async switchAccount(account: any) { + const storedAccounts = await getAccounts(); const token = storedAccounts.find(x => x.id === account.id).token; this.switchAccountWithToken(token); }, diff --git a/src/client/scripts/get-account-from-id.ts b/src/client/scripts/get-account-from-id.ts new file mode 100644 index 0000000000..065b41118c --- /dev/null +++ b/src/client/scripts/get-account-from-id.ts @@ -0,0 +1,7 @@ +import { get } from '@client/scripts/idb-proxy'; + +export async function getAccountFromId(id: string) { + const accounts = await get('accounts') as { token: string; id: string; }[]; + if (!accounts) console.log('Accounts are not recorded'); + return accounts.find(e => e.id === id); +} diff --git a/src/client/scripts/idb-proxy.ts b/src/client/scripts/idb-proxy.ts new file mode 100644 index 0000000000..21c4dcff65 --- /dev/null +++ b/src/client/scripts/idb-proxy.ts @@ -0,0 +1,38 @@ +// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、 +// indexedDBが使えない環境ではlocalStorageを使う +import { + get as iget, + set as iset, + del as idel, + createStore, +} from 'idb-keyval'; + +const fallbackName = (key: string) => `idbfallback::${key}`; + +let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true; + +if (idbAvailable) { + try { + await createStore('keyval-store', 'keyval'); + } catch (e) { + console.error('idb open error', e); + idbAvailable = false; + } +} + +if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.'); + +export async function get(key: string) { + if (idbAvailable) return iget(key); + return JSON.parse(localStorage.getItem(fallbackName(key))); +} + +export async function set(key: string, val: any) { + if (idbAvailable) return iset(key, val); + return localStorage.setItem(fallbackName(key), JSON.stringify(val)); +} + +export async function del(key: string) { + if (idbAvailable) return idel(key); + return localStorage.removeItem(fallbackName(key)); +} diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue index b7b88faeac..333d0ac392 100644 --- a/src/client/ui/_common_/sidebar.vue +++ b/src/client/ui/_common_/sidebar.vue @@ -135,7 +135,7 @@ export default defineComponent({ }, async openAccountMenu(ev) { - const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id); + const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)); const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) }); const accountItemPromises = storedAccounts.map(a => new Promise(res => { @@ -195,8 +195,8 @@ export default defineComponent({ }, 'closed'); }, - switchAccount(account: any) { - const storedAccounts = getAccounts(); + async switchAccount(account: any) { + const storedAccounts = await getAccounts(); const token = storedAccounts.find(x => x.id === account.id).token; this.switchAccountWithToken(token); }, diff --git a/src/client/ui/default.header.vue b/src/client/ui/default.header.vue index df2e99f13a..6fbdd625c7 100644 --- a/src/client/ui/default.header.vue +++ b/src/client/ui/default.header.vue @@ -101,7 +101,7 @@ export default defineComponent({ }, async openAccountMenu(ev) { - const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id); + const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)); const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) }); const accountItemPromises = storedAccounts.map(a => new Promise(res => { @@ -161,8 +161,8 @@ export default defineComponent({ }, 'closed'); }, - switchAccount(account: any) { - const storedAccounts = getAccounts(); + async switchAccount(account: any) { + const storedAccounts = await getAccounts(); const token = storedAccounts.find(x => x.id === account.id).token; this.switchAccountWithToken(token); }, diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue index b500ab582c..be907aa2a4 100644 --- a/src/client/ui/default.sidebar.vue +++ b/src/client/ui/default.sidebar.vue @@ -121,7 +121,7 @@ export default defineComponent({ }, async openAccountMenu(ev) { - const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id); + const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)); const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) }); const accountItemPromises = storedAccounts.map(a => new Promise(res => { @@ -181,8 +181,8 @@ export default defineComponent({ }, 'closed'); }, - switchAccount(account: any) { - const storedAccounts = getAccounts(); + async switchAccount(account: any) { + const storedAccounts = await getAccounts(); const token = storedAccounts.find(x => x.id === account.id).token; this.switchAccountWithToken(token); },