Skip to content

Commit 7be6826

Browse files
chrisbobbegnprice
authored andcommitted
autocomplete: Match user results by email match (prefix match)
Fixes #236.
1 parent 00c0840 commit 7be6826

File tree

2 files changed

+70
-12
lines changed

2 files changed

+70
-12
lines changed

lib/model/autocomplete.dart

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -790,15 +790,19 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
790790
if (store.isUserMuted(user.userId)) return null;
791791

792792
final cache = store.autocompleteViewManager.autocompleteDataCache;
793-
// TODO(#236) test email too, not just name
794793
final nameMatchQuality = _matchName(
795794
normalizedName: cache.normalizedNameForUser(user),
796795
normalizedNameWords: cache.normalizedNameWordsForUser(user));
797-
if (nameMatchQuality == null) return null;
796+
bool? matchesEmail;
797+
if (nameMatchQuality == null) {
798+
matchesEmail = _matchEmail(user, cache);
799+
if (!matchesEmail) return null;
800+
}
798801

799802
return UserMentionAutocompleteResult(
800803
userId: user.userId,
801-
rank: _rankUserResult(user, nameMatchQuality: nameMatchQuality));
804+
rank: _rankUserResult(user,
805+
nameMatchQuality: nameMatchQuality, matchesEmail: matchesEmail));
802806
}
803807

804808
NameMatchQuality? _matchName({
@@ -820,6 +824,12 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
820824
return null;
821825
}
822826

827+
bool _matchEmail(User user, AutocompleteDataCache cache) {
828+
final lowercaseEmail = cache.normalizedEmailForUser(user);
829+
if (lowercaseEmail == null) return false; // Email not known
830+
return lowercaseEmail.startsWith(_lowercase);
831+
}
832+
823833
/// A measure of a wildcard result's quality in the context of the query,
824834
/// from 0 (best) to one less than [_numResultRanks].
825835
///
@@ -829,18 +839,29 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
829839
/// A measure of a user result's quality in the context of the query,
830840
/// from 0 (best) to one less than [_numResultRanks].
831841
///
842+
/// When [nameMatchQuality] is non-null (the name matches),
843+
/// callers should skip computing [matchesEmail] and pass null for that.
844+
///
832845
/// See also [_rankWildcardResult].
833-
static int _rankUserResult(User user, {required NameMatchQuality nameMatchQuality}) {
834-
return switch (nameMatchQuality) {
835-
NameMatchQuality.exact => 1,
836-
NameMatchQuality.totalPrefix => 2,
837-
NameMatchQuality.wordPrefixes => 3,
838-
};
846+
static int _rankUserResult(User user, {
847+
required NameMatchQuality? nameMatchQuality,
848+
required bool? matchesEmail,
849+
}) {
850+
if (nameMatchQuality != null) {
851+
assert(matchesEmail == null);
852+
return switch (nameMatchQuality) {
853+
NameMatchQuality.exact => 1,
854+
NameMatchQuality.totalPrefix => 2,
855+
NameMatchQuality.wordPrefixes => 3,
856+
};
857+
}
858+
assert(matchesEmail == true);
859+
return 4;
839860
}
840861

841862
/// The number of possible values returned by
842863
/// [_rankWildcardResult] and [_rankUserResult].
843-
static const _numResultRanks = 4;
864+
static const _numResultRanks = 5;
844865

845866
@override
846867
String toString() {
@@ -888,9 +909,17 @@ class AutocompleteDataCache {
888909
??= normalizedNameForUser(user).split(' ');
889910
}
890911

912+
final Map<int, String?> _normalizedEmailsByUser = {};
913+
914+
/// The normalized `deliveryEmail` of [user], or null if that's null.
915+
String? normalizedEmailForUser(User user) {
916+
return _normalizedEmailsByUser[user.userId] ??= user.deliveryEmail?.toLowerCase();
917+
}
918+
891919
void invalidateUser(int userId) {
892920
_normalizedNamesByUser.remove(userId);
893921
_normalizedNameWordsByUser.remove(userId);
922+
_normalizedEmailsByUser.remove(userId);
894923
}
895924
}
896925

@@ -953,6 +982,12 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
953982
// special rank when the whole query appears contiguously
954983
// right after a word-boundary character.
955984
// Our [NameMatchQuality.wordPrefixes] seems smarter.
985+
// - An "exact" match quality on emails: probably not worth its complexity.
986+
// Emails are much more uniform in their endings than users' names are,
987+
// so a prefix match should be adequate. (If I've typed "[email protected]",
988+
// that'll probably be the only result. There might be an "[email protected]",
989+
// and an "exact" match would downrank that, but still that's just two items
990+
// to scan through.)
956991
// - A "word-boundary" match quality on user emails:
957992
// "words" is a wrong abstraction when matching on emails.
958993
// - Ranking some case-sensitive matches differently from case-insensitive

test/model/autocomplete_test.dart

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,14 +1036,37 @@ void main() {
10361036
checkPrecedes('so m', user1, user2);
10371037
});
10381038

1039+
test('email matched case-insensitively', () {
1040+
// "z" name to prevent accidental name match with example data
1041+
final user1 = eg.user(fullName: 'z', deliveryEmail: '[email protected]');
1042+
final user2 = eg.user(fullName: 'z', deliveryEmail: '[email protected]');
1043+
1044+
checkSameRank('[email protected]', user1, user2);
1045+
checkSameRank('email@e', user1, user2);
1046+
checkSameRank('email@', user1, user2);
1047+
checkSameRank('email', user1, user2);
1048+
checkSameRank('ema', user1, user2);
1049+
});
1050+
1051+
test('email match is by prefix only', () {
1052+
// "z" name to prevent accidental name match with example data
1053+
final user = eg.user(fullName: 'z', deliveryEmail: '[email protected]');
1054+
1055+
check(rankOf('e', user)).isNotNull();
1056+
check(rankOf('mail', user)).isNull();
1057+
check(rankOf('example', user)).isNull();
1058+
check(rankOf('example.com', user)).isNull();
1059+
});
1060+
10391061
test('full list of ranks', () {
1040-
final user1 = eg.user(fullName: 'some user');
1062+
final user1 = eg.user(fullName: 'some user', deliveryEmail: '[email protected]');
10411063
check([
10421064
rankOf('', WildcardMentionOption.all), // wildcard
10431065
rankOf('some user', user1), // user, exact name match
10441066
rankOf('some us', user1), // user, total-prefix name match
10451067
rankOf('so us', user1), // user, word-prefixes name match
1046-
]).deepEquals([0, 1, 2, 3]);
1068+
rankOf('email', user1), // user, no name match, email match
1069+
]).deepEquals([0, 1, 2, 3, 4]);
10471070
});
10481071
});
10491072

0 commit comments

Comments
 (0)