diff --git a/src/components/Main/MainView/ChannelView/ChannelViewContent/ChannelViewContentMain.vue b/src/components/Main/MainView/ChannelView/ChannelViewContent/ChannelViewContentMain.vue index 87d3508a33..f670d7ac5c 100644 --- a/src/components/Main/MainView/ChannelView/ChannelViewContent/ChannelViewContentMain.vue +++ b/src/components/Main/MainView/ChannelView/ChannelViewContent/ChannelViewContentMain.vue @@ -7,6 +7,7 @@ :is-reached-end="isReachedEnd" :is-reached-latest="isReachedLatest" :is-loading="isLoading" + :is-initial-load="isInitialLoad" :entry-message-id="entryMessageId" :last-loading-direction="lastLoadingDirection" @request-load-former="onLoadFormerMessagesRequest" @@ -29,6 +30,7 @@ :class="$style.element" :message-id="messageId" :is-archived="isArchived" + :is-initial-load="isInitialLoad" :is-entry-message="messageId === entryMessageId" :pinned-user-id="messagePinnedUserMap.get(messageId)" @change-height="onChangeHeight" @@ -85,6 +87,7 @@ const { isReachedEnd, isReachedLatest, isLoading, + isInitialLoad, lastLoadingDirection, unreadSince, onLoadFormerMessagesRequest, @@ -137,7 +140,7 @@ const toNewMessage = () => { } } - const element = unrefElement(scrollerRef) + const element = unrefElement(scrollerRef.value?.rootRef) if (!element) return element.scrollTo({ @@ -146,7 +149,7 @@ const toNewMessage = () => { } const handleScroll = () => { - const element = unrefElement(scrollerRef) + const element = unrefElement(scrollerRef.value?.rootRef) if (!element || isLoading.value) return const { scrollTop, scrollHeight, clientHeight } = element showToNewMessageButton.value = scrollHeight - 2 * clientHeight > scrollTop diff --git a/src/components/Main/MainView/MessageElement/ClipElement.vue b/src/components/Main/MainView/MessageElement/ClipElement.vue index 328825d9e9..6c3df45523 100644 --- a/src/components/Main/MainView/MessageElement/ClipElement.vue +++ b/src/components/Main/MainView/MessageElement/ClipElement.vue @@ -40,8 +40,9 @@ import type { MessageId } from '/@/types/entity-ids' import MessageQuoteListItemFooter from './Embeddings/MessageQuoteListItemFooter.vue' import MessageContents from './MessageContents.vue' import MessageTools, { useMessageToolsHover } from './MessageTools.vue' -import type { ChangeHeightData } from './composables/useElementRenderObserver' -import useElementRenderObserver from './composables/useElementRenderObserver' +import useElementRenderObserver, { + type ChangeHeightData +} from './composables/useElementRenderObserver' const props = defineProps<{ messageId: MessageId diff --git a/src/components/Main/MainView/MessageElement/MessageContents.vue b/src/components/Main/MainView/MessageElement/MessageContents.vue index 1f3bf65b3b..e556d34fe7 100644 --- a/src/components/Main/MainView/MessageElement/MessageContents.vue +++ b/src/components/Main/MainView/MessageElement/MessageContents.vue @@ -8,7 +8,11 @@ :updated-at="message.updatedAt" />
- + - - - + + + + +
@@ -39,6 +45,7 @@ diff --git a/src/components/Main/MainView/MessagesScroller/MessagesSkeleton.vue b/src/components/Main/MainView/MessagesScroller/MessagesSkeleton.vue new file mode 100644 index 0000000000..4aed495ae0 --- /dev/null +++ b/src/components/Main/MainView/MessagesScroller/MessagesSkeleton.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/src/components/Main/MainView/MessagesScroller/composables/useMessageScroller.ts b/src/components/Main/MainView/MessagesScroller/composables/useMessageScroller.ts new file mode 100644 index 0000000000..87a9520b8f --- /dev/null +++ b/src/components/Main/MainView/MessagesScroller/composables/useMessageScroller.ts @@ -0,0 +1,152 @@ +import type { MaybeRefOrGetter, Reactive, Ref } from 'vue' +import { reactive, toValue, watch } from 'vue' +import { ref } from 'vue' + +import type { ChangeHeightData } from '/@/components/Main/MainView/MessageElement/composables/useElementRenderObserver' +import type { LoadingDirection } from '/@/components/Main/MainView/MessagesScroller/composables/useMessagesFetcher' +import { defer } from '/@/lib/basic/timer' +import type { MessageId } from '/@/types/entity-ids' + +const useMessageScroller = ( + rootRef: Ref, + scrollerProps: Reactive<{ + messageIds: MessageId[] + lastLoadingDirection: LoadingDirection + entryMessageId?: MessageId + }>, + topSkeletonHeight: MaybeRefOrGetter = 0 +) => { + const ready = ref(false) + + const state = reactive({ + height: 0, + scrollTop: 0 + }) + + const onChangeHeight = async (payload: ChangeHeightData) => { + if (!rootRef.value) return + + if ( + scrollerProps.lastLoadingDirection === 'around' && + scrollerProps.entryMessageId + ) { + // エントリーメッセージがあり、かつ初回ロードの場合 + // エントリーより上にあった場合はスクロール位置をずらす + let isBeforeEntryMessage = false + for (const messageId of scrollerProps.messageIds) { + if (messageId === scrollerProps.entryMessageId) break + if (messageId === payload.id) { + isBeforeEntryMessage = true + break + } + } + + if (isBeforeEntryMessage) { + rootRef.value.scrollTo({ + top: rootRef.value.scrollTop + payload.heightDiff + }) + state.height = rootRef.value.scrollHeight + } + } else if ( + scrollerProps.lastLoadingDirection === 'latest' || + scrollerProps.lastLoadingDirection === 'former' + ) { + const scrollerTop = + rootRef.value.getBoundingClientRect().top + toValue(topSkeletonHeight) + + // 視界より上にある要素の高さが変わった場合のみ補正する + if (payload.top < scrollerTop) { + rootRef.value.scrollTo({ + top: rootRef.value.scrollTop + payload.heightDiff + }) + } + + const scrollBottom = rootRef.value.scrollTop + rootRef.value.clientHeight + + // 末尾付近を閲覧しているときは,末尾を保つ + if (state.height - scrollBottom <= 50) { + if (!rootRef.value) return + + // 複数の onChangeHeight が同フレーム内に複数回呼ばれる場合があるので, + // すべての更新を待ってからスクロールする + await defer() + rootRef.value.scrollTo({ top: rootRef.value.scrollHeight }) + } + + state.height = rootRef.value.scrollHeight + } + } + + const onEntryMessageLoaded = (relativePos: number) => { + if (!rootRef.value) return + + const rootHeight = rootRef.value.getBoundingClientRect().height + rootRef.value.scrollTo({ top: relativePos - rootHeight / 3 }) + } + + watch( + () => scrollerProps.messageIds, + (ids, prevIds) => { + if (!rootRef.value) return + /* state.height の更新を忘れないようにすること */ + + const newHeight = rootRef.value.scrollHeight + if ( + scrollerProps.lastLoadingDirection === 'latest' || + scrollerProps.lastLoadingDirection === 'former' + ) { + if (ids.length - prevIds.length === -1) { + // 削除された場合は何もしない + state.height = newHeight + return + } + // XXX: 追加時にここは0になる + if (ids.length - prevIds.length === 0) { + const scrollBottom = + rootRef.value.scrollTop + rootRef.value.clientHeight + + // 一番下のメッセージあたりを見ているときに、 + // 新規に一つ追加された場合は一番下までスクロール + if (state.height - 50 <= scrollBottom) { + rootRef.value.scrollTo({ top: newHeight }) + } + state.height = newHeight + + return + } + //上に追加された時はスクロール位置を変更する。 + if (scrollerProps.lastLoadingDirection === 'former') { + rootRef.value.scrollTo({ + top: rootRef.value.scrollTop + newHeight - state.height + }) + state.height = newHeight + } + + if (scrollerProps.lastLoadingDirection === 'latest') { + ready.value = false + + // チャンネルを移動したとき、 + rootRef.value.scrollTo({ + top: newHeight + }) + + state.height = newHeight + + defer(() => { + ready.value = true + }) + } + } else state.height = newHeight + }, + { deep: true, flush: 'post' } + ) + + return { + onChangeHeight, + onEntryMessageLoaded, + ready, + state + } +} + +export default useMessageScroller diff --git a/src/components/Main/MainView/MessagesScroller/composables/useMessageScrollerElementResizeObserver.ts b/src/components/Main/MainView/MessagesScroller/composables/useMessageScrollerElementResizeObserver.ts deleted file mode 100644 index a5d5a1effa..0000000000 --- a/src/components/Main/MainView/MessagesScroller/composables/useMessageScrollerElementResizeObserver.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Ref } from 'vue' - -import type { ChangeHeightData } from '/@/components/Main/MainView/MessageElement/composables/useElementRenderObserver' -import type { MessageId } from '/@/types/entity-ids' - -import type { LoadingDirection } from './useMessagesFetcher' - -const useMessageScrollerElementResizeObserver = ( - rootRef: Ref, - scrollerProps: { - messageIds: MessageId[] - lastLoadingDirection: LoadingDirection - entryMessageId?: MessageId - }, - viewPortState: { - height: number - skipResizeAdjustment?: boolean - } -) => { - const onChangeHeight = (payload: ChangeHeightData) => { - if (!rootRef.value) { - return - } - - if ( - scrollerProps.lastLoadingDirection === 'around' && - scrollerProps.entryMessageId - ) { - // エントリーメッセージがあり、かつ初回ロードの場合 - // エントリーより上にあった場合はスクロール位置をずらす - let isBeforeEntryMessage = false - for (const messageId of scrollerProps.messageIds) { - if (messageId === scrollerProps.entryMessageId) break - if (messageId === payload.id) { - isBeforeEntryMessage = true - break - } - } - - if (isBeforeEntryMessage) { - rootRef.value.scrollTop += payload.heightDiff - viewPortState.height = rootRef.value.scrollHeight - } - } else if ( - scrollerProps.lastLoadingDirection === 'latest' || - scrollerProps.lastLoadingDirection === 'former' - ) { - // 古いメッセージを読み込んですぐはここで高さの調整をしない - if (viewPortState.skipResizeAdjustment) { - return - } - rootRef.value.scrollTop += payload.heightDiff - viewPortState.height = rootRef.value.scrollHeight - } - } - - const onEntryMessageLoaded = (relativePos: number) => { - if (!rootRef.value) { - return - } - const rootHeight = rootRef.value.getBoundingClientRect().height - rootRef.value.scrollTop = relativePos - rootHeight / 3 - } - - return { onChangeHeight, onEntryMessageLoaded } -} - -export default useMessageScrollerElementResizeObserver diff --git a/src/components/Main/MainView/MessagesScroller/composables/useMessagesFetcher.ts b/src/components/Main/MainView/MessagesScroller/composables/useMessagesFetcher.ts index de7079ff7f..1ad11a5759 100644 --- a/src/components/Main/MainView/MessagesScroller/composables/useMessagesFetcher.ts +++ b/src/components/Main/MainView/MessagesScroller/composables/useMessagesFetcher.ts @@ -213,6 +213,7 @@ const useMessageFetcher = ( const init = () => { resetRenderedContent() + isInitialLoad.value = true if (props.entryMessageId) { onLoadAroundMessagesRequest(props.entryMessageId) } else { diff --git a/src/components/UI/DeferredRender.vue b/src/components/UI/DeferredRender.vue new file mode 100644 index 0000000000..2818b50ea0 --- /dev/null +++ b/src/components/UI/DeferredRender.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/UI/MarkdownContent.vue b/src/components/UI/MarkdownContent.vue index 2d125db7d4..e279177154 100644 --- a/src/components/UI/MarkdownContent.vue +++ b/src/components/UI/MarkdownContent.vue @@ -10,17 +10,25 @@