-
Notifications
You must be signed in to change notification settings - Fork 309
Prepare API models to support polls (read-only) #823
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
Conversation
4b914be
to
8ce33d0
Compare
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! Looking forward to seeing the full version of this PR.
One high-level comment below, and one other:
Normally in our API code we just link to API docs and that covers the need for documentation. Here there aren't adequate API docs; so
- Please link instead to whatever sources you've made use of to work out what the API is — e.g. the web implementation, the zulip-mobile implementation, the server implementation.
- There are some doc comments on the API types in zulip-mobile. Those can I think be copied over mostly verbatim to describe the API semantics here — they're mostly playing the role that the docs at https://zulip.com/api/ normally play for us.
lib/api/model/model.dart
Outdated
@@ -570,6 +573,46 @@ enum MessageFlag { | |||
String toJson() => _$MessageFlagEnumMap[this]!; | |||
} | |||
|
|||
/// https://zulip.com/api/get-messages#response | |||
@JsonSerializable(fieldRename: FieldRename.snake) | |||
class Submessage { |
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.
Let's put this in the same file as the details of polls, and call it submessage.dart
.
- That way it's a bit more general than just polls, and makes a natural home for todo lists when we add those in the future.
- Once we have this feature all working, I'd like to avoid thinking about submessages as much as possible. 🙂 And one generally can do that — they don't come up except when specifically working on polls. So it's helpful to have this type not be in between these core message classes that come up all over the Zulip app model.
48f73c3
to
ea4617c
Compare
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.
Two other comments, from just taking a quick look today.
lib/api/model/model.dart
Outdated
// Only null when processed by store. | ||
// See [Polls]. | ||
@JsonKey(disallowNullValue: true, required: true) | ||
List<Submessage>? submessages; |
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.
Let's actually store null here in the case where the server gave an empty list, too. That's an optimization because it means less keeping around of a lot of empty lists (each of which is its own separate data structure allocated somewhere).
An existing place we use that optimization is reactions
here on the same Message
class. Not a coincidence that's also on Message, because that's a type we have tons of lying around and so where there's incentive to make this kind of optimization.
lib/api/model/model.dart
Outdated
@JsonKey(readValue: WidgetData.readFromMessage) | ||
WidgetData? widgetData; |
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 needs a doc comment, because it doesn't correspond directly to anything in the Zulip API (and so the Zulip API docs linked from the class's doc don't fill that role).
c794815
to
c5f3103
Compare
I revamped the poll model and make it a part of the message object. Previously, the plan was to have a dedicated centralized store (like unreads) for polls, where we maintain a map keyed by the message ID. The old design has several downsides:
|
The new approach will set |
2d0ef6b
to
e6e333f
Compare
a2bddaf
to
1701b2c
Compare
This update should have addressed most comments except the ones I commented on. Will get back to finishing off those later. |
18b1f75
to
c6edba9
Compare
Pushed again to remove preprocessing on |
0a9cf7f
to
6b94ed3
Compare
6b94ed3
to
632fa30
Compare
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! Here's a complete review of the first few commits, describing the bulk of the API:
0d7a464 api [nfc]: Avoid the use of dynamic on Message.
47cc913 api: Add Submessage.
451dad9 submessage: Add SubmessageData classes for polls.
Those are close to merge now; the comments below are mostly small. I've inserted three additional commits for changes that were simpler to handle that way:
721e0d8 api [nfc]: Expand comments on Submessage; put fields in logical order
afd3c9b api: Give defaults to question/options on PollWidgetExtraData
9a975b4 api [nfc]: Expand comments on SubmessageData and subclasses
Then let's split up this PR, since the thread has gotten long. So after acting on the comments below, please drop the remaining commits from this PR and send them as a follow-up PR:
2043822 api: Construct polls data store from submessages.
de62dba event: Handle submessage event for polls.
acbd557 (optional) test: Add a test helper for inspecting logs.
6b94ed3 poll: Support read-only poll widget UI.
test/api/model/submessage_test.dart
Outdated
void main() { | ||
group('Message.submessages', () { | ||
test('no crash on unrecognized submessage type', () { | ||
final submessageJson = { |
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 sounds like it should be valid JSON for a submessage, but it isn't quite; "base" is a good term for what it actually is, which is some JSON we'll use to construct JSON for submessages
final submessageJson = { | |
final baseJson = { |
test/example_data.dart
Outdated
@@ -614,6 +631,29 @@ ReactionEvent reactionEvent(Reaction reaction, ReactionOp op, int messageId) { | |||
); | |||
} | |||
|
|||
final pollWidgetDataFavoriteLetter = { |
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: should appear in the commit that starts using it
test/example_data.dart
Outdated
@@ -614,6 +631,29 @@ ReactionEvent reactionEvent(Reaction reaction, ReactionOp op, int messageId) { | |||
); | |||
} | |||
|
|||
final pollWidgetDataFavoriteLetter = { |
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.
final pollWidgetDataFavoriteLetter = { | |
const pollWidgetDataFavoriteLetter = { |
so this doesn't get mutated by one test, causing another to fail very confusingly
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.
separate nit: this isn't about events but rather messages, so should go in the messages section of the file; right next to eg.submessage
would be a good spot
lib/api/model/submessage.dart
Outdated
UnsupportedWidgetData({required this.json}); | ||
|
||
final Object? json; |
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: keep ordering consistent
UnsupportedWidgetData({required this.json}); | |
final Object? json; | |
final Object? json; | |
UnsupportedWidgetData({required this.json}); |
lib/api/model/submessage.dart
Outdated
@JsonKey(includeToJson: true) | ||
WidgetType get widgetType => WidgetType.unknown; | ||
|
||
UnsupportedWidgetData({required this.json}); |
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: can keep to the same pattern as other WidgetData subclasses:
UnsupportedWidgetData({required this.json}); | |
UnsupportedWidgetData.fromJson(this.json); |
(compare UnexpectedEvent
as an example)
lib/api/model/submessage.dart
Outdated
/// The index of last [option] added by the sender. | ||
@JsonKey(name: 'idx') | ||
final int latestOptionIndex; |
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 doesn't really mean [option]
, as in the option
field of this class, does it? (I'm not sure what that would mean if it did.) So it should just say "option" rather than "[option]".
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.
Then what does it mean that it's the index of the last option (or "latest", in the field name) added by the sender? Does that mean it's not the index of this option itself — the one that this submessage is about adding — but some other option instead? (Like the last one this sender added before this one?)
After reading through some of the related code, I think what this really wants to say is it's an index, or more concretely a sequence number, for this option… but counting among only the options added by this same sender.
So let's stick with the API's name — "latest" seems misleading, and "option" is redundant, leaving "index" which would in itself be clearer than "idx" but not enough to justify the discrepancy from the server API. And then explain in a comment what sort of "index" is meant:
/// The index of last [option] added by the sender. | |
@JsonKey(name: 'idx') | |
final int latestOptionIndex; | |
/// A sequence number for this option, among options added to this poll | |
/// by this [Submessage.senderId]. | |
/// | |
/// See [PollEventSubmessage.optionKey]. | |
final int idx; |
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.
Also expanded this to optionIndex
-> idx
of PollEventSubmessage.optionKey
.
lib/api/model/submessage.dart
Outdated
option.voters.add(senderId); | ||
case PollVoteOp.remove: | ||
option.voters.remove(senderId); | ||
case PollVoteOp.unknown: |
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.
case PollVoteOp.unknown: | |
case PollVoteOp.unknown: // TODO(log) | |
return; |
lib/api/model/submessage.dart
Outdated
|
||
/// As in [PollEventSubmessage.type]. | ||
@JsonEnum(fieldRename: FieldRename.snake) | ||
enum PollEventSubmessageType { |
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: let's put this right after PollEventSubmessage — it's closely tied to that base class (for example the fromJson
there has a switch that's on exactly these values), and it provides the outline of what the subclasses will be.
lib/api/model/submessage.dart
Outdated
final String question; | ||
final List<String> options; |
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 it's best to match the existing clients' behavior — that simplifies away any attempt to verify that unexpected shapes of data never happen. I've added a commit to do that:
afd3c9b api: Give defaults to question/options on PollWidgetExtraData
a1334af
to
3ad8035
Compare
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! One nit below, and then this will be ready to merge.
lib/api/model/submessage.dart
Outdated
|
||
final Map<String, Object?> json; | ||
|
||
UnknownPollEventSubmessage({required this.json}); |
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: same as #823 (comment)
Updated the PR. |
Signed-off-by: Zixuan James Li <[email protected]>
We are not storing the list of `Submessage`s directly on the Message objects as they are pretty obscure until we further parse them into meaningful data structures for things like polls and todos. Signed-off-by: Zixuan James Li <[email protected]>
The sealed class `SubmessageData` is not actually in use, we could potentially implement a discriminator utilizing the sealed class to deserialize individual submessage content, but it is far easier to do so when we have access to the full list of submessages. `SubmessageData` is there for self-documentation. It is also worth noting that much of these class definitions are based on previous reverse engineering effort and the web implementation. See: - https://github.com/zulip/zulip/blob/304d948416465c1a085122af5d752f03d6797003/web/shared/src/poll_data.ts - https://github.com/zulip/zulip-mobile/blob/2217c858e207f9f092651dd853051843c3f04422/src/api/modelTypes.js#L800-L861 Due to the flexibility of the submessage API, these classes tend to be intentionally defensive against unknown or invalid values. Signed-off-by: Zixuan James Li <[email protected]>
f9dca65
to
06ce603
Compare
Thanks! Looks good — merging. |
No description provided.