diff --git a/lib/model/emoji.dart b/lib/model/emoji.dart index a06f854948..fa62887a4b 100644 --- a/lib/model/emoji.dart +++ b/lib/model/emoji.dart @@ -242,6 +242,17 @@ class EmojiStoreImpl with EmojiStore { /// retrieving the data. Map>? _serverEmojiData; + static final _popularEmojiCodes = (() { + assert(zulipPopularEmojis.every((c) => + c.emojiType == ReactionType.unicodeEmoji)); + return Set.of(zulipPopularEmojis.map((c) => c.emojiCode)); + })(); + + static bool _isPopularEmoji(EmojiCandidate candidate) { + return candidate.emojiType == ReactionType.unicodeEmoji + && _popularEmojiCodes.contains(candidate.emojiCode); + } + List? _allEmojiCandidates; EmojiCandidate _emojiCandidateFor({ @@ -411,21 +422,29 @@ class EmojiAutocompleteQuery extends ComposeAutocompleteQuery { EmojiAutocompleteResult? testCandidate(EmojiCandidate candidate) { final matchQuality = match(candidate); if (matchQuality == null) return null; - return EmojiAutocompleteResult(_rankResult(matchQuality), candidate); + return EmojiAutocompleteResult( + _rankResult(matchQuality, candidate), candidate); } /// A measure of the result's quality in the context of the query, /// ranked from 0 (best) to one less than [_numResultRanks]. - static int _rankResult(EmojiMatchQuality matchQuality) { + static int _rankResult(EmojiMatchQuality matchQuality, EmojiCandidate candidate) { // Compare sort_emojis in Zulip web:shared/src/typeahead.ts . // // Behavior differences we should or might copy, TODO(#1068): - // * Web ranks popular emoji > custom emoji > others; we don't yet. // * Web ranks matches starting at a word boundary ahead of // other non-prefix matches; we don't yet. + // * Relatedly, web favors popular emoji only upon a word-aligned match. // * Web ranks each name of a Unicode emoji separately. // // Behavior differences that web should probably fix, TODO(web): + // * Among popular emoji with non-exact matches, + // web doesn't prioritize prefix over word-aligned; we do. + // (This affects just one case: for query "o", + // we put :octopus: before :working_on_it:.) + // * Web only counts an emoji as "popular" for ranking if the query + // is a prefix of a single word in the name; so "thumbs_" or "working_on_i" + // lose the ranking boost for :thumbs_up: and :working_on_it: respectively. // * Web starts with only case-sensitive exact matches ("perfect matches"), // and puts case-insensitive exact matches just ahead of prefix matches; // it also distinguishes prefix matches by case-sensitive vs. not. @@ -436,15 +455,24 @@ class EmojiAutocompleteQuery extends ComposeAutocompleteQuery { // because emoji with the same name will mostly both match or both not; // but it breaks if the Unicode emoji was a literal match. + final isPopular = EmojiStoreImpl._isPopularEmoji(candidate); + final isCustomEmoji = switch (candidate.emojiType) { + // The web implementation calls this condition `is_realm_emoji`, + // but its actual semantics is it's true for the Zulip extra emoji too. + // See `zulip_emoji` in web:src/emoji.ts . + ReactionType.realmEmoji || ReactionType.zulipExtraEmoji => true, + ReactionType.unicodeEmoji => false, + }; return switch (matchQuality) { EmojiMatchQuality.exact => 0, - EmojiMatchQuality.prefix => 1, - EmojiMatchQuality.other => 2, + EmojiMatchQuality.prefix => isPopular ? 1 : isCustomEmoji ? 3 : 4, + // TODO word-boundary vs. not + EmojiMatchQuality.other => isPopular ? 2 : isCustomEmoji ? 5 : 6, }; } /// The number of possible values returned by [_rankResult]. - static const _numResultRanks = 3; + static const _numResultRanks = 7; // Compare get_emoji_matcher in Zulip web:shared/src/typeahead.ts . @visibleForTesting