From 8eed3f14f9f5bb730971182d5c2a2b3327bf29a7 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 16 Sep 2026 13:57:48 +0300 Subject: [PATCH 01/13] restore store-based convo display, but still use fullyLoaded --- src/composables/useConversation.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/composables/useConversation.js b/src/composables/useConversation.js index 8bb25fdbd..24d8bae53 100644 --- a/src/composables/useConversation.js +++ b/src/composables/useConversation.js @@ -51,7 +51,6 @@ export function useConversation(statusId, expanded) { return idA < idB ? -1 : 1 } } - const fullConversation = ref(new Set([mainStatus.value?.id].filter(Boolean))) const fullyLoaded = ref(false) const conversationId = computed( () => mainStatus.value?.statusnet_conversation_id, @@ -64,11 +63,15 @@ export function useConversation(statusId, expanded) { return [] } - if (!expanded.value) { + if (!expanded.value || !fullyLoaded.value) { return [currentStatus.value] } - return [...fullConversation.value.keys()] + const fullConversation = useStatusesStore().conversations.get( + conversationId.value, + ) + + return [...fullConversation.keys()] .map((k) => useStatusesStore().allStatuses.get(k)) .filter((status) => status.type != 'repeat') // Old backend behavior? .toSorted(sortById) @@ -111,12 +114,6 @@ export function useConversation(statusId, expanded) { timestamp, }) - fullConversation.value = new Set([ - ...ancestors, - mainStatus.value, - ...descendants - ].map(({ id }) => id)) - await nextTick() fullyLoaded.value = true } else { @@ -129,9 +126,7 @@ export function useConversation(statusId, expanded) { }) useStatusesStore().addNewStatuses({ statuses: [status] }) - fullConversation.value = new Set([ - currentStatus.value, - ].map(({ id }) => id)) + fetchConversation() } catch (error) { console.error(error) From bbb8b10bdf852fd9d93a7f811ae546316c3e3039 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 16 Sep 2026 14:11:48 +0300 Subject: [PATCH 02/13] comment --- src/composables/useConversation.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/composables/useConversation.js b/src/composables/useConversation.js index 24d8bae53..e2aca729f 100644 --- a/src/composables/useConversation.js +++ b/src/composables/useConversation.js @@ -67,6 +67,36 @@ export function useConversation(statusId, expanded) { return [currentStatus.value] } + /* It took me a week or so to figure this out. + * + * Virtual scrolling can compensate for posts being prepended to content, + * and prepended posts changing height. The problem is that it has to happen + * in prepended (i.e. either above visible post and/or above screen boundary) + * + * Here's the problem: showing conversation from store can be broken. UI might + * already know that some posts belong to a conversation, because you were + * mentioned in it, but doesn't know the rest of it. It ends up displaying this + * "partial" conversation, and then the rest loads in. + * + * Problem is, this partial conversation can be very fragmented, with missing + * pieces appearing in-between posts. These pieces don't have proper heights + * assigned to them yet but their neigbours do and neither me nor virtual + * scrolling knows how to compensate for it, it ends up either not compensating + * or compensating wrong. + * + * Using "fullyLoaded" ref helps with this, to ensure that we have stable + * conversation expansion process of focused post -> entire convo + * + * With fullyLoaded: + * **id:3** -> id:1 id:2 id:3 id:4 id:5 id:6 + * + * Without fullyLoaded: + * id:1 **id:3** id:5 -> id:1 id:2 **id:3** id:4 id:5 id:6 + * (focused post is **id:3**) + * + * After initial load, fullyLoaded remains true, allowing newer updates to + * appear in conversation. + */ const fullConversation = useStatusesStore().conversations.get( conversationId.value, ) From 310e17d3f2b47d2fdb879f8c96388f83721d8d18 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 16 Sep 2026 14:19:44 +0300 Subject: [PATCH 03/13] remove broken placeholder size switch --- src/components/conversation/conversation.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 9855c01cd..10f353fe1 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -181,12 +181,8 @@ export default { // Placeholder heights. const { fontSize, navbarSize, panelHeaderSize } = useInterfaceSizes() - const mutedStatusHeight = computed(() => fontSize.value * 1.5) const normalStatusHeight = computed(() => fontSize.value * 10) - const getPlaceholderHeight = (id) => - conversation.value.find((item) => item.id === id)?.muted - ? mutedStatusHeight - : normalStatusHeight + const getPlaceholderHeight = (id) => normalStatusHeight const offset = computed(() => { // The fontsize after navbar is the little gap between navbar and content if (isPage.value) { From 06596ee6c0c23984ec15310d4b08cae3c2c86b23 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 16 Sep 2026 14:20:36 +0300 Subject: [PATCH 04/13] todo --- src/components/status/status.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/status/status.js b/src/components/status/status.js index ec685b0d2..170d1a7b2 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -312,6 +312,7 @@ const Status = { hasMentionsLine() { return this.mentionsLine.length > 0 }, + // TODO move muting logic into some store muteReasons() { return [ this.userIsMuted ? 'user' : null, From 4495223ef5545ecbca97f4752b97c05fad6d4da0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 16 Sep 2026 14:39:30 +0300 Subject: [PATCH 05/13] tree view + dive fixes --- src/components/conversation/conversation.js | 4 ++-- src/components/conversation/conversation.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 10f353fe1..429480816 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -314,8 +314,8 @@ export default { return linearScrollTo(ids) } } - const diveIntoStatus = (id) => scrollTo(new Set([id])) - const diveToTopLevel = () => scrollTo(new Set([currentAncestors.value[0].id])) + const diveIntoStatus = (id) => setFocused(id) + const diveToTopLevel = () => setFocused(currentAncestors.value[0].id) watch(focusedId, async (neu, old) => { // Ignoring initial update (null -> id) since that is handled by scroll compensation diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index b6fd34ebb..893d84591 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -125,7 +125,7 @@ Date: Wed, 16 Sep 2026 14:39:56 +0300 Subject: [PATCH 06/13] fixes & cleanup --- src/components/conversation/conversation.js | 12 +++--------- src/components/status/status.js | 3 +++ src/components/status/status.vue | 2 +- src/composables/useVirtualScrolling.js | 6 ------ 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 429480816..f6f4e79e0 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -149,7 +149,7 @@ export default { return result } - // External virtual scrolling + // # External virtual scrolling const unsuspendableIds = ref(new Set()) const suspendable = computed( () => !isExpanded.value && unsuspendableIds.value.size === 0, @@ -176,7 +176,8 @@ export default { emit('suspendableStateChange', { suspend: value, id: statusId.value }), ) onUnmounted(() => resizeObserver.value.disconnect()) - // Internal virtual scrolling + + // # Internal virtual scrolling const virtualScrollingEnabled = ref(isExpanded.value) // Placeholder heights. @@ -194,10 +195,6 @@ export default { } }) - const anchorIds = computed( - () => new Set([mainStatus.value?.id, currentStatus.value?.id]), - ) - // # Linear style stuff const isLinearView = computed(() => displayStyle.value !== 'tree') const linearElement = useTemplateRef('linear') @@ -218,8 +215,6 @@ export default { offset, scrollPositionInstance: scroller, scrollCompensation: linearScrollCompensation, - anchorIds, - collapseMode: 'item', getPlaceholderHeight, }) const changeSuspendStateLinearLocal = (e) => { @@ -297,7 +292,6 @@ export default { resetTreeScrollVirtualization() }) - const treeViewIsSimple = computed( () => !mergedConfig.value.conversationTreeAdvanced, ) diff --git a/src/components/status/status.js b/src/components/status/status.js index 170d1a7b2..79c6f4050 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -192,6 +192,9 @@ const Status = { user() { return useUsersStore().findUser(this.mainStatus.user.id) }, + isTreeView() { + return this.mergedConfig.conversationDisplay === 'tree' + }, simpleTree() { return !this.mergedConfig.conversationTreeAdvanced }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index fa288145f..0306ff2cb 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -235,7 +235,7 @@ />