From fc56b1269084f4556e2a03f6dd3ffd893a5e0063 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 20 Aug 2021 19:38:16 +0900
Subject: [PATCH] =?UTF-8?q?refactor:=20localStorage=E3=81=AEaccounts?=
 =?UTF-8?q?=E3=81=AFindexedDB=E3=81=A7=E4=BF=9D=E6=8C=81=E3=81=99=E3=82=8B?=
 =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=20(#7609)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

* fix lint

* fix indexeddb available detection

* remove debugging code

* fix lint

* resolve https://github.com/misskey-dev/misskey/pull/7609/files/ba756204b77ce6e1189b8443e9641f2d02119621#diff-f565878e8202f0037b830c780b7c0932dc1bb5fd3d05ede14d72d10efbc3740c
Firefoxでの動作を改善

* fix lint

* fix lint

* add changelog
---
 CHANGELOG.md                              |  1 +
 src/client/account.ts                     | 57 +++++++++++++++++------
 src/client/init.ts                        |  9 ++++
 src/client/pages/settings/accounts.vue    | 10 ++--
 src/client/scripts/get-account-from-id.ts |  7 +++
 src/client/scripts/idb-proxy.ts           | 38 +++++++++++++++
 src/client/ui/_common_/sidebar.vue        |  6 +--
 src/client/ui/default.header.vue          |  6 +--
 src/client/ui/default.sidebar.vue         |  6 +--
 9 files changed, 112 insertions(+), 28 deletions(-)
 create mode 100644 src/client/scripts/get-account-from-id.ts
 create mode 100644 src/client/scripts/idb-proxy.ts

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<Account> {
 		})
 		.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);
 		},
-- 
GitLab