@@ -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) {
@@ -724,6 +727,23 @@ abstract class AutocompleteQuery {
724727 }
725728}
726729
730+ /// The match quality of a [User.fullName] to a mention autocomplete query.
731+ ///
732+ /// All matches are case-insensitive.
733+ enum NameMatchQuality {
734+ /// The query matches the whole name exactly.
735+ exact,
736+
737+ /// The name starts with the query.
738+ totalPrefix,
739+
740+ /// All of the query's words have matches in the words of the name
741+ /// that appear in order.
742+ ///
743+ /// A "match" means the word in the name starts with the query word.
744+ wordPrefixes,
745+ }
746+
727747/// Any autocomplete query in the compose box's content input.
728748abstract class ComposeAutocompleteQuery extends AutocompleteQuery {
729749 ComposeAutocompleteQuery (super .raw);
@@ -770,14 +790,39 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
770790
771791 final cache = store.autocompleteViewManager.autocompleteDataCache;
772792 // TODO(#236) test email too, not just name
773- if (! _testName (user, cache)) return null ;
793+ final nameMatchQuality = _matchName (
794+ lowercaseName: cache.lowercaseNameForUser (user),
795+ lowercaseNameWords: cache.lowercaseNameWordsForUser (user));
796+ if (nameMatchQuality == null ) return null ;
774797
775798 return UserMentionAutocompleteResult (
776- userId: user.userId, rank: _rankUserResult (user));
799+ userId: user.userId,
800+ rank: _rankUserResult (user, nameMatchQuality: nameMatchQuality));
777801 }
778802
779- bool _testName (User user, AutocompleteDataCache cache) {
780- return _testContainsQueryWords (cache.lowercaseNameWordsForUser (user));
803+ NameMatchQuality ? _matchName ({
804+ required String lowercaseName,
805+ required List <String > lowercaseNameWords,
806+ }) {
807+ int matchLength = 0 ;
808+ while (
809+ matchLength < lowercaseName.length && matchLength < _lowercase.length
810+ && lowercaseName[matchLength] == _lowercase[matchLength]
811+ ) {
812+ matchLength++ ;
813+ }
814+
815+ if (matchLength == lowercaseName.length && matchLength == _lowercase.length) {
816+ return NameMatchQuality .exact;
817+ } else if (matchLength == _lowercase.length) {
818+ return NameMatchQuality .totalPrefix;
819+ }
820+
821+ if (_testContainsQueryWords (lowercaseNameWords)) {
822+ return NameMatchQuality .wordPrefixes;
823+ }
824+
825+ return null ;
781826 }
782827
783828 /// A measure of a wildcard result's quality in the context of the query,
@@ -790,11 +835,17 @@ class MentionAutocompleteQuery extends ComposeAutocompleteQuery {
790835 /// from 0 (best) to one less than [_numResultRanks] .
791836 ///
792837 /// See also [_rankWildcardResult] .
793- static int _rankUserResult (User user) => 1 ;
838+ static int _rankUserResult (User user, {required NameMatchQuality nameMatchQuality}) {
839+ return switch (nameMatchQuality) {
840+ NameMatchQuality .exact => 1 ,
841+ NameMatchQuality .totalPrefix => 2 ,
842+ NameMatchQuality .wordPrefixes => 3 ,
843+ };
844+ }
794845
795846 /// The number of possible values returned by
796847 /// [_rankWildcardResult] and [_rankUserResult] .
797- static const _numResultRanks = 2 ;
848+ static const _numResultRanks = 4 ;
798849
799850 @override
800851 String toString () {
@@ -886,6 +937,30 @@ sealed class MentionAutocompleteResult extends ComposeAutocompleteResult {
886937 /// A measure of the result's quality in the context of the query.
887938 ///
888939 /// Used internally by [MentionAutocompleteView] for ranking the results.
940+ // See also [MentionAutocompleteView._usersByRelevance];
941+ // results with equal [rank] will appear in the order they were put in
942+ // by that method.
943+ //
944+ // Compare sort_recipients in Zulip web:
945+ // https://github.com/zulip/zulip/blob/afdf20c67/web/src/typeahead_helper.ts#L472
946+ //
947+ // Behavior we have that web doesn't and might like to follow:
948+ // - A "word-prefixes" match quality on user names:
949+ // see [NameMatchQuality.wordPrefixes], which we rank on.
950+ //
951+ // Behavior web has that seems undesired, which we don't plan to follow:
952+ // - Ranking humans above bots, even when the bots have higher relevance
953+ // and better match quality. If there's a bot participating in the
954+ // current conversation and I start typing its name, why wouldn't we want
955+ // that as a top result? Issue: https://github.com/zulip/zulip/issues/35467
956+ // - A "word-boundary" match quality on user and user-group names:
957+ // special rank when the whole query appears contiguously
958+ // right after a word-boundary character.
959+ // Our [NameMatchQuality.wordPrefixes] seems smarter.
960+ // - A "word-boundary" match quality on user emails:
961+ // "words" is a wrong abstraction when matching on emails.
962+ // - Ranking some case-sensitive matches differently from case-insensitive
963+ // matches. Users will expect a lowercase query to be adequate.
889964 int get rank;
890965}
891966
0 commit comments