-
Notifications
You must be signed in to change notification settings - Fork 306
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
store: Add RecentDmConversationsView view-model #210
store: Add RecentDmConversationsView view-model #210
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 @chrisbobbe! This looks good — mainly comment-comments and small style comments below, plus one point about testing the call to notifyListeners
.
In the commit message of the main commit:
store: Add RecentDmConversationsView view-model
Toward #119, the list of recent DM conversations.
Inspired by how we do this in zulip-mobile, including in the tests.
I think it'd be good to say more explicitly that much of the code was transcribed from zulip-mobile and mention the specific filename src/pm-conversations/pmConversationsModel.js
. (And the tests' filename too.) If someone's reading the Git history and wants to understand how the code came to be as it is, that'll be something they want to go consult.
That also makes for a nice proof of concept: we've talked a few times about being able to effectively transcribe some kinds of code from JS to Dart in order to save work in the rewrite, and I think this is the most complex example yet where we've done so.
lib/api/model/model.dart
Outdated
/// For docs, search for "recent_private_conversations:" | ||
/// in <https://zulip.com/api/register-queue>. | ||
@JsonSerializable(fieldRename: FieldRename.snake) | ||
class RecentDmConversation { |
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 initial_snapshot.dart
at the end of that file, I think.
Unlike users, streams, subscriptions, and messages, this shape of object really only appears in one place in the API; and it's also not an object we'll be keeping around in our data structures. So it's really quite specific to the initial snapshot.
lib/api/model/model.dart
Outdated
factory RecentDmConversation.fromJson(Map<String, dynamic> json) => | ||
_$RecentDmConversationFromJson(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:
factory RecentDmConversation.fromJson(Map<String, dynamic> json) => | |
_$RecentDmConversationFromJson(json); | |
factory RecentDmConversation.fromJson(Map<String, dynamic> json) => | |
_$RecentDmConversationFromJson(json); |
test/example_data.dart
Outdated
zulipMergeBase: zulipMergeBase ?? recentZulipVersion, | ||
alertWords: alertWords ?? ['klaxon'], | ||
customProfileFields: customProfileFields ?? [], | ||
recentPrivateConversations: recentPrivateConversations ?? [], // TODO add recentPrivateConversations to default |
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 I'm happy to leave this empty in the default, just like we do the custom profile fields.
Like the latter, and unlike subscriptions and streams (and users), this isn't something that will be needed in the background of a wide range of tests that aren't primarily about it. Rather, the only tests that I expect will need this to be nonempty are the tests that are about this subsystem specifically, and that therefore should be explicitly specifying this data anyway.
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.
Makes sense.
Also, I realized after making this commit—
test [nfc]: Let callers customize initial snapshot from example data
This will be helpful when we add tests for the recent-DM-
conversations view model, coming up.
—that I didn't actually end up using eg.initialSnapshot
for the new tests. RecentDmConversationsView
doesn't take an InitialSnapshot
as input; it takes a List<RecentDmConversation>
that's supposed to be taken from the initial snapshot, and I just built a List<RecentDmConversation>
directly.
lib/model/narrow.dart
Outdated
@@ -70,6 +70,9 @@ class StreamNarrow extends Narrow { | |||
@override | |||
ApiNarrow apiEncode() => [ApiNarrowStream(streamId)]; | |||
|
|||
@override | |||
String toString() => 'StreamNarrow(streamId: $streamId)'; |
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.
String toString() => 'StreamNarrow(streamId: $streamId)'; | |
String toString() => 'StreamNarrow($streamId)'; |
This way it matches the constructor.
Also, our making the constructor's parameter positional like that reflects a judgement that it's clear this way without the name and the name would only add a bit of noise. So the same applies here.
(Similarly TopicNarrow below.)
lib/model/narrow.dart
Outdated
/// A [DmNarrow] from an item in [InitialSnapshot.recentPrivateConversations]. | ||
factory DmNarrow.ofRecentDmConversation(RecentDmConversation c, {required int selfUserId}) { | ||
return DmNarrow( | ||
allRecipientIds: [...c.userIds, selfUserId]..sort(), |
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.
allRecipientIds: [...c.userIds, selfUserId]..sort(), | |
allRecipientIds: [...conversation.userIds, selfUserId]..sort(), |
/// particularly in the helper _insertSorted. | ||
void handleMessageEvent(MessageEvent event) { |
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.
Per my other comment above, and adapted from the corresponding zulip-mobile comment:
/// particularly in the helper _insertSorted. | |
void handleMessageEvent(MessageEvent event) { | |
/// particularly in the helper _insertSorted. | |
/// | |
/// (*) In fact at present that's the only possible case. | |
/// The alternative will become possible when we have the analogue of | |
/// zulip-mobile's FETCH_MESSAGES_COMPLETE, with fetches done for a | |
/// [MessageListView] reporting their messages back via the [PerAccountStore] | |
/// to all our central data structures. | |
/// Then that can race with a new-message event: for example, | |
/// say we get a fetch-messages result that includes the just-sent | |
/// message 1002, and only after that get the event about message 1001, | |
/// sent moments earlier. The event queue always delivers events in order, so | |
/// even the race is possible only because we fetch messages outside of the | |
/// event queue. | |
void handleMessageEvent(MessageEvent event) { |
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.
Great; thanks!
check(setupView() | ||
..handleMessageEvent(MessageEvent(id: 1, message: eg.streamMessage()))) | ||
..map.deepEquals(expected.map) |
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, it's a little disconcerting that these two lines appear to be two cascade calls on the same receiver, but in fact are quite separate due to that one last paren at the end of the handleMessageEvent
line.
The simplest solution is probably to indent the handleMessageEvent
line another two spaces, similar to how we handle if-conditions that would otherwise abut the body:
check(setupView() | |
..handleMessageEvent(MessageEvent(id: 1, message: eg.streamMessage()))) | |
..map.deepEquals(expected.map) | |
check(setupView() | |
..handleMessageEvent(MessageEvent(id: 1, message: eg.streamMessage()))) | |
..map.deepEquals(expected.map) |
Here's a fun alternative solution I'd also be happy with:
check(setupView() | |
..handleMessageEvent(MessageEvent(id: 1, message: eg.streamMessage()))) | |
..map.deepEquals(expected.map) | |
check(setupView() | |
..handleMessageEvent(MessageEvent(id: 1, message: eg.streamMessage())) | |
) ..map.deepEquals(expected.map) |
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.
Interesting; yeah. In an earlier draft I had this:
check(setupView()
..handleMessageEvent(MessageEvent(id: 1, message: eg.streamMessage()))
)
..map.deepEquals(expected.map)
but then there's that closing paren all by itself on a line. I'll go with your second proposal for my next revision.
// This is here mostly for checked documentation of what's in | ||
// baseState, to help in reading the other test cases. |
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 is here mostly for checked documentation of what's in | |
// baseState, to help in reading the other test cases. | |
// This is here mostly for checked documentation of what | |
// setupView returns, to help in reading the other test cases. |
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.
bump
..sorted.deepEquals([key([1]), key([1, 2])]); | ||
}); | ||
|
||
test('stream message -> do nothing', () { |
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 addition to checking map
and sorted
, the other thing that would be nice to check here is that notifyListeners
didn't get called. (I think this should be straightforward by registering a listener.)
That's the analogue of the expect(state).toBe(baseState);
on the zulip-mobile side, in the persistent-data-structures world where object identity is the signal that nothing has changed.
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 and in fact, the tests should definitely check the inverse fact: that notifyListeners
is indeed called in the cases where things do change. A bug in that direction would be a nasty one.
..sorted.deepEquals([key([1]), key([1, 2])]); | ||
}); | ||
|
||
test('existing conversation, not newest in 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.
(This case can leave out a check as to whether notifyListeners
is called; it doesn't need to be because nothing changed, and in zulip-mobile we return the old state which is the equivalent of not calling it, but in this implementation it does get called, and that's fine because this should be a rare case and there's really no need for optimizations on it.)
2c29cdd
to
88ac528
Compare
Thanks for the review! Revision pushed. |
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! All looks good modulo two nits below.
// This is here mostly for checked documentation of what's in | ||
// baseState, to help in reading the other test cases. |
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.
bump
import '../api/model/events.dart'; | ||
import 'narrow.dart'; | ||
|
||
/// A view-model for the recent DM conversations UI. |
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: I think this parses better with some hyphens in that attributive compound noun:
/// A view-model for the recent DM conversations UI. | |
/// A view-model for the recent-DM-conversations UI. |
88ac528
to
cc4e929
Compare
Thanks! Revision pushed. |
Technically maxFileUploadSizeMib is for the whole server, I think, but it seems fine to treat it as realm info for the purpose of ordering these. See Greg's bulleted list here: zulip#183 (review)
This will be helpful for test-failure output, particularly with DmNarrows as we start using those as Map keys, soon. Of our current Narrow subclasses, this just leaves AllMessagesNarrow without a custom `toString` implementation. That's OK, because the default `toString` already says "AllMessagesNarrow", which is all we need to know about the narrow.
Toward zulip#119, the list of recent DM conversations. Much of this code was transcribed from zulip-mobile; in particular, from: src/pm-conversations/pmConversationsModel.js src/pm-conversations/__tests__/pmConversationsModel-test.js
cc4e929
to
3550d87
Compare
Thanks! LGTM; merging. |
Toward #119, the list of recent DM conversations.
Inspired by how we do this in zulip-mobile, including in the tests.