mirror of
https://github.com/discourse/discourse.git
synced 2025-01-29 05:28:30 +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 { cancel, next, schedule } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { modifier as modifierFn } from "ember-modifier";
|
import { modifier as modifierFn } from "ember-modifier";
|
||||||
|
import { emojiSearch } from "pretty-text/emoji";
|
||||||
import { eq, gt, includes, notEq } from "truth-helpers";
|
import { eq, gt, includes, notEq } from "truth-helpers";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import FilterInput from "discourse/components/filter-input";
|
import FilterInput from "discourse/components/filter-input";
|
||||||
|
@ -52,6 +53,7 @@ export default class EmojiPicker extends Component {
|
||||||
@service capabilities;
|
@service capabilities;
|
||||||
@service site;
|
@service site;
|
||||||
|
|
||||||
|
@tracked isFiltering = false;
|
||||||
@tracked filteredEmojis = null;
|
@tracked filteredEmojis = null;
|
||||||
@tracked scrollObserverEnabled = true;
|
@tracked scrollObserverEnabled = true;
|
||||||
@tracked scrollDirection = "up";
|
@tracked scrollDirection = "up";
|
||||||
|
@ -152,9 +154,12 @@ export default class EmojiPicker extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
didInputFilter(value) {
|
didInputFilter(value) {
|
||||||
|
this.isFiltering = true;
|
||||||
if (!value?.length) {
|
if (!value?.length) {
|
||||||
cancel(this.debouncedFilterHandler);
|
cancel(this.debouncedFilterHandler);
|
||||||
|
this.visibleSections = DEFAULT_VISIBLE_SECTIONS;
|
||||||
this.filteredEmojis = null;
|
this.filteredEmojis = null;
|
||||||
|
this.isFiltering = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,13 +179,14 @@ export default class EmojiPicker extends Component {
|
||||||
debouncedDidInputFilter(filter = "") {
|
debouncedDidInputFilter(filter = "") {
|
||||||
filter = filter.toLowerCase();
|
filter = filter.toLowerCase();
|
||||||
|
|
||||||
this.filteredEmojis = this.flatEmojis.filter(
|
const results = emojiSearch(filter, {
|
||||||
(emoji) =>
|
exclude: this.site.denied_emojis,
|
||||||
emoji.name.toLowerCase().includes(filter) ||
|
}).slice(0, 50);
|
||||||
emoji.search_aliases?.any((alias) =>
|
|
||||||
alias.toLowerCase().includes(filter)
|
this.filteredEmojis =
|
||||||
)
|
this.flatEmojis.filter((emoji) => results.includes(emoji.name)) ?? [];
|
||||||
);
|
|
||||||
|
this.isFiltering = false;
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
if (this.scrollableNode) {
|
if (this.scrollableNode) {
|
||||||
|
@ -450,7 +456,6 @@ export default class EmojiPicker extends Component {
|
||||||
@filterAction={{withEventValue this.didInputFilter}}
|
@filterAction={{withEventValue this.didInputFilter}}
|
||||||
@icons={{hash right="magnifying-glass"}}
|
@icons={{hash right="magnifying-glass"}}
|
||||||
@containerClass="emoji-picker__filter"
|
@containerClass="emoji-picker__filter"
|
||||||
autofocus={{true}}
|
|
||||||
placeholder={{i18n "chat.emoji_picker.search_placeholder"}}
|
placeholder={{i18n "chat.emoji_picker.search_placeholder"}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -491,7 +496,8 @@ export default class EmojiPicker extends Component {
|
||||||
</DButton>
|
</DButton>
|
||||||
{{/each-in}}
|
{{/each-in}}
|
||||||
</div>
|
</div>
|
||||||
{{#if this.sections.length}}
|
|
||||||
|
{{#if this.emojiStore.list}}
|
||||||
<div class="emoji-picker__scrollable-content" {{this.scrollListener}}>
|
<div class="emoji-picker__scrollable-content" {{this.scrollListener}}>
|
||||||
<div
|
<div
|
||||||
class="emoji-picker__sections"
|
class="emoji-picker__sections"
|
||||||
|
@ -499,7 +505,7 @@ export default class EmojiPicker extends Component {
|
||||||
{{on "keydown" this.onSectionsKeyDown}}
|
{{on "keydown" this.onSectionsKeyDown}}
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
{{#if (notEq this.filteredEmojis null)}}
|
{{#if this.term.length}}
|
||||||
<div class="emoji-picker__section filtered">
|
<div class="emoji-picker__section filtered">
|
||||||
{{#each this.filteredEmojis as |emoji|}}
|
{{#each this.filteredEmojis as |emoji|}}
|
||||||
<img
|
<img
|
||||||
|
@ -518,95 +524,101 @@ export default class EmojiPicker extends Component {
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="emoji-picker__no-results">
|
{{#if this.isFiltering}}
|
||||||
{{i18n "chat.emoji_picker.no_results"}}
|
<div class="spinner-container">
|
||||||
{{replaceEmoji ":crying_cat_face:"}}
|
<div class="spinner medium"></div>
|
||||||
</p>
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p class="emoji-picker__no-results">
|
||||||
|
{{i18n "chat.emoji_picker.no_results"}}
|
||||||
|
{{replaceEmoji ":crying_cat_face:"}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</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}}
|
{{/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>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -51,16 +51,16 @@ export default class EmojiStore extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetContext(context) {
|
resetContext(context) {
|
||||||
this.contexts[context] = this.#defaultEmojis;
|
this.contexts[context] = [];
|
||||||
this.#persistRecentEmojisForContext(this.#defaultEmojis, context);
|
this.#persistRecentEmojisForContext([], context);
|
||||||
}
|
|
||||||
|
|
||||||
get #defaultEmojis() {
|
|
||||||
return this.siteSettings.default_emoji_reactions.split("|").filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#recentEmojisForContext(context) {
|
#recentEmojisForContext(context) {
|
||||||
return this.contexts[context] ?? this.#defaultEmojis;
|
return (
|
||||||
|
this.contexts[context] ??
|
||||||
|
this.store.getObject(this.#emojisStorekeyForContext(context)) ??
|
||||||
|
[]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addEmojiToContext(emoji, context) {
|
#addEmojiToContext(emoji, context) {
|
||||||
|
|
|
@ -17,6 +17,12 @@ export default {
|
||||||
group: "smileys_\u0026_emotion",
|
group: "smileys_\u0026_emotion",
|
||||||
search_aliases: ["smiley_cat", "star_struck"],
|
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": [
|
"people_&_body": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -70,7 +70,7 @@ module("Integration | Component | emoji-picker-content", function (hooks) {
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(".emoji-picker__section.filtered > img")
|
.dom(".emoji-picker__section.filtered > img")
|
||||||
.exists({ count: 1 }, "it filters the emojis list");
|
.exists({ count: 2 }, "it filters the emojis list");
|
||||||
assert
|
assert
|
||||||
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')
|
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')
|
||||||
.exists("it filters the correct emoji");
|
.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"]')
|
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')
|
||||||
.exists("it is case insensitive");
|
.exists("it is case insensitive");
|
||||||
|
|
||||||
await fillIn(".filter-input", "smiley_cat");
|
await fillIn(".filter-input", "grinning");
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')
|
.dom('.emoji-picker__section.filtered > img[alt="grinning"]')
|
||||||
|
|
|
@ -21,14 +21,6 @@ module("Unit | Service | emoji-store", function (hooks) {
|
||||||
this.emojiStore.reset();
|
this.emojiStore.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(".favoritesForContext", function (assert) {
|
|
||||||
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
|
|
||||||
"+1",
|
|
||||||
"heart",
|
|
||||||
"tada",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(".trackEmojiForContext", function (assert) {
|
test(".trackEmojiForContext", function (assert) {
|
||||||
this.emojiStore.trackEmojiForContext("grinning", "topic");
|
this.emojiStore.trackEmojiForContext("grinning", "topic");
|
||||||
const storedEmojis = new KeyValueStore(STORE_NAMESPACE).getObject(
|
const storedEmojis = new KeyValueStore(STORE_NAMESPACE).getObject(
|
||||||
|
@ -37,13 +29,10 @@ module("Unit | Service | emoji-store", function (hooks) {
|
||||||
|
|
||||||
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
|
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
|
||||||
"grinning",
|
"grinning",
|
||||||
"+1",
|
|
||||||
"heart",
|
|
||||||
"tada",
|
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
storedEmojis,
|
storedEmojis,
|
||||||
["grinning", "+1", "heart", "tada"],
|
["grinning"],
|
||||||
"it persists the tracked emojis"
|
"it persists the tracked emojis"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -73,19 +62,11 @@ module("Unit | Service | emoji-store", function (hooks) {
|
||||||
|
|
||||||
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
|
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
|
||||||
"grinning",
|
"grinning",
|
||||||
"+1",
|
|
||||||
"heart",
|
|
||||||
"tada",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.emojiStore.trackEmojiForContext("cat", "chat");
|
this.emojiStore.trackEmojiForContext("cat", "chat");
|
||||||
|
|
||||||
assert.deepEqual(this.emojiStore.favoritesForContext("chat"), [
|
assert.deepEqual(this.emojiStore.favoritesForContext("chat"), ["cat"]);
|
||||||
"cat",
|
|
||||||
"+1",
|
|
||||||
"heart",
|
|
||||||
"tada",
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(".resetContext", function (assert) {
|
test(".resetContext", function (assert) {
|
||||||
|
@ -93,11 +74,7 @@ module("Unit | Service | emoji-store", function (hooks) {
|
||||||
|
|
||||||
this.emojiStore.resetContext("topic");
|
this.emojiStore.resetContext("topic");
|
||||||
|
|
||||||
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
|
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), []);
|
||||||
"+1",
|
|
||||||
"heart",
|
|
||||||
"tada",
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(".diversity", function (assert) {
|
test(".diversity", function (assert) {
|
||||||
|
@ -115,6 +92,7 @@ module("Unit | Service | emoji-store", function (hooks) {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sort emojis by frequency", function (assert) {
|
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");
|
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"), [
|
assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [
|
||||||
"cat",
|
"cat",
|
||||||
"dog",
|
"dog",
|
||||||
"+1",
|
"grinning",
|
||||||
"heart",
|
|
||||||
"tada",
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,7 @@ const REDUCED_WIDTH_THRESHOLD = 500;
|
||||||
export default class ChatMessageActionsDesktop extends Component {
|
export default class ChatMessageActionsDesktop extends Component {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service site;
|
@service site;
|
||||||
|
@service siteSettings;
|
||||||
@service emojiStore;
|
@service emojiStore;
|
||||||
|
|
||||||
@tracked size = FULL;
|
@tracked size = FULL;
|
||||||
|
@ -35,8 +36,13 @@ export default class ChatMessageActionsDesktop extends Component {
|
||||||
popper = null;
|
popper = null;
|
||||||
|
|
||||||
get favoriteReactions() {
|
get favoriteReactions() {
|
||||||
|
const defaultReactions = this.siteSettings.default_emoji_reactions
|
||||||
|
.split("|")
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
return this.emojiStore
|
return this.emojiStore
|
||||||
.favoritesForContext(`channel_${this.message.channel.id}`)
|
.favoritesForContext(`channel_${this.message.channel.id}`)
|
||||||
|
.concat(defaultReactions)
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map(
|
.map(
|
||||||
(emoji) =>
|
(emoji) =>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user