@@ -479,6 +479,9 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
479
479
required PerAccountStore store,
480
480
required Narrow narrow,
481
481
}) {
482
+ // See also [MentionAutocompleteQuery._rankUserResult];
483
+ // that ranking takes precedence over this.
484
+
482
485
int ? streamId;
483
486
TopicName ? topic;
484
487
switch (narrow) {
@@ -725,6 +728,23 @@ abstract class AutocompleteQuery {
725
728
}
726
729
}
727
730
731
+ /// The match quality of a [User.fullName] to a mention autocomplete query.
732
+ ///
733
+ /// All matches are case-insensitive.
734
+ enum NameMatchQuality {
735
+ /// The query matches the whole name exactly.
736
+ exact,
737
+
738
+ /// The name starts with the query.
739
+ totalPrefix,
740
+
741
+ /// All of the query's words have matches in the words of the name
742
+ /// that appear in order.
743
+ ///
744
+ /// A "match" means the word in the name starts with the query word.
745
+ wordPrefixes,
746
+ }
747
+
728
748
/// Any autocomplete query in the compose box's content input.
729
749
abstract class ComposeAutocompleteQuery extends AutocompleteQuery {
730
750
ComposeAutocompleteQuery (super .raw);
@@ -771,14 +791,33 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
771
791
772
792
final cache = store.autocompleteViewManager.autocompleteDataCache;
773
793
// TODO(#236) test email too, not just name
774
- if (! _testName (user, cache)) return null ;
794
+ final nameMatchQuality = _matchName (
795
+ normalizedName: cache.normalizedNameForUser (user),
796
+ normalizedNameWords: cache.normalizedNameWordsForUser (user));
797
+ if (nameMatchQuality == null ) return null ;
775
798
776
799
return UserMentionAutocompleteResult (
777
- userId: user.userId, rank: _rankUserResult (user));
800
+ userId: user.userId,
801
+ rank: _rankUserResult (user, nameMatchQuality: nameMatchQuality));
778
802
}
779
803
780
- bool _testName (User user, AutocompleteDataCache cache) {
781
- return _testContainsQueryWords (cache.normalizedNameWordsForUser (user));
804
+ NameMatchQuality ? _matchName ({
805
+ required String normalizedName,
806
+ required List <String > normalizedNameWords,
807
+ }) {
808
+ if (normalizedName.startsWith (_lowercase)) {
809
+ if (normalizedName.length == _lowercase.length) {
810
+ return NameMatchQuality .exact;
811
+ } else {
812
+ return NameMatchQuality .totalPrefix;
813
+ }
814
+ }
815
+
816
+ if (_testContainsQueryWords (normalizedNameWords)) {
817
+ return NameMatchQuality .wordPrefixes;
818
+ }
819
+
820
+ return null ;
782
821
}
783
822
784
823
/// A measure of a wildcard result's quality in the context of the query,
@@ -791,11 +830,17 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
791
830
/// from 0 (best) to one less than [_numResultRanks] .
792
831
///
793
832
/// See also [_rankWildcardResult] .
794
- static int _rankUserResult (User user) => 1 ;
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
+ };
839
+ }
795
840
796
841
/// The number of possible values returned by
797
842
/// [_rankWildcardResult] and [_rankUserResult] .
798
- static const _numResultRanks = 2 ;
843
+ static const _numResultRanks = 4 ;
799
844
800
845
@override
801
846
String toString () {
@@ -888,6 +933,30 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
888
933
/// A measure of the result's quality in the context of the query.
889
934
///
890
935
/// Used internally by [MentionAutocompleteView] for ranking the results.
936
+ // See also [MentionAutocompleteView._usersByRelevance];
937
+ // results with equal [rank] will appear in the order they were put in
938
+ // by that method.
939
+ //
940
+ // Compare sort_recipients in Zulip web:
941
+ // https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
942
+ //
943
+ // Behavior we have that web doesn't and might like to follow:
944
+ // - A "word-prefixes" match quality on user names:
945
+ // see [NameMatchQuality.wordPrefixes], which we rank on.
946
+ //
947
+ // Behavior web has that seems undesired, which we don't plan to follow:
948
+ // - Ranking humans above bots, even when the bots have higher relevance
949
+ // and better match quality. If there's a bot participating in the
950
+ // current conversation and I start typing its name, why wouldn't we want
951
+ // that as a top result? Issue: https://github.com/zulip/zulip/issues/35467
952
+ // - A "word-boundary" match quality on user and user-group names:
953
+ // special rank when the whole query appears contiguously
954
+ // right after a word-boundary character.
955
+ // Our [NameMatchQuality.wordPrefixes] seems smarter.
956
+ // - A "word-boundary" match quality on user emails:
957
+ // "words" is a wrong abstraction when matching on emails.
958
+ // - Ranking some case-sensitive matches differently from case-insensitive
959
+ // matches. Users will expect a lowercase query to be adequate.
891
960
int get rank;
892
961
}
893
962
0 commit comments