Skip to content

local echo (6.75/7): Revised commit for local echo (sans retrieving failed outbox content) #1538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 12, 2025

Conversation

chrisbobbe
Copy link
Collaborator

@chrisbobbe chrisbobbe commented May 30, 2025

Here's a revised version of a substantive commit from #1453:

9c0b955 msglist: Add and manage outbox message objects in message list view

That just leaves one more substantive commit from that PR:

1fa9f24 msglist: Support retrieving failed outbox message content

@chrisbobbe chrisbobbe requested a review from gnprice May 30, 2025 00:06
@chrisbobbe chrisbobbe added the integration review Added by maintainers when PR may be ready for integration label May 30, 2025
@gnprice
Copy link
Member

gnprice commented May 30, 2025

Thanks! I won't get to reading this until I'm back after next week.

The title's numbering collides with #1535; I saw the title and initially thought this was a new version of that PR. Perhaps this is 6.75/7? 😛

@chrisbobbe chrisbobbe changed the title local echo (6.5/7): Revised commit for local echo (sans retrieving failed outbox content) local echo (6.75/7): Revised commit for local echo (sans retrieving failed outbox content) May 30, 2025
@chrisbobbe
Copy link
Collaborator Author

Oh! Indeed.

Perhaps this is 6.75/7? 😛

Yes, that's how I named the branch and how I meant to name the PR. 😛

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've read through all the non-test changes and part of the tests. Generally this looks good; comments below.

Comment on lines 369 to 389
messages.clear();
outboxMessages.clear();
middleMessage = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: keep messages and middleMessage together (like they are in their declarations)

Suggested change
messages.clear();
outboxMessages.clear();
middleMessage = 0;
messages.clear();
middleMessage = 0;
outboxMessages.clear();

while (items.isNotEmpty && items.last is! MessageListMessageItem) {
items.removeLast();
}
assert(items.none((e) => e is MessageListOutboxMessageItem));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will loop through the entire list of items, which may be long with a lot of message history.

Let's avoid that kind of expensive check even in debug builds of the app — the message list already feels somewhat sluggish in debug mode. Instead we rely on the tests and their checkInvariants function to confirm these invariants are satisfied.

Comment on lines 661 to 669
// We'll make the bottom slice start at the last visible message, if any.
_removeOutboxMessageItems();
for (final message in result.messages) {
if (!_messageVisible(message)) continue;
middleMessage = messages.length;
_addMessage(message);
// Now [middleMessage] is the last message (the one just added).
}
_reprocessOutboxMessages();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this logic will have outboxMessages get filled in before the fetch even completes. That feels kind of odd. This is a view-model for the message list, and what we actually show in the message list in that state is just a loading indicator and no messages.

And in the future with #82, we won't necessarily want them even after the fetch, depending on the value that haveNewest ends up with after the fetch. (Just like you pointed out at #1515 (comment) for new actual messages.)

So how about we have _syncOutboxMessages get called only here in the fetch-result handler, rather than before the fetch? Then in #1515 we can straightforwardly make it conditional on haveNewest, and do the same in fetchNewer.

Probably that's best done as a separate commit on top, to facilitate merging the main commit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense; I agree.

Probably that's best done as a separate commit on top, to facilitate merging the main commit.

Hmm, since #1515 is already merged, the main commit will need to check haveNewest before adding outbox messages to items.

Comment on lines 828 to 833
bool removed = _removeOutboxMessagesWhere((message) =>
message is StreamOutboxMessage
&& message.conversation.streamId == event.streamId
&& message.conversation.topic == event.topicName);

removed |= _removeMessagesWhere((message) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: handle the normal messages first, since they come earlier in the list

Comment on lines 853 to 857
for (final outboxMessage in store.outboxMessages.values) {
if (_shouldAddOutboxMessage(
outboxMessage,
wasUnmuted: outboxMessage is StreamOutboxMessage
&& outboxMessage.conversation.streamId == event.streamId
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole stanza goes away if we let fetchInitial take care of syncing in the outbox messages when appropriate.

_addMessage(message);
if (event.localMessageId != null) {
final localMessageId = int.parse(event.localMessageId!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int.parse should always use explicit radix

Comment on lines 170 to +180
checkState().equals(OutboxMessageState.hidden);
checkNotNotified();

async.elapse(kLocalEchoDebounceDuration);
checkState().equals(OutboxMessageState.waiting);
checkNotifiedOnce();

await receiveMessage(eg.dmMessage(from: eg.selfUser, to: [eg.otherUser]));
check(store.outboxMessages).isEmpty();
checkNotifiedOnce();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add all these checks as a prep commit? Then if there are any that change with the main change (or if there are none), that will be more visible.

Comment on lines 2818 to 2819
// the bottom slice of `model.messages` is empty
check(model.items).length.equals(model.middleItem);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this check is really about validating the behavior of middleItem, not items

Suggested change
// the bottom slice of `model.messages` is empty
check(model.items).length.equals(model.middleItem);
check(model.middleItem).equals(model.items.length);

check(model.items.sublist(model.middleItem))
..first.isA<MessageListOutboxMessageItem>()
.message.identicalTo(model.outboxMessages.first)
..every((item) => item.not((it) => it.isA<MessageListMessageItem>()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "is not MessageListMessageItem" check is redundant with the "is MessageListOutboxMessageItem" check just above, given the overall structure of items: all items for outbox-messages come after all items for normal messages. And that latter structure was verified in the big for (int j = 0; j < allMessages.length; j++) loop above.

So this every can be dropped. And then the .sublist plus .first can be simplified down.

@chrisbobbe chrisbobbe force-pushed the pr-local-echo-6.75 branch from 109562b to c7f1bea Compare June 12, 2025 07:56
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed. This revision postpones copying MessageStore.outboxMessages to the message-list's outboxMessages until haveNewest becomes true. It also doesn't add new, just-sent outbox messages unless haveNewest is true.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks great; small comments just on the tests.

/// because outbox messages are considered newer than regular messages.
///
/// This does not call [notifyListeners].
void _syncOutboxMessagesFromStore() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I like this name change, too (adding "from store").

Comment on lines 3050 to 3052
check(model.items[model.middleItem])
.isA<MessageListOutboxMessageItem>()
.message.identicalTo(model.outboxMessages.first);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can make this a bit more visibly parallel to the normal-message case just below it:

Suggested change
check(model.items[model.middleItem])
.isA<MessageListOutboxMessageItem>()
.message.identicalTo(model.outboxMessages.first);
check(model.items[model.middleItem]).isA<MessageListOutboxMessageItem>()
.message.identicalTo(model.outboxMessages.first);

Comment on lines 1132 to 1134
check(model)
..fetched.isFalse()
..outboxMessages.isEmpty();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This second check is implied by the first one plus an invariant, right?

Hmm, we could make that invariant more explicitly checked, though, in checkInvariants:

   if (!model.fetched) {
     check(model)
       ..messages.isEmpty()
+      ..outboxMessages.isEmpty()

… I guess the existing test explicitly checks messages is empty, even though that's similarly redundant, because it's helpful for parallelism with the steps above and below. So this check makes sense to include for the same reason. For that same parallelism, though, it's probably clearest to leave it as a separate check call that comes after the checkHasMessageIds, rather than consolidate with the check here on fetched.

Comment on lines 343 to 345
connection.prepare(json: newestResult(foundOldest: false,
messages: [eg.streamMessage(stream: stream, topic: 'topic')]).toJson());
final fetchFuture = model.fetchNewer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit for realism: use newerResult (the anchor will be different)

@chrisbobbe chrisbobbe force-pushed the pr-local-echo-6.75 branch from c7f1bea to d0f454d Compare June 12, 2025 19:49
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

chrisbobbe and others added 2 commits June 12, 2025 16:00
With comments on additional notifyListeners calls we expect when we
start having the message list actually show the local echo in its
various states.
This adds some overhead on message-event handling, linear in the
number of outbox messages in a view. We rely on that number being
small.

We add outboxMessages as a list independent from messages on
_MessageSequence.  Because outbox messages are not rendered
(the raw content is shown as plain text), we leave the 1-1
relationship between `messages` and `contents` unchanged.

When computing `items`, we now start to look at `outboxMessages` as
well, with the guarantee that the items related to outbox messages
always come after those for other messages.  Look for places that call
`_processOutboxMessage(int index)` for references, and the changes to
`checkInvariants` on how this affects the message list invariants.

This implements minimal support to display outbox message message item
widgets in the message list, without indicators for theirs states.
Retrieving content from failed sent requests and the full UI are
implemented in a later commit.

Co-authored-by: Chris Bobbe <[email protected]>
@gnprice
Copy link
Member

gnprice commented Jun 12, 2025

Thanks! Looks good; merging.

@gnprice gnprice force-pushed the pr-local-echo-6.75 branch from d0f454d to 11f05dc Compare June 12, 2025 23:01
@gnprice gnprice merged commit 11f05dc into zulip:main Jun 12, 2025
1 check passed
@chrisbobbe chrisbobbe deleted the pr-local-echo-6.75 branch June 12, 2025 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants