@@ -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,33 @@ 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+ 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 ;
782821 }
783822
784823 /// A measure of a wildcard result's quality in the context of the query,
@@ -791,11 +830,17 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
791830 /// from 0 (best) to one less than [_numResultRanks] .
792831 ///
793832 /// 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+ }
795840
796841 /// The number of possible values returned by
797842 /// [_rankWildcardResult] and [_rankUserResult] .
798- static const _numResultRanks = 2 ;
843+ static const _numResultRanks = 4 ;
799844
800845 @override
801846 String toString () {
@@ -888,6 +933,30 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
888933 /// A measure of the result's quality in the context of the query.
889934 ///
890935 /// 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.
891960 int get rank;
892961}
893962
0 commit comments