Virtual Scrolling 2.0 #3573

Merged
hj merged 102 commits from virtual-scrolling-2.0 into develop 2026-09-21 16:25:05 +00:00
Member

Internal changes:

  • Refactored Conversation component to use Composition API
    • Cleaned up, optimized and separated tree/threaded view-related things into a useTreeConversationTopology composable
    • Stuff that Conversation and Chat view share is separated into useConversation composable
  • Removed old timeline-based virtual scrolling
  • Introduced new virtual scrolling provided by useVirtualScrolling composable
    • Takes in a list of statuses and transforms it into a list of element.
      • Each element can be either a status or spacer.
        • Status is just status as is
        • Spacer only has height (+ some extra stuff for debugging)
      • Each element tracks its top offset and height
      • Spacers are grouped together into one big spacer, with height equal to sum of spacers
    • Unlike vue-virtual-scroller it accounts for "suspend" state, i.e. posts with playing media or open reply form don't get replaced by spacer
    • Virtualization happens even when not on conversation view, i.e. it also affects posts on timeline.
    • Virtualization also applied separately to timeline.
Internal changes: - Refactored `Conversation` component to use Composition API - Cleaned up, optimized and separated tree/threaded view-related things into a `useTreeConversationTopology` composable - Stuff that `Conversation` and `Chat view` share is separated into `useConversation` composable - Removed old timeline-based virtual scrolling - Introduced new virtual scrolling provided by `useVirtualScrolling` composable - Takes in a list of statuses and transforms it into a list of element. - Each element can be either a `status` or `spacer`. - Status is just status as is - Spacer only has height (+ some extra stuff for debugging) - Each element tracks its top offset and height - Spacers are grouped together into one big spacer, with height equal to sum of spacers - Unlike `vue-virtual-scroller` it accounts for "suspend" state, i.e. posts with playing media or open reply form don't get replaced by spacer - Virtualization happens even when not on conversation view, i.e. it also affects posts on timeline. - Virtualization also applied separately to timeline.
hj added 31 commits 2026-09-09 17:34:09 +00:00
fix
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline failed
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was canceled
e9197a6b66
changelog
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
1d9b458e74
hj changed title from Virtual Scrolling 2.0 to WIP: Virtual Scrolling 2.0 2026-09-09 17:41:51 +00:00

Local review findings by astra

Reviewed commit 1d9b458e7482963846cd83b2f4aeaa8c7bdc9b58 against develop at b90938c7bca9fb1e58c783e0a24a4f937616f614.

1. P1 — Navigation throws for status IDs starting with a digit

src/components/conversation/conversation.js:85

The selector interpolates the ID without quotes:

document.querySelector(`.Status[data-status-id=${id}]`)

An attribute-selector value beginning with a digit is invalid CSS. Calling diveIntoStatus('123456789012345') rejects with a SyntaxError, before reaching the scrolling helper. This affects navigation through tryScrollTo, including inline expansion/collapse and thread-dive actions. Quote and escape the attribute value.

Reproduced in a focused Chromium test using the component's actual setup/method.

2. P2 — Hidden posts throw during mounting

src/components/status/status.js:600–602

When hideStatus is true, the root v-if renders a comment node. this.resizeObserver.observe(this.$el) then throws:

TypeError: Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element'.

Observe the actual element ref and account for hidden/visible transitions, rather than unconditionally observing $el once at mount.

Reproduced in Chromium by mounting Status with hideStatus forced true.

3. P2 — The tree-depth setting is capped at three visible levels

src/composables/useTreeConversationTopology.js:17–18

const maxDepth = mergedConfig.value.maxDepthInThread - 2
return Math.min(1, maxDepth)

This reverses the previous lower-bound check. With maxDepthInThread = 6, a depth-two node is marked hidden, so its children are not expanded despite being within the configured depth. This should use Math.max(1, maxDepth).

Reproduced with a six-post chain in a focused test.

4. P2 — Layout shifts can leave visible posts virtualized

src/composables/useVirtualScrolling.js:88–91

Boundary refreshes watch viewport height, scroll Y, the conversation's own total height, and its body ref. They do not track changes to its position caused by other elements.

For example, collapsing an earlier inline conversation can move later spacer-only conversations into the viewport without changing any of those watched inputs. The later posts remain spacers until another refresh trigger occurs. Visibility needs to be refreshed for external layout changes as well as local height/scroll changes.

Reproduced in a mounted Chromium fixture: shrinking a preceding sibling from 2,000px to zero moves the conversation into view, but its chart entry remains spacer instead of status.

5. P2 — Firefox compatibility issue in the scrolling helper

src/composables/useScrollPosition.js:27–30

The helper unconditionally calls the native DOM method element.scrollIntoViewIfNeeded(options), with no fallback or polyfill found in the source. Can I Use lists this method as unsupported in Firefox; MDN describes it as proprietary/WebKit-specific.

The affected navigation actions are therefore expected to throw when this helper is reached in Firefox. Ordinary scrolling does not invoke it. A rejection also leaves inProgress true because resetting it is not in a finally block. Prefer standard scrollIntoView with visibility checks and try/finally.

Compatibility/source finding, not an application-level Firefox reproduction. A direct Firefox verification attempt was blocked by a Playwright page-creation error. This is distinct from the numeric-selector issue above; an alphabetic-leading ID would avoid that earlier failure.

Verification

  • Existing Chromium unit suite: 37 test files passed; 471 tests passed, 2 skipped.
  • Production build: passed.
  • Four additional local review tests: four expected failures, reproducing findings 1–4.
  • No live-instance end-to-end verification; Firefox application reproduction remains pending.
## Local review findings by astra Reviewed commit `1d9b458e7482963846cd83b2f4aeaa8c7bdc9b58` against `develop` at `b90938c7bca9fb1e58c783e0a24a4f937616f614`. ### 1. P1 — Navigation throws for status IDs starting with a digit **`src/components/conversation/conversation.js:85`** The selector interpolates the ID without quotes: ```js document.querySelector(`.Status[data-status-id=${id}]`) ``` An attribute-selector value beginning with a digit is invalid CSS. Calling `diveIntoStatus('123456789012345')` rejects with a `SyntaxError`, before reaching the scrolling helper. This affects navigation through `tryScrollTo`, including inline expansion/collapse and thread-dive actions. Quote and escape the attribute value. **Reproduced in a focused Chromium test using the component's actual setup/method.** ### 2. P2 — Hidden posts throw during mounting **`src/components/status/status.js:600–602`** When `hideStatus` is true, the root `v-if` renders a comment node. `this.resizeObserver.observe(this.$el)` then throws: ```text TypeError: Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'Element'. ``` Observe the actual element ref and account for hidden/visible transitions, rather than unconditionally observing `$el` once at mount. **Reproduced in Chromium by mounting Status with `hideStatus` forced true.** ### 3. P2 — The tree-depth setting is capped at three visible levels **`src/composables/useTreeConversationTopology.js:17–18`** ```js const maxDepth = mergedConfig.value.maxDepthInThread - 2 return Math.min(1, maxDepth) ``` This reverses the previous lower-bound check. With `maxDepthInThread = 6`, a depth-two node is marked `hidden`, so its children are not expanded despite being within the configured depth. This should use `Math.max(1, maxDepth)`. **Reproduced with a six-post chain in a focused test.** ### 4. P2 — Layout shifts can leave visible posts virtualized **`src/composables/useVirtualScrolling.js:88–91`** Boundary refreshes watch viewport height, scroll Y, the conversation's own total height, and its body ref. They do not track changes to its position caused by other elements. For example, collapsing an earlier inline conversation can move later spacer-only conversations into the viewport without changing any of those watched inputs. The later posts remain spacers until another refresh trigger occurs. Visibility needs to be refreshed for external layout changes as well as local height/scroll changes. **Reproduced in a mounted Chromium fixture: shrinking a preceding sibling from 2,000px to zero moves the conversation into view, but its chart entry remains `spacer` instead of `status`.** ### 5. P2 — Firefox compatibility issue in the scrolling helper **`src/composables/useScrollPosition.js:27–30`** The helper unconditionally calls the native DOM method `element.scrollIntoViewIfNeeded(options)`, with no fallback or polyfill found in the source. [Can I Use](https://caniuse.com/scrollintoviewifneeded) lists this method as unsupported in Firefox; [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded) describes it as proprietary/WebKit-specific. The affected navigation actions are therefore expected to throw when this helper is reached in Firefox. Ordinary scrolling does not invoke it. A rejection also leaves `inProgress` true because resetting it is not in a `finally` block. Prefer standard `scrollIntoView` with visibility checks and `try/finally`. **Compatibility/source finding, not an application-level Firefox reproduction.** A direct Firefox verification attempt was blocked by a Playwright page-creation error. This is distinct from the numeric-selector issue above; an alphabetic-leading ID would avoid that earlier failure. ### Verification - Existing Chromium unit suite: **37 test files passed; 471 tests passed, 2 skipped**. - Production build: **passed**. - Four additional local review tests: **four expected failures**, reproducing findings 1–4. - No live-instance end-to-end verification; Firefox application reproduction remains pending.
hj added 7 commits 2026-09-10 16:10:44 +00:00
scroll.
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
b7943cac84
fix
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
be8c5c2532
fix more issues astra found
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
ca7b866229
Author
Member
  1. P1 — Navigation throws for status IDs starting with a digit

fixed

  1. P2 — Hidden posts throw during mounting

fixed

  1. P2 — The tree-depth setting is capped at three visible levels

fixed

  1. P2 — Firefox compatibility issue in the scrolling helper

fixed

We no longer do scroll to element, instead rely on scroll compensation of virtual scrolling

>1. P1 — Navigation throws for status IDs starting with a digit fixed >2. P2 — Hidden posts throw during mounting fixed >3. P2 — The tree-depth setting is capped at three visible levels fixed >5. P2 — Firefox compatibility issue in the scrolling helper fixed We no longer do scroll to element, instead rely on scroll compensation of virtual scrolling
lint
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
313a1758a5
hj added 2 commits 2026-09-10 20:08:16 +00:00
virtual scrolling for timelines™
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
f6279c4478
Author
Member
  1. P2 — Layout shifts can leave visible posts virtualized

I added virtualization to timelines, it should fix this issue

>4. P2 — Layout shifts can leave visible posts virtualized I added virtualization to timelines, it should fix this issue
unlike composition api, refs are not reactive in options api
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
200d304190
Member

Expanding a repeated status will not show the thread of the original
image

Expanding a repeated status will not show the thread of the original ![image](/attachments/09bf7ca5-bc2c-4458-9dea-81c5b8c5323c)
177 KiB
hj added 4 commits 2026-09-11 16:10:15 +00:00
lint
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
41ecad7fc0
hj added 9 commits 2026-09-15 23:04:41 +00:00
omg i think i actually did it
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
5c316ff175
focusedId shenanigans
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
c4b3eff348
fix
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
6d94cfb8fe
brilliant idea
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
29de7a1d5d
fix reprooted posts
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
d4c5e031c2
hj added 3 commits 2026-09-16 11:20:09 +00:00
remove broken placeholder size switch
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
310e17d3f2
hj added 6 commits 2026-09-16 13:02:45 +00:00
cleanup
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
5d1e5ea4d5
hj added 3 commits 2026-09-16 17:44:55 +00:00
more tests
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
d0b00db195
lint & cleanup
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
83f009cfed
make it a bit more clear how suspendable state works, also fix for it
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
353c0f4a12
remove border on conversations when it's a separate page
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
d5c4f91bb1
hj added 2 commits 2026-09-17 14:20:49 +00:00
virtual scrolling test
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
7586199271
hj changed title from WIP: Virtual Scrolling 2.0 to Virtual Scrolling 2.0 2026-09-17 14:37:20 +00:00
hj added 3 commits 2026-09-17 17:30:30 +00:00
fix quotes appearing expandable
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
6be2c6bc56
Author
Member

Expanding a repeated status will not show the thread of the original

fixed now

>Expanding a repeated status will not show the thread of the original fixed now
Author
Member

/oc review

/oc review
First-time contributor

opencode-agent:job=84:generation=1

Review of virtual-scrolling-2.0 @ 6be2c6bc56d722ab513d386da14453a7b60a5eea against develop @ b90938c7bca9fb1e58c783e0a24a4f937616f614 (82 commits, 36 files, +2607/−1178), triggered by the /oc review request in this PR.

Verification performed

  • Full unit suite at the exact head commit (vitest in browser mode, chromium headless): 41 test files, 490 passed, 2 skipped — including the 5 new composable spec files.
  • biome check: clean. stylelint '**/*.scss' '**/*.vue': clean.
  • Base SHA is the exact merge-base of the claimed head, so the diff reviewed is the precise base→head delta.

Findings

P2 — user_profile.js:37: provided profileUserId is permanently null

provide() {
  return {
    profileUserId: this.userId,
  }
}

The function form of provide is evaluated once at init, when this.userId is still null (it is only assigned later, in created()load(), asynchronously for name-based profile URLs). Plain provide values are not reactive, so every descendant injects profileUserId: null forever. Consequences:

  • Status.shouldNotMute() (status.js:391) loses the "don't mute the profile owner's own posts on their timeline" exemption — on a user profile page, posts by that user matching mute words will now render as muted, a behavior regression vs. develop.
  • The in-profile attributes user_profile.vue still puts on <Timeline> are dead fallthrough attributes now that Timeline no longer declares the prop — the whole Timeline → Conversation → Status chain silently depends on the broken injection.

Fix: provide a computed, which Vue unwraps and keeps reactive for injectors:
provide() { return { profileUserId: computed(() => this.userId) } }

P3 — useConversation.js:27: ref compared to a constant — streamingEnabled is always false

const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
...
mastoUserSocketStatus === WSConnectionStatus.JOINED,

mastoUserSocketStatus is a ref here (in the old mapState version this was ported from, this.mastoUserSocketStatus was an unwrapped number). Comparing the ref object to WSConnectionStatus.JOINED (1) is always false, and since the ref is never .value-accessed, the computed doesn't even track socket state. Effect: fetchStatus(newVal) at useConversation.js:193 now fires on every focus change even when the streaming socket is joined — redundant fetching, and the "skip when streaming" optimization is silently dead.
Fix: mastoUserSocketStatus.value === WSConnectionStatus.JOINED.
(Note: the new useConversation.spec.js doesn't cover this branch, which is why it slipped through.)

P3 — developer_tab.vue:33: dead "virtual scrolling" debug toggle
This PR removes the virtualScrolling definition from default_config_state.js, but developer_tab.vue still renders <BooleanSetting path="virtualScrolling">. The toggle now points at a nonexistent config and does nothing, since virtualization is unconditionally on for timelines. Either remove the toggle or repoint it at whatever should control the new behavior.

P4 — conversation.js:129: legnth typo

const lastStatus = computed(
  () => conversation.value[conversation.value.legnth - 1],
)

lastStatus is always undefined, so getStatusClasses() never applies -last — and conversation.scss's new .conversation-status:not(.-last) { border-bottom: ... } therefore also draws a trailing border under the last status in linear view.

P4 — timeline.scss:7: dead rule left over from the old implementation
.vue-recycle-scroller.timeline { overflow: visible; } — the new timeline is a plain <div class="timeline" role="feed">; no element in it ever gets vue-recycle-scroller. Looks like a leftover from a vue-virtual-scroller-based iteration.

Nits / robustness

  • useVirtualScrolling.scrollTo() dereferences element.top without checking that find() matched; an if (!element) return guard would make the watch(focusedId) path in conversation.js:312 resilient if a focused id is ever absent from the chart.
  • The heights/unsuspendibleIds maps only shrink via reset(); the Timeline instance never resets them, so they grow monotonically during long scrolling sessions. Fine for now, but a prune would help very long timelines.
  • Spacer <div>s in timeline.vue lack aria-hidden="true", unlike the conversation spacers which have it.
  • useConversation.fetchConversation(): the "status already known" branch has no error handling, so a failed context fetch is an unhandled rejection (loadError only covers the initial-fetch branch). Pre-existing in the old component, but a good opportunity now that it moved into a composable.
  • conversation.js still imports/registers ChatMessageList and PostStatusForm although neither appears in the template (carried over from base), and onUnmounted(() => { timelineChange(null, timelineRef.value) // ???? }) carries an unresolved ???? comment into the PR.

Overall

The direction is sound and clearly better than the old index-approximation approach: the fullyLoaded gate (with its excellent write-up in useConversation.js) solves the fragmented-partial-conversation compensation problem, suspend-state handling is a real advantage over vue-virtual-scroller, and the new composables come with genuinely useful tests. The scroll-compensation heuristics are intricate but well-commented. I'd ask for the P2 (broken profileUserId provide) and the two P3s (dead streamingEnabled, dead developer toggle) to be addressed before merge; nothing else looks structurally blocking.

Reviewed commit 6be2c6bc56d722ab513d386da14453a7b60a5eea against develop at b90938c7bca9fb1e58c783e0a24a4f937616f614.

opencode-agent:job=84:generation=1 Review of `virtual-scrolling-2.0` @ `6be2c6bc56d722ab513d386da14453a7b60a5eea` against `develop` @ `b90938c7bca9fb1e58c783e0a24a4f937616f614` (82 commits, 36 files, +2607/−1178), triggered by the `/oc review` request in this PR. ### Verification performed - Full unit suite at the exact head commit (vitest in browser mode, chromium headless): **41 test files, 490 passed, 2 skipped** — including the 5 new composable spec files. - `biome check`: clean. `stylelint '**/*.scss' '**/*.vue'`: clean. - Base SHA is the exact merge-base of the claimed head, so the diff reviewed is the precise base→head delta. ### Findings **P2 — `user_profile.js:37`: provided `profileUserId` is permanently `null`** ```js provide() { return { profileUserId: this.userId, } } ``` The function form of `provide` is evaluated once at init, when `this.userId` is still `null` (it is only assigned later, in `created()` → `load()`, asynchronously for name-based profile URLs). Plain provide values are not reactive, so every descendant injects `profileUserId: null` forever. Consequences: - `Status.shouldNotMute()` (status.js:391) loses the "don't mute the profile owner's own posts on their timeline" exemption — on a user profile page, posts by that user matching mute words will now render as muted, a behavior regression vs. `develop`. - The `in-profile` attributes `user_profile.vue` still puts on `<Timeline>` are dead fallthrough attributes now that `Timeline` no longer declares the prop — the whole `Timeline → Conversation → Status` chain silently depends on the broken injection. Fix: provide a computed, which Vue unwraps and keeps reactive for injectors: `provide() { return { profileUserId: computed(() => this.userId) } }` **P3 — `useConversation.js:27`: ref compared to a constant — `streamingEnabled` is always `false`** ```js const { mastoUserSocketStatus } = storeToRefs(useStreamingStore()) ... mastoUserSocketStatus === WSConnectionStatus.JOINED, ``` `mastoUserSocketStatus` is a **ref** here (in the old `mapState` version this was ported from, `this.mastoUserSocketStatus` was an unwrapped number). Comparing the ref object to `WSConnectionStatus.JOINED` (`1`) is always false, and since the ref is never `.value`-accessed, the computed doesn't even track socket state. Effect: `fetchStatus(newVal)` at useConversation.js:193 now fires on every focus change even when the streaming socket is joined — redundant fetching, and the "skip when streaming" optimization is silently dead. Fix: `mastoUserSocketStatus.value === WSConnectionStatus.JOINED`. (Note: the new `useConversation.spec.js` doesn't cover this branch, which is why it slipped through.) **P3 — `developer_tab.vue:33`: dead "virtual scrolling" debug toggle** This PR removes the `virtualScrolling` definition from `default_config_state.js`, but `developer_tab.vue` still renders `<BooleanSetting path="virtualScrolling">`. The toggle now points at a nonexistent config and does nothing, since virtualization is unconditionally on for timelines. Either remove the toggle or repoint it at whatever should control the new behavior. **P4 — `conversation.js:129`: `legnth` typo** ```js const lastStatus = computed( () => conversation.value[conversation.value.legnth - 1], ) ``` `lastStatus` is always `undefined`, so `getStatusClasses()` never applies `-last` — and `conversation.scss`'s new `.conversation-status:not(.-last) { border-bottom: ... }` therefore also draws a trailing border under the last status in linear view. **P4 — `timeline.scss:7`: dead rule left over from the old implementation** `.vue-recycle-scroller.timeline { overflow: visible; }` — the new timeline is a plain `<div class="timeline" role="feed">`; no element in it ever gets `vue-recycle-scroller`. Looks like a leftover from a `vue-virtual-scroller`-based iteration. ### Nits / robustness - `useVirtualScrolling.scrollTo()` dereferences `element.top` without checking that `find()` matched; an `if (!element) return` guard would make the `watch(focusedId)` path in `conversation.js:312` resilient if a focused id is ever absent from the chart. - The `heights`/`unsuspendibleIds` maps only shrink via `reset()`; the Timeline instance never resets them, so they grow monotonically during long scrolling sessions. Fine for now, but a prune would help very long timelines. - Spacer `<div>`s in `timeline.vue` lack `aria-hidden="true"`, unlike the conversation spacers which have it. - `useConversation.fetchConversation()`: the "status already known" branch has no error handling, so a failed context fetch is an unhandled rejection (`loadError` only covers the initial-fetch branch). Pre-existing in the old component, but a good opportunity now that it moved into a composable. - `conversation.js` still imports/registers `ChatMessageList` and `PostStatusForm` although neither appears in the template (carried over from base), and `onUnmounted(() => { timelineChange(null, timelineRef.value) // ???? })` carries an unresolved `????` comment into the PR. ### Overall The direction is sound and clearly better than the old index-approximation approach: the `fullyLoaded` gate (with its excellent write-up in `useConversation.js`) solves the fragmented-partial-conversation compensation problem, suspend-state handling is a real advantage over `vue-virtual-scroller`, and the new composables come with genuinely useful tests. The scroll-compensation heuristics are intricate but well-commented. I'd ask for the P2 (broken `profileUserId` provide) and the two P3s (dead `streamingEnabled`, dead developer toggle) to be addressed before merge; nothing else looks structurally blocking. *Reviewed commit `6be2c6bc56d722ab513d386da14453a7b60a5eea` against `develop` at `b90938c7bca9fb1e58c783e0a24a4f937616f614`.*
re-enable scroll compensation for timeline
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
32115f4783
opencode review fixes
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
1f90d85505
lint
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
4b33009ce0
Author
Member

/oc review

/oc review
First-time contributor

opencode-agent:job=85:generation=1

Review of virtual-scrolling-2.0 @ 4b33009ce0c01a8e69216fb29598987c49607504 against develop @ d61049c744830e37c26e09013cfd04128f7ed3be (merge-base b90938c7bca9fb1e58c783e0a24a4f937616f614; 85 commits, 36 files, +2607/−1190), triggered by the /oc review request in this PR.

Verification performed

  • Full unit suite at the exact head commit (vitest in browser mode, chromium headless): 41 test files, 490 passed, 2 skipped.
  • biome check: clean. stylelint '**/*.scss' '**/*.vue': clean.
  • Diff reviewed is the precise merge-base→head delta.

Status of the previous round (job=84)

All prior findings are fixed in 1f90d85505 + 4b33009ce0: the profileUserId computed provide, the mastoUserSocketStatus.value comparison, the dead developer toggle, the legnth typo, the dead .vue-recycle-scroller.timeline rule, the scrollTo missing-anchor guard, aria-hidden on timeline spacers, unified fetchConversation error handling, and the unused ChatMessageList/PostStatusForm imports. Nice turnaround.

New findings

P1 — Conversation permalinks are broken: route param renamed, page component not updated
src/boot/routes.js renames the conversation route from /notice/:id to /notice/:statusId (and adds props: true), and status.vue/draft.vue/notification.vue/report.vue now all link with params: { statusId: ... }. But src/components/conversation-page/conversation-page.js:9 still reads the old key:

statusId() {
  return this.$route.params.id
}

$route.params.id is now always undefined for this route (the quotes route is separate and unaffected). props: true doesn't save it: ConversationPage declares no statusId prop, so the passed prop becomes a fallthrough attribute while the computed — which shadows the name — feeds undefined into <Conversation :status-id="statusId" is-page>. Result: clicking any status timestamp/permalink (or opening /notice/<id> directly) lands on the conversation page with statusId = undefinedfetchConversation() fetches /api/v1/statuses/undefined → the page shows the load-error spinner instead of the conversation. This is the primary "open a thread" navigation path. Fix: return this.$route.params.statusId (or declare the prop and drop the computed).

P3 — useVirtualScrolling.scrollTo(): the new not-found guard leaves the boundary watchers paused forever
useVirtualScrolling.js:287-293 calls pauseWatchers(), then — on the new miss path added in 4b33009cconsole.error(...) and return without resumeWatchers(). A single missed anchor (focused id absent from the chart) permanently freezes topScrollBoundary/bottomScrollBoundary updates for that instance, i.e. items stop flipping between status and spacer for the rest of its lifetime. In Conversation an expand/collapse cycle recovers via the enabled watcher, but Timeline's enabled is a constant ref(true), so there it would never recover. Wrap the body in try/finally (or resumeWatchers() before the early return).

P3 — status.js hideStatus watcher can leave a re-shown Status permanently unobserved
status.js:621-626 toggles the ResizeObserver in a watcher with the default (pre) flush, which runs before re-render, so this.$refs.root is the stale value: on un-hide it sees undefined (the hidden state) and calls disconnect(); after the element re-mounts nothing re-observes it, and the initial measurement in mounted() doesn't re-run — height updates (and heightChange events) silently stop for that status. Use { flush: 'post' } (or watch(() => this.$refs.root, ...)).

P3 — thread_tree.js: per-node O(n) subtree recomputation is O(n²) per conversation update
totalReplyCount/totalReplyDepth are computed on every ThreadTree instance, each recursively walking the whole conversation's reply graph (the per-evaluation sizes/depths memo doesn't help across instances). That's O(n²) work on every conversation change — previously these maps were computed once in the parent and passed down. This PR is specifically about making huge threads fast, and tree view is where the giant threads live. Consider hoisting both maps into useTreeConversationTopology (computed once per conversation) and providing them.

P4 — Dead heightChange wiring in ThreadTree
Every ThreadTree node creates a ResizeObserver (thread_tree.js:20,27-31) and emits heightChange, but nothing listens: conversation.vue's top-level <ThreadTree> (lines 127-136) doesn't bind @height-change, and the recursive child binding in thread_tree.vue no longer forwards it either. Harmless dead code plus one extra ResizeObserver per node — either wire it to a tree-level chart or drop it.

P4 — useConversation.js:99-103: unguarded store lookup during render

const fullConversation = useStatusesStore().conversations.get(conversationId.value)
return [...fullConversation.keys()]...

wipeUserStatuses (blocking a user) can delete the conversation's entry from the store map; if that happens while the conversation is expanded and fullyLoaded, this computed throws TypeError: undefined.keys during render. The main status surviving makes it unlikely, but a ?? new Map() fallback (or an explicit check that sets fullyLoaded = false) is cheap insurance.

Nits

  • timeline.js:163: onUnmounted(() => { timelineChange(null, timelineRef.value) // ???? }) — the unresolved ???? is still carried along; worth resolving the question or deleting the comment.
  • useConversation.js:105: status.type != 'repeat'!== for consistency.
  • useInterfaceSizes.js: parseInt(string.slice(0, -3), 10) on --navbarSize/--panelHeaderSize yields NaN if a theme ever omits the variable, silently poisoning offset and thus all boundary math. A default (like App.scss's var(--navbarSize, 3.5rem)) would harden it.
  • useVirtualScrolling.js:183: grouped spacers join all member ids into the v-for key (spacer.id = [...spacer.ids].join()) — for long stretches outside the viewport this builds (and diffs) very long strings on every chart update. A stable synthetic key (e.g. first id + index) would be cheaper.

Overall

The re-enable of timeline scroll compensation (32115f47) looks right, and the fixes from the previous round are all in properly — including the regression-prone ones (the computed provide unwraps correctly through Options-API inject, and streamingEnabled now tracks socket state). The one thing I'd consider blocking is the P1 permalink regression: it breaks every conversation link in the UI, the test suite doesn't cover the conversation page, and it's a two-line fix. The scrollTo/hideStatus watcher issues are the next priorities as they silently and permanently degrade virtualization until a remount.

Reviewed commit 4b33009ce0c01a8e69216fb29598987c49607504 against develop at d61049c744830e37c26e09013cfd04128f7ed3be.

opencode-agent:job=85:generation=1 Review of `virtual-scrolling-2.0` @ `4b33009ce0c01a8e69216fb29598987c49607504` against `develop` @ `d61049c744830e37c26e09013cfd04128f7ed3be` (merge-base `b90938c7bca9fb1e58c783e0a24a4f937616f614`; 85 commits, 36 files, +2607/−1190), triggered by the `/oc review` request in this PR. ### Verification performed - Full unit suite at the exact head commit (vitest in browser mode, chromium headless): **41 test files, 490 passed, 2 skipped**. - `biome check`: clean. `stylelint '**/*.scss' '**/*.vue'`: clean. - Diff reviewed is the precise merge-base→head delta. ### Status of the previous round (job=84) All prior findings are fixed in `1f90d85505` + `4b33009ce0`: the `profileUserId` computed provide, the `mastoUserSocketStatus.value` comparison, the dead developer toggle, the `legnth` typo, the dead `.vue-recycle-scroller.timeline` rule, the `scrollTo` missing-anchor guard, `aria-hidden` on timeline spacers, unified `fetchConversation` error handling, and the unused `ChatMessageList`/`PostStatusForm` imports. Nice turnaround. ### New findings **P1 — Conversation permalinks are broken: route param renamed, page component not updated** `src/boot/routes.js` renames the conversation route from `/notice/:id` to `/notice/:statusId` (and adds `props: true`), and `status.vue`/`draft.vue`/`notification.vue`/`report.vue` now all link with `params: { statusId: ... }`. But `src/components/conversation-page/conversation-page.js:9` still reads the old key: ```js statusId() { return this.$route.params.id } ``` `$route.params.id` is now always `undefined` for this route (the quotes route is separate and unaffected). `props: true` doesn't save it: ConversationPage declares no `statusId` prop, so the passed prop becomes a fallthrough attribute while the computed — which shadows the name — feeds `undefined` into `<Conversation :status-id="statusId" is-page>`. Result: clicking any status timestamp/permalink (or opening `/notice/<id>` directly) lands on the conversation page with `statusId = undefined` → `fetchConversation()` fetches `/api/v1/statuses/undefined` → the page shows the load-error spinner instead of the conversation. This is the primary "open a thread" navigation path. Fix: `return this.$route.params.statusId` (or declare the prop and drop the computed). **P3 — `useVirtualScrolling.scrollTo()`: the new not-found guard leaves the boundary watchers paused forever** `useVirtualScrolling.js:287-293` calls `pauseWatchers()`, then — on the new miss path added in `4b33009c` — `console.error(...)` and `return` **without** `resumeWatchers()`. A single missed anchor (focused id absent from the chart) permanently freezes `topScrollBoundary`/`bottomScrollBoundary` updates for that instance, i.e. items stop flipping between `status` and `spacer` for the rest of its lifetime. In Conversation an expand/collapse cycle recovers via the `enabled` watcher, but Timeline's `enabled` is a constant `ref(true)`, so there it would never recover. Wrap the body in `try/finally` (or `resumeWatchers()` before the early return). **P3 — `status.js` `hideStatus` watcher can leave a re-shown Status permanently unobserved** `status.js:621-626` toggles the ResizeObserver in a watcher with the default (`pre`) flush, which runs *before* re-render, so `this.$refs.root` is the stale value: on un-hide it sees `undefined` (the hidden state) and calls `disconnect()`; after the element re-mounts nothing re-observes it, and the initial measurement in `mounted()` doesn't re-run — height updates (and `heightChange` events) silently stop for that status. Use `{ flush: 'post' }` (or `watch(() => this.$refs.root, ...)`). **P3 — `thread_tree.js`: per-node O(n) subtree recomputation is O(n²) per conversation update** `totalReplyCount`/`totalReplyDepth` are computed on every ThreadTree instance, each recursively walking the whole conversation's reply graph (the per-evaluation `sizes`/`depths` memo doesn't help across instances). That's O(n²) work on every conversation change — previously these maps were computed once in the parent and passed down. This PR is specifically about making huge threads fast, and tree view is where the giant threads live. Consider hoisting both maps into `useTreeConversationTopology` (computed once per conversation) and providing them. **P4 — Dead `heightChange` wiring in ThreadTree** Every ThreadTree node creates a `ResizeObserver` (`thread_tree.js:20,27-31`) and emits `heightChange`, but nothing listens: `conversation.vue`'s top-level `<ThreadTree>` (lines 127-136) doesn't bind `@height-change`, and the recursive child binding in `thread_tree.vue` no longer forwards it either. Harmless dead code plus one extra ResizeObserver per node — either wire it to a tree-level chart or drop it. **P4 — `useConversation.js:99-103`: unguarded store lookup during render** ```js const fullConversation = useStatusesStore().conversations.get(conversationId.value) return [...fullConversation.keys()]... ``` `wipeUserStatuses` (blocking a user) can delete the conversation's entry from the store map; if that happens while the conversation is expanded and `fullyLoaded`, this computed throws `TypeError: undefined.keys` during render. The main status surviving makes it unlikely, but a `?? new Map()` fallback (or an explicit check that sets `fullyLoaded = false`) is cheap insurance. ### Nits - `timeline.js:163`: `onUnmounted(() => { timelineChange(null, timelineRef.value) // ???? })` — the unresolved `????` is still carried along; worth resolving the question or deleting the comment. - `useConversation.js:105`: `status.type != 'repeat'` — `!==` for consistency. - `useInterfaceSizes.js`: `parseInt(string.slice(0, -3), 10)` on `--navbarSize`/`--panelHeaderSize` yields `NaN` if a theme ever omits the variable, silently poisoning `offset` and thus all boundary math. A default (like `App.scss`'s `var(--navbarSize, 3.5rem)`) would harden it. - `useVirtualScrolling.js:183`: grouped spacers join all member ids into the `v-for` key (`spacer.id = [...spacer.ids].join()`) — for long stretches outside the viewport this builds (and diffs) very long strings on every chart update. A stable synthetic key (e.g. first id + index) would be cheaper. ### Overall The re-enable of timeline scroll compensation (`32115f47`) looks right, and the fixes from the previous round are all in properly — including the regression-prone ones (the computed `provide` unwraps correctly through Options-API inject, and `streamingEnabled` now tracks socket state). The one thing I'd consider blocking is the **P1 permalink regression**: it breaks every conversation link in the UI, the test suite doesn't cover the conversation page, and it's a two-line fix. The `scrollTo`/`hideStatus` watcher issues are the next priorities as they silently and permanently degrade virtualization until a remount. *Reviewed commit `4b33009ce0c01a8e69216fb29598987c49607504` against `develop` at `d61049c744830e37c26e09013cfd04128f7ed3be`.*
hj added 12 commits 2026-09-21 14:34:20 +00:00
lint
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline failed
ci/woodpecker/pr/test-e2e unknown status
a51351c4bc
hj added 3 commits 2026-09-21 15:14:18 +00:00
fix thread display diving
Some checks failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline failed
ci/woodpecker/pr/test-e2e unknown status
d8e81e7904
use even more stable spacer indexes for better dom reuse, fix tests
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/test-e2e Pipeline was successful
61de78b7ea
Author
Member

/oc review

/oc review
First-time contributor

opencode-agent:job=86:generation=1

Review of virtual-scrolling-2.0 @ 61de78b7ea52ae7d97c216acecb3d5ed03807707 against develop @ d61049c744830e37c26e09013cfd04128f7ed3be (merge-base b90938c7bca9fb1e58c783e0a24a4f937616f614; 102 commits, 46 files, +2676/−1543), triggered by the /oc review request in this PR.

Verification performed

  • Full unit suite at the exact head commit (vitest in browser mode, chromium headless): 41 test files, 490 passed, 2 skipped.
  • biome check: clean. stylelint '**/*.scss' '**/*.vue': clean.
  • Production build (vite build): passed.
  • Diff reviewed is the precise merge-base→head delta; 17 new commits since the previous round (4b33009ce0).

Status of the previous round (job=85)

All findings are fixed and verified in source:

  • P1 permalink regression: conversation-page is gone entirely; the conversation route now mounts Conversation directly with props: (route) => ({ statusId: route.params.statusId, isPage: true }), and every permalink (status.vue, notification.vue, report.vue, draft.vue, both buttons_definitions.js actions) uses params: { statusId }. Cleanest possible fix.
  • P3 scrollTo watcher freeze: pauseWatchers() now happens only after the not-found guard, and the success path still ends in resumeWatchers() — no path leaves the boundary watchers paused anymore.
  • P3 hideStatus watcher: now { flush: 'post' }, so it observes the freshly re-mounted root instead of the stale ref.
  • P3 O(n²) subtree recomputation: totalReplyCount/totalReplyDepth are computed once in useTreeConversationTopology and provided down as Maps; ThreadTree just injects them. Per-node work is now O(1).
  • P4 dead heightChange wiring: the ResizeObserver and the emit were removed from ThreadTree (trees aren't virtualized).
  • P4 unguarded store lookup: ?? new Map() guard added. I also checked wipeUserStatuses — it removes from conversations and allStatuses atomically, so the per-status lookup can't realistically miss afterwards.
  • Nits from last round: !==, the // ???? comment, reading the real --navbar-height/--panel-header-height variables, and stable i${index} spacer keys — all addressed.

New findings

Nothing blocking this round. Small notes:

Nit — old_default_config_state.js:111: stale virtualScrolling: true default. The setting was removed from default_config_state.js and the developer tab, but the old-config migration defaults still carry it. Harmless (nothing reads it anymore), but it can go.

Nit — streamingEnabled branch still untested. useConversation.spec.js covers unknown-status, expand, statusId-change and replies flows, but not the socket-state branch (the fixed mastoUserSocketStatus.value comparison). A test would pin it so it can't regress back to a constant.

Note — branch is behind develop. The PR diff currently shows the recently-merged "search by author" feature (UserSelectorInput, the accountId search param, related i18n keys) being removed. That's an artifact of the branch predating those develop commits (merge-base b90938c7), not an intentional revert — the PR reports mergeable, but merging or rebasing develop before final merge would keep the diff honest.

Note — PR description drift. The body says chat view shares useConversation, but no chat component imports it (only Conversation does). Cosmetic.

Also verified while re-reading the new wiring: the thread-display toggle chain is correct — Status emits toggleThreadDisplay without a payload, but the button only renders when threadDisplayState is provided (i.e. inside ThreadTree), and ThreadTree re-emits with its own statusId, so Conversation.toggleThreadDisplay(id) always receives the right node.

Overall

Every previously reported issue is fixed properly — including the regression-prone ones — the two riskiest paths (permalink navigation, diving/thread re-rooting) got dedicated fix commits plus test updates, and the border restyling is scoped correctly (:not(.-last) now only applies in linear view). Suite, linters and production build are green at the exact head. Nothing blocking from my side; just update the branch with develop before merge so the diff stops claiming to delete the search-by-author feature.

Reviewed commit 61de78b7ea52ae7d97c216acecb3d5ed03807707 against develop at d61049c744830e37c26e09013cfd04128f7ed3be.

opencode-agent:job=86:generation=1 Review of `virtual-scrolling-2.0` @ `61de78b7ea52ae7d97c216acecb3d5ed03807707` against `develop` @ `d61049c744830e37c26e09013cfd04128f7ed3be` (merge-base `b90938c7bca9fb1e58c783e0a24a4f937616f614`; 102 commits, 46 files, +2676/−1543), triggered by the `/oc review` request in this PR. ### Verification performed - Full unit suite at the exact head commit (vitest in browser mode, chromium headless): **41 test files, 490 passed, 2 skipped**. - `biome check`: clean. `stylelint '**/*.scss' '**/*.vue'`: clean. - Production build (`vite build`): **passed**. - Diff reviewed is the precise merge-base→head delta; 17 new commits since the previous round (`4b33009ce0`). ### Status of the previous round (job=85) All findings are fixed and verified in source: - **P1 permalink regression**: `conversation-page` is gone entirely; the `conversation` route now mounts `Conversation` directly with `props: (route) => ({ statusId: route.params.statusId, isPage: true })`, and every permalink (`status.vue`, `notification.vue`, `report.vue`, `draft.vue`, both `buttons_definitions.js` actions) uses `params: { statusId }`. Cleanest possible fix. - **P3 `scrollTo` watcher freeze**: `pauseWatchers()` now happens only after the not-found guard, and the success path still ends in `resumeWatchers()` — no path leaves the boundary watchers paused anymore. - **P3 `hideStatus` watcher**: now `{ flush: 'post' }`, so it observes the freshly re-mounted root instead of the stale ref. - **P3 O(n²) subtree recomputation**: `totalReplyCount`/`totalReplyDepth` are computed once in `useTreeConversationTopology` and provided down as Maps; `ThreadTree` just injects them. Per-node work is now O(1). - **P4 dead `heightChange` wiring**: the ResizeObserver and the emit were removed from `ThreadTree` (trees aren't virtualized). - **P4 unguarded store lookup**: `?? new Map()` guard added. I also checked `wipeUserStatuses` — it removes from `conversations` and `allStatuses` atomically, so the per-status lookup can't realistically miss afterwards. - Nits from last round: `!==`, the `// ????` comment, reading the real `--navbar-height`/`--panel-header-height` variables, and stable `i${index}` spacer keys — all addressed. ### New findings Nothing blocking this round. Small notes: **Nit — `old_default_config_state.js:111`: stale `virtualScrolling: true` default.** The setting was removed from `default_config_state.js` and the developer tab, but the old-config migration defaults still carry it. Harmless (nothing reads it anymore), but it can go. **Nit — `streamingEnabled` branch still untested.** `useConversation.spec.js` covers unknown-status, expand, statusId-change and replies flows, but not the socket-state branch (the fixed `mastoUserSocketStatus.value` comparison). A test would pin it so it can't regress back to a constant. **Note — branch is behind `develop`.** The PR diff currently shows the recently-merged "search by author" feature (`UserSelectorInput`, the `accountId` search param, related i18n keys) being *removed*. That's an artifact of the branch predating those develop commits (merge-base `b90938c7`), not an intentional revert — the PR reports mergeable, but merging or rebasing develop before final merge would keep the diff honest. **Note — PR description drift.** The body says chat view shares `useConversation`, but no chat component imports it (only `Conversation` does). Cosmetic. Also verified while re-reading the new wiring: the thread-display toggle chain is correct — `Status` emits `toggleThreadDisplay` without a payload, but the button only renders when `threadDisplayState` is provided (i.e. inside `ThreadTree`), and `ThreadTree` re-emits with its own `statusId`, so `Conversation.toggleThreadDisplay(id)` always receives the right node. ### Overall Every previously reported issue is fixed properly — including the regression-prone ones — the two riskiest paths (permalink navigation, diving/thread re-rooting) got dedicated fix commits plus test updates, and the border restyling is scoped correctly (`:not(.-last)` now only applies in linear view). Suite, linters and production build are green at the exact head. Nothing blocking from my side; just update the branch with develop before merge so the diff stops claiming to delete the search-by-author feature. *Reviewed commit `61de78b7ea52ae7d97c216acecb3d5ed03807707` against `develop` at `d61049c744830e37c26e09013cfd04128f7ed3be`.*
hj merged commit bf46de5e00 into develop 2026-09-21 16:25:05 +00:00
hj deleted branch virtual-scrolling-2.0 2026-09-21 16:25:05 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
pleroma/pleroma-fe!3573
No description provided.