Repository navigation
[2.x] fix: Stop a realtime update duplicating a discussion in the list - #4993
Merged
Merged
Conversation
The discussion list adds and removes discussions by matching on the model instance. Realtime hands it a discussion resolved through the store, and the store returns a fresh instance whenever it no longer holds the one already on screen — a discussion that has scrolled out, say. The reference match then misses the copy in the list, so the discussion is added a second time and shows twice until the page is refreshed. Match by discussion id instead, so a fresh instance of a discussion already listed replaces it rather than duplicating it. Also fix removeDiscussion dropping every item after the one it removes from the pending list — splice(index) with no length — which could clear part of the list. A forum without realtime never adds to the list this way, so its only path here is deletion, where the id match and the old reference match are equivalent.
This was referenced Aug 27, 2026
imorland
pushed a commit
that referenced
this pull request
Aug 31, 2026
…#5003) Returning to a tab that had been idle long enough for the websocket to die shows a discussion listed twice, until the reader refreshes. `refresh()` empties `extraDiscussions`, because it routes through `clear()`. `revalidate()` (#4889) deliberately clears nothing before it asks the API — that is the point of it, the list stays on screen — so it rebuilds `pages` and leaves `extraDiscussions` alone. The first page it gets back contains exactly the discussions realtime had put there, since a new post is what moved them to the top, and the list then renders them from both halves at once. `addDiscussion` has no caller in core; only realtime adds discussions this way, so only a forum with realtime enabled can reach the duplicate at all. Nothing on the path calls `deleteDiscussion`, which is why matching there by id rather than by reference (#4993) does not close it. - Reconcile after the revalidation lands, dropping from `extraDiscussions` only the ids the new pages actually contain. Not a blanket clear: a revalidation that failed resolves rather than rejecting and leaves `pages` untouched, and clearing then would take realtime's additions off a list that was never reloaded. - Stop `getAllItems()` counting `extraDiscussions` twice. It concatenated them onto `super.getAllItems()`, which flattens the `getPages()` this class had already overridden to prepend them. Harmless where it is read today (`isEmpty()`), wrong for anything that counts. Co-authored-by: Claude Opus 5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes proposed in this pull request:
DiscussionListStateadds and removes discussions by matching on the model instance (indexOf). Realtime hands the list a discussion resolved throughapp.store.pushPayload, and the store returns a fresh instance whenever it no longer holds the one already on screen — a discussion that has scrolled out of the loaded pages, for instance. The reference match then fails to find the copy in the list, soaddDiscussionadds the discussion a second time, and it shows twice until the page is refreshed. (Reported against rc.7: "left the tab idle, came back to a duplicate discussion; refreshing fixes it.")deleteDiscussionnow matches bydiscussion.id()rather than object reference, so a fresh instance of a discussion already listed replaces it instead of duplicating it.this.extraDiscussions.splice(index)— missing its length argument, it removed the matched item and everything after it — tosplice(index, 1).nullid is guarded, so an unsaved discussion (should one ever reach the list) can't match another bynull === null.A note on forums without realtime:
addDiscussionhas no caller in core or any bundled extension — only realtime adds discussions to the list this way — so a non-realtime forum never hits the duplicate path at all. Its only route through this code is discussion deletion (PostControls,DiscussionControls), which passes the canonical list instance; there the id match and the old reference match are equivalent, so behaviour is unchanged.Reviewers should focus on:
extraDiscussionsonly ever receives store-resolved discussions), so ids are present and unique.splice(index)→splice(index, 1)change is correct — the old form was removing a tail of the pending list.Necessity
Confirmed
Required changes: