@@ -479,6 +479,9 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
479479 required PerAccountStore store,
480480 required Narrow narrow,
481481 }) {
482+ // See also [MentionAutocompleteQuery._rankUserResult];
483+ // that ranking takes precedence over this.
484+
482485 int ? streamId;
483486 TopicName ? topic;
484487 switch (narrow) {
@@ -725,6 +728,23 @@ abstract class AutocompleteQuery {
725728 }
726729}
727730
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+
728748/// Any autocomplete query in the compose box's content input.
729749abstract class ComposeAutocompleteQuery extends AutocompleteQuery {
730750 ComposeAutocompleteQuery (super .raw);
@@ -771,14 +791,39 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
771791
772792 final cache = store.autocompleteViewManager.autocompleteDataCache;
773793 // 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 ;
775798
776799 return UserMentionAutocompleteResult (
777- userId: user.userId, rank: _rankUserResult (user));
800+ userId: user.userId,
801+ rank: _rankUserResult (user, nameMatchQuality: nameMatchQuality));
778802 }
779803
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+ int matchLength = 0 ;
809+ while (
810+ matchLength < normalizedName.length && matchLength < _lowercase.length
811+ && normalizedName[matchLength] == _lowercase[matchLength]
812+ ) {
813+ matchLength++ ;
814+ }
815+
816+ if (matchLength == normalizedName.length && matchLength == _lowercase.length) {
817+ return NameMatchQuality .exact;
818+ } else if (matchLength == _lowercase.length) {
819+ return NameMatchQuality .totalPrefix;
820+ }
821+
822+ if (_testContainsQueryWords (normalizedNameWords)) {
823+ return NameMatchQuality .wordPrefixes;
824+ }
825+
826+ return null ;
782827 }
783828
784829 /// A measure of a wildcard result's quality in the context of the query,
@@ -791,11 +836,17 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
791836 /// from 0 (best) to one less than [_numResultRanks] .
792837 ///
793838 /// See also [_rankWildcardResult] .
794- static int _rankUserResult (User user) => 1 ;
839+ static int _rankUserResult (User user, {required NameMatchQuality nameMatchQuality}) {
840+ return switch (nameMatchQuality) {
841+ NameMatchQuality .exact => 1 ,
842+ NameMatchQuality .totalPrefix => 2 ,
843+ NameMatchQuality .wordPrefixes => 3 ,
844+ };
845+ }
795846
796847 /// The number of possible values returned by
797848 /// [_rankWildcardResult] and [_rankUserResult] .
798- static const _numResultRanks = 2 ;
849+ static const _numResultRanks = 4 ;
799850
800851 @override
801852 String toString () {
@@ -888,6 +939,30 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
888939 /// A measure of the result's quality in the context of the query.
889940 ///
890941 /// Used internally by [MentionAutocompleteView] for ranking the results.
942+ // See also [MentionAutocompleteView._usersByRelevance];
943+ // results with equal [rank] will appear in the order they were put in
944+ // by that method.
945+ //
946+ // Compare sort_recipients in Zulip web:
947+ // https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
948+ //
949+ // Behavior we have that web doesn't and might like to follow:
950+ // - A "word-prefixes" match quality on user names:
951+ // see [NameMatchQuality.wordPrefixes], which we rank on.
952+ //
953+ // Behavior web has that seems undesired, which we don't plan to follow:
954+ // - Ranking humans above bots, even when the bots have higher relevance
955+ // and better match quality. If there's a bot participating in the
956+ // current conversation and I start typing its name, why wouldn't we want
957+ // that as a top result? Issue: https://github.com/zulip/zulip/issues/35467
958+ // - A "word-boundary" match quality on user and user-group names:
959+ // special rank when the whole query appears contiguously
960+ // right after a word-boundary character.
961+ // Our [NameMatchQuality.wordPrefixes] seems smarter.
962+ // - A "word-boundary" match quality on user emails:
963+ // "words" is a wrong abstraction when matching on emails.
964+ // - Ranking some case-sensitive matches differently from case-insensitive
965+ // matches. Users will expect a lowercase query to be adequate.
891966 int get rank;
892967}
893968
0 commit comments