summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-08-21 17:59:29 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-08-21 17:59:29 +0900
commitf00ceedae48e7969ca9e80f0af2280bf060421ec (patch)
tree620bb82f6a2ce41f3b3b3d187242bd5bc8e35171 /src/client
parentMerge branch 'develop' (diff)
parent12.89.0 (diff)
downloadmisskey-f00ceedae48e7969ca9e80f0af2280bf060421ec.tar.gz
misskey-f00ceedae48e7969ca9e80f0af2280bf060421ec.tar.bz2
misskey-f00ceedae48e7969ca9e80f0af2280bf060421ec.zip
Merge branch 'develop'
Diffstat (limited to 'src/client')
-rw-r--r--src/client/account.ts60
-rw-r--r--src/client/components/notes.vue2
-rw-r--r--src/client/components/notifications.vue6
-rw-r--r--src/client/components/post-form.vue7
-rw-r--r--src/client/init.ts16
-rw-r--r--src/client/pages/notifications.vue18
-rw-r--r--src/client/pages/settings/accounts.vue10
-rw-r--r--src/client/pages/settings/delete-account.vue67
-rw-r--r--src/client/pages/settings/index.vue1
-rw-r--r--src/client/pages/settings/other.vue19
-rw-r--r--src/client/pages/timeline.vue39
-rw-r--r--src/client/scripts/autocomplete.ts4
-rw-r--r--src/client/scripts/get-account-from-id.ts7
-rw-r--r--src/client/scripts/idb-proxy.ts38
-rw-r--r--src/client/ui/_common_/sidebar.vue6
-rw-r--r--src/client/ui/default.header.vue6
-rw-r--r--src/client/ui/default.sidebar.vue6
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);
},