-
Notifications
You must be signed in to change notification settings - Fork 263
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
Conversation
Thanks for the notes. I think I will just push to your branch and continue using this PR. |
ee42b76
to
f85b8b8
Compare
Added the test cases. |
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; |
There was a problem hiding this comment.
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();
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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)):
(Possibly I should put that in docs somewhere. The way I found it just now is by a Zulip search within |
Relying on the usual updates through dependencies does not work here because But it might not be necessary. An alternative would be having |
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. |
1b319ab
to
fe0bc78
Compare
test/model/message_list_test.dart
Outdated
|
||
await store.handleEvent(eg.updateMessageMoveEvent(movedMessages, | ||
newTopic: movedMessages[0].topic, | ||
origTopic: movedMessages[0].topic |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
lib/api/model/model.dart
Outdated
@@ -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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Pushed to fix the CI error. |
Ah it looks like there's a rebase conflict. Could one of you please resolve that; maybe Zixuan? |
Conflict resolved. (It was related to 7a2c9d2). |
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]>
a763545
to
71d4ad2
Compare
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]>
71d4ad2
to
ee3281d
Compare
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]>
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]>
ee3281d
to
868ac54
Compare
This should be ready for review now. Thanks! |
There was a problem hiding this 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.
test/widgets/message_list_test.dart
Outdated
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'); |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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
).
There was a problem hiding this comment.
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.
test/widgets/message_list_test.dart
Outdated
check(tester.widget<TextField>(channelContentInputFinder)) | ||
..decoration.isNotNull().hintText.equals('Message #${channel.name} > example topic') | ||
..controller.isNotNull().text.equals('Some text'); |
There was a problem hiding this comment.
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.
test/widgets/message_list_test.dart
Outdated
..controller.isNotNull().text.equals('Some text'); | ||
prepareGetMessageResponse([message]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
..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.
lib/model/message_list.dart
Outdated
} | ||
} | ||
|
||
void _messagesMovedFromNarrow(List<int> messageIds) { |
There was a problem hiding this comment.
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
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]>
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]>
868ac54
to
627b0f3
Compare
Thanks. Updated the PR. |
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]>
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]>
Thanks for the revision! Looks good — merging. I made a few commit-message tweaks, all in the summary prefixes:
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.) |
627b0f3
to
e9b5f6f
Compare
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.)
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.)
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 listhas 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'snarrow has changed, and if so tell the page state to update.
For comparison, see zulip-mobile's
src/events/doEventActionSideEffects.js
,or web.