From 1eda7c85652c6e4295626ab94bc4084aaa141872 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 14 Feb 2021 22:26:07 +0900 Subject: [PATCH] Chat UI (#7197) * wip * wip * wip * wip * refactor * Update note.vue * wip --- src/client/components/global/avatar.vue | 4 +- src/client/components/notes.vue | 4 +- src/client/components/notifications.vue | 2 +- src/client/components/sidebar.vue | 14 +- src/client/directives/follow-append.ts | 22 +- src/client/init.ts | 1 + src/client/scripts/paging.ts | 96 +- src/client/scripts/scroll.ts | 8 + src/client/sidebar.ts | 6 + src/client/style.scss | 7 - src/client/ui/_common_/header.vue | 18 +- src/client/ui/chat/date-separated-list.vue | 154 +++ src/client/ui/chat/index.vue | 389 +++++++ src/client/ui/chat/note-header.vue | 115 ++ src/client/ui/chat/note-preview.vue | 112 ++ src/client/ui/chat/note.sub.vue | 137 +++ src/client/ui/chat/note.vue | 1126 ++++++++++++++++++++ src/client/ui/chat/notes.vue | 91 ++ src/client/ui/chat/post-form.vue | 771 ++++++++++++++ src/client/ui/chat/side.vue | 165 +++ src/client/ui/chat/sub-note-content.vue | 64 ++ src/client/ui/chat/timeline.vue | 190 ++++ 22 files changed, 3452 insertions(+), 44 deletions(-) create mode 100644 src/client/ui/chat/date-separated-list.vue create mode 100644 src/client/ui/chat/index.vue create mode 100644 src/client/ui/chat/note-header.vue create mode 100644 src/client/ui/chat/note-preview.vue create mode 100644 src/client/ui/chat/note.sub.vue create mode 100644 src/client/ui/chat/note.vue create mode 100644 src/client/ui/chat/notes.vue create mode 100644 src/client/ui/chat/post-form.vue create mode 100644 src/client/ui/chat/side.vue create mode 100644 src/client/ui/chat/sub-note-content.vue create mode 100644 src/client/ui/chat/timeline.vue diff --git a/src/client/components/global/avatar.vue b/src/client/components/global/avatar.vue index 9f8b0eeca..d2f25fa41 100644 --- a/src/client/components/global/avatar.vue +++ b/src/client/components/global/avatar.vue @@ -1,8 +1,8 @@ diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 997380919..bd6d5bb4f 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -8,7 +8,7 @@
- @@ -19,7 +19,7 @@
- diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue index 9759cc239..552b22dd3 100644 --- a/src/client/components/notifications.vue +++ b/src/client/components/notifications.vue @@ -5,7 +5,7 @@ - diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue index 251f68527..d07dd294a 100644 --- a/src/client/components/sidebar.vue +++ b/src/client/components/sidebar.vue @@ -55,6 +55,14 @@ import { sidebarDef } from '@/sidebar'; import { getAccounts, addAccount, login } from '@/account'; export default defineComponent({ + props: { + defaultHidden: { + type: Boolean, + required: false, + default: false, + } + }, + data() { return { host: host, @@ -63,7 +71,7 @@ export default defineComponent({ connection: null, menuDef: sidebarDef, iconOnly: false, - hidden: false, + hidden: this.defaultHidden, faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram }; }, @@ -112,7 +120,9 @@ export default defineComponent({ methods: { calcViewState() { this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.sidebarDisplay === 'icon'); - this.hidden = (window.innerWidth <= 650); + if (!this.defaultHidden) { + this.hidden = (window.innerWidth <= 650); + } }, show() { diff --git a/src/client/directives/follow-append.ts b/src/client/directives/follow-append.ts index 26f9e9f82..9490dcf78 100644 --- a/src/client/directives/follow-append.ts +++ b/src/client/directives/follow-append.ts @@ -3,12 +3,24 @@ import { getScrollContainer, getScrollPosition } from '@/scripts/scroll'; export default { mounted(src, binding, vn) { - const ro = new ResizeObserver((entries, observer) => { - const pos = getScrollPosition(src); - const container = getScrollContainer(src); + if (binding.value === false) return; + + let isBottom = true; + + const container = getScrollContainer(src)!; + container.addEventListener('scroll', () => { + const pos = getScrollPosition(container); const viewHeight = container.clientHeight; const height = container.scrollHeight; - if (pos + viewHeight > height - 32) { + isBottom = (pos + viewHeight > height - 32); + console.log(isBottom); + }, { passive: true }); + container.scrollTop = container.scrollHeight; + + const ro = new ResizeObserver((entries, observer) => { + console.log(isBottom); + if (isBottom) { + const height = container.scrollHeight; container.scrollTop = height; } }); @@ -20,6 +32,6 @@ export default { }, unmounted(src, binding, vn) { - src._ro_.unobserve(src); + if (src._ro_) src._ro_.unobserve(src); } } as Directive; diff --git a/src/client/init.ts b/src/client/init.ts index 17feca4c8..c3be85a85 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -182,6 +182,7 @@ const app = createApp(await ( !$i ? import('@/ui/visitor.vue') : ui === 'deck' ? import('@/ui/deck.vue') : ui === 'desktop' ? import('@/ui/desktop.vue') : + ui === 'chat' ? import('@/ui/chat/index.vue') : import('@/ui/default.vue') ).then(x => x.default)); diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts index 3d9668f10..a8f122412 100644 --- a/src/client/scripts/paging.ts +++ b/src/client/scripts/paging.ts @@ -1,9 +1,11 @@ import { markRaw } from 'vue'; import * as os from '@/os'; -import { onScrollTop, isTopVisible } from './scroll'; +import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll'; const SECOND_FETCH_LIMIT = 30; +// reversed: items 配列の中身を逆順にする(新しい方が最後) + export default (opts) => ({ emits: ['queue'], @@ -122,10 +124,41 @@ export default (opts) => ({ limit: SECOND_FETCH_LIMIT + 1, ...(this.pagination.offsetMode ? { offset: this.offset, - } : this.pagination.reversed ? { - sinceId: this.items[0].id, } : { - untilId: this.items[this.items.length - 1].id, + untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, + }), + }).then(items => { + for (const item of items) { + markRaw(item); + } + if (items.length > SECOND_FETCH_LIMIT) { + items.pop(); + this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); + this.more = true; + } else { + this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); + this.more = false; + } + this.offset += items.length; + this.moreFetching = false; + }, e => { + this.moreFetching = false; + }); + }, + + async fetchMoreFeature() { + if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; + this.moreFetching = true; + let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; + if (params && params.then) params = await params; + const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; + await os.api(endpoint, { + ...params, + limit: SECOND_FETCH_LIMIT + 1, + ...(this.pagination.offsetMode ? { + offset: this.offset, + } : { + sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, }), }).then(items => { for (const item of items) { @@ -147,25 +180,44 @@ export default (opts) => ({ }, prepend(item) { - const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el)); - - if (isTop) { - // Prepend the item - this.items.unshift(item); - - // オーバーフローしたら古いアイテムは捨てる - if (this.items.length >= opts.displayLimit) { - this.items = this.items.slice(0, opts.displayLimit); - this.more = true; - } - } else { - this.queue.push(item); - onScrollTop(this.$el, () => { - for (const item of this.queue) { - this.prepend(item); + if (this.pagination.reversed) { + const container = getScrollContainer(this.$el); + const pos = getScrollPosition(this.$el); + const viewHeight = container.clientHeight; + const height = container.scrollHeight; + const isBottom = (pos + viewHeight > height - 32); + if (isBottom) { + // オーバーフローしたら古いアイテムは捨てる + if (this.items.length >= opts.displayLimit) { + this.items = this.items.slice(-opts.displayLimit); + this.more = true; } - this.queue = []; - }); + } else { + + } + this.items.push(item); + // TODO + } else { + const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el)); + + if (isTop) { + // Prepend the item + this.items.unshift(item); + + // オーバーフローしたら古いアイテムは捨てる + if (this.items.length >= opts.displayLimit) { + this.items = this.items.slice(0, opts.displayLimit); + this.more = true; + } + } else { + this.queue.push(item); + onScrollTop(this.$el, () => { + for (const item of this.queue) { + this.prepend(item); + } + this.queue = []; + }); + } } }, diff --git a/src/client/scripts/scroll.ts b/src/client/scripts/scroll.ts index 18c336689..bc6d1530c 100644 --- a/src/client/scripts/scroll.ts +++ b/src/client/scripts/scroll.ts @@ -54,6 +54,14 @@ export function scroll(el: Element, top: number) { } } +export function scrollToTop(el: Element) { + scroll(el, 0); +} + +export function scrollToBottom(el: Element) { + scroll(el, 99999); // TODO: ちゃんと計算する +} + export function isBottom(el: Element, asobi = 0) { const container = getScrollContainer(el); const current = container diff --git a/src/client/sidebar.ts b/src/client/sidebar.ts index 98a70d2d1..d7822e9e0 100644 --- a/src/client/sidebar.ts +++ b/src/client/sidebar.ts @@ -141,6 +141,12 @@ export const sidebarDef = { localStorage.setItem('ui', 'deck'); location.reload(); } + }, { + text: 'Chat (β)', + action: () => { + localStorage.setItem('ui', 'chat'); + location.reload(); + } }, { text: i18n.locale.desktop + ' (β)', action: () => { diff --git a/src/client/style.scss b/src/client/style.scss index 1ac9b4e0b..14e8c8731 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -308,13 +308,6 @@ hr { box-shadow: none; } -._loadMore { - @extend ._panel; - @extend ._button; - width: 100%; - padding: 12px 0; -} - ._borderButton { @extend ._button; display: block; diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue index f662f6144..f150653a8 100644 --- a/src/client/ui/_common_/header.vue +++ b/src/client/ui/_common_/header.vue @@ -1,5 +1,5 @@