FIX: emoji-picker minor improvements (#30645)

- uses emojiSearch to ensure we have the same result than autocomplete when filtering emojis (for example search aliases were not working correctly because of this)
- reset the visible sections when clearing filter to ensure we are not attempting to display all the emojis at once which would be slow
- prevents flashing of the full emoji list before showing filtered results
- correctly reset recent favorites and only show them when used
This commit is contained in:
Joffrey JAFFEUX 2025-01-08 22:10:50 +01:00 committed by GitHub
parent 6811296b24
commit 40f7941f2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 134 additions and 134 deletions

View File

@ -7,6 +7,7 @@ import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { cancel, next, schedule } from "@ember/runloop";
import { service } from "@ember/service";
import { modifier as modifierFn } from "ember-modifier";
import { emojiSearch } from "pretty-text/emoji";
import { eq, gt, includes, notEq } from "truth-helpers";
import DButton from "discourse/components/d-button";
import FilterInput from "discourse/components/filter-input";
@ -52,6 +53,7 @@ export default class EmojiPicker extends Component {
@service capabilities;
@service site;
@tracked isFiltering = false;
@tracked filteredEmojis = null;
@tracked scrollObserverEnabled = true;
@tracked scrollDirection = "up";
@ -152,9 +154,12 @@ export default class EmojiPicker extends Component {
@action
didInputFilter(value) {
this.isFiltering = true;
if (!value?.length) {
cancel(this.debouncedFilterHandler);
this.visibleSections = DEFAULT_VISIBLE_SECTIONS;
this.filteredEmojis = null;
this.isFiltering = false;
return;
}
@ -174,13 +179,14 @@ export default class EmojiPicker extends Component {
debouncedDidInputFilter(filter = "") {
filter = filter.toLowerCase();
this.filteredEmojis = this.flatEmojis.filter(
(emoji) =>
emoji.name.toLowerCase().includes(filter) ||
emoji.search_aliases?.any((alias) =>
alias.toLowerCase().includes(filter)
)
);
const results = emojiSearch(filter, {
exclude: this.site.denied_emojis,
}).slice(0, 50);
this.filteredEmojis =
this.flatEmojis.filter((emoji) => results.includes(emoji.name)) ?? [];
this.isFiltering = false;
schedule("afterRender", () => {
if (this.scrollableNode) {
@ -450,7 +456,6 @@ export default class EmojiPicker extends Component {
@filterAction={{withEventValue this.didInputFilter}}
@icons={{hash right="magnifying-glass"}}
@containerClass="emoji-picker__filter"
autofocus={{true}}
placeholder={{i18n "chat.emoji_picker.search_placeholder"}}
/>
@ -491,7 +496,8 @@ export default class EmojiPicker extends Component {
</DButton>
{{/each-in}}
</div>
{{#if this.sections.length}}
{{#if this.emojiStore.list}}
<div class="emoji-picker__scrollable-content" {{this.scrollListener}}>
<div
class="emoji-picker__sections"
@ -499,7 +505,7 @@ export default class EmojiPicker extends Component {
{{on "keydown" this.onSectionsKeyDown}}
role="button"
>
{{#if (notEq this.filteredEmojis null)}}
{{#if this.term.length}}
<div class="emoji-picker__section filtered">
{{#each this.filteredEmojis as |emoji|}}
<img
@ -518,95 +524,101 @@ export default class EmojiPicker extends Component {
loading="lazy"
/>
{{else}}
<p class="emoji-picker__no-results">
{{i18n "chat.emoji_picker.no_results"}}
{{replaceEmoji ":crying_cat_face:"}}
</p>
{{#if this.isFiltering}}
<div class="spinner-container">
<div class="spinner medium"></div>
</div>
{{else}}
<p class="emoji-picker__no-results">
{{i18n "chat.emoji_picker.no_results"}}
{{replaceEmoji ":crying_cat_face:"}}
</p>
{{/if}}
{{/each}}
</div>
{{else}}
{{#each-in this.groups as |section emojis|}}
{{#if emojis}}
<div
class={{concatClass
"emoji-picker__section"
(if (notEq this.filteredEmojis null) "hidden")
}}
data-section={{section}}
role="region"
aria-label={{i18n
(concat "chat.emoji_picker." section)
translatedFallback=section
}}
>
<div class="emoji-picker__section-title-container">
<h2 class="emoji-picker__section-title">
{{i18n
(concat "chat.emoji_picker." section)
translatedFallback=section
}}
</h2>
{{#if (eq section "favorites")}}
<DButton
@icon="trash-can"
class="btn-transparent"
@action={{this.clearFavorites}}
/>
{{/if}}
</div>
<div class="emoji-picker__section-emojis">
{{! we always want the first emoji for tabbing}}
{{#let (get emojis "0") as |emoji|}}
<img
width="32"
height="32"
class="emoji"
src={{tonableEmojiUrl
emoji
this.emojiStore.diversity
}}
tabindex="0"
data-emoji={{emoji.name}}
data-tonable={{if emoji.tonable "true"}}
alt={{emoji.name}}
title={{tonableEmojiTitle
emoji
this.emojiStore.diversity
}}
loading="lazy"
/>
{{/let}}
{{#if (includes this.visibleSections section)}}
{{#each emojis as |emoji index|}}
{{! first emoji has already been rendered, we don't want to re render or would lose focus}}
{{#if (gt index 0)}}
<img
width="32"
height="32"
class="emoji"
src={{tonableEmojiUrl
emoji
this.emojiStore.diversity
}}
tabindex="-1"
data-emoji={{emoji.name}}
data-tonable={{if emoji.tonable "true"}}
alt={{emoji.name}}
title={{tonableEmojiTitle
emoji
this.emojiStore.diversity
}}
loading="lazy"
/>
{{/if}}
{{/each}}
{{/if}}
</div>
</div>
{{/if}}
{{/each-in}}
{{/if}}
{{#each-in this.groups as |section emojis|}}
{{#if emojis}}
<div
class={{concatClass
"emoji-picker__section"
(if (notEq this.filteredEmojis null) "hidden")
}}
data-section={{section}}
role="region"
aria-label={{i18n
(concat "chat.emoji_picker." section)
translatedFallback=section
}}
>
<div class="emoji-picker__section-title-container">
<h2 class="emoji-picker__section-title">
{{i18n
(concat "chat.emoji_picker." section)
translatedFallback=section
}}
</h2>
{{#if (eq section "favorites")}}
<DButton
@icon="trash-can"
class="btn-transparent"
@action={{this.clearFavorites}}
/>
{{/if}}
</div>
<div class="emoji-picker__section-emojis">
{{! we always want the first emoji for tabbing}}
{{#let (get emojis "0") as |emoji|}}
<img
width="32"
height="32"
class="emoji"
src={{tonableEmojiUrl
emoji
this.emojiStore.diversity
}}
tabindex="0"
data-emoji={{emoji.name}}
data-tonable={{if emoji.tonable "true"}}
alt={{emoji.name}}
title={{tonableEmojiTitle
emoji
this.emojiStore.diversity
}}
loading="lazy"
/>
{{/let}}
{{#if (includes this.visibleSections section)}}
{{#each emojis as |emoji index|}}
{{! first emoji has already been rendered, we don't want to re render or would lose focus}}
{{#if (gt index 0)}}
<img
width="32"
height="32"
class="emoji"
src={{tonableEmojiUrl
emoji
this.emojiStore.diversity
}}
tabindex="-1"
data-emoji={{emoji.name}}
data-tonable={{if emoji.tonable "true"}}
alt={{emoji.name}}
title={{tonableEmojiTitle
emoji
this.emojiStore.diversity
}}
loading="lazy"
/>
{{/if}}
{{/each}}
{{/if}}
</div>
</div>
{{/if}}
{{/each-in}}
</div>
</div>
{{else}}

View File

@ -51,16 +51,16 @@ export default class EmojiStore extends Service {
}
resetContext(context) {
this.contexts[context] = this.#defaultEmojis;
this.#persistRecentEmojisForContext(this.#defaultEmojis, context);
}
get #defaultEmojis() {
return this.siteSettings.default_emoji_reactions.split("|").filter(Boolean);
this.contexts[context] = [];
this.#persistRecentEmojisForContext([], context);
}
#recentEmojisForContext(context) {
return this.contexts[context] ?? this.#defaultEmojis;
return (
this.contexts[context] ??
this.store.getObject(this.#emojisStorekeyForContext(context)) ??
[]
);
}
#addEmojiToContext(emoji, context) {

View File

@ -17,6 +17,12 @@ export default {
group: "smileys_\u0026_emotion",
search_aliases: ["smiley_cat", "star_struck"],
},
{
name: "smiley_cat",
tonable: false,
url: "/images/emoji/twitter/smiley_cat.png?v=12",
group: "smileys_\u0026_emotion",
},
],
"people_&_body": [
{

View File

@ -70,7 +70,7 @@ module("Integration | Component | emoji-picker-content", function (hooks) {
assert
.dom(".emoji-picker__section.filtered > img")
.exists({ count: 1 }, "it filters the emojis list");
.exists({ count: 2 }, "it filters the emojis list");
assert
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')
.exists("it filters the correct emoji");
@ -81,7 +81,7 @@ module("Integration | Component | emoji-picker-content", function (hooks) {
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')
.exists("it is case insensitive");
await fillIn(".filter-input", "smiley_cat");
await fillIn(".filter-input", "grinning");
assert
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')

View File

@ -21,14 +21,6 @@ module("Unit | Service | emoji-store", function (hooks) {
this.emojiStore.reset();
});
test(".favoritesForContext", function (assert) {
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
"+1",
"heart",
"tada",
]);
});
test(".trackEmojiForContext", function (assert) {
this.emojiStore.trackEmojiForContext("grinning", "topic");
const storedEmojis = new KeyValueStore(STORE_NAMESPACE).getObject(
@ -37,13 +29,10 @@ module("Unit | Service | emoji-store", function (hooks) {
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
"grinning",
"+1",
"heart",
"tada",
]);
assert.deepEqual(
storedEmojis,
["grinning", "+1", "heart", "tada"],
["grinning"],
"it persists the tracked emojis"
);
});
@ -73,19 +62,11 @@ module("Unit | Service | emoji-store", function (hooks) {
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
"grinning",
"+1",
"heart",
"tada",
]);
this.emojiStore.trackEmojiForContext("cat", "chat");
assert.deepEqual(this.emojiStore.favoritesForContext("chat"), [
"cat",
"+1",
"heart",
"tada",
]);
assert.deepEqual(this.emojiStore.favoritesForContext("chat"), ["cat"]);
});
test(".resetContext", function (assert) {
@ -93,11 +74,7 @@ module("Unit | Service | emoji-store", function (hooks) {
this.emojiStore.resetContext("topic");
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
"+1",
"heart",
"tada",
]);
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), []);
});
test(".diversity", function (assert) {
@ -115,6 +92,7 @@ module("Unit | Service | emoji-store", function (hooks) {
});
test("sort emojis by frequency", function (assert) {
this.emojiStore.trackEmojiForContext("grinning", "topic");
this.emojiStore.trackEmojiForContext("cat", "topic");
this.emojiStore.trackEmojiForContext("cat", "topic");
this.emojiStore.trackEmojiForContext("cat", "topic");
@ -124,9 +102,7 @@ module("Unit | Service | emoji-store", function (hooks) {
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
"cat",
"dog",
"+1",
"heart",
"tada",
"grinning",
]);
});
});

View File

@ -28,6 +28,7 @@ const REDUCED_WIDTH_THRESHOLD = 500;
export default class ChatMessageActionsDesktop extends Component {
@service chat;
@service site;
@service siteSettings;
@service emojiStore;
@tracked size = FULL;
@ -35,8 +36,13 @@ export default class ChatMessageActionsDesktop extends Component {
popper = null;
get favoriteReactions() {
const defaultReactions = this.siteSettings.default_emoji_reactions
.split("|")
.filter(Boolean);
return this.emojiStore
.favoritesForContext(`channel_${this.message.channel.id}`)
.concat(defaultReactions)
.slice(0, 3)
.map(
(emoji) =>