Skip to content

Commit bb8935a

Browse files
committed
emoji: Rank "popular" > custom > other emoji
Fixes part of #1068.
1 parent 6c93b40 commit bb8935a

File tree

2 files changed

+81
-12
lines changed

2 files changed

+81
-12
lines changed

lib/model/emoji.dart

+34-6
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,17 @@ class EmojiStoreImpl with EmojiStore {
244244
];
245245
}
246246

247+
static final _popularEmojiCodes = (() {
248+
assert(_popularCandidates.every((c) =>
249+
c.emojiType == ReactionType.unicodeEmoji));
250+
return Set.of(_popularCandidates.map((c) => c.emojiCode));
251+
})();
252+
253+
static bool _isPopularEmoji(EmojiCandidate candidate) {
254+
return candidate.emojiType == ReactionType.unicodeEmoji
255+
&& _popularEmojiCodes.contains(candidate.emojiCode);
256+
}
257+
247258
EmojiCandidate _emojiCandidateFor({
248259
required ReactionType emojiType,
249260
required String emojiCode,
@@ -448,7 +459,8 @@ class EmojiAutocompleteQuery extends ComposeAutocompleteQuery {
448459
EmojiAutocompleteResult? testCandidate(EmojiCandidate candidate) {
449460
final matchQuality = match(candidate);
450461
if (matchQuality == null) return null;
451-
return EmojiAutocompleteResult(candidate, _rankResult(matchQuality));
462+
return EmojiAutocompleteResult(candidate,
463+
_rankResult(matchQuality, candidate));
452464
}
453465

454466
// Compare get_emoji_matcher in Zulip web:shared/src/typeahead.ts .
@@ -501,7 +513,7 @@ class EmojiAutocompleteQuery extends ComposeAutocompleteQuery {
501513

502514
/// A measure of the result's quality in the context of the query,
503515
/// ranked from 0 (best) to one less than [_numResultRanks].
504-
static int _rankResult(EmojiMatchQuality matchQuality) {
516+
static int _rankResult(EmojiMatchQuality matchQuality, EmojiCandidate candidate) {
505517
// See also [EmojiStoreImpl._generateAllCandidates];
506518
// emoji which this function ranks equally
507519
// will appear in the order they were put in by that method.
@@ -510,12 +522,19 @@ class EmojiAutocompleteQuery extends ComposeAutocompleteQuery {
510522
// https://github.com/zulip/zulip/blob/83a121c7e/web/shared/src/typeahead.ts#L322-L382
511523
//
512524
// Behavior differences we should or might copy, TODO(#1068):
513-
// * Web ranks popular emoji > custom emoji > others; we don't yet.
514525
// * Web ranks matches starting at a word boundary ahead of
515526
// other non-prefix matches; we don't yet.
527+
// * Relatedly, web favors popular emoji only upon a word-aligned match.
516528
// * Web ranks each name of a Unicode emoji separately.
517529
//
518530
// Behavior differences that web should probably fix, TODO(web):
531+
// * Among popular emoji with non-exact matches,
532+
// web doesn't prioritize prefix over word-aligned; we do.
533+
// (This affects just one case: for query "o",
534+
// we put :octopus: before :working_on_it:.)
535+
// * Web only counts an emoji as "popular" for ranking if the query
536+
// is a prefix of a single word in the name; so "thumbs_" or "working_on_i"
537+
// lose the ranking boost for :thumbs_up: and :working_on_it: respectively.
519538
// * Web starts with only case-sensitive exact matches ("perfect matches"),
520539
// and puts case-insensitive exact matches just ahead of prefix matches;
521540
// it also distinguishes prefix matches by case-sensitive vs. not.
@@ -526,15 +545,24 @@ class EmojiAutocompleteQuery extends ComposeAutocompleteQuery {
526545
// because emoji with the same name will mostly both match or both not;
527546
// but it breaks if the Unicode emoji was a literal match.
528547

548+
final isPopular = EmojiStoreImpl._isPopularEmoji(candidate);
549+
final isCustomEmoji = switch (candidate.emojiType) {
550+
// The web implementation calls this condition `is_realm_emoji`,
551+
// but its actual semantics is it's true for the Zulip extra emoji too.
552+
// See `zulip_emoji` in web:src/emoji.ts .
553+
ReactionType.realmEmoji || ReactionType.zulipExtraEmoji => true,
554+
ReactionType.unicodeEmoji => false,
555+
};
529556
return switch (matchQuality) {
530557
EmojiMatchQuality.exact => 0,
531-
EmojiMatchQuality.prefix => 1,
532-
EmojiMatchQuality.other => 2,
558+
EmojiMatchQuality.prefix => isPopular ? 1 : isCustomEmoji ? 3 : 4,
559+
// TODO word-boundary vs. not
560+
EmojiMatchQuality.other => isPopular ? 2 : isCustomEmoji ? 5 : 6,
533561
};
534562
}
535563

536564
/// The number of possible values returned by [_rankResult].
537-
static const _numResultRanks = 3;
565+
static const _numResultRanks = 7;
538566

539567
@override
540568
String toString() {

test/model/emoji_test.dart

+47-6
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,9 @@ void main() {
282282
await Future(() {});
283283
check(done).isTrue();
284284
check(view.results).deepEquals([
285-
isUnicodeResult(names: ['bookmark']),
286285
isRealmResult(emojiName: 'happy'),
287286
isZulipResult(),
287+
isUnicodeResult(names: ['bookmark']),
288288
]);
289289
});
290290

@@ -324,15 +324,17 @@ void main() {
324324
}
325325

326326
test('results end-to-end', () async {
327+
// (See more detailed rank tests below, on EmojiAutocompleteQuery.)
328+
327329
final unicodeEmoji = {
328330
'1f4d3': ['notebook'], '1f516': ['bookmark'], '1f4d6': ['book']};
329331

330332
// Empty query -> base ordering.
331333
check(await resultsOf('', unicodeEmoji: unicodeEmoji)).deepEquals([
334+
isZulipResult(),
332335
isUnicodeResult(names: ['notebook']),
333336
isUnicodeResult(names: ['bookmark']),
334337
isUnicodeResult(names: ['book']),
335-
isZulipResult(),
336338
]);
337339

338340
// With query, exact match precedes prefix match precedes other.
@@ -505,17 +507,56 @@ void main() {
505507
check(rankOf(query, a)!).isLessThan(rankOf(query, b)!);
506508
}
507509

510+
void checkSameRank(String query, EmojiCandidate a, EmojiCandidate b) {
511+
check(rankOf(query, a)!).equals(rankOf(query, b)!);
512+
}
513+
514+
final octopus = unicode(['octopus'], emojiCode: '1f419');
515+
final workingOnIt = unicode(['working_on_it'], emojiCode: '1f6e0');
516+
508517
test('ranks exact before prefix before other match', () {
509518
checkPrecedes('o', unicode(['o']), unicode(['onion']));
510519
checkPrecedes('o', unicode(['onion']), unicode(['book']));
511520
});
512521

522+
test('ranks popular before realm before other Unicode', () {
523+
checkPrecedes('o', octopus, realmCandidate('open_book'));
524+
checkPrecedes('o', realmCandidate('open_book'), unicode(['ok']));
525+
});
526+
527+
test('ranks Zulip extra emoji same as realm emoji', () {
528+
checkSameRank('z', zulipCandidate(), realmCandidate('zounds'));
529+
});
530+
531+
test('ranks exact-vs-not more significant than popular/custom/other', () {
532+
// Generic Unicode exact beats popular prefix…
533+
checkPrecedes('o', unicode(['o']), octopus);
534+
// … which really does count as popular, beating realm prefix.
535+
checkPrecedes('o', octopus, realmCandidate('open_book'));
536+
});
537+
538+
test('ranks popular-vs-not more significant than prefix/other', () {
539+
// Popular other beats realm prefix.
540+
checkPrecedes('o', workingOnIt, realmCandidate('open_book'));
541+
});
542+
543+
test('ranks prefix/other more significant than custom/other', () {
544+
// Generic Unicode prefix beats realm other.
545+
checkPrecedes('o', unicode(['ok']), realmCandidate('yo'));
546+
});
547+
513548
test('full list of ranks', () {
514549
check([
515-
rankOf('o', unicode(['o'])), // exact
516-
rankOf('o', unicode(['onion'])), // prefix
517-
rankOf('o', unicode(['book'])), // other
518-
]).deepEquals([0, 1, 2]);
550+
rankOf('o', unicode(['o'])), // exact (generic)
551+
rankOf('o', octopus), // prefix popular
552+
rankOf('o', workingOnIt), // other popular
553+
rankOf('o', realmCandidate('open_book')), // prefix realm
554+
rankOf('z', zulipCandidate()), // == prefix :zulip:
555+
rankOf('o', unicode(['ok'])), // prefix generic
556+
rankOf('o', realmCandidate('yo')), // other realm
557+
rankOf('p', zulipCandidate()), // == other :zulip:
558+
rankOf('o', unicode(['book'])), // other generic
559+
]).deepEquals([0, 1, 2, 3, 3, 4, 5, 5, 6]);
519560
});
520561
});
521562
}

0 commit comments

Comments
 (0)