-
Notifications
You must be signed in to change notification settings - Fork 380
api: Add update_message_flags events #299
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
api: Add update_message_flags events #299
Conversation
gnprice
left a comment
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! Comments below just on the algorithmic question about the second commit; haven't yet read the first commit.
| for (final messageId in event.messages) { | ||
| // TODO(perf) could be more efficient? | ||
| final index = _findMessageWithId(messageId); |
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 think this algorithm is fine. For marking
I think the most efficient possible with these data structures is probably
We could make it linear in
lib/model/message_list.dart
Outdated
| messagesToProcess = []; | ||
| for (final messageId in event.messages) { | ||
| // TODO(perf) could be more efficient? | ||
| final index = _findMessageWithId(messageId); | ||
| if (index != -1) { | ||
| messagesToProcess.add(messages[index]); |
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.
The algorithmic comment I'd make on a different axis, though, is that I'd rather not build up this list. Instead, this loop can go ahead and do the add/remove of flags itself. Can have a boolean local variable that says if it's add or remove (from looking once at the type of the event), and condition on that inside the loop.
gnprice
left a comment
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.
OK, and read the first commit too. Generally looks great; one comment below.
| final List<int>? userIds; | ||
| final int? streamId; | ||
| final String? 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 think we'll probably want a crunchy shell on ensuring that when type is private we always have userIds, and when it's stream we always have streamId and topic.
That is, in zulip-flutter I think we've been pretty successful in making it so that malformed server data can't make us outright crash or misbehave, and instead just interrupts talking to the server (thanks mostly to the autogenerated fromJson implementations), so I'd like to try to hold the line on that as an invariant. And I suspect that doing so in the code that consumes this data, if it's just three independent nullables like this, will be annoying and make that code hard to read.
One way to get the crunchy shell would be to have a base class and two subclasses, akin to Message itself. But another solution might be more expedient and would also suffice: just have our fromJson check those conditions, and throw if they're not met, in much the same way as the autogenerated code does when an expected field is missing. For example our fromJson can take a few lines to do that on the Map, before then calling the autogenerated implementation as usual.
The downside of that approach is that the consuming code is slightly uglier, with ! operators scattered around, than it would be with subclasses. But I think that's a good trade because there's not that much consuming code for this type, and the subclasses mean significantly more code here at the definitions. (For Message the tradeoff goes heavily the other way, because that's such a fundamental type and so much of our code consumes 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.
For example our
fromJsoncan take a few lines to do that on theMap, before then calling the autogenerated implementation as usual.
How about calling the autogenerated implementation first? Then we can do an exhaustive switch on type, which will be a MessageType enum value.
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.
Sure.
80fc7e9 to
59f24e9
Compare
|
Thanks for the review! Revision pushed. |
59f24e9 to
82f7876
Compare
|
Just updated to do crunchy-shell validation that |
lib/model/message_list.dart
Outdated
| bool didUpdateAny = false; | ||
| if (event.all) { | ||
| for (final message in messages) { | ||
| (isAdd ? message.flags.add : message.flags.remove).call(event.flag); |
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.
It looks like call is dynamically typed. If for example I replace remove with join here — a random other method that should take a different type of argument — then there's no error.
Can clean up by expanding things to be just slightly more explicit:
isAdd ? message.flags.add(event.flag) : message.flags.remove(event.flag);which also has the virtue of looking more like conventional function calls we have in the rest of our code.
lib/model/message_list.dart
Outdated
| if (event.all) { | ||
| for (final message in messages) { | ||
| (isAdd ? message.flags.add : message.flags.remove).call(event.flag); | ||
| didUpdateAny |= true; |
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: this can be simplified to a plain assignment, which is equivalent:
| didUpdateAny |= true; | |
| didUpdateAny = true; |
| final List<int>? userIds; | ||
| final int? streamId; | ||
| final String? 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.
Sure.
|
Thanks for the revision! Two comments above. Let's also have tests for the API code, since there's now nontrivial non-autogenerated logic in it… oh and also tests on the model code in the second commit 🙂 |
|
Oh right! I see I mentioned those in the PR description but forgot to follow through, thanks for the bump. 🙂 |
82f7876 to
d8a3ea0
Compare
|
Pushed a revision with both those sets of tests. |
gnprice
left a comment
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 revision! Sorry for the delay in review.
All looks good except a few small comments below. Please merge at will modulo those.
| check(mkEvent([MessageFlag.read])).message.flags.deepEquals([MessageFlag.read]); | ||
| }); | ||
|
|
||
| test('update_message_flags/remove: require messageDetails in mark-as-unread', () { |
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: put the tests in the same order as the code they test
test/api/model/events_test.dart
Outdated
| check(() => UpdateMessageFlagsRemoveEvent.fromJson(json)).returnsNormally(); | ||
| json['flag'] = 'read'; | ||
| check(() => UpdateMessageFlagsRemoveEvent.fromJson(json)).throws(); | ||
| json['message_details'] = { | ||
| '123': {'type': 'private', 'mentioned': false, 'user_ids': [2]} | ||
| }; | ||
| check(() => UpdateMessageFlagsRemoveEvent.fromJson(json)).returnsNormally(); |
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 test looks good except that seeing the re-use of the json object makes me just a bit worried that a fromJson implementation might mutate the data it's passed, and confuse the test. (I'm not sure we don't already have such a fromJson somewhere.)
Something like { ...baseJson, 'flag': 'read' } would leave me reassured. (In principle I guess the messages array could be mutated too, but that seems less likely and I wouldn't worry.)
test/model/message_list_test.dart
Outdated
| UpdateMessageFlagsAddEvent mkAddEvent( | ||
| MessageFlag flag, | ||
| List<int> messageIds, { | ||
| bool all = false |
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:
| bool all = false | |
| bool all = false, |
d8a3ea0 to
ea173e4
Compare
After this, we'll be able to show a message's flags in the UI, such as unread markers (zulip#79).
ea173e4 to
8da16f1
Compare
|
Thanks! Merged, with those tweaks. |
| check(() => UpdateMessageFlagsRemoveEvent.fromJson({ | ||
| ...baseJson, 'message_details': {'123': {'type': 'private', 'mentioned': false, 'user_ids': [2]}}, | ||
| })).returnsNormally(); |
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 test should set 'flag': 'read' too, right?
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.
Eep, thanks! 89fd1f7
Thanks to Greg for catching this: #299 (comment)
We'll need to handle these events in the upcoming unread-messages model, for #253.
This PR also includes a draft commit:
That would be helpful toward #79, but I'm really unsure about the efficiency of using
_findMessageWithId(binary search) in this hot codepath. (The most common call will be for messages getting marked as read on scroll.) If that turns out to be OK, I can give that commit the tests it needs, and unmark as draft.