-
Notifications
You must be signed in to change notification settings - Fork 306
autocomplete: Sort user-mention autocomplete results #608
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: Sort user-mention autocomplete results #608
Conversation
20fefe8
to
cefaf97
Compare
The code may still have some bugs, and it is unclean, but it would be great to know if I am on the right path for this problem! And I will be more than happy to have feedback on my draft proposal. |
cefaf97
to
7141342
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.
Sure. Here's some high-level comments.
And I will be more than happy to have feedback on my draft proposal.
Ah indeed. 🙂 Replied there.
7141342
to
b419e14
Compare
Thanks for the review! Revision pushed, but this time changed the whole implementation to almost match it with the Web app. |
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.
997dd5a
to
981de92
Compare
Thanks for the review! Pushed the changes. When you think it's ready, please let me know so I proceed with writing tests. |
981de92
to
22953d3
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!
This strategy looks good; see comments below. I didn't cover all the points that I might comment on if doing a full review, but I tried to cover all the data structures and algorithms — I think all the data structures and algorithms here are basically what we want modulo these comments.
I'll be on vacation after today, so @chrisbobbe will review the next few revisions. There's a significant amount of complexity here, so he'll probably have plenty of comments to make about code style, dartdoc, tests, potentially also some of the data structures and algorithms, and likely also some correctness details I'd missed. He might merge the PR while I'm away, if he feels it's ready, and might flag some questions he'd like my input on. (Potentially both — we can always revise the code later.)
In any case I'll look forward to seeing when I'm back a version of this PR that's been polished up and is either merged, or nearly ready to merge 🙂
Yep, happy to review! 🙂 I see this PR is currently marked as a draft, so I'll plan to review it once it's marked as ready for review and you've addressed Greg's feedback above. |
22953d3
to
804287b
Compare
Thanks @gnprice for the review! Revision pushed with the feedback addressed @chrisbobbe! Ready for your review now! |
804287b
to
5508672
Compare
lib/model/recent_senders.dart
Outdated
if (_ids.isEmpty) { | ||
_ids.add(id); | ||
} else { | ||
int i = _ids.possibleIndexOf(id); |
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.
Instead of using List.indexOf
or List.indexWhere
which uses the linear search, we could take advantage of the fact this list is sorted and use binary search to find the index.
lib/model/recent_senders.dart
Outdated
_ids.add(id); | ||
} else { | ||
int i = _ids.possibleIndexOf(id); | ||
if (i >= 0) { // the [id] already exists, so do not add 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.
I preferred this way of checking for element's presence, instead of using List.contains
which again uses the linear search.
lib/model/recent_senders.dart
Outdated
low = mid + 1; | ||
} | ||
} | ||
return -low - 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.
Using -low - 1
instead of just -low
will differentiate between the case where the element is found at index 0 and the case where the element is not found, but should be placed at the very start of the list.
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 explain this in a code comment. 🙂 It's exactly the kind of thing that any reader will be interested in, and most readers will just be looking at the code, without also reading through this PR thread.
edit: hmm, it's not clear in the GitHub UI I'm seeing right now, but this was meant as a reply to your comment #608 (comment)
We're considering adding another `Map` field to RecentDmConversationsView; see PR #608. If we do, this dartdoc will want this added explicitness.
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! Comments below, and I see there's a test failing in CI. Does that failure reproduce when you run tools/check
locally?
The new code will also need new tests.
lib/model/recent_senders.dart
Outdated
required User userA, | ||
required User userB, | ||
required int streamId, | ||
// TODO(#493): may turn this into a non-nullable string. |
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.
What's the connection with #493?
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.
Removed it this time! At first I thought, if the compose box is populated with a topic narrow, we may show the user results based on that topic narrow, even if we're in the stream view. But now looking at the web, it's not the case.
lib/model/recent_senders.dart
Outdated
extension Max<T extends num> on Iterable<T> { | ||
/// Finds the maximum number in an [Iterable]. | ||
/// | ||
/// Returns null if the [Iterable] is empty. | ||
T? max() => isNotEmpty ? reduce(math.max) : null; | ||
} |
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 seem to be used. Let's simplify by removing 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.
Thanks for spotting this. It was used in the previous implementation, and I forgot to remove it.
// The ID of the latest messages exchanged with other users. | ||
final Map<int, int> _dmRecencyData = {}; |
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: use dartdoc formatting, like the others (so ///
instead of //
)
Also, can we find a clearer name for this field? In a class named RecentDmConversationsView
, the name _dmRecencyData
seems redundant, and it also seems less specific than it should be. 🙂
I think we can make the dartdoc more specific as well. The type (Map<int, int>
) doesn't tell us if the map is keyed by message IDs or user IDs, since the key and value types are both int
. That would be good to make clearer so the reader doesn't have to search for clues elsewhere, like the places where the field gets used. There are also a few other relevant facts that a reader would naturally wonder about.
So for example:
/// Map from user ID to the latest message ID in any conversation with the user.
///
/// Both 1:1 and group DM conversations are considered.
/// The self-user ID is excluded even if there is a self-DM conversation.
///
/// (The identified message was not necessarily sent by the identified user;
/// it might have been sent by anyone in its conversation.)
final Map<int, int> _latestMessageByRecipient = {};
What do you think?
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.
Thank you! This is a well-descriptive dartdoc. Added it.
/// The ID of the latest message exchanged with this user. | ||
/// | ||
/// Returns -1 if there has been no DM message exchanged ever. | ||
int getRecencyIndex(final int userId) => _dmRecencyData[userId] ?? -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.
How about we pull the ?? -1
part out to callers? It seems helpful toward simplifying this function's job, which is already somewhat complex to describe accurately (see my previous comment on _dmRecencyData
).
Then, I think it would be helpful to change the name to something more transparent. So for example:
/// The latest message ID in any conversation with the given user, if any.
///
/// Both 1:1 and group DM conversations are considered.
/// Gives null for the self-user ID even if there is a self-DM conversation.
///
/// (An identified message was not necessarily sent by the given user;
/// it might have been sent by anyone in its conversation.)
int? latestMessageWithRecipient(final int userId) => _latestMessageByRecipient[userId];
lib/model/recent_senders.dart
Outdated
idTracker.add(messageId); | ||
} | ||
|
||
void processStreamMessage(Message message) { |
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 name makes it seem like a StreamMessage
is specifically expected, but the param's type is the more general type Message
.
I first noticed this when look at this method's callers, which look odd because they're passing messages that are not known to be StreamMessage
s.
Could fix by renaming to processMessage
, or by narrowing the param's type to StreamMessage
instead of Message
.
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.
Please also add a dartdoc for this method, to make it clear what callers are signing up for when they call it. Maybe we can also find a more specific verb than "process"; I see it calls some helpers addStreamMessage
and addTopicMessage
; perhaps this should be named processMessage
?
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.
Could fix by renaming to processMessage, or by narrowing the param's type to StreamMessage instead of Message.
Went with processAsStreamMessage
. I thought when using processMessage
, one would think that EACH message is processed; and when narrowing the param's type to StreamMessage
, each caller should always make sure they're passing the correct message type by using a check, for example:
if (message is StreamMessage) {
recentSenders.processStreamMessage(message);
}
I think it would be cleaner to add this check in the method itself and use a somewhat descriptive method name.
What do you think?
Maybe we can also find a more specific verb than "process".
I think the verb "process" fits good in here as it extracts some data from the message and keeps track of it in a data structure. But I would appreciate a better verb from your side!
Edited: Renamed it to handleMessage
to match it with handleMessageEvent
declared in model\recent_dm_conversations.dart
lib/model/recent_senders.dart
Outdated
low = mid + 1; | ||
} | ||
} | ||
return -low - 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.
Let's explain this in a code comment. 🙂 It's exactly the kind of thing that any reader will be interested in, and most readers will just be looking at the code, without also reading through this PR thread.
edit: hmm, it's not clear in the GitHub UI I'm seeing right now, but this was meant as a reply to your comment #608 (comment)
lib/model/recent_senders.dart
Outdated
/// A data structure to keep track of message ids. | ||
class IdTracker { |
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 MessageIdTracker
would be a more helpful name for this, since a lot of the nearby code is dealing with user IDs. 🙂
With that name, the dartdoc as written would stand out as redundant/useless, though, wouldn't it…but that could be dealt with by removing it. But if there's anything else that's interesting about this class (other than that it's a data structure that keeps track of message IDs), that could be good to put in the dartdoc.
lib/model/autocomplete.dart
Outdated
if (!userA.isBot && userB.isBot) { | ||
return -1; | ||
} else if (userA.isBot && !userB.isBot) { | ||
return 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.
This part doesn't seem like it belongs in a method named compareByDms
, as Greg pointed out above: #608 (comment)
lib/model/autocomplete.dart
Outdated
final Map<String, String> _namesLowercased = {}; | ||
|
||
String nameLowercased(String name) { | ||
return _namesLowercased[name] ??= name.toLowerCase(); | ||
} |
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.
Is there a reason not to key this by user ID, like _nameWordsByUser
is? (If doing that, we should make sure invalidateUser
acts appropriately on the lowercased-name data.)
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.
How about _lowercasedNameByUser
and lowercasedNameForUser
, on the pattern of _nameWordsByUser
and nameWordsForUser
?
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.
Is there a reason not to key this by user ID, like
_nameWordsByUser
is?
Two reasons: a) to not pass the whole User
object while we can do the work with just one of its properties. b) there can be multiple users with the same name so we can save the space a little bit by having just one key-value pair for all those user names.
However, this will bring some complexity when adding the appropriate logic in invalidateUser
. Let's assume we somehow pass fullName
to 'invalidateUser' besides userId
to remove the lowercase name. It will remove the user name for all the users that share the same name.
So I am not sure whether to key it by userId
or fullName
?
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, I think best to key it by user ID, the same way as _nameWordsByUser
.
It's uncommon for users to have the same name, so there's minimal space savings to be had from that, which makes it not worth the complexity of worrying about interactions there.
Relatedly: in nameWordsForUser
we're calling toLowerCase
on user.fullName
, just like this does. So let's have that step share this same cache.
lib/model/message_list.dart
Outdated
@@ -381,6 +381,7 @@ class MessageListView with ChangeNotifier, _MessageSequence { | |||
for (final message in result.messages) { | |||
if (_messageVisible(message)) { | |||
_addMessage(message); | |||
store.recentSenders.processStreamMessage(message); |
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 you talk a bit about the performance considerations of this?
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 am not hundred percent sure if it's an expensive task or not. I think it does relatively simple task of adding IdTracker
to a map two times.
31deb52
to
6797b79
Compare
Thanks for the review! Revision pushed with some comments above. Please have a look. The tests are failing locally for me too. I was waiting for the overall approach of this feature to be steady to start writing tests for it. I will do it after now, but at the same time, I would appreciate your feedback on the recent revision! |
This field will be used to maintain a list of sorted users based on the most relevant autocomplete criteria in the upcoming commits. Co-authored-by: Greg Price <[email protected]>
In @-mention autocomplete, users are suggested based on: 1. Recent DM conversations. Fixes part of zulip#228
…ta structures These data structures are used to keep track of user messages in topics and streams.
In @-mention autocomplete, users are suggested based on: 1. Recent activity in the current stream. 2. Recent DM conversations.
In @-mention autocomplete, users are suggested based on: 1. Recent activity in the current stream. 2. Recent DM conversations. 3. Human vs. Bot users.
In @-mention autocomplete, users are suggested based on: 1. Recent activity in the current stream. 2. Recent DM conversations. 3. Human vs. Bot users. 4. Alphabetical order. Fixes: zulip#228
88f4872
to
5d6dc03
Compare
Thanks for the review! Pushed the recent DMs revision here although I wasn't sure to create a new PR for this too. Please have a look. The other criteria will come in the following PRs. |
9b75e44
to
7f21d5f
Compare
Thanks! Yeah, now that I think about it: please close this PR and open a new one with the same contents. In particular that will be helpful because the last round of review above covers changes that will be part of the several different PRs in the series; by letting this PR thread end here, that review will remain accessible when looking at each of those PRs. If we continued in this same thread as the PR for the first part of the changes, then that review would get pushed up into history and potentially into the "hidden" area. |
I have opened #693 as the first part of this PR. Please have a look. |
Closing this in favor of its successor PRs #693 and #692. Thanks @sm-sayedi for all your work on this so far! |
7f21d5f
to
c710a5c
Compare
In @-mention autocomplete, users are suggested based on:
If one option exhausts, the other is considered for the suggestions in the preceding order.
Note: This PR will no longer receive any new code pushes as it is divided into other PRs. See #693, #692, #828, #849.