diff --git a/lib/model/emoji.dart b/lib/model/emoji.dart index 753b034c52..eede09b1d1 100644 --- a/lib/model/emoji.dart +++ b/lib/model/emoji.dart @@ -283,12 +283,18 @@ class EmojiStoreImpl with EmojiStore { List _generateAllCandidates() { final results = []; + // Include the "popular" emoji, in their canonical order + // relative to each other. + results.addAll(zulipPopularEmojis); + final namesOverridden = { for (final emoji in realmEmoji.values) emoji.name, 'zulip', }; // TODO(log) if _serverEmojiData missing for (final entry in (_serverEmojiData ?? {}).entries) { + if (_popularEmojiCodes.contains(entry.key)) continue; + final allNames = entry.value; final String emojiName; final List? aliases; diff --git a/test/model/emoji_test.dart b/test/model/emoji_test.dart index c60421d5bf..486d6b90dc 100644 --- a/test/model/emoji_test.dart +++ b/test/model/emoji_test.dart @@ -108,6 +108,9 @@ void main() { ..aliases.isEmpty(); } + List> arePopularCandidates = zulipPopularEmojis.map( + (c) => isUnicodeCandidate(c.emojiCode, null)).toList(); + group('allEmojiCandidates', () { // TODO test emojiDisplay of candidates matches emojiDisplayFor @@ -123,6 +126,40 @@ void main() { return store; } + test('popular emoji appear even when no server emoji data', () { + final store = prepare(unicodeEmoji: null); + check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, + isZulipCandidate(), + ]); + }); + + test('popular emoji appear in their canonical order', () { + // In the server's emoji data, have the popular emoji in a permuted order, + // and interspersed with other emoji. + final store = prepare(unicodeEmoji: { + '1f603': ['smiley'], + for (final candidate in zulipPopularEmojis.skip(3)) + candidate.emojiCode: [candidate.emojiName, ...candidate.aliases], + '1f34a': ['orange', 'tangerine', 'mandarin'], + for (final candidate in zulipPopularEmojis.take(3)) + candidate.emojiCode: [candidate.emojiName, ...candidate.aliases], + '1f516': ['bookmark'], + }); + // In the allEmojiCandidates result, the popular emoji come first + // and are in their canonical order, even though the other Unicode emoji + // are in the same order they were given in. + check(store.allEmojiCandidates()).deepEquals([ + for (final candidate in zulipPopularEmojis) + isUnicodeCandidate(candidate.emojiCode, + [candidate.emojiName, ...candidate.aliases]), + isUnicodeCandidate('1f603', ['smiley']), + isUnicodeCandidate('1f34a', ['orange', 'tangerine', 'mandarin']), + isUnicodeCandidate('1f516', ['bookmark']), + isZulipCandidate(), + ]); + }); + test('realm emoji overrides Unicode emoji', () { final store = prepare(realmEmoji: { '1': eg.realmEmojiItem(emojiCode: '1', emojiName: 'smiley'), @@ -131,6 +168,7 @@ void main() { '1f603': ['smiley'], }); check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, isUnicodeCandidate('1f516', ['bookmark']), isRealmCandidate(emojiCode: '1', emojiName: 'smiley'), isZulipCandidate(), @@ -144,6 +182,7 @@ void main() { '1f34a': ['orange', 'tangerine', 'mandarin'], }); check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, isUnicodeCandidate('1f34a', ['orange', 'mandarin']), isRealmCandidate(emojiCode: '1', emojiName: 'tangerine'), isZulipCandidate(), @@ -157,6 +196,7 @@ void main() { '1f34a': ['orange', 'tangerine', 'mandarin'], }); check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, isUnicodeCandidate('1f34a', ['tangerine', 'mandarin']), isRealmCandidate(emojiCode: '1', emojiName: 'orange'), isZulipCandidate(), @@ -166,6 +206,7 @@ void main() { test('updates on setServerEmojiData', () { final store = prepare(); check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, isZulipCandidate(), ]); @@ -173,6 +214,7 @@ void main() { '1f516': ['bookmark'], })); check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, isUnicodeCandidate('1f516', ['bookmark']), isZulipCandidate(), ]); @@ -181,6 +223,7 @@ void main() { test('updates on RealmEmojiUpdateEvent', () { final store = prepare(); check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, isZulipCandidate(), ]); @@ -188,6 +231,7 @@ void main() { '1': eg.realmEmojiItem(emojiCode: '1', emojiName: 'happy'), })); check(store.allEmojiCandidates()).deepEquals([ + ...arePopularCandidates, isRealmCandidate(emojiCode: '1', emojiName: 'happy'), isZulipCandidate(), ]); @@ -220,6 +264,9 @@ void main() { isZulipCandidate()); } + List> arePopularResults = zulipPopularEmojis.map( + (c) => isUnicodeResult(emojiCode: c.emojiCode)).toList(); + PerAccountStore prepare({ Map realmEmoji = const {}, Map>? unicodeEmoji, @@ -245,6 +292,7 @@ void main() { await Future(() {}); check(done).isTrue(); check(view.results).deepEquals([ + ...arePopularResults, isRealmResult(emojiName: 'happy'), isZulipResult(), isUnicodeResult(names: ['bookmark']), @@ -286,6 +334,45 @@ void main() { return view.results; } + test('results preserve order of popular emoji within each rank', () async { + // In other words, the sorting by rank is a stable sort. + + // Full results list matches allEmojiCandidates. + check(prepare().allEmojiCandidates()) + .deepEquals([...arePopularCandidates, isZulipCandidate()]); + check(await resultsOf('')) + .deepEquals([...arePopularResults, isZulipResult()]); + + // Same list written out explicitly, for comparison with the cases below. + check(await resultsOf('')).deepEquals([ + isUnicodeResult(names: ['+1', 'thumbs_up', 'like']), + isUnicodeResult(names: ['tada']), + isUnicodeResult(names: ['smile']), + isUnicodeResult(names: ['heart', 'love', 'love_you']), + isUnicodeResult(names: ['working_on_it', 'hammer_and_wrench', 'tools']), + isUnicodeResult(names: ['octopus']), + isZulipResult(), + ]); + + check(await resultsOf('t')).deepEquals([ + // prefix + isUnicodeResult(names: ['+1', 'thumbs_up', 'like']), + isUnicodeResult(names: ['tada']), + isUnicodeResult(names: ['working_on_it', 'hammer_and_wrench', 'tools']), + // other + isUnicodeResult(names: ['heart', 'love', 'love_you']), + isUnicodeResult(names: ['octopus']), + ]); + + check(await resultsOf('h')).deepEquals([ + // prefix + isUnicodeResult(names: ['heart', 'love', 'love_you']), + isUnicodeResult(names: ['working_on_it', 'hammer_and_wrench', 'tools']), + // other + isUnicodeResult(names: ['+1', 'thumbs_up', 'like']), + ]); + }); + test('results end-to-end', () async { // (See more detailed rank tests below, on EmojiAutocompleteQuery.) @@ -294,6 +381,7 @@ void main() { // Empty query -> base ordering. check(await resultsOf('', unicodeEmoji: unicodeEmoji)).deepEquals([ + ...arePopularResults, isZulipResult(), isUnicodeResult(names: ['notebook']), isUnicodeResult(names: ['bookmark']),