From 72a49f334a58db61e2f977f5e53f28c1491f9da8 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Tue, 21 Sep 2021 21:04:59 +0900 Subject: [PATCH] =?UTF-8?q?enhance(client):=20=E3=83=AA=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=80=81=E3=82=A2=E3=83=B3=E3=83=86=E3=83=8A=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=82=92=E5=80=8B=E5=88=A5?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=A8=E3=81=97=E3=81=A6=E5=88=86?= =?UTF-8?q?=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +- src/client/components/ui/menu.vue | 33 +++--- src/client/menu.ts | 24 +++- src/client/os.ts | 2 +- src/client/pages/antenna-timeline.vue | 147 ++++++++++++++++++++++++ src/client/pages/timeline.vue | 58 ++-------- src/client/pages/user-list-timeline.vue | 147 ++++++++++++++++++++++++ src/client/router.ts | 2 + src/client/ui/_common_/sidebar.vue | 2 +- 9 files changed, 348 insertions(+), 70 deletions(-) create mode 100644 src/client/pages/antenna-timeline.vue create mode 100644 src/client/pages/user-list-timeline.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index b60fd7cfa..cf5621fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,14 @@ --> ## 12.x.x (unreleased) -- ActivityPub: deliverキューのメモリ使用量を削減 ### Improvements - ActivityPub: リモートユーザーのDeleteアクティビティに対応 - ActivityPub: add resolver check for blocked instance +- ActivityPub: deliverキューのメモリ使用量を削減 - アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように - 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように +- リスト、アンテナタイムラインを個別ページとして分割 - UIの改善 ### Bugfixes diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue index d652d9b84..26b4b04b1 100644 --- a/src/client/components/ui/menu.vue +++ b/src/client/components/ui/menu.vue @@ -41,7 +41,7 @@ </template> <script lang="ts"> -import { defineComponent, ref } from 'vue'; +import { defineComponent, ref, unref } from 'vue'; import { focusPrev, focusNext } from '@client/scripts/focus'; import contains from '@client/scripts/contains'; @@ -79,21 +79,26 @@ export default defineComponent({ }; }, }, - created() { - const items = ref(this.items.filter(item => item !== undefined)); + watch: { + items: { + handler() { + const items = ref(unref(this.items).filter(item => item !== undefined)); - for (let i = 0; i < items.value.length; i++) { - const item = items.value[i]; - - if (item && item.then) { // if item is Promise - items.value[i] = { type: 'pending' }; - item.then(actualItem => { - items.value[i] = actualItem; - }); - } + for (let i = 0; i < items.value.length; i++) { + const item = items.value[i]; + + if (item && item.then) { // if item is Promise + items.value[i] = { type: 'pending' }; + item.then(actualItem => { + items.value[i] = actualItem; + }); + } + } + + this._items = items; + }, + immediate: true } - - this._items = items; }, mounted() { if (this.viaKeyboard) { diff --git a/src/client/menu.ts b/src/client/menu.ts index 8e65496cf..4929b6428 100644 --- a/src/client/menu.ts +++ b/src/client/menu.ts @@ -1,9 +1,10 @@ -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import { search } from '@client/scripts/search'; import * as os from '@client/os'; import { i18n } from '@client/i18n'; import { $i } from './account'; import { unisonReload } from '@client/scripts/unison-reload'; +import { router } from './router'; export const menuDef = { notifications: { @@ -58,7 +59,26 @@ export const menuDef = { title: 'lists', icon: 'fas fa-list-ul', show: computed(() => $i != null), - to: '/my/lists', + active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')), + action: (ev) => { + const items = ref([{ + type: 'pending' + }]); + os.api('users/lists/list').then(lists => { + const _items = [...lists.map(list => ({ + type: 'link', + text: list.name, + to: `/timeline/list/${list.id}` + })), null, { + type: 'link', + to: '/my/lists', + text: i18n.locale.manageLists, + icon: 'fas fa-cog', + }]; + items.value = _items; + }); + os.popupMenu(items, ev.currentTarget || ev.target); + }, }, groups: { title: 'groups', diff --git a/src/client/os.ts b/src/client/os.ts index 812533279..7ae774dd9 100644 --- a/src/client/os.ts +++ b/src/client/os.ts @@ -372,7 +372,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: }); } -export function popupMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { +export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { return new Promise((resolve, reject) => { let dispose; popup(import('@client/components/ui/popup-menu.vue'), { diff --git a/src/client/pages/antenna-timeline.vue b/src/client/pages/antenna-timeline.vue new file mode 100644 index 000000000..425bec698 --- /dev/null +++ b/src/client/pages/antenna-timeline.vue @@ -0,0 +1,147 @@ +<template> +<div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }"> + <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> + <div class="tl _block"> + <XTimeline ref="tl" class="tl" + :key="antennaId" + src="antenna" + :antenna="antennaId" + :sound="true" + @before="before()" + @after="after()" + @queue="queueUpdated" + /> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent, defineAsyncComponent, computed } from 'vue'; +import Progress from '@client/scripts/loading'; +import XTimeline from '@client/components/timeline.vue'; +import { scroll } from '@client/scripts/scroll'; +import * as os from '@client/os'; +import * as symbols from '@client/symbols'; + +export default defineComponent({ + components: { + XTimeline, + }, + + props: { + antennaId: { + type: String, + required: true + } + }, + + data() { + return { + antenna: null, + queue: 0, + [symbols.PAGE_INFO]: computed(() => this.antenna ? { + title: this.antenna.name, + icon: 'fas fa-satellite', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-calendar-alt', + text: this.$ts.jumpToSpecifiedDate, + handler: this.timetravel + }, { + icon: 'fas fa-cog', + text: this.$ts.settings, + handler: this.settings + }], + } : null), + }; + }, + + computed: { + keymap(): any { + return { + 't': this.focus + }; + }, + }, + + watch: { + antennaId: { + async handler() { + this.antenna = await os.api('antennas/show', { + antennaId: this.antennaId + }); + }, + immediate: true + } + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + }, + + queueUpdated(q) { + this.queue = q; + }, + + top() { + scroll(this.$el, 0); + }, + + async timetravel() { + const { canceled, result: date } = await os.dialog({ + title: this.$ts.date, + input: { + type: 'date' + } + }); + if (canceled) return; + + this.$refs.tl.timetravel(new Date(date)); + }, + + settings() { + this.$router.push(`/my/antennas/${this.antennaId}`); + }, + + focus() { + (this.$refs.tl as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.tqmomfks { + padding: var(--margin); + + > .new { + position: sticky; + top: calc(var(--stickyTop, 0px) + 16px); + z-index: 1000; + width: 100%; + + > button { + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; + } + } + + > .tl { + background: var(--bg); + border-radius: var(--radius); + overflow: clip; + } + + &.min-width_800px { + max-width: 800px; + margin: 0 auto; + } +} +</style> diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue index 4b5b90e6a..9dda82462 100644 --- a/src/client/pages/timeline.vue +++ b/src/client/pages/timeline.vue @@ -6,11 +6,8 @@ <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> <div class="tl _block"> <XTimeline ref="tl" class="tl" - :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" + :key="src" :src="src" - :list="list ? list.id : null" - :antenna="antenna ? antenna.id : null" - :channel="channel ? channel.id : null" :sound="true" @before="before()" @after="after()" @@ -41,10 +38,6 @@ export default defineComponent({ data() { return { src: 'home', - list: null, - antenna: null, - channel: null, - menuOpened: false, queue: 0, [symbols.PAGE_INFO]: computed(() => ({ title: this.$ts.timeline, @@ -116,32 +109,10 @@ export default defineComponent({ src() { this.showNav = false; }, - list(x) { - this.showNav = false; - if (x != null) this.antenna = null; - if (x != null) this.channel = null; - }, - antenna(x) { - this.showNav = false; - if (x != null) this.list = null; - if (x != null) this.channel = null; - }, - channel(x) { - this.showNav = false; - if (x != null) this.antenna = null; - if (x != null) this.list = null; - }, }, created() { this.src = this.$store.state.tl.src; - if (this.src === 'list') { - this.list = this.$store.state.tl.arg; - } else if (this.src === 'antenna') { - this.antenna = this.$store.state.tl.arg; - } else if (this.src === 'channel') { - this.channel = this.$store.state.tl.arg; - } }, methods: { @@ -164,12 +135,9 @@ export default defineComponent({ async chooseList(ev) { const lists = await os.api('users/lists/list'); const items = lists.map(list => ({ + type: 'link', text: list.name, - action: () => { - this.list = list; - this.src = 'list'; - this.saveSrc(); - } + to: `/timeline/list/${list.id}` })); os.popupMenu(items, ev.currentTarget || ev.target); }, @@ -177,13 +145,10 @@ export default defineComponent({ async chooseAntenna(ev) { const antennas = await os.api('antennas/list'); const items = antennas.map(antenna => ({ + type: 'link', text: antenna.name, indicate: antenna.hasUnreadNote, - action: () => { - this.antenna = antenna; - this.src = 'antenna'; - this.saveSrc(); - } + to: `/timeline/antenna/${antenna.id}` })); os.popupMenu(items, ev.currentTarget || ev.target); }, @@ -191,15 +156,10 @@ export default defineComponent({ async chooseChannel(ev) { const channels = await os.api('channels/followed'); const items = channels.map(channel => ({ + type: 'link', text: channel.name, indicate: channel.hasUnreadNote, - action: () => { - // NOTE: チャンネルタイムラインをこのコンポーネントで表示するようにすると投稿フォームはどうするかなどの問題が生じるのでとりあえずページ遷移で - //this.channel = channel; - //this.src = 'channel'; - //this.saveSrc(); - this.$router.push(`/channels/${channel.id}`); - } + to: `/channels/${channel.id}` })); os.popupMenu(items, ev.currentTarget || ev.target); }, @@ -207,10 +167,6 @@ export default defineComponent({ saveSrc() { this.$store.set('tl', { src: this.src, - arg: - this.src === 'list' ? this.list : - this.src === 'antenna' ? this.antenna : - this.channel }); }, diff --git a/src/client/pages/user-list-timeline.vue b/src/client/pages/user-list-timeline.vue new file mode 100644 index 000000000..491fe948c --- /dev/null +++ b/src/client/pages/user-list-timeline.vue @@ -0,0 +1,147 @@ +<template> +<div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }"> + <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> + <div class="tl _block"> + <XTimeline ref="tl" class="tl" + :key="listId" + src="list" + :list="listId" + :sound="true" + @before="before()" + @after="after()" + @queue="queueUpdated" + /> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent, defineAsyncComponent, computed } from 'vue'; +import Progress from '@client/scripts/loading'; +import XTimeline from '@client/components/timeline.vue'; +import { scroll } from '@client/scripts/scroll'; +import * as os from '@client/os'; +import * as symbols from '@client/symbols'; + +export default defineComponent({ + components: { + XTimeline, + }, + + props: { + listId: { + type: String, + required: true + } + }, + + data() { + return { + list: null, + queue: 0, + [symbols.PAGE_INFO]: computed(() => this.list ? { + title: this.list.name, + icon: 'fas fa-list-ul', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-calendar-alt', + text: this.$ts.jumpToSpecifiedDate, + handler: this.timetravel + }, { + icon: 'fas fa-cog', + text: this.$ts.settings, + handler: this.settings + }], + } : null), + }; + }, + + computed: { + keymap(): any { + return { + 't': this.focus + }; + }, + }, + + watch: { + listId: { + async handler() { + this.list = await os.api('users/lists/show', { + listId: this.listId + }); + }, + immediate: true + } + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + }, + + queueUpdated(q) { + this.queue = q; + }, + + top() { + scroll(this.$el, 0); + }, + + settings() { + this.$router.push(`/my/lists/${this.listId}`); + }, + + async timetravel() { + const { canceled, result: date } = await os.dialog({ + title: this.$ts.date, + input: { + type: 'date' + } + }); + if (canceled) return; + + this.$refs.tl.timetravel(new Date(date)); + }, + + focus() { + (this.$refs.tl as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.eqqrhokj { + padding: var(--margin); + + > .new { + position: sticky; + top: calc(var(--stickyTop, 0px) + 16px); + z-index: 1000; + width: 100%; + + > button { + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; + } + } + + > .tl { + background: var(--bg); + border-radius: var(--radius); + overflow: clip; + } + + &.min-width_800px { + max-width: 800px; + margin: 0 auto; + } +} +</style> diff --git a/src/client/router.ts b/src/client/router.ts index 225ee44e3..573f285c7 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -48,6 +48,8 @@ const defaultRoutes = [ { path: '/channels/:channelId/edit', component: page('channel-editor'), props: true }, { path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) }, { path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) }, + { path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) }, + { path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) }, { path: '/my/notifications', component: page('notifications') }, { path: '/my/favorites', component: page('favorites') }, { path: '/my/messages', component: page('messages') }, diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue index 43b64d133..65f3d7dbd 100644 --- a/src/client/ui/_common_/sidebar.vue +++ b/src/client/ui/_common_/sidebar.vue @@ -19,7 +19,7 @@ </MkA> <template v-for="item in menu"> <div v-if="item === '-'" class="divider"></div> - <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime> + <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime> <i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span> <span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span> </component>