-
Notifications
You must be signed in to change notification settings - Fork 321
autocomplete: Support user-group mentions; match users by email; rank results by match quality #1775
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
base: main
Are you sure you want to change the base?
Conversation
lib/model/autocomplete.dart
Outdated
final Map<int, String> _normalizedNamesByUser = {}; | ||
final Map<int, String> _lowercaseNamesByUser = {}; |
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]: Renames/doc changes for explicitness about lowercase
I think the reason to call these fields and methods "normalize" rather than "lowercase" is to anticipate having them be normalized for diacritics as well as for case.
1863ddd
to
8ca8bdb
Compare
Thanks! Revision pushed, addressing that comment and also simplifying the email-match logic as discussed in the office. |
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 building this! Here's a review of everything but the next-to-last commit:
15be5b4 autocomplete: Match user results by email match (prefix match)
lib/model/autocomplete.dart
Outdated
if (matchLength == lowercaseName.length && matchLength == _lowercase.length) { | ||
return NameMatchQuality.exact; | ||
} else if (matchLength == _lowercase.length) { | ||
return NameMatchQuality.totalPrefix; | ||
} | ||
|
||
if (_testContainsQueryWords(lowercaseNameWords)) { |
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.
All these conditions are possible only if the query is no longer than the name, right?
So we could check _lowercase.length <= lowercaseName.length
at the top, and then simplify by referring only to _lowercase.length
in the loop.
lib/model/autocomplete.dart
Outdated
int matchLength = 0; | ||
while ( | ||
matchLength < lowercaseName.length && matchLength < _lowercase.length | ||
&& lowercaseName[matchLength] == _lowercase[matchLength] | ||
) { | ||
matchLength++; | ||
} |
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.
Or perhaps better: this is effectively checking lowercaseName.startswith(_lowercase)
, right?
I think we can first check that, using that method; then distinguish .exact
by checking if the lengths are equal.
lib/model/autocomplete.dart
Outdated
final nameMatchQuality = _matchName( | ||
lowercaseName: cache.lowercaseNameForUser(user), | ||
lowercaseNameWords: cache.lowercaseNameWordsForUser(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.
This seems probably cleaner to read if _matchName
handles looking these up from the cache, like _testName
before:
final nameMatchQuality = _matchName( | |
lowercaseName: cache.lowercaseNameForUser(user), | |
lowercaseNameWords: cache.lowercaseNameWordsForUser(user)); | |
final nameMatchQuality = _matchName(user, cache); |
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 see, the point of this comes in that later commit where user groups get matched: that's a new call site which does different lookups on the cache. OK, sure.
final user1 = eg.user(fullName: 'So Many Ideas'); | ||
final user2 = eg.user(fullName: 'Some Merry 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.
🙂
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 was really struggling to come up with actual normal names to use 😅
lib/model/autocomplete.dart
Outdated
/// The number of possible values returned by | ||
/// [_rankWildcardResult] and [_rankUserResult]. | ||
static const _numResultRanks = 10; |
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: also _rankUserGroupResult
lib/widgets/autocomplete.dart
Outdated
final userGroup = store.getGroup(id); | ||
if (userGroup == null) { | ||
// Don't crash on theoretical race between async results-filtering | ||
// and losing data for the group. | ||
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. I guess if that is possible, it probably also applies to the user case above:
final user = store.getUser(userId)!; // must exist because UserMentionAutocompleteResult
test/model/autocomplete_test.dart
Outdated
// The bucket-sort-by-rank step has its own tests; | ||
// see "MentionAutocompleteQuery ranking. |
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: missing close-quote?
.deepEquals([WildcardMentionOption.all, WildcardMentionOption.topic]); | ||
check(getUsersFromResults(results1.skip(2))) | ||
.deepEquals([1, 5, 4, 2, 7, 3, 6]); | ||
// 6. User groups by name alphabetical order. |
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 item isn't like the others — this ordered list is of ranking criteria, not of classes of results.
Instead, change the first item to something like:
- Wildcards before individual users; user groups after.
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.
(Well, adjusted a bit for the fact that users matched by email come after groups.)
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 email matching is short-circuited by the fact that this is an empty query, which always matches users by name.
test/model/autocomplete_test.dart
Outdated
isUser(2), isUser(3), | ||
isUserGroup(3), isUserGroup(2), |
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 interesting — why is the order reversed between these?
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 the users, 2 ("User Two") goes before 3 ("User Three") by DM recency. For the groups, 3 ("User Group Three") goes before 2 ("User Group Two") by alphabet.
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, makes sense; and the comment to that effect is helpful, thanks.
test/model/compose_test.dart
Outdated
@@ -316,6 +316,22 @@ hello | |||
check(wildcardMention(WildcardMentionOption.topic, store: store())) | |||
.equals('@**topic**'); | |||
}); | |||
|
|||
group('user group', () { | |||
final userGroup = eg.userGroup(id: 123, name: 'Group 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.
nit: ID isn't relevant to understanding these tests, so can omit
final userGroup = eg.userGroup(id: 123, name: 'Group Name'); | |
final userGroup = eg.userGroup(name: 'Group Name'); |
(contrasts with the user case, where it can appear in the output)
8ca8bdb
to
ae92100
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 for the revision! All looks good now except a couple of nits; and that email commit, which I guess I'll read next.
final user = store.getUser(userId)!; // must exist because UserMentionAutocompleteResult | ||
final user = store.getUser(userId); | ||
if (user == 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.
autocomplete: Remove a probably-wrong null check
I've been waffling on this and haven't managed to convince myself
that the null-check was correct.
nit: "null assertion"? This is still something I'd call a null check — it's checking whether this is null.
test/model/autocomplete_test.dart
Outdated
isUser(2), isUser(3), | ||
isUserGroup(3), isUserGroup(2), |
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, makes sense; and the comment to that effect is helpful, thanks.
lib/widgets/autocomplete.dart
Outdated
// TODO(i18n) language-appropriate space character; check active keyboard? | ||
// (maybe handle centrally in `controller`) | ||
replacementString = '${userMention(user, silent: query.silent, users: store)} '; | ||
case WildcardMentionAutocompleteResult(:var wildcardOption): | ||
replacementString = '${wildcardMention(wildcardOption, store: store)} '; | ||
case UserGroupMentionAutocompleteResult(groupId:final 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.
nit: propagate name change (here and another case below)
case UserGroupMentionAutocompleteResult(groupId:final id): | |
case UserGroupMentionAutocompleteResult(:final groupId): |
Thanks! Revision pushed. |
ae92100
to
1f3d350
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.
OK, and read the emails commit — just one comment, for a test I think will be quick to add.
checkPrecedes('so m', userGroup1, userGroup2); | ||
}); | ||
|
||
test('email matched case-insensitively', () { |
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 one other test that'd be good to have here is that emails are matched by prefix only: so e.g. check that "[email protected]" doesn't match a query "mail" or "example" or "example.com".
I've been waffling on this and haven't managed to convince myself that the "!" was correct.
Since 9815022, this method already takes the whole store, which it uses in order to identify muted users. So it can get the autocomplete-data cache from the store just as well as its caller can.
Like we do in emoji autocomplete. This commit doesn't make any changes to the results ordering; bucketSort sorts stably, and the input is still just the wildcard results followed by the user results. Soon, though, we'd like to rank by match quality and add user-group results (for zulip#233) interleaved with user results. Bucket sorting will help us do this without making many intermediate copies of lists of results; see discussion: https://chat.zulip.org/#narrow/channel/48-mobile/topic/user-group.20mentions.20.23F233/near/2216353 Co-authored-by: Greg Price <[email protected]>
… test This will make it more smoothly generalize to situations where different types of results are more intermingled.
This makes this function better match the end-to-end behavior of our autocomplete ordering, now that that involves this ranking notion. Co-authored-by: Chris Bobbe <[email protected]>
This group also checks that wildcard results come before user results.
Co-authored-by: Greg Price <[email protected]>
1f3d350
to
7cfc4ac
Compare
Thanks, done! PTAL. |
Fixes #236.
Fixes #233.
Screenshots coming soon.