mirror of
https://github.com/discourse/discourse.git
synced 2025-01-16 05:53:11 +08:00
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:
parent
6811296b24
commit
40f7941f2b
|
@ -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}}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -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"]')
|
||||
|
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) =>
|
||||
|
|
Loading…
Reference in New Issue
Block a user