diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-08-21 17:59:29 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-08-21 17:59:29 +0900 |
| commit | f00ceedae48e7969ca9e80f0af2280bf060421ec (patch) | |
| tree | 620bb82f6a2ce41f3b3b3d187242bd5bc8e35171 /src/client | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.89.0 (diff) | |
| download | misskey-f00ceedae48e7969ca9e80f0af2280bf060421ec.tar.gz misskey-f00ceedae48e7969ca9e80f0af2280bf060421ec.tar.bz2 misskey-f00ceedae48e7969ca9e80f0af2280bf060421ec.zip | |
Merge branch 'develop'
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/account.ts | 60 | ||||
| -rw-r--r-- | src/client/components/notes.vue | 2 | ||||
| -rw-r--r-- | src/client/components/notifications.vue | 6 | ||||
| -rw-r--r-- | src/client/components/post-form.vue | 7 | ||||
| -rw-r--r-- | src/client/init.ts | 16 | ||||
| -rw-r--r-- | src/client/pages/notifications.vue | 18 | ||||
| -rw-r--r-- | src/client/pages/settings/accounts.vue | 10 | ||||
| -rw-r--r-- | src/client/pages/settings/delete-account.vue | 67 | ||||
| -rw-r--r-- | src/client/pages/settings/index.vue | 1 | ||||
| -rw-r--r-- | src/client/pages/settings/other.vue | 19 | ||||
| -rw-r--r-- | src/client/pages/timeline.vue | 39 | ||||
| -rw-r--r-- | src/client/scripts/autocomplete.ts | 4 | ||||
| -rw-r--r-- | src/client/scripts/get-account-from-id.ts | 7 | ||||
| -rw-r--r-- | src/client/scripts/idb-proxy.ts | 38 | ||||
| -rw-r--r-- | src/client/ui/_common_/sidebar.vue | 6 | ||||
| -rw-r--r-- | src/client/ui/default.header.vue | 6 | ||||
| -rw-r--r-- | src/client/ui/default.sidebar.vue | 6 |
17 files changed, 246 insertions, 66 deletions
diff --git a/src/client/account.ts b/src/client/account.ts index 2b860b3ddf..ee1d845493 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を同期 @@ -10,6 +11,7 @@ type Account = { token: string; isModerator: boolean; isAdmin: boolean; + isDeleted: boolean; }; const data = localStorage.getItem('account'); @@ -17,22 +19,45 @@ 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 + try { + 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, + }), + }); + } catch (e) {} + //#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 +72,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 +94,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/components/notes.vue b/src/client/components/notes.vue index ba3b7d2b39..919cb29952 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -118,6 +118,8 @@ export default defineComponent({ &:not(.noGap) { > .notes { + background: var(--bg); + .qtqtichx { background: var(--panel); border-radius: var(--radius); diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue index 9db47e08d6..e91f18a693 100644 --- a/src/client/components/notifications.vue +++ b/src/client/components/notifications.vue @@ -7,7 +7,7 @@ <p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p> <div v-else> - <XList class="notifications" :items="items" v-slot="{ item: notification }" :no-gap="true"> + <XList class="elsfgstc" :items="items" v-slot="{ item: notification }" :no-gap="true"> <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> <XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> </XList> @@ -141,4 +141,8 @@ export default defineComponent({ text-align: center; color: var(--fg); } + +.elsfgstc { + background: var(--panel); +} </style> diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index 221dc74313..657053cc93 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -339,7 +339,12 @@ export default defineComponent({ this.cw = init.cw; this.useCw = init.cw != null; if (init.poll) { - this.poll = init.poll; + this.poll = { + choices: init.poll.choices.map(x => x.text), + multiple: init.poll.multiple, + expiresAt: init.poll.expiresAt, + expiredAfter: init.poll.expiredAfter, + }; } this.visibility = init.visibility; this.localOnly = init.localOnly; diff --git a/src/client/init.ts b/src/client/init.ts index 1580ef3e08..194ece886b 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'; @@ -301,6 +310,13 @@ for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { } if ($i) { + if ($i.isDeleted) { + dialog({ + type: 'warning', + text: i18n.locale.accountDeletionInProgress, + }); + } + if ('Notification' in window) { // 許可を得ていなかったらリクエスト if (Notification.permission === 'default') { diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue index 6b16b85b78..633718a90b 100644 --- a/src/client/pages/notifications.vue +++ b/src/client/pages/notifications.vue @@ -1,6 +1,6 @@ <template> -<div class=""> - <XNotifications class="_content" @before="before" @after="after" page/> +<div class="clupoqwt" v-size="{ min: [800] }"> + <XNotifications class="notifications" @before="before" @after="after" page/> </div> </template> @@ -43,3 +43,17 @@ export default defineComponent({ } }); </script> + +<style lang="scss" scoped> +.clupoqwt { + &.min-width_800px { + background: var(--bg); + padding: 32px 0; + + > .notifications { + max-width: 800px; + margin: 0 auto; + } + } +} +</style> 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/pages/settings/delete-account.vue b/src/client/pages/settings/delete-account.vue new file mode 100644 index 0000000000..3af1879857 --- /dev/null +++ b/src/client/pages/settings/delete-account.vue @@ -0,0 +1,67 @@ +<template> +<FormBase> + <FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo> + <FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo> + <FormButton @click="deleteAccount" danger v-if="!$i.isDeleted">{{ $ts._accountDelete.requestAccountDelete }}</FormButton> + <FormButton disabled v-else>{{ $ts._accountDelete.inProgress }}</FormButton> +</FormBase> +</template> + +<script lang="ts"> +import { defineAsyncComponent, defineComponent } from 'vue'; +import FormInfo from '@client/components/form/info.vue'; +import FormBase from '@client/components/form/base.vue'; +import FormGroup from '@client/components/form/group.vue'; +import FormButton from '@client/components/form/button.vue'; +import * as os from '@client/os'; +import { debug } from '@client/config'; +import { signout } from '@client/account'; +import * as symbols from '@client/symbols'; + +export default defineComponent({ + components: { + FormBase, + FormButton, + FormGroup, + FormInfo, + }, + + emits: ['info'], + + data() { + return { + [symbols.PAGE_INFO]: { + title: this.$ts._accountDelete.accountDelete, + icon: 'fas fa-exclamation-triangle' + }, + debug, + } + }, + + mounted() { + this.$emit('info', this[symbols.PAGE_INFO]); + }, + + methods: { + async deleteAccount() { + const { canceled, result: password } = await os.dialog({ + title: this.$ts.password, + input: { + type: 'password' + } + }); + if (canceled) return; + + await os.apiWithDialog('i/delete-account', { + password: password + }); + + await os.dialog({ + title: this.$ts._accountDelete.started, + }); + + signout(); + } + } +}); +</script> diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index 17b373fcd8..e7e2506020 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -132,6 +132,7 @@ export default defineComponent({ case 'account-info': return defineAsyncComponent(() => import('./account-info.vue')); case 'update': return defineAsyncComponent(() => import('./update.vue')); case 'registry': return defineAsyncComponent(() => import('./registry.vue')); + case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue')); case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue')); } if (page.value.startsWith('registry/keys/system/')) { diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue index f73ff9cb21..6857950350 100644 --- a/src/client/pages/settings/other.vue +++ b/src/client/pages/settings/other.vue @@ -26,7 +26,7 @@ <FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink> <FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink> - <FormButton @click="closeAccount" danger>{{ $ts.closeAccount }}</FormButton> + <FormLink to="./delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink> </FormBase> </template> @@ -41,7 +41,6 @@ import FormButton from '@client/components/form/button.vue'; import * as os from '@client/os'; import { debug } from '@client/config'; import { defaultStore } from '@client/store'; -import { signout } from '@client/account'; import { unisonReload } from '@client/scripts/unison-reload'; import * as symbols from '@client/symbols'; @@ -92,22 +91,6 @@ export default defineComponent({ os.popup(import('@client/components/taskmanager.vue'), { }, {}, 'closed'); }, - - closeAccount() { - os.dialog({ - title: this.$ts.password, - input: { - type: 'password' - } - }).then(({ canceled, result: password }) => { - if (canceled) return; - os.api('i/delete-account', { - password: password - }).then(() => { - signout(); - }); - }); - } } }); </script> diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue index 119815e2ae..f54549b982 100644 --- a/src/client/pages/timeline.vue +++ b/src/client/pages/timeline.vue @@ -1,5 +1,5 @@ <template> -<div class="cmuxhskf" v-hotkey.global="keymap"> +<div class="cmuxhskf" v-hotkey.global="keymap" v-size="{ min: [800] }"> <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block _isolated"/> <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block _isolated" fixed/> <div class="tabs"> @@ -19,17 +19,19 @@ </div> </div> <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> - <XTimeline ref="tl" class="tl" - :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" - :src="src" - :list="list ? list.id : null" - :antenna="antenna ? antenna.id : null" - :channel="channel ? channel.id : null" - :sound="true" - @before="before()" - @after="after()" - @queue="queueUpdated" - /> + <div class="tl"> + <XTimeline ref="tl" class="tl" + :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" + :src="src" + :list="list ? list.id : null" + :antenna="antenna ? antenna.id : null" + :channel="channel ? channel.id : null" + :sound="true" + @before="before()" + @after="after()" + @queue="queueUpdated" + /> + </div> </div> </template> @@ -231,6 +233,7 @@ export default defineComponent({ padding: 0 8px; white-space: nowrap; overflow: auto; + border-bottom: solid 0.5px var(--divider); // 影の都合上 position: relative; @@ -287,8 +290,16 @@ export default defineComponent({ } } - > .tl { - border-top: solid 0.5px var(--divider); + &.min-width_800px { + > .tl { + background: var(--bg); + padding: 32px 0; + + > .tl { + max-width: 800px; + margin: 0 auto; + } + } } } </style> diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts index 99c54c69c5..924d6a62ee 100644 --- a/src/client/scripts/autocomplete.ts +++ b/src/client/scripts/autocomplete.ts @@ -65,7 +65,7 @@ export class Autocomplete { */ private onInput() { const caretPos = this.textarea.selectionStart; - const text = this.text.substr(0, caretPos).split('\n').pop(); + const text = this.text.substr(0, caretPos).split('\n').pop()!; const mentionIndex = text.lastIndexOf('@'); const hashtagIndex = text.lastIndexOf('#'); @@ -83,7 +83,7 @@ export class Autocomplete { const isMention = mentionIndex != -1; const isHashtag = hashtagIndex != -1; - const isEmoji = emojiIndex != -1; + const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); let opened = false; 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); }, |