Skip to content
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

Handle moved messages in MessageStore and message lists #787

Merged
merged 9 commits into from
Aug 9, 2024

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Jul 4, 2024

Fixes most of #150. (After this lands we should do a quick scan to identify anything else that needs updating on message moves — there are several new data structures in the store that we've merged in recent weeks.)

A couple of months ago I spent a few hours writing part of an implementation of #150. It's mostly complete, apart from the lack of tests… and any bugs it has that will be found by writing those tests.

@PIG208 since you're picking up #150 shortly, I spent a small amount of time just now cleaning up that draft branch and rebasing atop the changes from #750, and writing down what remains to be done to finish it. So please start from this, which will let you avoid duplicating that work.

I believe there are three remaining things this needs:

  • Tests. As usual, these should go in the same commit that makes the changes.

    There's a variety of kinds of test cases this needs,
    but here's one in particular that may not be apparent:

    If the generation += 1 line is commented out, the message list
    has a race bug where a fetchOlder starts; we reset (because
    messages were moved into the narrow); and then the fetch returns
    and appends in the wrong spot. Also a related race bug where it's
    fetchInitial, and it lacks the moved messages. So there should be
    test cases exercising each of those situations, which would fail if
    that line gets commented out.

  • Update StreamMessage.displayRecipient. This goes in a followup commit.

    If we wanted, we could update it by looking up the new stream's
    name. To enable MessageStoreImpl to do that, we'd pass it a
    StreamStore, the same way we do with Unreads.

    But in fact the only time displayRecipient is useful is when we
    don't have data on that stream. So there's not much point in
    looking it up here. Instead, make the field nullable, and just
    set it to null when the message is moved between streams.

  • Handle UpdateMessageEvent.propagateMode. This also goes in a followup commit. If it gets complex (or if the rest of the branch before it gets complex), let's split it out as a followup PR.

    This probably means
    _MessageListPageState having its own notion of the current narrow,
    which is initialized with widget.narrow but can change.
    Then _MessageListState._modelChanged can check if the model's
    narrow has changed, and if so tell the page state to update.

    For comparison, see zulip-mobile's src/events/doEventActionSideEffects.js,
    or web.

@PIG208
Copy link
Member

PIG208 commented Jul 4, 2024

Thanks for the notes. I think I will just push to your branch and continue using this PR.

@PIG208 PIG208 force-pushed the pr-move-messages branch 3 times, most recently from ee42b76 to f85b8b8 Compare July 9, 2024 20:55
@PIG208
Copy link
Member

PIG208 commented Jul 9, 2024

Added the test cases.
This is still failing because we always notifies the listener if there are affected message present, and message moves might also notify the listener. I think we want to let the caller handle notifications instead.

@PIG208 PIG208 force-pushed the pr-move-messages branch from f85b8b8 to fab7ff2 Compare July 9, 2024 21:15
final result = await getMessages(store.connection,
narrow: narrow.apiEncode(),
anchor: NumericAnchor(messages[0].id),
includeAnchor: false,
numBefore: kMessageListFetchBatchSize,
numAfter: 0,
);
if (this.generation > generation) return;
Copy link
Member

@PIG208 PIG208 Jul 9, 2024

Choose a reason for hiding this comment

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

Not sure if we want to skip the finally block as well. At least we shouldn't need to notify the listeners again.

    } finally {
      _fetchingOlder = false;
      _updateEndMarkers();
      notifyListeners();
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

In the current revision:

     } finally {
       _fetchingOlder = false;
-      _updateEndMarkers();
-      notifyListeners();
+      if (this.generation == generation) {
+        _updateEndMarkers();
+        notifyListeners();
+      }
     }

The thing is that the reason there's a notifyListeners there, and an _updateEndMarkers, is as a followup to the _fetchingOlder update. Changing _fetchingOlder changes what set of end markers should be present; and when the end markers change, listeners should be notified (so that e.g. the UI can update to reflect the change).

We could get an effect like this, because _fetchingOlder may already be false. But the way to do that would be with an explicit check on _fetchingOlder: no _updateEndMarkers etc. if we're not actually changing the value.

In fact, this actually points to a bug (that was introduced in my draft): if we abort because the generation advanced, then we should not set _fetchingOlder to false. It might happen to be true by the time we reach this point, because the user might have scrolled up again after the new fetchInitial completed, causing a new fetchOlder. If it is true after the generation advanced, then the fact that it's true doesn't belong to this fetchOlder call, but instead to that newer fetchOlder call, and so this fetchOlder call shouldn't be interfering.

So please fix that bug from my draft 🙂, and also add a test that would reproduce the bug.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the elaboration. I think encapsulating _fetchingOlder update with the generation check should be sufficient as a fix.

@PIG208 PIG208 force-pushed the pr-move-messages branch from fab7ff2 to 6d8aa56 Compare July 9, 2024 21:29
@gnprice
Copy link
Member Author

gnprice commented Jul 9, 2024

This is still failing because we always notifies the listener if there are affected message present, and message moves might also notify the listener.

Hmm I see, so the listeners end up getting notified twice?

I think this is probably a case where we should just be OK with the duplication. Here's what I wrote on a different PR a few weeks ago (#648 (comment)):

Generally I think it's OK to have a little duplication of those calls — for example if some event can cause it to be called two or three times, and it'd take a lot of refactoring or bookkeeping to prevent that and call just once, then that's OK. If some listener callback is expensive enough to make that a problem, then it should have appropriate memoization, or defer the real work until the next frame starts, or something, in order to mitigate that.

But this [in that other PR] is potentially hundreds of times, so that gets me a bit concerned.

(Possibly I should put that in docs somewhere. The way I found it just now is by a Zulip search within #mobile-github — that's been a side benefit I've quite enjoyed from having that GitHub firehose appear in Zulip.)

@PIG208 PIG208 force-pushed the pr-move-messages branch from 6d8aa56 to c61cfe8 Compare July 9, 2024 22:39
@PIG208
Copy link
Member

PIG208 commented Jul 11, 2024

This probably means
_MessageListPageState having its own notion of the current narrow,
which is initialized with widget.narrow but can change.
Then _MessageListState._modelChanged can check if the model's
narrow has changed, and if so tell the page state to update.

Relying on the usual updates through dependencies does not work here because MessageListPage is a parent of MessageList. At first I was thinking if we want to push a route for MessageListPage to Navigator (similar to zulip/zulip-mobile#5251), as a way to notify _MessageListPageState from _MessageListState.

But it might not be necessary. An alternative would be having MessageListPage pass a callback function to MessageList, and call it when _modelChanged happens to detect change in narrow.

@gnprice
Copy link
Member Author

gnprice commented Jul 11, 2024

having MessageListPage pass a callback function to MessageList, and call it when _modelChanged happens to detect change in narrow.

Yeah, exactly.

We wouldn't want to push a new route — that'd leave the old one still on the nav stack, and make a new _MessageListPage State with a new _MessageListState and MessageListView. What zulip-mobile does is to mutate the route parameters on the existing route. If there's a way to do that with Flutter's navigator API then that could also be a good solution; I haven't looked into that.

@PIG208 PIG208 force-pushed the pr-move-messages branch 5 times, most recently from 1b319ab to fe0bc78 Compare July 12, 2024 05:47

await store.handleEvent(eg.updateMessageMoveEvent(movedMessages,
newTopic: movedMessages[0].topic,
origTopic: movedMessages[0].topic
Copy link
Member

Choose a reason for hiding this comment

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

I wonder how this is possibly a move. Does internal move within a topic narrow actually happen?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm. Yeah, this case seems like it should be a no-op and shouldn't happen. So maybe it should just look like this (in the implementation):

          case (true,  true ): return; // TODO(log) no-op move

@@ -576,7 +576,10 @@ class StreamMessage extends Message {
@JsonKey(includeToJson: true)
String get type => 'stream';

final String displayRecipient;
// This is not nullable API-wise, but if the message move across streams we
// might set [displayRecipient] to null to invalidate it.
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this comment warrants dart doc given how it is mainly explaining the motivation for the @JsonKey here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I think a plain implementation comment (like here) is fine.

@PIG208 PIG208 marked this pull request as ready for review July 12, 2024 05:48
@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Jul 12, 2024
@gnprice gnprice requested a review from chrisbobbe July 12, 2024 07:17
@PIG208 PIG208 force-pushed the pr-move-messages branch from fe0bc78 to fd35ea4 Compare July 12, 2024 17:42
@PIG208
Copy link
Member

PIG208 commented Jul 12, 2024

Pushed to fix the CI error.

@chrisbobbe
Copy link
Collaborator

Ah it looks like there's a rebase conflict. Could one of you please resolve that; maybe Zixuan?

@PIG208 PIG208 force-pushed the pr-move-messages branch from fd35ea4 to 02fec92 Compare July 19, 2024 22:35
@PIG208
Copy link
Member

PIG208 commented Jul 19, 2024

Conflict resolved. (It was related to 7a2c9d2).

PIG208 added a commit to gnprice/zulip-flutter that referenced this pull request Aug 2, 2024
Navigation change is only intended for TopicNarrow. For StreamNarrow,
propagateMode is unused.

See also:

- zulip/zulip-mobile#5251
- zulip#787 (comment)

Fixes: zulip#150

Signed-off-by: Zixuan James Li <[email protected]>
@PIG208 PIG208 force-pushed the pr-move-messages branch from a763545 to 71d4ad2 Compare August 2, 2024 23:45
PIG208 added a commit to gnprice/zulip-flutter that referenced this pull request Aug 2, 2024
Navigation change is only intended for TopicNarrow. For StreamNarrow,
propagateMode is unused.

See also:

- zulip/zulip-mobile#5251
- zulip#787 (comment)

Fixes: zulip#150

Signed-off-by: Zixuan James Li <[email protected]>
@PIG208 PIG208 force-pushed the pr-move-messages branch from 71d4ad2 to ee3281d Compare August 2, 2024 23:55
PIG208 added a commit to gnprice/zulip-flutter that referenced this pull request Aug 2, 2024
As noted in:

zulip#787 (review)

There is an discrepency between the API docs and the actual server
behavior on the presence of the "new_topic" field on the update message
event. While it was claimed to be always present when a move occurs, it
is actually not the case when the message moves across channels without
chaning its topic.

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to gnprice/zulip-flutter that referenced this pull request Aug 2, 2024
Navigation change is only intended for TopicNarrow. For StreamNarrow,
propagateMode is unused.

See also:

- zulip/zulip-mobile#5251
- zulip#787 (comment)

Fixes: zulip#150

Signed-off-by: Zixuan James Li <[email protected]>
@PIG208 PIG208 force-pushed the pr-move-messages branch from ee3281d to 868ac54 Compare August 2, 2024 23:56
@PIG208
Copy link
Member

PIG208 commented Aug 3, 2024

This should be ready for review now. Thanks!

Copy link
Member Author

@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 @PIG208 for the revision! Just a handful of small comments now; then this will be all ready to merge.

group('Update Narrow on message move', () {
final channel = eg.stream(name: 'move test stream');
final otherChannel = eg.stream(name: 'other move test stream');
final narrow = TopicNarrow(channel.streamId, 'example topic');
Copy link
Member Author

Choose a reason for hiding this comment

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

This implicitly assumes that eg.streamMessage will by default pick a topic name of "example topic". That's to be avoided because it makes this test code non-self-contained for the reader — the tests only work because of that arbitrary choice that's made inside eg.streamMessage.

Instead, the test should make explicit that this narrow is supposed to contain the messages that appear later. A straightforward way to do that is to make a local variable topic and use it in all the places that need to be talking about the same topic.

(Probably we should make eg.streamMessage generate the topic name at random, to help prevent this pattern, the same way we do stream/channel IDs and message IDs and so on. From a quick experiment just now, there's a handful of existing tests that would need fixing for that; and none of those assume what the default topic name is, only that it's some constant value, which is also an assumption to avoid making implicit but is a different one.)

Copy link
Member Author

Choose a reason for hiding this comment

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

A straightforward way to do that is to make a local variable topic and use it in all the places that need to be talking about the same topic.

The other easy way that comes to mind is to not specify the topic name at all, but get it from the message with message.topic (and/or indirect versions of the same, like TopicNarrow.ofMessage).

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably we should make eg.streamMessage generate the topic name at random, to help prevent this pattern, the same way we do stream/channel IDs and message IDs and so on.

Sent #932 for this.

and none of those assume what the default topic name is, only that it's some constant value, which is also an assumption to avoid making implicit but is a different one.

I ended up deciding this assumption was fine, though; it's convenient in a variety of tests. So I made the randomness happen only once per run, and documented that.

Comment on lines 651 to 654
check(tester.widget<TextField>(channelContentInputFinder))
..decoration.isNotNull().hintText.equals('Message #${channel.name} > example topic')
..controller.isNotNull().text.equals('Some text');
Copy link
Member Author

Choose a reason for hiding this comment

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

Cool, good idea adding this check — it makes a baseline for the reader to compare the check below to.

Comment on lines 653 to 656
..controller.isNotNull().text.equals('Some text');
prepareGetMessageResponse([message]);
Copy link
Member Author

Choose a reason for hiding this comment

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

nit:

Suggested change
..controller.isNotNull().text.equals('Some text');
prepareGetMessageResponse([message]);
..controller.isNotNull().text.equals('Some text');
prepareGetMessageResponse([message]);

Generally in a test case when there's a sequence like

  • make something happen or change
  • check the state of the world
  • make something else happen or change
  • check the new state of the world
  • etc.

I find it helpful, for the reader, to group those change/check pairs into stanzas using blank lines.

}
}

void _messagesMovedFromNarrow(List<int> messageIds) {
Copy link
Member Author

Choose a reason for hiding this comment

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

nit: group this with the other _messagesMoved… methods, above _handlePropagateMode

PIG208 added a commit to gnprice/zulip-flutter that referenced this pull request Aug 9, 2024
As noted in:

zulip#787 (review)

There is an discrepency between the API docs and the actual server
behavior on the presence of the "new_topic" field on the update message
event. While it was claimed to be always present when a move occurs, it
is actually not the case when the message moves across channels without
chaning its topic.

Signed-off-by: Zixuan James Li <[email protected]>
PIG208 added a commit to gnprice/zulip-flutter that referenced this pull request Aug 9, 2024
Navigation change is only intended for TopicNarrow. For StreamNarrow,
propagateMode is unused.

See also:

- zulip/zulip-mobile#5251
- zulip#787 (comment)

Fixes: zulip#150

Signed-off-by: Zixuan James Li <[email protected]>
@PIG208 PIG208 force-pushed the pr-move-messages branch from 868ac54 to 627b0f3 Compare August 9, 2024 18:12
@PIG208
Copy link
Member

PIG208 commented Aug 9, 2024

Thanks. Updated the PR.

PIG208 and others added 9 commits August 9, 2024 11:30
As noted in:

zulip#787 (review)

There is an discrepency between the API docs and the actual server
behavior on the presence of the "new_topic" field on the update message
event. While it was claimed to be always present when a move occurs, it
is actually not the case when the message moves across channels without
chaning its topic.

Signed-off-by: Zixuan James Li <[email protected]>
We still repeat origTopic and etc when creating the UpdateMessageEvents.
This is necessary because our example data do not fallback to the topic
of the provided messages yet.

Signed-off-by: Zixuan James Li <[email protected]>
When testing, in some cases it is more convenient to prepare the
original messages with the topic/stream prior to the move or the other
way around. To keep the fallback behavior consistent in either cases, we
introduce two variants that produce an UpdateMessageEvent suitable for
different contexts.

Signed-off-by: Zixuan James Li <[email protected]>
We want to only allow it to be nullable after deserialization, because
the nullability does not belong to the API, and it exists only for later
implementing invalidation of the field when a stream message is moved to
a different stream.

Signed-off-by: Zixuan James Li <[email protected]>
We already handle the case where only a message's content is edited.
This handles the case where messages are moved too, between topics
and/or channels.

This introduces more notifyListener calls, and the listeners can be
notified more than once per UpdateMessage event. This is expected.

---

If the `generation += 1` line is commented out, the message list
has a race bug where a fetchOlder starts; we reset (because
messages were moved into the narrow); and then the fetch returns
and appends in the wrong spot. These races are detailed in the "fetch
races" test group.

---

StreamMessage.displayRecipient no longer carries the up-to-date display
name of the stream when the message has been moved to another stream. We
invalidate it by setting it to null.

The only time StreamMessage.displayRecipient is useful is when we
don't have data on that stream. This is the reason why we don't go ahead
and lookup the stream store when such a stream move happen, because the
stream store likely will not have that information if we ever need to
use displayRecipient as the fallback.

---

We have relatively more tests for the topic moves, because there are
more cases that make a difference to consider. There is some overlap
between the tests like "(channel, topic) -> (new channel, new topic)" and
other tests where only one of topic/channel is changed. We include both
because the parameterized test cases are easy to modify and it doesn't
hurt to cover another realistic scenario handled by the same code path.

Co-authored-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
Previously, messageWithMarker already has its new topic in the message
list when initialized. A subsequent event that moves the message from
elsewhere to said topic does not logically make sense. This fixed that
by moving the message back and forth.

We can't simply move the message from elsewhere because we need to
record the initial rects of the messages.

Signed-off-by: Zixuan James Li <[email protected]>
This allows us to switch narrow when a move happens.

Signed-off-by: Zixuan James Li <[email protected]>
Navigation change is only intended for TopicNarrow. For StreamNarrow,
propagateMode is unused.

See also:

- zulip/zulip-mobile#5251
- zulip#787 (comment)

Fixes: zulip#150

Signed-off-by: Zixuan James Li <[email protected]>
@gnprice
Copy link
Member Author

gnprice commented Aug 9, 2024

Thanks for the revision! Looks good — merging.

I made a few commit-message tweaks, all in the summary prefixes:

-    model: Expect the absence of newTopic for certain moves.
+    message: Expect the absence of newTopic for certain moves.

-    message_test [nfc]: Rename message move test for accuracy.
+    message test [nfc]: Rename message move test for accuracy.

-    model [nfc]: Make displayRecipient nullable.
+    message [nfc]: Make displayRecipient nullable.

The "test" modifier in a summary prefix is always separated by a space, not underscore.

Rather than "model", these can be more specific by saying "message", referring to the MessageStore part of the model. (The first commit is all about that code directly. The later displayRecipient commit doesn't touch that code itself… but the change it's making to Message can be thought of as being entirely for the purpose of Message's role in MessageStore, where those objects get mutated.)

@gnprice gnprice merged commit e9b5f6f into zulip:main Aug 9, 2024
1 check passed
@gnprice gnprice deleted the pr-move-messages branch August 9, 2024 18:38
gnprice added a commit to gnprice/zulip-flutter that referenced this pull request Sep 9, 2024
This avoids a test accidentally coming to depend on the particular
topic name that `eg.streamMessage` happens to choose by default.

Discussed previously at:
  zulip#787 (comment)

For convenience, we preserve the fact that successive calls to
`eg.streamMessage` without a `topic` argument will get the same
topic each time.  (This saves various test cases from having to
specify the topic just to ensure all their messages have some
common topic.)
chrisbobbe pushed a commit that referenced this pull request Sep 9, 2024
This avoids a test accidentally coming to depend on the particular
topic name that `eg.streamMessage` happens to choose by default.

Discussed previously at:
  #787 (comment)

For convenience, we preserve the fact that successive calls to
`eg.streamMessage` without a `topic` argument will get the same
topic each time.  (This saves various test cases from having to
specify the topic just to ensure all their messages have some
common topic.)
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