msglist: Drop outbox message when its message arrives in a fetch - #2413
Merged
chrisbobbe merged 3 commits intoAug 6, 2026
Merged
Conversation
This remove-and-cancel-timers sequence is about to get a fourth call site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the send request succeeds, remember the new message ID on the OutboxMessage. Nothing reads it yet; this prepares for recognizing the sent message when it arrives in a fetch (zulip#2397). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes part of zulip#2397. When the event queue is stuck but other requests are succeeding (see e.g. zulip#1884 and zulip#514 for ways that can happen), a sent message can arrive in a message fetch with no message event to remove the corresponding outbox message. The user would see the message stuck in a "sending" state, next to the delivered copy of the same message if that copy is in view. Now reconcileMessages drops any outbox message whose message is in the store, recognized by the message ID recorded from the send response (zulip#2397). Message lists respond by removing their copy of the outbox message, through a new method named for this cause of removal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rajveermalviya
approved these changes
Aug 6, 2026
rajveermalviya
left a comment
Member
There was a problem hiding this comment.
Thanks @chrisbobbe! All LGTM and tests great, please go ahead and merge.
Collaborator
Author
|
Thanks for the review! Done. |
chrisbobbe
added a commit
to chrisbobbe/zulip-flutter
that referenced
this pull request
Aug 14, 2026
Partly fixes zulip#2397. In PR zulip#2413, we started dropping an outbox message when its event arrives in a fetch. This helps in the case where the event stream is stuck (see e.g. zulip#1884 and zulip#514 for ways this can happen). It should be much rarer in practice, but it's technically possible for a fetch to arrive with the message before we get the send request's success. This commit handles that ordering, as anticipated in my comment on zulip#2397: zulip#2397 (comment) In particular, if the send request is still in flight when the message arrives in a fetch, now we clear the outbox message when the send request succeeds, which is when we're able to know the message ID corresponding to the outbox message. (This means the duplicate-message symptom described in zulip#2397 can actually still happen -- between the fetch arriving and the send-message-success -- but that's unavoidable because we can't associate the outbox message with a real message ID until we get the send-success response.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chrisbobbe
added a commit
to chrisbobbe/zulip-flutter
that referenced
this pull request
Aug 17, 2026
Partly fixes zulip#2397. In PR zulip#2413, we started dropping an outbox message when its message arrives in a fetch. This helps in the case where the event stream is stuck (see e.g. zulip#1884 and zulip#514 for ways this can happen). It should be much rarer in practice, but it's technically possible for a fetch to arrive with the message before we get the send request's success. This commit handles that ordering, as anticipated in my comment on zulip#2397: zulip#2397 (comment) In particular, if the send request is still in flight when the message arrives in a fetch, now we clear the outbox message when the send request succeeds, which is when we're able to know the message ID corresponding to the outbox message. (This means the duplicate-message symptom described in zulip#2397 can actually still happen -- between the fetch arriving and the send-message-success -- but that's unavoidable because we can't associate the outbox message with a real message ID until we get the send-success response.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chrisbobbe
added a commit
to chrisbobbe/zulip-flutter
that referenced
this pull request
Aug 17, 2026
The tests for dropping an outbox message when its message arrives in a fetch (from PRs zulip#2413 and zulip#2425) drive the store directly, which models a fetch made by some other message list. Nothing covered the view's own fetch returning the message. That case relies on an ordering inside the fetch: reconcileMessages deletes the outbox message from the store before _syncOutboxMessagesFromStore copies outbox messages to the view. If those were reordered, the view would show the message twice -- once fetched and once as a stale outbox copy, the duplicate symptom of zulip#2397 -- and no test would fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chrisbobbe
added a commit
to chrisbobbe/zulip-flutter
that referenced
this pull request
Aug 17, 2026
As part of making MessageListView more robust against a stuck event stream (e.g. zulip#514, zulip#2415), we'd like to add support for invalidating `haveNewest: true`, for the case where newer messages are learned about through *other* MessageListViews' fetches. Example: - Long-polling is stuck, but message sends and fetches are succeeding. - You send a message; it succeeds but the event doesn't arrive. - You pop and re-push the message-list page where you sent the message, and the new message appears. - Because the new message was seen in a fetch, the outbox placeholder is removed. (This is recent behavior, from zulip#2413 and zulip#2425.) -> But the outbox placeholder was removed *in all message lists where it appeared*, not just the current one. E.g. if you opened a channel narrow at the newest messages, then opened a topic narrow and sent your message from there. - While long-polling is stuck, the other message list (the channel narrow in this example) won't get the new message except if it happens to do an independent message fetch, e.g. if the user pops and re-pushes that message-list page too. Planned solution (the third bullet point in my comment on zulip#2397): since we know the new message belongs in the other narrow (the channel narrow) and that it's newer than the latest messages there, we'll invalidate the other narrow's `haveNewest: true`, to make sure it eventually gets updated with the new message, and shows an accurate "still-loading-newer" state until that update arrives. To prepare for that solution, this commit ensures that newestFetchedMessageId didn't get surpassed by new-message events that may have landed in the list before the long-poll got stuck. Since we anchor fetch-newer requests with newestFetchedMessageId, it's important that newestFetchedMessageId not be lower than any of the latest messages we're already showing in the list, else the fetch-newer request would end up duplicating those messages. This hasn't caused a live message-duplicating bug because we've never triggered a fetch-newer request when the list contains messages that landed there via new-message events. We only apply new-message events when haveNewest is true, and we only make fetch-newer requests when haveNewest is false, and the transition from true to false has long been impossible (except on _reset() when the whole state is cleared, including all tracked messages). Message moves don't need the same treatment. A move into the view resets and refetches (see _messagesMovedIntoMessageList), and other moves don't add messages to the view. When the view has no messages when a new-message event arrives, also fill in oldestFetchedMessageId, keeping the anchors' meanings symmetric. Also assert the resulting invariant in checkInvariants: [messages] doesn't extend outside the range bounded by oldestFetchedMessageId and newestFetchedMessageId. Without the change here, the newest-side bound fails in existing tests that append a message on an event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
This PR fixes the common case of #2397:
I did the following for a manual test on my iPhone:
Optionally, to see item 3 in #2397 (comment):
Tap "back" and see that the outbox placeholder is also gone from the first message list, but that the delivered message isn't present. We can fix that in a followup.