refactor: localStorageのaccountsはindexedDBで保持するように (#7609)

* accountsストアはindexedDBで保持するように

* fix lint

* fix indexeddb available detection

* remove debugging code

* fix lint

* resolve ba756204b7 (diff-f565878e8202f0037b830c780b7c0932dc1bb5fd3d05ede14d72d10efbc3740c)
Firefoxでの動作を改善

* fix lint

* fix lint

* add changelog
This commit is contained in:
tamaina 2021-08-20 19:38:16 +09:00 committed by GitHub
parent 2f0d8c2b07
commit 7b27889cde
9 changed files with 112 additions and 28 deletions

View File

@ -11,6 +11,7 @@
### Improvements ### Improvements
- 依存関係の更新 - 依存関係の更新
- localStorageのaccountsはindexedDBで保持するように
### Bugfixes ### Bugfixes
- チャンネルを作成しているとアカウントを削除できないのを修正 - チャンネルを作成しているとアカウントを削除できないのを修正

View File

@ -1,7 +1,8 @@
import { get, set } from '@client/scripts/idb-proxy';
import { reactive } from 'vue'; import { reactive } from 'vue';
import { apiUrl } from '@client/config'; import { apiUrl } from '@client/config';
import { waiting } from '@client/os'; import { waiting } from '@client/os';
import { unisonReload } from '@client/scripts/unison-reload'; import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -17,22 +18,43 @@ const data = localStorage.getItem('account');
// TODO: 外部からはreadonlyに // TODO: 外部からはreadonlyに
export const $i = data ? reactive(JSON.parse(data) as Account) : null; export const $i = data ? reactive(JSON.parse(data) as Account) : null;
export function signout() { export async function signout() {
waiting();
localStorage.removeItem('account'); 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=/`; document.cookie = `igi=; path=/`;
location.href = '/';
if (accounts.length > 0) login(accounts[0].token);
else unisonReload();
} }
export function getAccounts() { export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
const accountsData = localStorage.getItem('accounts'); return (await get('accounts')) || [];
const accounts: { id: Account['id'], token: Account['token'] }[] = accountsData ? JSON.parse(accountsData) : [];
return accounts;
} }
export function addAccount(id: Account['id'], token: Account['token']) { export async function addAccount(id: Account['id'], token: Account['token']) {
const accounts = getAccounts(); const accounts = await getAccounts();
if (!accounts.some(x => x.id === id)) { 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<Account> {
}) })
.then(res => { .then(res => {
// When failed to authenticate user // When failed to authenticate user
if (res.status >= 400 && res.status < 500) { if (res.status !== 200 && res.status < 500) {
return signout(); return signout();
} }
@ -69,15 +91,22 @@ export function updateAccount(data) {
} }
export function refreshAccount() { 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(); waiting();
if (_DEV_) console.log('logging as token ', token); if (_DEV_) console.log('logging as token ', token);
const me = await fetchAccount(token); const me = await fetchAccount(token);
localStorage.setItem('account', JSON.stringify(me)); 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(); unisonReload();
} }

View File

@ -4,6 +4,15 @@
import '@client/style.scss'; 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 * as Sentry from '@sentry/browser';
import { Integrations } from '@sentry/tracing'; import { Integrations } from '@sentry/tracing';
import { computed, createApp, watch, markRaw } from 'vue'; import { computed, createApp, watch, markRaw } from 'vue';

View File

@ -48,10 +48,10 @@ export default defineComponent({
title: this.$ts.accounts, title: this.$ts.accounts,
icon: 'fas fa-users', 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, accounts: null,
init: () => os.api('users/show', { init: async () => os.api('users/show', {
userIds: this.storedAccounts.map(x => x.id) userIds: (await this.storedAccounts).map(x => x.id)
}).then(accounts => { }).then(accounts => {
this.accounts = accounts; this.accounts = accounts;
}), }),
@ -104,8 +104,8 @@ export default defineComponent({
}, 'closed'); }, 'closed');
}, },
switchAccount(account: any) { async switchAccount(account: any) {
const storedAccounts = getAccounts(); const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token; const token = storedAccounts.find(x => x.id === account.id).token;
this.switchAccountWithToken(token); this.switchAccountWithToken(token);
}, },

View File

@ -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);
}

View File

@ -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));
}

View File

@ -135,7 +135,7 @@ export default defineComponent({
}, },
async openAccountMenu(ev) { 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 accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
const accountItemPromises = storedAccounts.map(a => new Promise(res => { const accountItemPromises = storedAccounts.map(a => new Promise(res => {
@ -195,8 +195,8 @@ export default defineComponent({
}, 'closed'); }, 'closed');
}, },
switchAccount(account: any) { async switchAccount(account: any) {
const storedAccounts = getAccounts(); const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token; const token = storedAccounts.find(x => x.id === account.id).token;
this.switchAccountWithToken(token); this.switchAccountWithToken(token);
}, },

View File

@ -101,7 +101,7 @@ export default defineComponent({
}, },
async openAccountMenu(ev) { 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 accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
const accountItemPromises = storedAccounts.map(a => new Promise(res => { const accountItemPromises = storedAccounts.map(a => new Promise(res => {
@ -161,8 +161,8 @@ export default defineComponent({
}, 'closed'); }, 'closed');
}, },
switchAccount(account: any) { async switchAccount(account: any) {
const storedAccounts = getAccounts(); const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token; const token = storedAccounts.find(x => x.id === account.id).token;
this.switchAccountWithToken(token); this.switchAccountWithToken(token);
}, },

View File

@ -121,7 +121,7 @@ export default defineComponent({
}, },
async openAccountMenu(ev) { 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 accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
const accountItemPromises = storedAccounts.map(a => new Promise(res => { const accountItemPromises = storedAccounts.map(a => new Promise(res => {
@ -181,8 +181,8 @@ export default defineComponent({
}, 'closed'); }, 'closed');
}, },
switchAccount(account: any) { async switchAccount(account: any) {
const storedAccounts = getAccounts(); const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token; const token = storedAccounts.find(x => x.id === account.id).token;
this.switchAccountWithToken(token); this.switchAccountWithToken(token);
}, },