-
Notifications
You must be signed in to change notification settings - Fork 309
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
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.
lib/model/message_list.dart
Outdated
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.
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
fromJson
can 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. |
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.
@@ -23,6 +23,24 @@ void main() { | |||
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.