From f565c5f730b8d90233e050ff2abf803870e5c4c8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Aug 2021 19:19:31 +0900 Subject: [PATCH] Improve chat UI (wip) --- src/client/router.ts | 169 +++++++++------ src/client/ui/chat/index.vue | 246 +++++----------------- src/client/ui/chat/pages/channel.vue | 259 +++++++++++++++++++++++ src/client/ui/chat/pages/timeline.vue | 221 +++++++++++++++++++ src/client/ui/chat/side.vue | 11 +- src/client/ui/chat/timeline.vue | 292 -------------------------- 6 files changed, 637 insertions(+), 561 deletions(-) create mode 100644 src/client/ui/chat/pages/channel.vue create mode 100644 src/client/ui/chat/pages/timeline.vue delete mode 100644 src/client/ui/chat/timeline.vue diff --git a/src/client/router.ts b/src/client/router.ts index 2081c1020..225ee44e3 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -4,85 +4,114 @@ import MkLoading from '@client/pages/_loading_.vue'; import MkError from '@client/pages/_error_.vue'; import MkTimeline from '@client/pages/timeline.vue'; import { $i } from './account'; +import { ui } from '@client/config'; -const page = (path: string) => defineAsyncComponent({ - loader: () => import(`./pages/${path}.vue`), +const page = (path: string, ui?: string) => defineAsyncComponent({ + loader: ui ? () => import(`./ui/${ui}/pages/${path}.vue`) : () => import(`./pages/${path}.vue`), loadingComponent: MkLoading, errorComponent: MkError, }); let indexScrollPos = 0; +const defaultRoutes = [ + // NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる + { path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') }, + { path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) }, + { path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) }, + { path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, + { path: '/@:acct/room', props: true, component: page('room/room') }, + { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) }, + { path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) }, + { path: '/announcements', component: page('announcements') }, + { path: '/about', component: page('about') }, + { path: '/about-misskey', component: page('about-misskey') }, + { path: '/featured', component: page('featured') }, + { path: '/docs', component: page('docs') }, + { path: '/theme-editor', component: page('theme-editor') }, + { path: '/advanced-theme-editor', component: page('advanced-theme-editor') }, + { path: '/docs/:doc(.*)', component: page('doc'), props: route => ({ doc: route.params.doc }) }, + { path: '/explore', component: page('explore') }, + { path: '/explore/tags/:tag', props: true, component: page('explore') }, + { path: '/federation', component: page('federation') }, + { path: '/emojis', component: page('emojis') }, + { path: '/search', component: page('search') }, + { path: '/pages', name: 'pages', component: page('pages') }, + { path: '/pages/new', component: page('page-editor/page-editor') }, + { path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) }, + { path: '/gallery', component: page('gallery/index') }, + { path: '/gallery/new', component: page('gallery/edit') }, + { path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) }, + { path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) }, + { path: '/channels', component: page('channels') }, + { path: '/channels/new', component: page('channel-editor') }, + { 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: '/my/notifications', component: page('notifications') }, + { path: '/my/favorites', component: page('favorites') }, + { path: '/my/messages', component: page('messages') }, + { path: '/my/mentions', component: page('mentions') }, + { path: '/my/messaging', name: 'messaging', component: page('messaging/index') }, + { path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) }, + { path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) }, + { path: '/my/drive', name: 'drive', component: page('drive') }, + { path: '/my/drive/folder/:folder', component: page('drive') }, + { path: '/my/follow-requests', component: page('follow-requests') }, + { path: '/my/lists', component: page('my-lists/index') }, + { path: '/my/lists/:list', component: page('my-lists/list') }, + { path: '/my/groups', component: page('my-groups/index') }, + { path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) }, + { path: '/my/antennas', component: page('my-antennas/index') }, + { path: '/my/antennas/create', component: page('my-antennas/create') }, + { path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true }, + { path: '/my/clips', component: page('my-clips/index') }, + { path: '/scratchpad', component: page('scratchpad') }, + { path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) }, + { path: '/instance', component: page('instance/index') }, + { path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, + { path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, + { path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, + { path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) }, + { path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) }, + { path: '/games/reversi', component: page('reversi/index') }, + { path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, + { path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') }, + { path: '/api-console', component: page('api-console') }, + { path: '/preview', component: page('preview') }, + { path: '/test', component: page('test') }, + { path: '/auth/:token', component: page('auth') }, + { path: '/miauth/:session', component: page('miauth') }, + { path: '/authorize-follow', component: page('follow') }, + { path: '/share', component: page('share') }, + { path: '/:catchAll(.*)', component: page('not-found') } +]; + +const chatRoutes = [ + { path: '/timeline', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) }, + { path: '/timeline/home', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) }, + { path: '/timeline/local', component: page('timeline', 'chat'), props: route => ({ src: 'local' }) }, + { path: '/timeline/social', component: page('timeline', 'chat'), props: route => ({ src: 'social' }) }, + { path: '/timeline/global', component: page('timeline', 'chat'), props: route => ({ src: 'global' }) }, + { path: '/channels/:channelId', component: page('channel', 'chat'), props: route => ({ channelId: route.params.channelId }) }, +]; + +function margeRoutes(routes: any[]) { + const result = defaultRoutes; + for (const route of routes) { + const found = result.findIndex(x => x.path === route.path); + if (found > -1) { + result[found] = route; + } else { + result.unshift(route); + } + } + return result; +} + export const router = createRouter({ history: createWebHistory(), - routes: [ - // NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる - { path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') }, - { path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) }, - { path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) }, - { path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, - { path: '/@:acct/room', props: true, component: page('room/room') }, - { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) }, - { path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) }, - { path: '/announcements', component: page('announcements') }, - { path: '/about', component: page('about') }, - { path: '/about-misskey', component: page('about-misskey') }, - { path: '/featured', component: page('featured') }, - { path: '/docs', component: page('docs') }, - { path: '/theme-editor', component: page('theme-editor') }, - { path: '/advanced-theme-editor', component: page('advanced-theme-editor') }, - { path: '/docs/:doc(.*)', component: page('doc'), props: route => ({ doc: route.params.doc }) }, - { path: '/explore', component: page('explore') }, - { path: '/explore/tags/:tag', props: true, component: page('explore') }, - { path: '/search', component: page('search') }, - { path: '/pages', name: 'pages', component: page('pages') }, - { path: '/pages/new', component: page('page-editor/page-editor') }, - { path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) }, - { path: '/gallery', component: page('gallery/index') }, - { path: '/gallery/new', component: page('gallery/edit') }, - { path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) }, - { path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) }, - { path: '/channels', component: page('channels') }, - { path: '/channels/new', component: page('channel-editor') }, - { 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: '/my/notifications', component: page('notifications') }, - { path: '/my/favorites', component: page('favorites') }, - { path: '/my/messages', component: page('messages') }, - { path: '/my/mentions', component: page('mentions') }, - { path: '/my/messaging', name: 'messaging', component: page('messaging/index') }, - { path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) }, - { path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) }, - { path: '/my/drive', name: 'drive', component: page('drive') }, - { path: '/my/drive/folder/:folder', component: page('drive') }, - { path: '/my/follow-requests', component: page('follow-requests') }, - { path: '/my/lists', component: page('my-lists/index') }, - { path: '/my/lists/:list', component: page('my-lists/list') }, - { path: '/my/groups', component: page('my-groups/index') }, - { path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) }, - { path: '/my/antennas', component: page('my-antennas/index') }, - { path: '/my/clips', component: page('my-clips/index') }, - { path: '/scratchpad', component: page('scratchpad') }, - { path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) }, - { path: '/instance', component: page('instance/index') }, - { path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, - { path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, - { path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, - { path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) }, - { path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) }, - { path: '/games/reversi', component: page('reversi/index') }, - { path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, - { path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') }, - { path: '/api-console', component: page('api-console') }, - { path: '/preview', component: page('preview') }, - { path: '/test', component: page('test') }, - { path: '/auth/:token', component: page('auth') }, - { path: '/miauth/:session', component: page('miauth') }, - { path: '/authorize-follow', component: page('follow') }, - { path: '/share', component: page('share') }, - { path: '/:catchAll(.*)', component: page('not-found') } - ], + routes: margeRoutes(ui === 'chat' ? chatRoutes : []), // なんかHacky // 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする scrollBehavior(to) { diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue index 6e433de12..db663c453 100644 --- a/src/client/ui/chat/index.vue +++ b/src/client/ui/chat/index.vue @@ -73,54 +73,16 @@
-
-
- - - - - -
- -
-
{{ instanceName }}
- - - - - - -
+
+
- - - + + + + + + +
@@ -139,7 +101,7 @@ import XSidebar from '@client/ui/_common_/sidebar.vue'; import XWidgets from './widgets.vue'; import XCommon from '../_common_/common.vue'; import XSide from './side.vue'; -import XTimeline from './timeline.vue'; +import XHeader from '../_common_/header.vue'; import XHeaderClock from './header-clock.vue'; import * as os from '@client/os'; import { router } from '@client/router'; @@ -147,6 +109,7 @@ import { menuDef } from '@client/menu'; import { search } from '@client/scripts/search'; import copyToClipboard from '@client/scripts/copy-to-clipboard'; import { store } from './store'; +import * as symbols from '@client/symbols'; export default defineComponent({ components: { @@ -154,29 +117,12 @@ export default defineComponent({ XSidebar, XWidgets, XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる - XTimeline, + XHeader, XHeaderClock, }, provide() { return { - navHook: (path) => { - switch (path) { - case '/timeline/home': this.tl = 'home'; return; - case '/timeline/local': this.tl = 'local'; return; - case '/timeline/social': this.tl = 'social'; return; - case '/timeline/global': this.tl = 'global'; return; - - default: - if (path.startsWith('/channels/')) { - this.tl = `channel:${ path.replace('/channels/', '') }`; - return; - } - //os.pageWindow(path); - this.$refs.side.navigate(path); - break; - } - }, sideViewHook: (path) => { this.$refs.side.navigate(path); } @@ -185,7 +131,7 @@ export default defineComponent({ data() { return { - tl: store.state.tl, + pageInfo: null, lists: null, antennas: null, followedChannels: null, @@ -197,18 +143,30 @@ export default defineComponent({ }; }, + computed: { + menu() { + return [{ + icon: 'fas fa-columns', + text: this.$ts.openInSideView, + action: () => { + this.$refs.side.navigate(this.$route.path); + } + }, { + icon: 'fas fa-window-maximize', + text: this.$ts.openInWindow, + action: () => { + os.pageWindow(this.$route.path); + } + }]; + } + }, + created() { if (window.innerWidth < 1024) { localStorage.setItem('ui', 'default'); location.reload(); } - router.beforeEach((to, from) => { - this.$refs.side.navigate(to.fullPath); - // search?q=foo のようなクエリを受け取れるようにするため、return falseはできない - //return false; - }); - os.api('users/lists/list').then(lists => { this.lists = lists; }); @@ -225,18 +183,22 @@ export default defineComponent({ os.api('channels/featured', { limit: 20 }).then(channels => { this.featuredChannels = channels; }); - - this.$watch('tl', () => { - if (this.tl.startsWith('channel:')) { - os.api('channels/show', { channelId: this.tl.replace('channel:', '') }).then(channel => { - this.currentChannel = channel; - }); - } - store.set('tl', this.tl); - }, { immediate: true }); }, methods: { + changePage(page) { + console.log(page); + if (page == null) return; + if (page[symbols.PAGE_INFO]) { + this.pageInfo = page[symbols.PAGE_INFO]; + document.title = `${this.pageInfo.title} | ${instanceName}`; + } + }, + + onTransition() { + if (window._scroll) window._scroll(); + }, + showMenu() { this.$refs.menu.show(); }, @@ -245,59 +207,18 @@ export default defineComponent({ os.post(); }, - 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)); - }, - search() { search(); }, - async inChannelSearch() { - const { canceled, result: query } = await os.dialog({ - title: this.$ts.inChannelSearch, - input: true - }); - if (canceled || query == null || query === '') return; - router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.currentChannel.id}`); + back() { + history.back(); }, top() { window.scroll({ top: 0, behavior: 'smooth' }); }, - async toggleChannelFollow() { - if (this.currentChannel.isFollowing) { - await os.apiWithDialog('channels/unfollow', { - channelId: this.currentChannel.id - }); - this.currentChannel.isFollowing = false; - } else { - await os.apiWithDialog('channels/follow', { - channelId: this.currentChannel.id - }); - this.currentChannel.isFollowing = true; - } - }, - - openChannelMenu(ev) { - os.modalMenu([{ - text: this.$ts.copyUrl, - icon: 'fas fa-link', - action: () => { - copyToClipboard(`${url}/channels/${this.currentChannel.id}`); - } - }], ev.currentTarget || ev.target); - }, - onTransition() { if (window._scroll) window._scroll(); }, @@ -516,87 +437,24 @@ export default defineComponent({ background: var(--panel); > .header { - $padding: 8px; - display: flex; z-index: 1000; height: $header-height; - padding: $padding; - box-sizing: border-box; background-color: var(--panel); border-bottom: solid 0.5px var(--divider); user-select: none; + } - > .left { - display: flex; - align-items: center; - flex: 1; - min-width: 0; - - > .icon { - height: ($header-height - ($padding * 2)); - width: ($header-height - ($padding * 2)); - padding: 10px; - box-sizing: border-box; - margin-right: 4px; - opacity: 0.6; - } - - > .title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; - font-weight: bold; - - > .description { - opacity: 0.6; - font-size: 0.8em; - font-weight: normal; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - - > .right { - display: flex; - align-items: center; - min-width: 0; - margin-left: auto; - padding-left: 8px; - - > .instance { - margin-right: 16px; - font-size: 0.9em; - } - - > .clock { - margin-right: 16px; - } - - > .button { - height: ($header-height - ($padding * 2)); - width: ($header-height - ($padding * 2)); - box-sizing: border-box; - position: relative; - border-radius: 5px; - - &:hover { - background: rgba(0, 0, 0, 0.05); - } - - &.follow.followed { - color: var(--accent); - } - } - } + > .body { + width: 100%; + box-sizing: border-box; + overflow: auto; } } > .side { width: 350px; border-left: solid 4px var(--divider); + background: var(--panel); &.widgets.sideViewOpening { @media (max-width: 1400px) { diff --git a/src/client/ui/chat/pages/channel.vue b/src/client/ui/chat/pages/channel.vue new file mode 100644 index 000000000..76b334487 --- /dev/null +++ b/src/client/ui/chat/pages/channel.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/src/client/ui/chat/pages/timeline.vue b/src/client/ui/chat/pages/timeline.vue new file mode 100644 index 000000000..0f9cd7f11 --- /dev/null +++ b/src/client/ui/chat/pages/timeline.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/src/client/ui/chat/side.vue b/src/client/ui/chat/side.vue index 7ad39c7a1..5ccfad1b7 100644 --- a/src/client/ui/chat/side.vue +++ b/src/client/ui/chat/side.vue @@ -1,11 +1,9 @@ @@ -130,7 +128,6 @@ export default defineComponent({ top: 0; height: $header-height; width: 100%; - line-height: $header-height; font-weight: bold; //background-color: var(--panel); -webkit-backdrop-filter: blur(32px); @@ -153,6 +150,10 @@ export default defineComponent({ position: relative; } } + + > .body { + + } } diff --git a/src/client/ui/chat/timeline.vue b/src/client/ui/chat/timeline.vue deleted file mode 100644 index 0fbcbfb71..000000000 --- a/src/client/ui/chat/timeline.vue +++ /dev/null @@ -1,292 +0,0 @@ - - - - -