From f6279c4478a9ca383e9ebe451f3a5d99671080e1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 10 Sep 2026 23:07:08 +0300 Subject: [PATCH] =?UTF-8?q?virtual=20scrolling=20for=20timelines=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/conversation/conversation.js | 84 +++++++++++++++----- src/components/conversation/conversation.vue | 38 +++------ src/components/timeline/timeline.js | 31 +++++++- src/components/timeline/timeline.vue | 23 ++++-- 4 files changed, 121 insertions(+), 55 deletions(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 72fd45ba9..d6ab361d7 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,6 +1,14 @@ import { get } from 'lodash-es' import { storeToRefs } from 'pinia' -import { computed, provide, ref, toRefs, useTemplateRef, watch } from 'vue' +import { + computed, + onUnmounted, + provide, + ref, + toRefs, + useTemplateRef, + watch, +} from 'vue' import { useRouter } from 'vue-router' import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue' @@ -62,7 +70,8 @@ export default { PostStatusForm, RichContent, }, - setup(props) { + emits: ['heightChange', 'suspendableStateChange'], + setup(props, { emit }) { // # Helpers const getStatusObject = (id) => useStatusesStore().allStatuses.get(id) const getConversationId = (statusId) => { @@ -190,6 +199,27 @@ export default { const { fontSize } = useInterfaceSizes() + // External virtual scrolling + const unsuspendableIds = ref(new Set()) + const suspendable = computed( + () => !isExpanded.value && unsuspendableIds.value.size === 0, + ) + const rootElement = useTemplateRef('root') + const updateVirtualHeight = (e) => { + const [entry] = e + emit('heightChange', { + id: statusId.value, + height: entry.contentRect.height, + element: rootElement, + }) + } + const resizeObserver = ref(new ResizeObserver(updateVirtualHeight)) + watch(rootElement, () => resizeObserver.value.observe(rootElement.value)) + watch(suspendable, (value) => + emit('suspendableStateChange', { suspend: value, id: statusId.value }), + ) + onUnmounted(() => resizeObserver.value.disconnect()) + // Placeholder heights. const mutedStatusHeight = computed(() => fontSize.value * 1.5) const normalStatusHeight = computed(() => fontSize.value * 10) @@ -215,6 +245,15 @@ export default { anchorRepeatId: currentStatus.value?.id, getPlaceholderHeight, }) + const changeSuspendStateLinearLocal = (e) => { + changeSuspendStateLinear(e) + const { id, suspend } = e + if (suspend) { + unsuspendableIds.value.add(id) + } else { + unsuspendableIds.value.delete(id) + } + } // # Tree style stuff const isTreeView = computed(() => displayStyle.value === 'tree') @@ -240,20 +279,24 @@ export default { scrollCompensation: treeScrollCompensation, getPlaceholderHeight, }) - - const currentLevel = computed(() => [currentStatus.value].filter(Boolean)) - const currentLevelElement = useTemplateRef('currentLevel') - const { - heightChart: heightChartCurrentLevel, - changeSuspendState: changeSuspendStateCurrentLevel, - updateVirtualHeight: updateVirtualHeightCurrentLevel, - } = useVirtualScrolling({ - list: currentLevel, - body: currentLevelElement, - scrollPositionInstance: scroller, - scrollCompensation: false, - getPlaceholderHeight, - }) + const changeSuspendStateAncestorsLocal = (e) => { + changeSuspendStateAncestors(e) + const { id, suspend } = e + if (suspend) { + unsuspendableIds.value.add(id) + } else { + unsuspendableIds.value.delete(id) + } + } + const changeSuspendStateCurrentLevelLocal = (e) => { + changeSuspendStateLinear(e) + const { id, suspend } = e + if (suspend) { + unsuspendableIds.value.add(id) + } else { + unsuspendableIds.value.delete(id) + } + } const treeViewIsSimple = computed( () => !mergedConfig.value.conversationTreeAdvanced, @@ -299,7 +342,7 @@ export default { // ## Linear virtual scrolling heightChartLinear, - changeSuspendStateLinear, + changeSuspendStateLinearLocal, updateVirtualHeightLinear, // # Tree style stuff @@ -307,11 +350,10 @@ export default { // ## Tree virtual scrolling heightChartAncestors, - changeSuspendStateAncestors, + changeSuspendStateAncestorsLocal, updateVirtualHeightAncestors, - heightChartCurrentLevel, - changeSuspendStateCurrentLevel, - updateVirtualHeightCurrentLevel, + + changeSuspendStateCurrentLevelLocal, // ## Tree state // ### Topology diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 8d4c4f967..abfd57d48 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -112,7 +112,7 @@ @goto="setFocused" @dive="diveIntoStatus(element.item.id)" - @suspendable-state-change="changeSuspendStateAncestors" + @suspendable-state-change="changeSuspendStateAncestorsLocal" @height-change="updateVirtualHeightAncestors" />
-
- - - -
+ @goto="setFocused" + @dive="diveIntoStatus" + @toggle-expanded="toggleExpanded" + @show-thread-recursively="showThreadRecursively" + @suspendable-state-change="changeSuspendStateCurrentLevelLocal" + />
(skipPinned.value ? !pinned : true)) }) + // Virtual scrolling + const { fontSize } = useInterfaceSizes() + + // Placeholder heights. + const mutedStatusHeight = computed(() => fontSize.value * 1.5) + const normalStatusHeight = computed(() => fontSize.value * 10) + const getPlaceholderHeight = (id) => + filteredVisibleStatuses.value.find((item) => item.id === id)?.muted + ? mutedStatusHeight + : normalStatusHeight + + const body = useTemplateRef('timeline') + const { heightChart, changeSuspendState, updateVirtualHeight } = + useVirtualScrolling({ + list: filteredVisibleStatuses, + body, + scrollPositionInstance: useScrollPosition(), + scrollCompensation: false, + getPlaceholderHeight, + }) + // Counter const count = computed(() => timeline.value.order.length) const newStatusCount = computed(() => timeline.value.newStatusCount) @@ -240,6 +265,10 @@ const Timeline = { timeline, filteredVisibleStatuses, + heightChart, + updateVirtualHeight, + changeSuspendState, + count, showLoadButton, diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 8388f59e7..88cfdf066 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -82,12 +82,23 @@ ref="timeline" role="feed" > - +