-
Notifications
You must be signed in to change notification settings - Fork 306
autocomplete: In user-mention autocomplete results give priority to users in DM conversations #693
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
autocomplete: In user-mention autocomplete results give priority to users in DM conversations #693
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, mostly small.
..sorted.deepEquals(expected.sorted); | ||
..sorted.deepEquals(expected.sorted) | ||
..latestMessagesByRecipient.deepEquals(expected.latestMessagesByRecipient); | ||
}); |
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.
Huh, does this existing test ('existing conversation, not newest in conversation') want a check(listenersNotified).isFalse();
at the end? That sounds like it might be a nice independent improvement, if you'd like to include that in a separate commit.
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.
Added it in a separate commit, but check(listenersNotified).isTrue();
instead of check(listenersNotified).isFalse();
.
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.
Ah, hmm. In this case it's unnecessary to notify listeners, and slightly undesirable, since nothing in the data structure actually changes, right? Quoting the comment in handleMessageEvent
in the case that this test exercises:
// The conversation already has a newer message.
// This should be impossible as long as we only listen for messages coming
// through the event system, which sends events in order.
// Anyway, do nothing.
I think it's not really bad to notify listeners. This case isn't even supposed to be exercised at all, currently, and it may rarely get exercised after #650 in certain race conditions. But I don't think we should have a check that expects the slightly undesirable behavior.
Since the test caught my attention as possibly missing something, though, it might be helpful to add something to it, so the next reader doesn't have to wonder the same thing. Maybe a commented-out check; something like this:
// (listeners are notified unnecessarily, but that's OK)
// check(listenersNotified).isFalse();
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.
Separately, let's add a ..latestMessagesByRecipient.deepEquals
check to this test, in the commit that introduces latestMessagesByRecipient
.
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 on my comment at #693 (comment) :
But I don't think we should have a check that expects the slightly undesirable behavior.
final map = Map.fromEntries(entries); | ||
final sorted = QueueList.from(entries.map((e) => e.key)); | ||
|
||
final msgsByUser = <int, int>{}; |
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 my preference for this variable name is just latestMessagesByRecipient
, on the pattern of map
and sorted
, which are named the same as the corresponding parameters of RecentDmConversationsView._
. That way I don't have to think about whether "msgs" means the same thing as "latestMessages", or if "ByUser" means the same thing as "ByRecipient".
final msgsByUser = <int, int>{}; | ||
for (final entry in entries) { | ||
final dmNarrow = entry.key; | ||
final msg = entry.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.
nit: Could use a more descriptive name than msg
, like maxMsgId
. I think maxMessageId
would also be fine; I don't mind seeing the word "message" written out.
lib/model/autocomplete.dart
Outdated
@@ -267,13 +267,48 @@ class MentionAutocompleteView extends ChangeNotifier { | |||
|
|||
List<User>? _sortedUsers; | |||
|
|||
List<User> sortByRelevance({required List<User> users}) { | |||
/// Determines which of the two users are more recent in DM conversations. |
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 "which […] is more recent"
lib/model/autocomplete.dart
Outdated
int compareByDms(User userA, User userB) { | ||
final recentDms = store.recentDmConversationsView; | ||
final aLatestMessageId = recentDms.latestMessagesByRecipient[userA.userId] ?? -1; | ||
final bLatestMessageId = recentDms.latestMessagesByRecipient[userB.userId] ?? -1; | ||
|
||
return bLatestMessageId.compareTo(aLatestMessageId); | ||
} |
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 there are six interesting cases that could happen:
- we have DMs with
userA
and withuserB
;userA
is more recent - we have DMs with
userA
and withuserB
;userB
is more recent - we have DMs with
userA
and withuserB
; they are equally recent - we have DMs with
userA
but not withuserB
- we have DMs with
userB
but not withuserA
- we don't have DMs with
userA
oruserB
In our unit tests for compareByDms
, let's have a test case that exercises each of these six cases.
Also, while this code is nice and compact, I noticed myself having some difficulty following it, and I think we can remove some of that difficulty without making the function much more verbose. In particular:
-1
is not a real message ID, so I found it distracting that the variables named…MessageId
could take-1
as a value. (I wondered if this type mismatch might lead to a bug, and I spent some time thinking about that.)- I think I slightly prefer spelling
b.compareTo(a)
as-a.compareTo(b)
, as we've done in a few places in the code. I'm not sure exactly why. Maybe because it feels more familiar to seea
beforeb
, and it feels nicer to see something familiar plus a simple operation on it, then to think through something unfamiliar? I'm not really sure. 🤔
Here's one possible way this could look, I think (untested):
int compareByDms(User userA, User userB) {
final recentDms = store.recentDmConversationsView;
final aLatestMessageId = recentDms.latestMessagesByRecipient[userA.userId];
final bLatestMessageId = recentDms.latestMessagesByRecipient[userB.userId];
return switch((aLatestMessageId, bLatestMessageId)) {
// negate because higher message IDs are more recent
(int a, int b) => -a.compareTo(b),
(int(), _ ) => -1,
(_, int()) => 1,
(_, _ ) => 0
};
}
test/model/autocomplete_test.dart
Outdated
for (final user in users) { | ||
store.addUser(user); | ||
} |
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 (final user in users) { | |
store.addUser(user); | |
} | |
store.addUsers(users); |
test/model/autocomplete_test.dart
Outdated
store = eg.store( | ||
initialSnapshot: eg.initialSnapshot( | ||
recentPrivateConversations: dmConversations | ||
)); |
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.
Can be a bit more compact:
store = eg.store( | |
initialSnapshot: eg.initialSnapshot( | |
recentPrivateConversations: dmConversations | |
)); | |
store = eg.store(initialSnapshot: eg.initialSnapshot( | |
recentPrivateConversations: dmConversations)); |
lib/model/autocomplete.dart
Outdated
void handleMessageEvent(MessageEvent event) { | ||
_refreshStaleUserResults(); | ||
} | ||
|
||
void handleOlderMessages() { | ||
_refreshStaleUserResults(); | ||
} |
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.
handleMessageEvent
makes sense, because a new message might cause RecentDmConversationsView.latestMessagesByRecipient
to update. But do we need handleOlderMessages
? That won't cause RecentDmConversationsView.latestMessagesByRecipient
to update.
…ah, I guess this will be used when we start considering the current topic/stream, which I see you're working on in #692. Makes sense.
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, with these additional calls to _refreshStaleUserResults
, the dartdoc of MentionAutocompleteView.refreshStaleUserResults
could benefit from an update. Here it is now:
/// Recompute user results for the current query, if any.
///
/// Called in particular when we get a [RealmUserEvent].
It looks like it might imply that it's only called on a [RealmUserEvent]
, and that's not accurate anymore.
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.
…ah, I guess this will be used when we start considering the current topic/stream, which I see you're working on in #692. Makes sense.
I removed it for this PR and will add it to the next one where it is used. Firstly, I thought this was necessary for RecentDmConversationsView.latestMessagesByRecipient
.
return results; // TODO(#228) sort for most relevant first | ||
return results; |
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.
Since the sorting described in #228 isn't accomplished in this commit, and in fact this commit doesn't do any sorting—that comes later—let's not remove the TODO in this commit. You could move it somewhere else, if you think there's a better place for it (maybe in _sortUsers
?).
lib/model/autocomplete.dart
Outdated
if (_sortedUsers != sortedUsers) { | ||
// The list of users this loop has been working from has become stale. | ||
// Abort so _startSearch can retry with the new list. | ||
throw ConcurrentModificationError(); | ||
} |
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.
ConcurrentModificationError
is convenient because that's the error that gets caught in _startSearch
, but it's not the right error for what we're signaling here. Quoting its dartdoc:
/// Error occurring when a collection is modified during iteration.
That was accurate about the ConcurrentModificationError
that our code was dealing with before this commit. It was thrown from iterator.moveNext()
, where iterator
was from a mutable collection on the store. That collection was vulnerable to being modified while the query was being processed.
Here, though, we want to trigger a retry because _sortedUsers
was invalidated. That is caused by…hmm, I guess by various events that are about users. But it isn't caused by a collection being modified during iteration.
Could just throw and catch an instance of a new custom error class, instead.
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.
Relatedly, at this commit, the description of one of our tests becomes wrong:
test('MentionAutocompleteView mutating store.users while in progress causes retry', () async {
I believe the test still passes because we invalidate _sortedUsers
when a new-user event arrives. But the reason is no longer about anything being mutated, so we should change the description. Maybe '… new-user event while in progress causes retry'?
Also there's a comment later in the test that refers to mutation:
// new result sticks; no "zombie" result from `store.users` pre-mutation
It would be good to exercise all the different things that can cause a retry…hmm. Maybe we could put the bulk of this test's code into a reusable function , and swap out the store.addUser
line for an action that the caller could specify by passing a function, and that passed function could do these different things in different tests:
- new-user event
- remove-user event
- update-user event
- new-message event
- (
handleOlderMessages
?)
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 ConcurrentModificationError
is fine here. Conceptually, what we are doing is iterating through a collection that got modified: _sortedUsers
can be thought of as a collection which is a view (in the database sense) on store.users
. Because store.users
got modified, effectively _sortedUsers
did too.
It's true that the way that "users sorted for autocomplete" collection is implemented is that there's a particular List<User>
(so it's a "materialized view" in the database sense), and iterating through the collection iterates through that, and when the underlying "users" collection gets modified we throw away the list and replace it. That List<User>
didn't get modified. But that list is an implementation detail in the "users sorted for autocomplete" collection that we're conceptually iterating through.
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 would be good to exercise all the different things that can cause a retry…hmm. Maybe […]
Yeah, this would be a good change. We're now depending on those _sortedUsers = null;
lines in order for this to have the right behavior; so we should extend the tests so that those are exercised. We should aim to make it so that if any one of those lines were deleted, there's some test that would fail.
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.
@gnprice, @chrisbobbe
There's this one thing that I just noticed recently about this part and that is when store.users
is modified, the _computeResults
is called twice, once when _startSearch
is called through refreshStaleUserResults
and secondly when ConcurrentModificationError
is thrown which causes a retry.
So what I recommend is the following change in _startSearch
:
- while (true) {
try {
newResults = await _computeResults(query);
- break;
} on ConcurrentModificationError {
// Retry
// TODO backoff?
+ 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.
Hmm interesting. Is that a problem already in main?
If so, let's understand it there, and fix it in a prep commit. That reduces the amount of complexity we're debugging at a time.
If not, then what is this code doing differently from what's in main that causes this problem?
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 interesting. Is that a problem already in main?
Yeah, this call duplication existed from the main. Also, no ConcurrentModificationError
would be thrown on RealmUserUpdateEvent
. Both of them are fixed in the same commit.
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. In the new revision, it looks like this is the relevant commit — is that right?
7354ce7 autocomplete: Terminate the already-running search when there is a new one
That commit would definitely benefit from some more explanation in the commit message about what problem it's solving and how. See our docs on commit descriptions:
https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html#commit-description
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.
Added the description.
9a6757b
to
cf3737d
Compare
Thanks @chrisbobbe and @gnprice for the review! Pushed the revision. PTAL. Also added replies to some of your comments above. |
cf3737d
to
05db60b
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! Comments below.
In this review, I noticed an opportunity to simplify MentionAutocompleteView
's responsibilities and improve the UX in some ways. (Actually Greg noticed it—we're in the office today and I asked for his thoughts on the retry.) We noticed it after you drew our attention to the unnecessary retry on ConcurrentModificationError
, and in particular, it was quite helpful that you made a separate commit focused specifically on that! 🙂
In particular, let's try these things:
- Remove
MentionAutocompleteView.refreshStaleUserResults
. - Keep that retry on
ConcurrentModificationError
as it was. - For the list of sorted users, calculate it just once in the lifetime of the
MentionAutocompleteView
instance, without recalculating it on certain events.
This does mean accepting some staleness in the results, including when the user is looking at an autocomplete menu without typing anything to change their query. However:
- A
MentionAutocompleteView
instance corresponds to just one autocomplete interaction, and those interactions aren't expected to last for more than a few moments, from when the user starts typing a name to when they select an option from the list. - There can be a UX benefit to letting the list become slightly stale: it's nicer when menus don't jump around while we're trying to select an item, and that can happen if we're live-updating in response to events.
MentionAutocompleteView
's code will be simpler.- The sorting computation is expensive and will get more expensive as we implement the rest of autocomplete: Sort user-mention autocomplete results #228. If re-sorting on various events causes dropped frames, like when the user is scrolling through the autocomplete list, it's unfortunate, and probably not offset by much benefit. (As a user, it's easy to recover from an out-of-date sorting in the list: just refine your query by typing another character or two in your query.)
check(setupView() | ||
..addListener(() { listenersNotified = true; }) | ||
..handleMessageEvent(MessageEvent(id: 1, message: message)) | ||
) ..map.deepEquals({ | ||
key([1, 3]): 300, | ||
key([1]): 200, | ||
key([1, 2]): 100, | ||
}) | ||
..sorted.deepEquals([key([1, 3]), key([1]), key([1, 2])]) | ||
..latestMessagesByRecipient.deepEquals({1: 300, 2: 100, 3: 300}); |
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 preferred the indentation in the previous revision:
check(setupView() | |
..addListener(() { listenersNotified = true; }) | |
..handleMessageEvent(MessageEvent(id: 1, message: message)) | |
) ..map.deepEquals({ | |
key([1, 3]): 300, | |
key([1]): 200, | |
key([1, 2]): 100, | |
}) | |
..sorted.deepEquals([key([1, 3]), key([1]), key([1, 2])]) | |
..latestMessagesByRecipient.deepEquals({1: 300, 2: 100, 3: 300}); | |
check(setupView() | |
..addListener(() { listenersNotified = true; }) | |
..handleMessageEvent(MessageEvent(id: 1, message: message)) | |
) ..map.deepEquals({ | |
key([1, 3]): 300, | |
key([1]): 200, | |
key([1, 2]): 100, | |
}) | |
..sorted.deepEquals([key([1, 3]), key([1]), key([1, 2])]) | |
..latestMessagesByRecipient.deepEquals({1: 300, 2: 100, 3: 300}); |
, on the pattern of existing tests. I think this helps show that the ..map
and the ..sorted
and the ..latestMessagesByRecipient
are grouped together as similar tasks. (Similarly in another test, below.)
final message = eg.dmMessage(id: 300, from: eg.selfUser, | ||
to: [eg.user(userId: 1), eg.user(userId: 3)]); |
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 previous revision's two-space indent (here and in the other test below)
final message = eg.dmMessage(id: 300, from: eg.selfUser, | |
to: [eg.user(userId: 1), eg.user(userId: 3)]); | |
final message = eg.dmMessage(id: 300, from: eg.selfUser, | |
to: [eg.user(userId: 1), eg.user(userId: 3)]); |
lib/model/autocomplete.dart
Outdated
/// Recompute user results for the current query, if any. | ||
/// | ||
/// Called in particular when we get a [RealmUserEvent]. | ||
/// Called in particular when we get a [RealmUserEvent], a [MessageEvent], or | ||
/// [MessageListView.fetchOlder] is called. |
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.
autocomplete: Introduce a field for sorted users
The last part isn't accurate at this commit, right; this doesn't get called when [MessageListView.fetchOlder]
is called.
lib/model/autocomplete.dart
Outdated
while (true) { | ||
try { | ||
newResults = await _computeResults(query); | ||
break; | ||
} on ConcurrentModificationError { | ||
// Retry | ||
// TODO backoff? | ||
} | ||
try { | ||
newResults = await _computeResults(query); | ||
} on ConcurrentModificationError { | ||
// Terminate this search as there is a new search going on | ||
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.
Interesting. Yes, it does seem like the retry is unnecessary—as long as there actually is a new search happening.
The comment says there is a new search going on, and I think it's probably right. But it seems vulnerable to being wrong, especially if some code in a different area is changed, in the future, without considering whether this comment is still true after the change.
One possible approach is to add to the comment exactly why we think it's true, so at least it's more obvious if some future change makes it untrue.
But I think there's a different approach that avoids this issue and, independently of that, would simplify much of the autocomplete code and give a nicer UX in some ways. In this approach, we keep the retry and drop the new search—by removing the MentionAutocompleteView.refreshStaleUserResults
method. I'll say more about this approach in my overall review comment.
lib/model/store.dart
Outdated
users[-1] = user; | ||
users.remove(-1); |
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 don't love the use of -1
here.
If we had a "user ID" type that excluded the value -1
, if such a thing were possible in the type system, we would surely apply it to users
, as something like Map<UserId, User>
, and this code would be rejected.
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 sure, we cannot call this approach a 100% safe one, but I thought userIds
are most probably positive int
s, but I am not sure and I couldn't find any doc confirming this.
One other approach would be the following:
users.remove(user);
users.add(user);
What do you say?
lib/model/store.dart
Outdated
// The following two lines will eventually not change [users], but are here to | ||
// trigger a modification to the collection as only changing the properties | ||
// of [user] does not signal a modification of [users] where needed. | ||
// | ||
// One example of this is [MentionAutocompleteView._computeResults] which | ||
// throws a `ConcurrentModificationError` when there is a modification done | ||
// to [users]. | ||
users[-1] = user; | ||
users.remove(-1); |
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 appreciate the explanation in this comment. It sounds like before this commit, an update-user event would not cause an in-progress computation to terminate (via ConcurrentModificationError
), and now it does cause the computation to terminate.
This is a change in the app's observable behavior, so it doesn't belong in a foo test: […]
commit. Occasionally a commit that's marked "test" will change some app code, but those changes shouldn't change user-facing behavior. (For example, you might adjust test-related comments, as in 9c8a283, or add a test-only debugFoo
field to a class, or something.)
test/model/autocomplete_test.dart
Outdated
check(results).single | ||
.isA<UserMentionAutocompleteResult>() | ||
.userId.equals(1); |
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:
check(results).single | |
.isA<UserMentionAutocompleteResult>() | |
.userId.equals(1); | |
check(results).single | |
.isA<UserMentionAutocompleteResult>() | |
.userId.equals(1); |
(and in some other tests)
@@ -235,14 +235,11 @@ class MentionAutocompleteView extends ChangeNotifier { | |||
_startSearch(MentionAutocompleteQuery query) async { | |||
List<MentionAutocompleteResult>? newResults; | |||
|
|||
while (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.
autocomplete: Terminate the already-running search when there is a new one
Before this fix, when there was a search in progress in `_startSearch`,
and then for some reason, `_startSearch` was called again […]
Nit: When the reason for the new search is that the query has changed, we actually do terminate the already-running search. See the query != _query
check in _computeResults
, and the test described as 'MentionAutocompleteView new query during computation replaces old'.
3ad170b
to
783cb10
Compare
Thank you @chrisbobbe for the review! Pushed the revision. PTAL. The only problem is that some tests are failing, specifically I am also curious about one thing and that is, now that we don't want to listen to live updates for the autocomplete results in a single session, why should we retry the results while we have an in-progress computation? |
So in the existing code, if a user gets added or removed from That's a situation where we're forced to act in order to finish the search at all; the underlying primitives we're using don't give us the option of just brushing off the change and letting the results be slightly stale, or stale as to just the one user affected. The idea that's new here in #693 (review) is that where we do have the choice, we'd rather just live with slightly stale results. So in particular that means we don't want to cause a ConcurrentModificationError in situations where we wouldn't already encounter 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.
Thanks for the revision!
The only problem is that some tests are failing, specifically
MentionAutocompleteView mutating store.users while in progress causes retry
in8aa9658
: autocomplete [nfc]: Remove the refreshStaleUserResults method and […]
Interesting; yeah, I've just reproduced this test failure. Here is a proposed fix for that:
Here is the output:
00:04 +112 -1: MentionAutocompleteView mutating store.users while in progress causes retry [E]
Expected: a bool that:
is true
Actual: <false>
package:checks/src/checks.dart 85:9 check.<fn>
package:checks/src/checks.dart 708:12 _TestContext.expect
package:checks/src/extensions/core.dart 108:13 BoolChecks.isTrue
test/model/autocomplete_test.dart 295:17 main.<fn>
Looking at the test code, it looks like the test expected the search to be complete (check(done).isTrue();
) but in fact it was not (done
was false at that time).
It's fixed by adding another await Future(() {});
in there:
diff --git test/model/autocomplete_test.dart test/model/autocomplete_test.dart
index 41f49d144..0726fc7a6 100644
--- test/model/autocomplete_test.dart
+++ test/model/autocomplete_test.dart
@@ -292,6 +292,8 @@ void main() {
await Future(() {});
check(done).isFalse();
await Future(() {});
+ check(done).isFalse();
+ await Future(() {});
check(done).isTrue();
check(view.results).single
.isA<UserMentionAutocompleteResult>()
It seems like before this commit, the redundant search from refreshStaleUserResults
was finishing before the retry/ConcurrentModificationError
search finished. With the search from refreshStaleUserResults
removed, the test now needs to wait a little longer for the retry-search to finish. Specifically, it seems like it has to wait for another iteration of the Dart event loop.
When we were writing MentionAutocompleteView
, Greg recommended this article to understand Dart's event-loop architecture: https://web.archive.org/web/20170704074724/https://webdev.dartlang.org/articles/performance/event-loop
The reason for this line in _computeResults
—
// CPU perf: End this task; enqueue a new one for resuming this work
await Future(() {});
—is to give a chance for other code, such as UI animations, to use the CPU between batches of 1000 query.testUser
calls. Some Zulip orgs have very many users, and we would probably drop some animation frames if one event-loop iteration had to wait for 20,000+ query.testUser
calls to be done at once. So, we do the computation asynchronously in batches of 1000.
More later; I have some thoughts after reading Greg's comment at #693 (comment).
..sorted.deepEquals(expected.sorted); | ||
..sorted.deepEquals(expected.sorted) | ||
..latestMessagesByRecipient.deepEquals(expected.latestMessagesByRecipient); | ||
}); |
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 on my comment at #693 (comment) :
But I don't think we should have a check that expects the slightly undesirable behavior.
@@ -125,23 +125,11 @@ class AutocompleteViewManager { | |||
assert(removed); | |||
} | |||
|
|||
void handleRealmUserAddEvent(RealmUserAddEvent 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.
autocomplete [nfc]: Remove the `refreshStaleUserResults` method
This method was used to search for the new user results in autocomplete
when the users in store were modified during an in-progress computation.
Along with this, `_startSearch` would do the same thing by retrying,
resulting in duplicate actions. By removing this, the unnecessary search
for user results is prevented.
Since this commit changes the app's behavior, it's not [nfc]
.
One important change is: if a search computation isn't in progress when our user data changes, then we won't start a new search anymore. For example:
- You type
@Sayed M
- You see that your name is in the list
- You stop typing
- From your computer, you make a new user named "Sayed Mahmood Sayedi (Test Account)".
What happens next is affected by the changes in this commit. Before this commit, that new user would appear in the list. At this commit, the user would not appear. That behavior change is OK, for the reasons I mentioned at #693 (review) . But in the commit message, let's emphasize that the commit is about intentionally removing some live-updating behavior. A link to that GitHub comment would give the reader helpful context on that decision, or you could summarize it.
As you've pointed out, this commit removes a redundant computation in the (rare?) case where a computation is in progress when the users
collection is changed.
(It also removes a computation that isn't redundant. Before this commit, if a RealmUserUpdateEvent
arrived when search was in progress, we would start a new search to replace the old one. This commit drops that new search, because, as you've found, the retry on ConcurrentModificationError
isn't triggered in the update-user case. But again, that's OK; this commit is about simplifying this system by making it not care if its output is slightly outdated.)
@gnprice, it occurred to me after reading your comment above: if we compute the sorted-users list just once per If this is right, Greg, maybe this might be a good commit sequence for this PR: e77e644 recent-dm-conversations test [nfc]: Add a missing check of Then a commit that computes a user list sorted by DM recency, but with this computation done only once (maybe in Then a commit that makes |
Thanks @gnprice for the comment and @chrisbobbe for the review!
Yeah, that's right, but it's not true until
Yeah |
This method was used to search for the new user results in autocomplete when the users in store were modified during an in-progress computation. Along with this, `_startSearch` would do the same thing by retrying, resulting in duplicate actions. By removing this, the unnecessary search for user results is prevented. Besides, this commit also removes some live-updating behavior of user-mention autocomplete results in order to provide a better UX. (To read more about this decision, check out zulip#693 (review))
783cb10
to
d65c114
Compare
Yes, that sounds right.
I like those first three as the start of the sequence, but there's another aspect of the current revision which I'd like to preserve: it has this commit introducing a sorted-users list without yet doing any actual sorting: and introduces the actual sorting after that. I like that structure because along with introducing the sorted-users list comes changing some of the infrastructural logic around it, and so that structure lets that logic be isolated from the changes that are concerned with things like DM recency. So I think a good sequence would be:
along with other commits interspersed as needed for things like adding test helpers, like these existing commits in the PR:
@sm-sayedi I think that's part of what's addressed in the commit sequence Chris proposed, and also my revised version — the change where we stop anticipating
I don't think this works, if I'm understanding you right. If you do final iterator = store.users.values.iterator;
while (iterator.moveNext()) {
final user = iterator.current();
/* … */
} Dart has a bit of a gap in reference documentation for the language itself, so I can't point to a completely on-point piece of docs for that. But that's what I'd expect from general cross-language conventions, from the docs on for-loops: |
And yeah, in |
You're right. For in loop is like a high level iterator, but what I meant was the for loop.
|
Ah. In that case if you try to fill in the |
This method was used to search for the new user results in autocomplete when the users in store were modified during an in-progress computation. Along with this, `_startSearch` would do the same thing by retrying, resulting in duplicate actions. By removing this, the unnecessary search for user results is prevented. Besides, this commit also removes some live-updating behavior of user-mention autocomplete results in order to provide a better UX. (To read more about this decision, check out zulip#693 (review))
d65c114
to
08fd062
Compare
This method was used to search for the new user results in autocomplete when the users in store were modified during an in-progress computation. Along with this, `_startSearch` would do the same thing by retrying, resulting in duplicate actions. By removing this, the unnecessary search for user results is prevented. Besides, this commit also removes some live-updating behavior of user-mention autocomplete results in order to provide a better UX. (To read more about this decision, check out zulip#693 (review))
08fd062
to
9da1bab
Compare
This method was used to search for the new user results in autocomplete when the users in store were modified during an in-progress computation. Along with this, `_startSearch` would do the same thing by retrying, resulting in duplicate actions. By removing this, the unnecessary search for user results is prevented. Besides, this commit also removes some live-updating behavior of user-mention autocomplete results in order to provide a better UX. (To read more about this decision, check out zulip#693 (review))
9da1bab
to
bca9c2c
Compare
Revision pushed @chrisbobbe. Please have a look. |
da4c9a0
to
9a1c213
Compare
Thanks for the revision! LGTM; marking for Greg's review. |
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 @sm-sayedi, and thanks @chrisbobbe for the previous reviews! Generally this looks good.
Various comments below. I think at this point they're all about style or tests, not the production behavior. Style and tests are important too (because they affect our future development work); but they're less likely to mean cascading changes to other parts of the PR, so that's a sign that it's closing in toward merge 🙂
@@ -1,3 +1,5 @@ | |||
import 'dart:math'; |
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 in commit messages:
e9eec48 recent_dm_conversations test [nfc]: Comment on a missing not-notified check
a4c640a recent-dm-conversations: Add latestMessagesByRecipient
data structure
In the summary lines, "recent_dm_conversations" or "recent-dm-conversations" is pretty long for a slug — it occupies a big chunk of the 76 or so columns available. So let's make an abbreviation for it. Say, "recent-dms".
This is like how for changes to the message list, we use "msglist" for the summary-line slug, and for notifications we say "notif". Using hyphens is less common, but we have a few commits saying "action-sheet" or "choose-account". (And once it's an abbreviation and not an identifier that appears in the code, I wouldn't want to use underscores, so e.g. not "recent_dms" — that would make it look like an identifier, misleadingly so.)
map: Map.fromEntries(entries), | ||
sorted: QueueList.from(entries.map((e) => e.key)), | ||
map: map, | ||
sorted: sorted, |
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: these two can remain inlined here — simplifies the diff, and I think also a bit simpler to read in the resulting code
final dmNarrow = entry.key; | ||
final maxMessageId = entry.value; | ||
for (final userId in dmNarrow.otherRecipientIds) { | ||
// only take the latest message of a user across all the conversations. |
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: either a complete sentence with initial capital and final period, or incomplete with neither, but not a period without an initial capital
} | ||
for (final recipient in key.otherRecipientIds) { |
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: as this function body gets more complex, it calls for a bit more organization. In particular let's separate it into stanzas for the different parts of its job. So here:
} | |
for (final recipient in key.otherRecipientIds) { | |
} | |
// Update [latestMessagesByRecipient]. | |
for (final recipient in key.otherRecipientIds) { |
and a blank line below before notifyListeners
; and a blank line and a comment "Update [map] and [sorted]." before the final prev =
line.
The most important effect of all that is to group together the map/sorted logic. That logic is fairly gnarly, so it's real helpful for the reader to be able to isolate it and not wonder about where its boundaries are or how it might intermingle with the other logic around it. Highlighting the boundaries wasn't particularly necessary when its boundaries were nearly the same as those of the function body, separated only by a few trivial lines above and below; but now that there's this new other piece of nontrivial logic, it becomes more needed.
..sorted.deepEquals([key([2]), key([1]), key([1, 2])]) | ||
..latestMessagesByRecipient.deepEquals({1: 200, 2: 300}); |
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:
..sorted.deepEquals([key([2]), key([1]), key([1, 2])]) | |
..latestMessagesByRecipient.deepEquals({1: 200, 2: 300}); | |
..sorted.deepEquals([key([2]), key([1]), key([1, 2])]) | |
..latestMessagesByRecipient.deepEquals({2: 300, 1: 200}); |
IOW, keep the expected map sorted (in the source code) by value, the same way the expected map
is above. That helps keep it easy for the reader to verify that the expected data is 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.
(similarly in a couple of other cases below)
/// returns a positive number if [userB] is more recent than [userA], | ||
/// and returns `0` if both [userA] and [userB] are equally recent | ||
/// or there is no DM exchanged with them whatsoever. | ||
static int compareByDms(User userA, User userB, {required PerAccountStore store}) { |
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.
static int compareByDms(User userA, User userB, {required PerAccountStore store}) { | |
@visibleForTesting | |
static int compareByDms(User userA, User userB, {required PerAccountStore store}) { |
Conceptually this is a private helper — it's exposed only because that's convenient for unit testing.
(We also don't have to expose it in order to write all the tests we like; we could do a more end-to-end test by running a query "" and seeing what order the results come in. But the meaning of this is fairly crisp, so it's basically harmless to expose if it's convenient for tests.)
test/model/autocomplete_test.dart
Outdated
|
||
final userA = eg.user(userId: 1); | ||
final userB = eg.user(userId: 2); | ||
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store); | ||
check(compareAB).isNegative(); |
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.
These test cases have a fair number of lines that are repetitive. Let's pull those out into a local helper function, like this:
final userA = eg.user(userId: 1); | |
final userB = eg.user(userId: 2); | |
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store); | |
check(compareAB).isNegative(); | |
check(compare1vs2()).isNegative(); |
That'll mean each test case adds a grand total of 7 or 8 lines instead of 11 or 12, which makes the whole list of test cases quite a lot more compact to look at. That's helpful for scanning through it and seeing what's covered and thinking of things that might not be.
Relatedly, it focuses each test case much more tightly on just the information that's distinctive in it, which is helpful for understanding what each individual test case is saying.
test/model/autocomplete_test.dart
Outdated
test('has DMs with userA and userB, latest with userB, prioritizes userB', () async { | ||
await prepare(dmConversations: [ | ||
RecentDmConversation(userIds: [2], maxMessageId: 200), | ||
RecentDmConversation(userIds: [1, 2], maxMessageId: 100), |
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.
There's a bit of a mismatch in the naming here. The descriptions call the users "A" and "B":
test('has DMs with userA and userB, latest with userA, prioritizes userA', () async {
as do the User
locals (though my suggestion above has those going away into a helper).
But then the actual data that expresses the content of the test case has "1" and "2", because it needs to be expressed as user IDs.
I think either way of naming will work; it just needs to be consistent, so that the reader doesn't have to translate back and forth in their head in order to understand the test case. If you want "A" and "B", the user IDs can be handled by having something like
const idA = 1;
const idB = 2;
up at the group level.
test/model/autocomplete_test.dart
Outdated
test('autocomplete suggests relevant users in the following order: ' | ||
'1. Users most recent in the DM conversations', () async { | ||
final users = [ |
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: indent continuation line further to avoid looking like part of the body:
test('autocomplete suggests relevant users in the following order: ' | |
'1. Users most recent in the DM conversations', () async { | |
final users = [ | |
test('autocomplete suggests relevant users in the following order: ' | |
'1. Users most recent in the DM conversations', () async { | |
final users = [ |
But taking a step back, if the test name feels like it needs to spill onto a second line, that's a sign that it's too long for a name 🙂
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's name will take more and more lines once we add other criteria. I thought about shrinking its name, but as this test is about exercising the different criteria users are suggested based on, and to provide the reader with enough context, I couldn't come up with a shorter name. I would appreciate any clue from your side!🙂
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 someone reading the source code, information is just as visible if it's in a comment as in the test name. So the most direct fix is to move the extra information from the name to a comment, e.g. replacing:
group('autocomplete suggests relevant users in the following order: '
'1. Users most recent in the DM conversations', () {
Future<void> checkResultsIn(Narrow narrow, {required List<int> expected}) async {
with:
group('autocomplete suggests relevant users in the intended order', () {
// The order should be:
// 1. Users most recent in the DM conversations
Future<void> checkResultsIn(Narrow narrow, {required List<int> expected}) async {
The role of a test name is to identify the test case. It appears in places like the status line that flutter test
shows you, with the name of a currently-running test, or in a heading line when a test fails; and it can be used with flutter test --name
to pick specific tests to run. If the name gets much longer than a line, it's not really readable in those formats anyway.
For example here's the names in your current revision:
(Usually I'd post command output like this as a code block, but I think for this discussion it's helpful to see how it looks on the screen.)
And here it is with the tweak above:
Still kind of long, but getting more manageable. And I think the details of the order don't come across particularly helpfully in any case.
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.
Separately: if there's a test where you feel like it takes more than a line to name what it's about, that's also a sign that the test itself may be combining too many different things. I suspect that's also something that's going on here, but it's relatively mild at this stage and will become more important in #692 where there are more ingredients in the sorting.
When an individual test case is testing too many ideas at once, it tends to make it so that when something breaks the test, it's hard to tell what the test intended; and as a result it's hard to tell whether the test just needs to be updated to expect the new output, or the test is telling you something you broke in the actual application code and should fix. The main point of a test case is to tell you when some future change accidentally broke the functionality the test is testing; so when that becomes hard to tell, it largely defeats the purpose of the test.
test/model/autocomplete_test.dart
Outdated
const streamNarrow = StreamNarrow(1); | ||
await checkResultsIn(streamNarrow, expected: [3, 0, 1, 2, 4]); | ||
|
||
const topicNarrow = TopicNarrow(1, 'topic'); | ||
await checkResultsIn(topicNarrow, expected: [3, 0, 1, 2, 4]); | ||
|
||
final dmNarrow = DmNarrow(allRecipientIds: [eg.selfUser.userId], selfUserId: eg.selfUser.userId); | ||
await checkResultsIn(dmNarrow, expected: [3, 0, 1, 2, 4]); | ||
|
||
const allMessagesNarrow = CombinedFeedNarrow(); | ||
// Results are in the original order as we do not sort them for | ||
// [CombinedFeedNarrow] because we can not access autocomplete for now. | ||
await checkResultsIn(allMessagesNarrow, expected: [0, 1, 2, 3, 4]); |
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.
These should be separate test cases, because each of them is logically independent of the others — checking something that doesn't need the others to have happened first. Having separate test cases is helpful because it means that if a change breaks some of them, you can immediately see which ones it breaks and doesn't break, rather than have to fiddle around editing the test in order to try each one in turn.
You can keep each of them pretty much just as compact as this by… actually it looks like you've basically already done all the work needed for that, with the checkResultsIn
helper 🙂. Just move the users
and dmConversations
locals inside that helper, so that they stay inside (transitively) test
calls rather than being at the top level of a group
.
This data structure is used to keep track of the latest message of each recipient across all DM conversations.
This method was used to search for the new user results in autocomplete when the users in store were modified during an in-progress computation. Along with this, `_startSearch` would do the same thing by retrying, resulting in duplicate actions. By removing this, the unnecessary search for user results is prevented. Besides, this commit also removes some live-updating behavior of user-mention autocomplete results in order to provide a better UX. To read more about this decision, see: zulip#693 (review)
f3c7a7f
to
4d5d823
Compare
Thanks @gnprice for the review with helpful comments! Revision pushed! Please have a look. Also, added replies to some of your comments in the previous review thread. |
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! This is very close to merge now.
Comments below, most of them quite small. There are also two open subthreads above:
#693 (comment)
#693 (comment)
test/model/autocomplete_test.dart
Outdated
test('MentionAutocompleteView mutating store.users while in progress is not ' | ||
'reflected in current query results', () async { |
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, so this test name doesn't really capture what's being tested (continuing the thread from https://github.com/zulip/zulip-flutter/pull/693/files#r1630509271 ):
- The results not reflecting the mutation is acceptable (and we've chosen to accept it), but not totally desired.
- The fact that some results do eventually appear — that's important, and something that didn't have to be the case if we had a buggy implementation.
So the latter is what the test is about.
test/model/autocomplete_test.dart
Outdated
check(done).isFalse(); | ||
await store.addUser(eg.user(userId: 10000, email: '[email protected]', fullName: 'User 10000')); | ||
await Future(() {}); | ||
check(done).isFalse(); | ||
await store.addUser(eg.user(userId: 11000, email: '[email protected]', fullName: 'User 11000')); | ||
await Future(() {}); | ||
check(done).isTrue(); |
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 makes me a bit nervous, because it's not clear from looking at this test code that the addUser
actually happened soon enough to test what this is meant to test.
The test needs the addUser
to finish at a time when the search is still unfinished. But here it looks like it could also be that the addUser
came too late, after the search was already complete.
To rule that out, I'd really like to have a check(done).isFalse()
after the addUser
(like there is in the existing test). Try to make that happen, for example by adding more users so that there's over 2000 of them.
lib/model/autocomplete.dart
Outdated
|
||
final users = store.users.values.toList(); | ||
return users..sort((userA, userB) => compareByRelevance( | ||
userA, userB, store: store)); |
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:
final users = store.users.values.toList(); | |
return users..sort((userA, userB) => compareByRelevance( | |
userA, userB, store: store)); | |
return store.users.values.toList() | |
..sort((userA, userB) => compareByRelevance(userA, userB, store: store)); |
lib/model/autocomplete.dart
Outdated
static int compareByRelevance( | ||
User userA, | ||
User userB, { | ||
required PerAccountStore store, | ||
}) { |
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:
static int compareByRelevance( | |
User userA, | |
User userB, { | |
required PerAccountStore store, | |
}) { | |
static int compareByRelevance(User userA, User userB, { | |
required PerAccountStore store, | |
}) { |
lib/model/autocomplete.dart
Outdated
userA, userB, store: store)); | ||
} | ||
|
||
static int compareByRelevance( |
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.
Oh and this should also be private — from the perspective of any other part of the app, the interface that reaches this code is only through MentionAutocompleteView.init
.
(Or perhaps make this visibleForTesting
and compareByDms
be private; compare #693 (comment) . But that choice will become clearer when we get to #692; right now there's no real distinction between these two methods 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.
I removed it for this PR as we can simply replace it with compareByDms
. I think we can add it when there are more criteria.
test/model/autocomplete_test.dart
Outdated
check(compareAB()).isGreaterThan(0); | ||
}); | ||
|
||
test('doesn\'t have DMs with userA or userB -> prioritizes neither', () async { |
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:
test('doesn\'t have DMs with userA or userB -> prioritizes neither', () async { | |
test('has no DMs with userA or userB -> prioritizes neither', () async { |
Shorter, and as a bonus gets rid of the need to backslash-escape 🙂
test/model/autocomplete_test.dart
Outdated
check(compareAB()).isNegative(); | ||
}); | ||
|
||
test('has DMs with userA and userB, latest with userB -> prioritizes userB', () async { | ||
await prepare(dmConversations: [ | ||
RecentDmConversation(userIds: [idB], maxMessageId: 200), | ||
RecentDmConversation(userIds: [idA, idB], maxMessageId: 100), | ||
]); | ||
check(compareAB()).isGreaterThan(0); |
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.
Huh, I guess there's no isPositive
.
Let's keep these parallel to each other as far as we can, so they look different only in the ways they really meaningfully are different. So isLessThan(0)
instead of isNegative()
.
test/model/autocomplete_test.dart
Outdated
DmNarrow(allRecipientIds: [eg.selfUser.userId], | ||
selfUserId: eg.selfUser.userId), |
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 tighten a bit:
DmNarrow(allRecipientIds: [eg.selfUser.userId], | |
selfUserId: eg.selfUser.userId), | |
DmNarrow.withUser(eg.selfUser.userId, selfUserId: eg.selfUser.userId), |
test/model/autocomplete_test.dart
Outdated
await check(checkResultsIn( | ||
const CombinedFeedNarrow(), | ||
expected: [0, 1, 2, 3, 4]) | ||
).throws((e) => e.isA<AssertionError>()); |
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 tighten like so:
).throws((e) => e.isA<AssertionError>()); | |
).throws<AssertionError>(); |
Though also just .throws()
would be fine — we don't need to be all that particular about how it fails.
This method was used to search for the new user results in autocomplete when the users in store were modified during an in-progress computation. Along with this, `_startSearch` would do the same thing by retrying, resulting in duplicate actions. By removing this, the unnecessary search for user results is prevented. Besides, this commit also removes some live-updating behavior of user-mention autocomplete results in order to provide a better UX. To read more about this decision, see: zulip#693 (review)
4d5d823
to
18e7da6
Compare
Thanks @gnprice for the review! Revision pushed. Also added two replies in previous sub-threads: #693 comment and #693 comment. |
Thanks! Generally the revisions look good. Replied above, and I think #693 (comment) is still open. |
18e7da6
to
3010865
Compare
Thanks @gnprice! The comment about the test was really helpful! 😃 Revision pushed. |
test/model/autocomplete_test.dart
Outdated
while (!done) { | ||
await Future(() {}); |
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 will become an infinite loop if something goes wrong such that the search never finishes. When a test breaks, it's annoying if the behavior is that it spins forever — it makes CI take much longer to show results (as the test runs until a timeout), and it gets in the way of seeing results from other tests.
That's why it's best to have some bound on the number of iterations. See my suggestion above at https://github.com/zulip/zulip-flutter/pull/693/files#r1632735033 . Or for examples in our existing tests in the repo, see test/widgets/message_list_test.dart
and search for "for (int "; there's a couple of tests there that do a fling-scroll, and don't want to hard-code an expectation for just how many frames it'll take before the scroll reaches the relevant point, but they do put an upper bound on how many they'll wait for.
3010865
to
16af601
Compare
Thank you @gnprice. Update 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 @sm-sayedi for all your work on this! This version now LGTM with two comments below.
I'll fix the one nit, and merge.
for (int i = 0; i < 3; i++) { | ||
await Future(() {}); | ||
check(view.results).single | ||
.isA<UserMentionAutocompleteResult>() | ||
.userId.equals(10000); | ||
} |
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 OK as is because it's only 3 iterations anyway, but if it were many more I'd want to have it break as soon as the thing is done (in addition to having an upper bound on the number of iterations). See my suggested code above, or those examples I mentioned in the message-list tests, which look like:
for (int i = 0; i < 30; i++) {
// Find the point in the fling where the fetch starts.
await tester.pump(const Duration(milliseconds: 100));
if (itemCount(tester)! > 101) break; // The loading indicator appeared.
}
Note the upper bound on iteration count (30) as well as the break
.
test/model/autocomplete_test.dart
Outdated
test('MentionAutocompleteView mutating store.users while in progress does not ' | ||
'interrupt the current query results', () async { |
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:
test('MentionAutocompleteView mutating store.users while in progress does not ' | |
'interrupt the current query results', () async { | |
test('MentionAutocompleteView mutating store.users while in progress does not ' | |
'prevent query from finishing', () async { |
"interrupt" reads to me as more about what happens internally — e.g. the old version does get interrupted, even though it then resumes and recovers. The test is really about the ultimate output, which is why the old version passes the test (modulo the detail about checking the new user 11000 is absent).
This field will be used to maintain a list of sorted users based on the most relevant autocomplete criteria in the upcoming commits. This commit also removes the retry logic for an in-progress query computation as there is no longer the cause that would necessitate such a retry.
In @-mention autocomplete, users are suggested based on: 1. Recent DM conversations. Fixes part of: zulip#228
16af601
to
37ad773
Compare
Thanks @gnprice and @chrisbobbe for all your continuous help on this!! Happy to see it finally merged. 🙂
Sorry for this, I just forgot to include that. As you said it's not a big matter for this small for loop, but maybe I will fix it anyway in an NFC commit in #692. |
In @-mention autocomplete users with whom recent DMs are exchanged are given priority.
Other criteria will be added in separate PRs.
Note: This is the first PR in the series of PRs #608 is divided into. Next PR in the series: #692.
Fixes part of: #228