From 6740a340cacb4605e9c5d6e0b536dc43ede14ca3 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 8 Jan 2025 11:41:36 +0100 Subject: [PATCH] DEV: unifies emoji picker (#28277) The chat emoji picker is renamed emoji-picker, and the old emoji-picker is removed. This commit doesn't attempt to fully rework a new emoji-picker but instead tries to migrate everything to one picker (the chat one) and add small changes. Other notable changes: - all the favorite emojis code has been mixed into one service which is able to store one state per context, favorites emojis will be stored for all topics, and for each chat channel. Meaning that if you always use a specific emoji in a channel, it will only show as favorite emoji in this channel. - a lot of static code has been removed which should improve initial load perf of discourse. Initially this code was around to improve the performance of the emoji picker rendering. - the emojis are now stored, once the full list has been loaded, if you close and reopen the picker it won't have to load them again. List of components: - `` will render a button which will open a dropdown - `` represents the content of the dropdown alone, it's useful when you want to render a picker from an action which is not the default picker button - `` just a simple wrapper over `` to make it easier to use it with `this.menu.show(...)` --------- Co-authored-by: Renato Atilio --- .../addon/components/emoji-value-list.hbs | 17 +- .../addon/components/emoji-value-list.js | 38 +- .../discourse/app/components/d-editor.hbs | 10 +- .../discourse/app/components/d-editor.js | 46 +- .../app/components/emoji-group-buttons.hbs | 75 - .../app/components/emoji-group-buttons.js | 3 - .../app/components/emoji-group-sections.hbs | 2725 ----------------- .../app/components/emoji-group-sections.js | 3 - .../discourse/app/components/emoji-picker.hbs | 154 - .../discourse/app/components/emoji-picker.js | 467 --- .../app/components/emoji-picker/content.gjs | 620 ++++ .../app/components/emoji-picker/detached.gjs | 12 + .../emoji-picker/diversity-menu.gjs | 71 + .../app/components/emoji-picker/index.gjs | 59 + .../discourse/app/components/filter-input.gjs | 6 +- .../app/components/user-status-picker.gjs | 39 +- .../form-kit/components/fk/control/menu.gjs | 3 +- .../app/instance-initializers/enable-emoji.js | 21 +- .../lib/virtual-element-from-text-range.js | 15 +- .../app/modifiers/close-on-click-outside.js | 4 +- .../discourse/app/services/emoji-store.js | 103 +- .../tests/acceptance/emoji-picker-test.js | 269 -- .../discourse/tests/acceptance/emoji-test.js | 9 +- ...er-preferences-account-user-status-test.js | 15 +- .../tests/acceptance/user-status-test.js | 20 +- .../tests/fixtures/emojis-fixtures.js | 47 + .../tests/helpers/emoji-picker-helper.js | 43 + .../tests/helpers/form-kit-assertions.js | 4 +- .../tests/helpers/form-kit-helper.js | 2 +- .../integration/components/d-editor-test.js | 39 +- .../components/emoji-picker-test.js | 243 +- .../components/filter-input-test.js | 56 + .../form-kit/controls/menu-test.gjs | 2 +- .../components/user-status-picker-test.gjs | 8 +- .../tests/unit/services/emoji-store-test.js | 126 +- .../addon/components/d-float-body.gjs | 16 +- .../float-kit/addon/components/d-menu.gjs | 14 +- .../float-kit/addon/lib/constants.js | 1 + .../float-kit/addon/lib/d-menu-instance.js | 2 + .../addon/modifiers/close-on-escape.js | 3 - .../javascripts/pretty-text/addon/emoji.js | 4 - app/assets/stylesheets/common/base/emoji.scss | 227 -- .../stylesheets/common/components/_index.scss | 3 + .../common/components/dropdown-menu.scss | 18 + .../common/components/emoji-picker.scss | 132 +- .../common/components/filter-input.scss | 6 +- .../common/components/user-status-picker.scss | 5 +- .../common/form-kit/_control-menu.scss | 13 +- app/assets/stylesheets/mobile/_index.scss | 1 - .../stylesheets/mobile/components/_index.scss | 3 + .../mobile/components/emoji-picker.scss | 57 + app/assets/stylesheets/mobile/emoji.scss | 18 - .../stylesheets/mobile/float-kit/d-menu.scss | 3 - app/controllers/emojis_controller.rb | 8 + config/locales/server.en.yml | 3 + config/routes.rb | 2 + config/site_settings.yml | 4 + lib/tasks/javascript.rake | 14 - .../chat-incoming-webhook-edit-form.gjs | 45 +- .../app/controllers/chat/emojis_controller.rb | 10 - .../discourse/components/browse-channels.gjs | 5 +- .../chat-channel-message-emoji-picker.gjs | 77 - .../discourse/components/chat-channel.gjs | 1 - .../components/chat-composer-dropdown.gjs | 7 +- .../discourse/components/chat-composer.hbs | 11 +- .../discourse/components/chat-composer.js | 47 +- .../components/chat-emoji-picker.hbs | 246 -- .../discourse/components/chat-emoji-picker.js | 424 --- .../chat-message-actions-desktop.gjs | 47 +- .../chat-message-actions-mobile.gjs | 25 +- .../discourse/components/chat-message.gjs | 54 +- .../chat/routes/channel-info-members.gjs | 4 +- ...channel-message-emoji-picker-connector.hbs | 1 - .../emoji-picker.hbs | 9 + .../discourse/helpers/tonable-emoji-title.js | 7 - .../discourse/helpers/tonable-emoji-url.js | 7 - .../discourse/initializers/chat-setup.js | 62 +- .../discourse/lib/chat-message-interactor.js | 64 +- .../discourse/lib/textarea-interactor.js | 9 + .../chat/emoji-picker-scroll-listener.js | 23 - .../chat-channel-emoji-picker-manager.js | 3 - .../services/chat-emoji-picker-manager.js | 91 - .../services/chat-emoji-reaction-store.js | 92 - .../services/emoji-picker-scroll-observer.js | 70 - .../services/interacted-chat-message.js | 7 + .../stylesheets/common/base-common.scss | 10 - .../stylesheets/common/chat-browse.scss | 4 +- .../common/chat-composer-dropdown.scss | 30 +- .../stylesheets/common/chat-composer.scss | 1 + .../common/chat-message-actions.scss | 6 - .../stylesheets/common/chat-message.scss | 6 +- .../chat/assets/stylesheets/common/index.scss | 2 - .../desktop/chat-composer-uploads.scss | 5 - .../mobile/chat-composer-dropdown.scss | 18 - .../stylesheets/mobile/chat-emoji-picker.scss | 26 - .../chat/assets/stylesheets/mobile/index.scss | 2 - plugins/chat/config/locales/server.en.yml | 1 - plugins/chat/config/routes.rb | 2 - plugins/chat/config/settings.yml | 4 - .../chat/spec/system/chat_composer_spec.rb | 6 +- .../spec/system/page_objects/chat/browse.rb | 2 +- .../system/page_objects/chat/chat_channel.rb | 11 + .../system/page_objects/chat/chat_thread.rb | 16 +- .../chat/spec/system/react_to_message_spec.rb | 15 +- .../acceptance/chat-composer-test.js | 2 +- .../chat-composer-placeholder-test.js | 8 +- .../components/chat-emoji-picker-test.js | 307 -- .../components/dc-filter-input-test.js | 55 - .../unit/helpers/format-chat-date-test.gjs | 42 - .../unit/helpers/tonable-emoji-title-test.gjs | 41 - .../lib/chat-emoji-reaction-store-test.js | 127 - .../chat-emoji-picker-manager-test.js | 125 - .../requests/emojis_controller_spec.rb | 10 +- spec/system/emojis/emoji_deny_list_spec.rb | 2 +- .../page_objects/components/form_kit.rb | 2 +- 115 files changed, 1939 insertions(+), 6328 deletions(-) delete mode 100644 app/assets/javascripts/discourse/app/components/emoji-group-buttons.hbs delete mode 100644 app/assets/javascripts/discourse/app/components/emoji-group-buttons.js delete mode 100644 app/assets/javascripts/discourse/app/components/emoji-group-sections.hbs delete mode 100644 app/assets/javascripts/discourse/app/components/emoji-group-sections.js delete mode 100644 app/assets/javascripts/discourse/app/components/emoji-picker.hbs delete mode 100644 app/assets/javascripts/discourse/app/components/emoji-picker.js create mode 100644 app/assets/javascripts/discourse/app/components/emoji-picker/content.gjs create mode 100644 app/assets/javascripts/discourse/app/components/emoji-picker/detached.gjs create mode 100644 app/assets/javascripts/discourse/app/components/emoji-picker/diversity-menu.gjs create mode 100644 app/assets/javascripts/discourse/app/components/emoji-picker/index.gjs rename plugins/chat/assets/javascripts/discourse/components/dc-filter-input.gjs => app/assets/javascripts/discourse/app/components/filter-input.gjs (91%) delete mode 100644 app/assets/javascripts/discourse/tests/acceptance/emoji-picker-test.js create mode 100644 app/assets/javascripts/discourse/tests/fixtures/emojis-fixtures.js create mode 100644 app/assets/javascripts/discourse/tests/helpers/emoji-picker-helper.js create mode 100644 app/assets/javascripts/discourse/tests/integration/components/filter-input-test.js create mode 100644 app/assets/stylesheets/common/components/dropdown-menu.scss rename plugins/chat/assets/stylesheets/common/chat-emoji-picker.scss => app/assets/stylesheets/common/components/emoji-picker.scss (61%) rename plugins/chat/assets/stylesheets/common/dc-filter-input.scss => app/assets/stylesheets/common/components/filter-input.scss (86%) create mode 100644 app/assets/stylesheets/mobile/components/emoji-picker.scss delete mode 100644 app/assets/stylesheets/mobile/emoji.scss create mode 100644 app/controllers/emojis_controller.rb delete mode 100644 plugins/chat/app/controllers/chat/emojis_controller.rb delete mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-channel-message-emoji-picker.gjs delete mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.hbs delete mode 100644 plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js delete mode 100644 plugins/chat/assets/javascripts/discourse/connectors/below-footer/chat-channel-message-emoji-picker-connector.hbs create mode 100644 plugins/chat/assets/javascripts/discourse/connectors/chat-composer-inline-buttons/emoji-picker.hbs delete mode 100644 plugins/chat/assets/javascripts/discourse/helpers/tonable-emoji-title.js delete mode 100644 plugins/chat/assets/javascripts/discourse/helpers/tonable-emoji-url.js delete mode 100644 plugins/chat/assets/javascripts/discourse/modifiers/chat/emoji-picker-scroll-listener.js delete mode 100644 plugins/chat/assets/javascripts/discourse/services/chat-channel-emoji-picker-manager.js delete mode 100644 plugins/chat/assets/javascripts/discourse/services/chat-emoji-picker-manager.js delete mode 100644 plugins/chat/assets/javascripts/discourse/services/chat-emoji-reaction-store.js delete mode 100644 plugins/chat/assets/javascripts/discourse/services/emoji-picker-scroll-observer.js create mode 100644 plugins/chat/assets/javascripts/discourse/services/interacted-chat-message.js delete mode 100644 plugins/chat/assets/stylesheets/mobile/chat-composer-dropdown.scss delete mode 100644 plugins/chat/assets/stylesheets/mobile/chat-emoji-picker.scss delete mode 100644 plugins/chat/test/javascripts/components/chat-emoji-picker-test.js delete mode 100644 plugins/chat/test/javascripts/components/dc-filter-input-test.js delete mode 100644 plugins/chat/test/javascripts/unit/helpers/format-chat-date-test.gjs delete mode 100644 plugins/chat/test/javascripts/unit/helpers/tonable-emoji-title-test.gjs delete mode 100644 plugins/chat/test/javascripts/unit/lib/chat-emoji-reaction-store-test.js delete mode 100644 plugins/chat/test/javascripts/unit/services/chat-emoji-picker-manager-test.js rename {plugins/chat/spec => spec}/requests/emojis_controller_spec.rb (76%) diff --git a/app/assets/javascripts/admin/addon/components/emoji-value-list.hbs b/app/assets/javascripts/admin/addon/components/emoji-value-list.hbs index 8562b1cd92d..a8f1f298de7 100644 --- a/app/assets/javascripts/admin/addon/components/emoji-value-list.hbs +++ b/app/assets/javascripts/admin/addon/components/emoji-value-list.hbs @@ -43,17 +43,8 @@ {{/if}}
- -
- - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/emoji-value-list.js b/app/assets/javascripts/admin/addon/components/emoji-value-list.js index b6a8a55ce3d..37972ceb6a2 100644 --- a/app/assets/javascripts/admin/addon/components/emoji-value-list.js +++ b/app/assets/javascripts/admin/addon/components/emoji-value-list.js @@ -1,17 +1,18 @@ import Component from "@ember/component"; import { action, set, setProperties } from "@ember/object"; import { schedule } from "@ember/runloop"; +import { service } from "@ember/service"; import { classNameBindings } from "@ember-decorators/component"; +import EmojiPickerDetached from "discourse/components/emoji-picker/detached"; import { emojiUrlFor } from "discourse/lib/text"; -import discourseLater from "discourse-common/lib/later"; import discourseComputed from "discourse-common/utils/decorators"; import { i18n } from "discourse-i18n"; @classNameBindings(":value-list", ":emoji-list") export default class EmojiValueList extends Component { + @service menu; + values = null; - emojiPickerIsActive = false; - isEditorFocused = false; @discourseComputed("values") collection(values) { @@ -30,13 +31,6 @@ export default class EmojiValueList extends Component { }); } - @action - closeEmojiPicker() { - this.collection.setEach("isEditing", false); - this.set("emojiPickerIsActive", false); - this.set("isEditorFocused", false); - } - @action emojiSelected(code) { if (!this._validateInput(code)) { @@ -62,9 +56,6 @@ export default class EmojiValueList extends Component { this.collection.addObject(newCollectionValue); this._saveValues(); } - - this.set("emojiPickerIsActive", false); - this.set("isEditorFocused", false); } @discourseComputed("collection") @@ -94,8 +85,7 @@ export default class EmojiValueList extends Component { } @action - editValue(index) { - this.closeEmojiPicker(); + editValue(index, event) { schedule("afterRender", () => { if (parseInt(index, 10) >= 0) { const item = this.collection[index]; @@ -104,12 +94,18 @@ export default class EmojiValueList extends Component { } } - this.set("isEditorFocused", true); - discourseLater(() => { - if (this.element && !this.isDestroying && !this.isDestroyed) { - this.set("emojiPickerIsActive", true); - } - }, 100); + this.menu.show(event.target, { + identifier: "emoji-picker", + groupIdentifier: "emoji-picker", + component: EmojiPickerDetached, + modalForMobile: true, + data: { + context: "chat", + didSelectEmoji: (emoji) => { + this._replaceValue(index, emoji); + }, + }, + }); }); } diff --git a/app/assets/javascripts/discourse/app/components/d-editor.hbs b/app/assets/javascripts/discourse/app/components/d-editor.hbs index f509d8e73a9..27a5f94ca07 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/app/components/d-editor.hbs @@ -76,12 +76,4 @@ /> - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 5d228de9830..13a106d11e7 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -10,6 +10,7 @@ import { translations } from "pretty-text/emoji/data"; import { resolveCachedShortUrls } from "pretty-text/upload-short-url"; import { Promise } from "rsvp"; import TextareaEditor from "discourse/components/composer/textarea-editor"; +import EmojiPickerDetached from "discourse/components/emoji-picker/detached"; import InsertHyperlink from "discourse/components/modal/insert-hyperlink"; import { ajax } from "discourse/lib/ajax"; import { SKIP } from "discourse/lib/autocomplete"; @@ -27,6 +28,7 @@ import { initUserStatusHtml, renderUserStatusHtml, } from "discourse/lib/user-status-on-autocomplete"; +import virtualElementFromTextRange from "discourse/lib/virtual-element-from-text-range"; import { isTesting } from "discourse-common/config/environment"; import discourseDebounce from "discourse-common/lib/debounce"; import deprecated from "discourse-common/lib/deprecated"; @@ -53,8 +55,9 @@ export function onToolbarCreate(func) { @classNames("d-editor") export default class DEditor extends Component { - @service("emoji-store") emojiStore; + @service emojiStore; @service modal; + @service menu; editorComponent = TextareaEditor; textManipulation; @@ -62,8 +65,6 @@ export default class DEditor extends Component { ready = false; lastSel = null; showLink = true; - emojiPickerIsActive = false; - emojiFilter = ""; isEditorFocused = false; processPreview = true; morphingOptions = { @@ -347,13 +348,25 @@ export default class DEditor extends Component { transformComplete: (v) => { if (v.code) { - this.emojiStore.track(v.code); + this.emojiStore.trackEmojiForContext(v.code, "topic"); return `${v.code}:`; } else { this.textManipulation.autocomplete({ cancel: true }); - this.set("emojiPickerIsActive", true); - this.set("emojiFilter", v.term); + const menuOptions = { + identifier: "emoji-picker", + component: EmojiPickerDetached, + modalForMobile: true, + data: { + didSelectEmoji: (emoji) => { + this.textManipulation.emojiSelected(emoji); + }, + term: v.term, + }, + }; + + const virtualElement = virtualElementFromTextRange(); + this.menuInstance = this.menu.show(virtualElement, menuOptions); return ""; } }, @@ -368,9 +381,10 @@ export default class DEditor extends Component { } if (term === "") { - if (this.emojiStore.favorites.length) { + const favorites = this.emojiStore.favoritesForContext("topic"); + if (favorites.length) { return resolve( - this.emojiStore.favorites + favorites .filter((f) => !this.site.denied_emojis?.includes(f)) .slice(0, 5) ); @@ -515,13 +529,6 @@ export default class DEditor extends Component { return true; } - @action - onEmojiPickerClose() { - if (!(this.isDestroyed || this.isDestroying)) { - this.set("emojiPickerIsActive", false); - } - } - /** * Represents a toolbar event object passed to toolbar buttons. * @@ -568,15 +575,6 @@ export default class DEditor extends Component { }; } - @action - emoji() { - if (this.disabled) { - return; - } - - this.set("emojiPickerIsActive", !this.emojiPickerIsActive); - } - @action toolbarButton(button) { if (this.disabled) { diff --git a/app/assets/javascripts/discourse/app/components/emoji-group-buttons.hbs b/app/assets/javascripts/discourse/app/components/emoji-group-buttons.hbs deleted file mode 100644 index 1ba331fc4a9..00000000000 --- a/app/assets/javascripts/discourse/app/components/emoji-group-buttons.hbs +++ /dev/null @@ -1,75 +0,0 @@ -{{! DO NOT EDIT THIS FILE!!! }} -{{! Update it by running `rake javascript:update_constants` }} - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/emoji-group-buttons.js b/app/assets/javascripts/discourse/app/components/emoji-group-buttons.js deleted file mode 100644 index de1579c0b8d..00000000000 --- a/app/assets/javascripts/discourse/app/components/emoji-group-buttons.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from "@ember/component"; - -export default class EmojiGroupButtons extends Component {} diff --git a/app/assets/javascripts/discourse/app/components/emoji-group-sections.hbs b/app/assets/javascripts/discourse/app/components/emoji-group-sections.hbs deleted file mode 100644 index 0f6817f595c..00000000000 --- a/app/assets/javascripts/discourse/app/components/emoji-group-sections.hbs +++ /dev/null @@ -1,2725 +0,0 @@ -{{! DO NOT EDIT THIS FILE!!! }} -{{! Update it by running `rake javascript:update_constants` }} - -
-
- {{i18n "emoji_picker.smileys_&_emotion"}} -
-
- {{replace-emoji ":grinning:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smiley:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smile:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":grin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":laughing:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sweat_smile:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rofl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":joy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":slightly_smiling_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":upside_down_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":melting_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wink:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blush:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":innocent:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":smiling_face_with_three_hearts:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":heart_eyes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":star_struck:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kissing_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kissing:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smiling_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kissing_closed_eyes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kissing_smiling_eyes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smiling_face_with_tear:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yum:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stuck_out_tongue:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":stuck_out_tongue_winking_eye:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":crazy_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":stuck_out_tongue_closed_eyes:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":money_mouth_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hugs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":face_with_hand_over_mouth:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":face_with_open_eyes_and_hand_over_mouth:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":face_with_peeking_eye:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shushing_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":thinking:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":saluting_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zipper_mouth_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_with_raised_eyebrow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":neutral_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":expressionless:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_mouth:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dotted_line_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_in_clouds:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smirk:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":unamused:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":roll_eyes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":grimacing:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_exhaling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lying_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":relieved:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pensive:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sleepy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":drooling_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sleeping:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mask:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_with_thermometer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_with_head_bandage:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nauseated_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_vomiting:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sneezing_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hot_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cold_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":woozy_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dizzy_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_with_spiral_eyes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":exploding_head:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cowboy_hat_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":partying_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":disguised_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sunglasses:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nerd_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_with_monocle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":confused:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_with_diagonal_mouth:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":worried:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":slightly_frowning_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":frowning_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":open_mouth:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hushed:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":astonished:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flushed:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pleading_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":face_holding_back_tears:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":frowning_with_open_mouth:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":anguished:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fearful:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cold_sweat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":disappointed_relieved:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cry:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sob:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scream:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":confounded:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":persevere:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":disappointed:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sweat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":weary:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tired_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yawning_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":triumph:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rage:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":angry:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":face_with_symbols_over_mouth:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":smiling_imp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":imp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":skull:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":skull_and_crossbones:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":poop:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clown_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":japanese_ogre:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":japanese_goblin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ghost:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":alien:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":space_invader:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":robot:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smiley_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smile_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":joy_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heart_eyes_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smirk_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kissing_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scream_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crying_cat_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pouting_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":see_no_evil:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hear_no_evil:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":speak_no_evil:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kiss:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":love_letter:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cupid:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gift_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sparkling_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heartpulse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heartbeat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":revolving_hearts:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":two_hearts:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heart_decoration:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_heart_exclamation:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":broken_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heart_on_fire:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mending_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":orange_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yellow_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":green_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blue_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":purple_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":brown_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":100:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":anger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":boom:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dizzy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sweat_drops:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dash:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hole:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bomb:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":speech_balloon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eye_in_speech_bubble:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":left_speech_bubble:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":right_anger_bubble:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":thought_balloon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zzz:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.people_&_body"}} -
-
- {{replace-emoji ":wave:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":raised_back_of_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":raised_hand_with_fingers_splayed:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":raised_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":vulcan_salute:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":rightwards_hand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":leftwards_hand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":palm_down_hand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":palm_up_hand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":ok_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":pinched_fingers:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":pinching_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":v:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":crossed_fingers:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":hand_with_index_finger_and_thumb_crossed:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":love_you_gesture:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":metal:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":call_me_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":point_left:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":point_right:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":point_up_2:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":fu:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":point_down:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":point_up:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":index_pointing_at_the_viewer:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":+1:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":-1:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":fist:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":facepunch:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":fist_left:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":fist_right:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":clap:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":raised_hands:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":heart_hands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":open_hands:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":palms_up_together:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":handshake:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":pray:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":writing_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":nail_care:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":selfie:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":muscle:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":mechanical_arm:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mechanical_leg:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":leg:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":foot:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":ear:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":hear_with_hearing_aid:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":nose:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":brain:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":anatomical_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lungs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tooth:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eyes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eye:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tongue:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lips:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":biting_lip:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":baby:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":child:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":boy:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":girl:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":adult:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":blonde_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":man:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":bearded_person:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":man_beard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":woman_beard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":man_red_haired:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_curly_haired:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_white_haired:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_bald:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":woman:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":woman_red_haired:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_red_hair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_curly_haired:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_curly_hair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_white_haired:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_white_hair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_bald:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_bald:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":blonde_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_blond_hair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":older_adult:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":older_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":older_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_frowning:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":frowning_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":frowning_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_pouting:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":pouting_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":pouting_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_gesturing_no:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":no_good_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":no_good_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_gesturing_ok:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":ok_man:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":ok_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_tipping_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":tipping_hand_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":tipping_hand_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_raising_hand:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":raising_hand_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":raising_hand_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":deaf_person:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":deaf_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":deaf_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":bowing_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_bowing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":bowing_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_facepalming:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_facepalming:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_facepalming:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_shrugging:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_shrugging:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_shrugging:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":health_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_health_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_health_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":student:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_student:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_student:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":teacher:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_teacher:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_teacher:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":judge:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_judge:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_judge:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":farmer:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_farmer:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_farmer:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":cook:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_cook:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_cook:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":mechanic:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_mechanic:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_mechanic:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":factory_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_factory_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_factory_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":office_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_office_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_office_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":scientist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_scientist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_scientist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":technologist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_technologist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_technologist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":singer:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_singer:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_singer:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":artist:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_artist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_artist:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":pilot:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_pilot:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_pilot:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":astronaut:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_astronaut:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_astronaut:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":firefighter:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_firefighter:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_firefighter:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":policeman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_police_officer:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":policewoman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":male_detective:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_detective:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":female_detective:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":guardsman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_guard:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":guardswoman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":ninja:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":construction_worker_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_construction_worker:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":construction_worker_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":person_with_crown:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":prince:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":princess:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_with_turban:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_wearing_turban:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_with_turban:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_with_gua_pi_mao:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_with_headscarf:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_in_tuxedo:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_in_tuxedo:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_in_tuxedo:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":bride_with_veil:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_with_veil:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_with_veil:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":pregnant_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":pregnant_man:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pregnant_person:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":breast_feeding:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_feeding_baby:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_feeding_baby:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_feeding_baby:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":angel:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji ":santa:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":mrs_claus:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":mx_claus:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":superhero:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_superhero:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_superhero:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":supervillain:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_supervillain:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_supervillain:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":mage:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_mage:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_mage:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":fairy:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_fairy:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_fairy:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":vampire:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_vampire:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_vampire:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":merperson:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":merman:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":mermaid:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":elf:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_elf:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_elf:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":genie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":man_genie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":woman_genie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zombie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":man_zombie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":woman_zombie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":troll:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":person_getting_massage:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":massage_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":massage_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_getting_haircut:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":haircut_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":haircut_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":walking_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_walking:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":walking_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_standing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_standing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_standing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_kneeling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_kneeling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_kneeling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_with_white_cane:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_with_probing_cane:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_with_probing_cane:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_in_motorized_wheelchair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_in_motorized_wheelchair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_in_motorized_wheelchair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_in_manual_wheelchair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_in_manual_wheelchair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_in_manual_wheelchair:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":running_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_running:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":running_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":dancer:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":man_dancing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":business_suit_levitating:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":dancing_women:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dancing_men:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":women_with_bunny_ears:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":person_in_steamy_room:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_in_steamy_room:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_in_steamy_room:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_climbing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_climbing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_climbing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":person_fencing:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":horse_racing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":skier:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":snowboarder:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":golfing_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_golfing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":golfing_woman:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":surfing_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_surfing:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":surfing_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":rowing_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_rowing_boat:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":rowing_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":swimming_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_swimming:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":swimming_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":basketball_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_bouncing_ball:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":basketball_woman:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":weight_lifting_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_lifting_weights:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":weight_lifting_woman:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":biking_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_biking:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":biking_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":mountain_biking_man:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_mountain_biking:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":mountain_biking_woman:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_cartwheeling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_cartwheeling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_cartwheeling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":people_wrestling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":men_wrestling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":women_wrestling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":person_playing_water_polo:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_playing_water_polo:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_playing_water_polo:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_playing_handball:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_playing_handball:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_playing_handball:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_juggling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_juggling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_juggling:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":person_in_lotus_position:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":man_in_lotus_position:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":woman_in_lotus_position:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":bath:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":sleeping_bed:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":people_holding_hands:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji - ":two_women_holding_hands:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":couple:" (hash lazy=true class="diversity" tabIndex="0")}} - {{replace-emoji - ":two_men_holding_hands:" - (hash lazy=true class="diversity" tabIndex="0") - }} - {{replace-emoji ":couplekiss_man_woman:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kiss_woman_man:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":couplekiss_man_man:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":couplekiss_woman_woman:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":couple_with_heart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":couple_with_heart_woman_man:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":couple_with_heart_man_man:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":couple_with_heart_woman_woman:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":family:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_woman_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_woman_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":family_man_woman_girl_boy:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":family_man_woman_boy_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":family_man_woman_girl_girl:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":family_man_man_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_man_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_man_girl_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_man_boy_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_man_girl_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_woman_woman_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_woman_woman_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":family_woman_woman_girl_boy:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":family_woman_woman_boy_boy:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":family_woman_woman_girl_girl:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":family_man_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_boy_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_girl_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_man_girl_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_woman_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_woman_boy_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_woman_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_woman_girl_boy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":family_woman_girl_girl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":speaking_head:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bust_in_silhouette:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":busts_in_silhouette:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":people_hugging:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":footprints:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.animals_&_nature"}} -
-
- {{replace-emoji ":monkey_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":monkey:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gorilla:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":orangutan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dog:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dog2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guide_dog:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":service_dog:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":poodle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wolf:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fox_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":raccoon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cat2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_cat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lion:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tiger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tiger2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":leopard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":horse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":racehorse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":unicorn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zebra:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":deer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bison:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ox:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":water_buffalo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cow2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pig:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pig2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":boar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pig_nose:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ram:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sheep:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":goat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dromedary_camel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":camel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":llama:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":giraffe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":elephant:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mammoth:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rhinoceros:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hippopotamus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mouse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mouse2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hamster:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rabbit:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rabbit2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chipmunk:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beaver:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hedgehog:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bear:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":polar_bear:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":koala:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":panda_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sloth:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":otter:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":skunk:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kangaroo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":badger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":paw_prints:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":turkey:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chicken:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rooster:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hatching_chick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":baby_chick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hatched_chick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bird:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":penguin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dove:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eagle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":duck:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":swan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":owl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dodo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":feather:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flamingo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":peacock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":parrot:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":frog:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crocodile:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":turtle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lizard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":snake:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dragon_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dragon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sauropod:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":t_rex:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":whale:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":whale2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dolphin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":seal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fish:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tropical_fish:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blowfish:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shark:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":octopus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shell:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":coral:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":snail:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":butterfly:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bug:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ant:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":honeybee:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beetle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lady_beetle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cricket:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cockroach:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":spider:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":spider_web:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scorpion:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mosquito:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fly:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":worm:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":microbe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bouquet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cherry_blossom:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_flower:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lotus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rosette:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rose:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wilted_flower:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hibiscus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sunflower:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blossom:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tulip:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":seedling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":potted_plant:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":evergreen_tree:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":deciduous_tree:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":palm_tree:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cactus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ear_of_rice:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":herb:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shamrock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":four_leaf_clover:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":maple_leaf:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fallen_leaf:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":leaves:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":empty_nest:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nest_with_eggs:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.food_&_drink"}} -
-
- {{replace-emoji ":grapes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":melon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":watermelon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tangerine:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lemon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":banana:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pineapple:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mango:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":apple:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":green_apple:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pear:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":peach:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cherries:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":strawberry:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blueberries:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kiwi_fruit:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tomato:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":olive:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":coconut:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":avocado:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eggplant:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":potato:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":carrot:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":corn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hot_pepper:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bell_pepper:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cucumber:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":leafy_green:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":broccoli:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":garlic:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":onion:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mushroom:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":peanuts:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beans:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chestnut:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bread:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":croissant:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":baguette_bread:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flatbread:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pretzel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bagel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pancakes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":waffle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cheese:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":meat_on_bone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":poultry_leg:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cut_of_meat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bacon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hamburger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fries:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pizza:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hotdog:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sandwich:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":taco:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":burrito:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tamale:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stuffed_flatbread:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":falafel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":egg:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fried_egg:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shallow_pan_of_food:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stew:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fondue:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bowl_with_spoon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":green_salad:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":popcorn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":butter:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":salt:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":canned_food:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bento:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rice_cracker:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rice_ball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rice:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":curry:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ramen:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":spaghetti:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sweet_potato:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oden:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sushi:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fried_shrimp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fish_cake:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":moon_cake:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dango:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dumpling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fortune_cookie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":takeout_box:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crab:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lobster:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shrimp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":squid:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oyster:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":icecream:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shaved_ice:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ice_cream:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":doughnut:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cookie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":birthday:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cake:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cupcake:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chocolate_bar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":candy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lollipop:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":custard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":honey_pot:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":baby_bottle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":milk_glass:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":coffee:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":teapot:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tea:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sake:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":champagne:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wine_glass:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cocktail:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tropical_drink:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beers:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clinking_glasses:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tumbler_glass:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pouring_liquid:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cup_with_straw:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bubble_tea:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beverage_box:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":maté:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ice_cube:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chopsticks:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":plate_with_cutlery:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fork_and_knife:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":spoon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hocho:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":jar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":amphora:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.travel_&_places"}} -
-
- {{replace-emoji ":earth_africa:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":earth_americas:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":earth_asia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":globe_with_meridians:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":world_map:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":japan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":compass:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mountain_snow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mountain:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":volcano:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mount_fuji:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":camping:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beach_umbrella:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":desert:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":desert_island:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":national_park:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stadium:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":classical_building:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":building_construction:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":brick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wood:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hut:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":houses:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":derelict_house:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":house:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":house_with_garden:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":office:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":post_office:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":european_post_office:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hospital:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bank:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hotel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":love_hotel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":convenience_store:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":school:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":department_store:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":factory:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":japanese_castle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":european_castle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wedding:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tokyo_tower:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":statue_of_liberty:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":church:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mosque:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hindu_temple:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":synagogue:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shinto_shrine:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kaaba:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fountain:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tent:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":foggy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":night_with_stars:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cityscape:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sunrise_over_mountains:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sunrise:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":city_sunset:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":city_sunrise:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bridge_at_night:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hotsprings:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":carousel_horse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":playground_slide:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ferris_wheel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":roller_coaster:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":barber:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":circus_tent:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":steam_locomotive:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":railway_car:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bullettrain_side:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bullettrain_front:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":train2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":metro:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":light_rail:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":station:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tram:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":monorail:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mountain_railway:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":train:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oncoming_bus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":trolleybus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":minibus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ambulance:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fire_engine:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":police_car:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oncoming_police_car:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":taxi:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oncoming_taxi:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":red_car:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oncoming_automobile:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blue_car:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pickup_truck:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":truck:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":articulated_lorry:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tractor:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":racing_car:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":motorcycle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":motor_scooter:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":manual_wheelchair:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":motorized_wheelchair:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":auto_rickshaw:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bike:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kick_scooter:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":skateboard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":roller_skate:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":busstop:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":motorway:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":railway_track:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oil_drum:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fuelpump:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wheel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rotating_light:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":traffic_light:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":vertical_traffic_light:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stop_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":construction:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":anchor:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ring_buoy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sailboat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":canoe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":speedboat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":passenger_ship:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ferry:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":motor_boat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ship:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":airplane:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":small_airplane:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flight_departure:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flight_arrival:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":parachute:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":seat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":helicopter:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":suspension_railway:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mountain_cableway:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":aerial_tramway:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":artificial_satellite:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rocket:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flying_saucer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bellhop_bell:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":luggage:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hourglass:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hourglass_flowing_sand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":watch:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":alarm_clock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stopwatch:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":timer_clock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mantelpiece_clock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock12:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock1230:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock1:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock130:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock230:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock3:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock330:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock4:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock430:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock5:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock530:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock6:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock630:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock7:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock730:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock8:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock830:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock9:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock930:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock10:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock1030:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock11:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clock1130:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":new_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":waxing_crescent_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":first_quarter_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":waxing_gibbous_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":full_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":waning_gibbous_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":last_quarter_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":waning_crescent_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crescent_moon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":new_moon_with_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":first_quarter_moon_with_face:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":last_quarter_moon_with_face:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":thermometer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sunny:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":full_moon_with_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sun_with_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ringer_planet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":star:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":star2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stars:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":milky_way:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cloud:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":partly_sunny:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":cloud_with_lightning_and_rain:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":sun_behind_small_cloud:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sun_behind_large_cloud:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sun_behind_rain_cloud:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cloud_with_rain:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cloud_with_snow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cloud_with_lightning:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tornado:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fog:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wind_face:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cyclone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rainbow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":closed_umbrella:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":open_umbrella:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":umbrella:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":parasol_on_ground:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zap:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":snowflake:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":snowman_with_snow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":snowman:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":comet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fire:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":droplet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ocean:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.activities"}} -
-
- {{replace-emoji ":jack_o_lantern:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":christmas_tree:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fireworks:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sparkler:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":firecracker:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sparkles:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":balloon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tada:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":confetti_ball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tanabata_tree:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bamboo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dolls:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flags:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wind_chime:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rice_scene:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":red_gift_envelope:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ribbon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gift:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":reminder_ribbon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tickets:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ticket:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":medal_military:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":trophy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":medal_sports:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":1st_place_medal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":2nd_place_medal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":3rd_place_medal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":soccer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":baseball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":softball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":basketball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":volleyball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":football:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rugby_football:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tennis:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flying_disc:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bowling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cricket_bat_and_ball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":field_hockey:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ice_hockey:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lacrosse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ping_pong:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":badminton:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":boxing_glove:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":martial_arts_uniform:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":goal_net:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":golf:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ice_skate:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fishing_pole_and_fish:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":diving_mask:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":running_shirt_with_sash:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ski:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sled:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":curling_stone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yo-yo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kite:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":8ball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crystal_ball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":magic_wand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nazar_amulet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hamsa:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":video_game:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":joystick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":slot_machine:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":game_die:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":jigsaw:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":teddy_bear:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":piñata:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mirror_ball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nesting_dolls:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":spades:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hearts:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":diamonds:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clubs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chess_pawn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_joker:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mahjong:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flower_playing_cards:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":performing_arts:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":framed_picture:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":art:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":thread:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sewing_needle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yarn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":knot:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.objects"}} -
-
- {{replace-emoji ":eyeglasses:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dark_sunglasses:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":goggles:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lab_coat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":safety_vest:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":necktie:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tshirt:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":jeans:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scarf:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gloves:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":coat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":socks:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dress:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kimono:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sari:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":one_piece_swimsuit:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":briefs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shorts:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bikini:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":womans_clothes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":purse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":handbag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pouch:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shopping:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":school_satchel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":thong_sandal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mans_shoe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":athletic_shoe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hiking_boot:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flat_shoe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":high_heel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sandal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ballet_shoes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":boot:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crown:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":womans_hat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tophat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mortar_board:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":billed_cap:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":military_helmet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rescue_worker_helmet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":prayer_beads:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lipstick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ring:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gem:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mute:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":speaker:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sound:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":loud_sound:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":loudspeaker:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mega:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":postal_horn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bell:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_bell:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":musical_score:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":musical_note:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":notes:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":studio_microphone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":level_slider:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":control_knobs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":microphone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":headphones:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":radio:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":saxophone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":accordion:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guitar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":musical_keyboard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":trumpet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":violin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":banjo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":drum:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":long_drum:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":iphone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":calling:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":phone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":telephone_receiver:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pager:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fax:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":battery:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":low_battery:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":electric_plug:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":computer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":desktop_computer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":printer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":keyboard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":computer_mouse:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":trackball:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":minidisc:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":floppy_disk:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cd:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dvd:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":abacus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":movie_camera:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":film_strip:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":film_projector:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clapper:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tv:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":camera:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":camera_flash:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":video_camera:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":vhs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mag_right:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":candle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bulb:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":flashlight:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":izakaya_lantern:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":diya_lamp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":notebook_with_decorative_cover:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":closed_book:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":open_book:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":green_book:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blue_book:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":orange_book:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":books:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":notebook:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ledger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":page_with_curl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scroll:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":page_facing_up:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":newspaper:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":newspaper_roll:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bookmark_tabs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bookmark:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":label:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":moneybag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":coin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yen:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dollar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":euro:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pound:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":money_with_wings:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":credit_card:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":receipt:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":email:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":e-mail:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":incoming_envelope:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":envelope_with_arrow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":outbox_tray:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":inbox_tray:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":package:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mailbox:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mailbox_closed:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mailbox_with_mail:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mailbox_with_no_mail:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":postbox:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ballot_box:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pencil2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_nib:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fountain_pen:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pen:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":paintbrush:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crayon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":memo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":briefcase:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":file_folder:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":open_file_folder:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":card_index_dividers:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":date:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":calendar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":spiral_notepad:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":spiral_calendar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":card_index:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chart_with_upwards_trend:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":chart_with_downwards_trend:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":bar_chart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clipboard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pushpin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":round_pushpin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":paperclip:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":paperclips:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":straight_ruler:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":triangular_ruler:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scissors:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":card_file_box:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":file_cabinet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wastebasket:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":unlock:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lock_with_ink_pen:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":closed_lock_with_key:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":key:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":old_key:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hammer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":axe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hammer_and_pick:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hammer_and_wrench:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dagger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crossed_swords:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gun:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":boomerang:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bow_and_arrow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shield:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":carpentry_saw:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wrench:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":screwdriver:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nut_and_bolt:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gear:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clamp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":balance_scale:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":probing_cane:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":link:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chains:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hook:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":toolbox:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":magnet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ladder:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":alembic:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":test_tube:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":petri_dish:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dna:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":microscope:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":telescope:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":satellite:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":syringe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":drop_of_blood:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pill:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":adhesive_bandage:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crutch:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stethoscope:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":xray:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":door:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":elevator:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mirror:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":window:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bed:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":couch_and_lamp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chair:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":toilet:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":plunger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shower:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bathtub:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mouse_trap:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":razor:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lotion_bottle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":safety_pin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":broom:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":basket:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":roll_of_toilet_paper:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bucket:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":soap:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bubbles:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":toothbrush:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sponge:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fire_extinguisher:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":shopping_cart:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":smoking:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":coffin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":headstone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":funeral_urn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":moyai:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":placard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":identification_card:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.symbols"}} -
-
- {{replace-emoji ":atm:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":put_litter_in_its_place:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":potable_water:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wheelchair:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mens:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":womens:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":restroom:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":baby_symbol:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wc:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":passport_control:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":customs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":baggage_claim:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":left_luggage:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":warning:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":children_crossing:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_entry:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_entry_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_bicycles:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_smoking:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":do_not_litter:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":non-potable_water:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_pedestrians:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":no_mobile_phones:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":underage:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":radioactive:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":biohazard:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_up:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_upper_right:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_right:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_lower_right:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_down:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_lower_left:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_left:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_upper_left:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_up_down:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":left_right_arrow:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":leftwards_arrow_with_hook:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":arrow_right_hook:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_heading_up:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_heading_down:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrows_clockwise:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrows_counterclockwise:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":back:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":end:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":on:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":soon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":top:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":place_of_worship:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":atom_symbol:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":om:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":star_of_david:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wheel_of_dharma:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yin_yang:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":latin_cross:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":orthodox_cross:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":star_and_crescent:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":peace_symbol:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":menorah:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":six_pointed_star:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":aries:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":taurus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gemini:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cancer:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":leo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":virgo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":libra:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scorpius:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sagittarius:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":capricorn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":aquarius:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pisces:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ophiuchus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":twisted_rightwards_arrows:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":repeat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":repeat_one:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_forward:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fast_forward:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":next_track_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":play_or_pause_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_backward:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rewind:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":previous_track_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_up_small:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_double_up:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_down_small:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":arrow_double_down:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pause_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":stop_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":record_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eject_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cinema:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":low_brightness:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":high_brightness:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":signal_strength:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":vibration_mode:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mobile_phone_off:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":female_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":male_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":transgender_symbol:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_multiplication_x:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_plus_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_minus_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_division_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_equals_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":infinity:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bangbang:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":interrobang:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":question:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":grey_question:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":grey_exclamation:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":exclamation:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wavy_dash:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":currency_exchange:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_dollar_sign:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":medical_symbol:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":recycle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fleur_de_lis:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":trident:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":name_badge:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":beginner:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":o:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_check_mark:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ballot_box_with_check:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":heavy_check_mark:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":x:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":negative_squared_cross_mark:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":curly_loop:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":loop:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":part_alternation_mark:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eight_spoked_asterisk:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eight_pointed_black_star:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sparkle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":copyright:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":registered:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tm:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hash:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":asterisk:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zero:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":one:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":two:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":three:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":four:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":five:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":six:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":seven:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eight:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nine:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":keycap_ten:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":capital_abcd:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":abcd:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":1234:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":symbols:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":abc:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":a:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ab:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":b:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cl:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cool:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":free:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":information_source:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":id:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":m:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":new:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ng:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":o2:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ok:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":parking:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sos:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":up:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":vs:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":koko:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sa:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u6708:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u6709:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u6307:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ideograph_advantage:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u5272:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u7121:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u7981:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":accept:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u7533:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u5408:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u7a7a:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":congratulations:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":secret:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u55b6:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":u6e80:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":red_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":orange_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yellow_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":green_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":large_blue_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":purple_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":brown_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_circle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":red_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":orange_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yellow_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":green_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":blue_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":purple_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":brown_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_large_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_large_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_medium_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_medium_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":black_medium_small_square:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji - ":white_medium_small_square:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":black_small_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_small_square:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":large_orange_diamond:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":large_blue_diamond:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":small_orange_diamond:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":small_blue_diamond:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":small_red_triangle:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":small_red_triangle_down:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":diamond_shape_with_a_dot_inside:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":radio_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_square_button:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_square_button:" (hash lazy=true tabIndex="0")}} -
-
-
-
- {{i18n "emoji_picker.flags"}} -
-
- {{replace-emoji ":checkered_flag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":triangular_flag_on_post:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":crossed_flags:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":black_flag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":white_flag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rainbow_flag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":transgender_flag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pirate_flag:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ascension_island:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":andorra:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":united_arab_emirates:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":afghanistan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":antigua_barbuda:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":anguilla:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":albania:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":armenia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":angola:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":antarctica:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":argentina:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":american_samoa:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":austria:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":australia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":aruba:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":aland_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":azerbaijan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bosnia_herzegovina:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":barbados:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bangladesh:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":belgium:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":burkina_faso:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bulgaria:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bahrain:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":burundi:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":benin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":st_barthelemy:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bermuda:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":brunei:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bolivia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":caribbean_netherlands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":brazil:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bahamas:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bhutan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":bouvet_island:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":botswana:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":belarus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":belize:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":canada:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cocos_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":congo_kinshasa:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":central_african_republic:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":congo_brazzaville:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":switzerland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cote_divoire:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cook_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chile:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cameroon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cn:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":colombia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":clipperton_island:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":costa_rica:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cuba:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cape_verde:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":curacao:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":christmas_island:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cyprus:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":czech_republic:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":de:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":diego_garcia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":djibouti:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":denmark:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dominica:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":dominican_republic:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":algeria:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ceuta_and_melilla:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ecuador:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":estonia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":egypt:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":western_sahara:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eritrea:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":es:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ethiopia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":eu:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":finland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fiji:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":falkland_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":micronesia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":faroe_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":fr:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gabon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":uk:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":grenada:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":georgia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":french_guiana:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guernsey:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ghana:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gibraltar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":greenland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":gambia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guinea:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guadeloupe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":equatorial_guinea:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":greece:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":south_georgia_south_sandwich_islands:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":guatemala:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guam:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guinea_bissau:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":guyana:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hong_kong:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":heard_and_mc_donald_islands:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":honduras:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":croatia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":haiti:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":hungary:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":canary_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":indonesia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ireland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":israel:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":isle_of_man:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":india:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":british_indian_ocean_territory:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":iraq:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":iran:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":iceland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":it:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":jersey:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":jamaica:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":jordan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":jp:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kenya:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kyrgyzstan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cambodia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kiribati:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":comoros:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":st_kitts_nevis:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":north_korea:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kr:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kuwait:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":cayman_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kazakhstan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":laos:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lebanon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":st_lucia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":liechtenstein:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sri_lanka:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":liberia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lesotho:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":lithuania:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":luxembourg:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":latvia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":libya:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":morocco:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":monaco:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":moldova:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":montenegro:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":st_martin:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":madagascar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":marshall_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":macedonia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mali:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":myanmar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mongolia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":macau:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":northern_mariana_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":martinique:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mauritania:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":montserrat:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":malta:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mauritius:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":maldives:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":malawi:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mexico:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":malaysia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mozambique:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":namibia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":new_caledonia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":niger:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":norfolk_island:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nigeria:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nicaragua:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":netherlands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":norway:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nepal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":nauru:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":niue:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":new_zealand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":oman:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":panama:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":peru:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":french_polynesia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":papua_new_guinea:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":philippines:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pakistan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":poland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":st_pierre_miquelon:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":pitcairn_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":puerto_rico:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":palestinian_territories:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":portugal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":palau:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":paraguay:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":qatar:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":reunion:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":romania:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":serbia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ru:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":rwanda:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":saudi_arabia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":solomon_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":seychelles:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sudan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sweden:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":singapore:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":st_helena:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":slovenia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":svalbard_and_jan_mayen:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":slovakia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sierra_leone:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":san_marino:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":senegal:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":somalia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":suriname:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":south_sudan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sao_tome_principe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":el_salvador:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":sint_maarten:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":syria:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":swaziland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tristan_da_cunha:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":turks_caicos_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":chad:" (hash lazy=true tabIndex="0")}} - {{replace-emoji - ":french_southern_territories:" - (hash lazy=true tabIndex="0") - }} - {{replace-emoji ":togo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":thailand:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tajikistan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tokelau:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":timor_leste:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":turkmenistan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tunisia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tonga:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tr:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":trinidad_tobago:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tuvalu:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":taiwan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":tanzania:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":ukraine:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":uganda:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":us_outlying_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":united_nations:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":us:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":uruguay:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":uzbekistan:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":vatican_city:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":st_vincent_grenadines:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":venezuela:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":british_virgin_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":us_virgin_islands:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":vietnam:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":vanuatu:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wallis_futuna:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":samoa:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":kosovo:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":yemen:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":mayotte:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":south_africa:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zambia:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":zimbabwe:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":england:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":scotland:" (hash lazy=true tabIndex="0")}} - {{replace-emoji ":wales:" (hash lazy=true tabIndex="0")}} -
-
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/emoji-group-sections.js b/app/assets/javascripts/discourse/app/components/emoji-group-sections.js deleted file mode 100644 index 0951a47984f..00000000000 --- a/app/assets/javascripts/discourse/app/components/emoji-group-sections.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from "@ember/component"; - -export default class EmojiGroupSections extends Component {} diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.hbs b/app/assets/javascripts/discourse/app/components/emoji-picker.hbs deleted file mode 100644 index 23e275e6089..00000000000 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.hbs +++ /dev/null @@ -1,154 +0,0 @@ -{{#if this.isActive}} - {{! template-lint-disable no-invalid-interactive no-pointer-down-event-binding }} -
- {{! template-lint-enable no-invalid-interactive no-pointer-down-event-binding }} -
- {{#if this.recentEmojis.length}} - - {{/if}} - - - - {{#each-in this.customEmojis as |group emojis|}} - - {{/each-in}} -
- -
-
- - - {{d-icon "magnifying-glass"}} -
- -
-
- -
- {{#if this.recentEmojis.length}} -
-
- {{i18n "emoji_picker.recent"}} - -
-
- {{#each this.recentEmojis as |emoji|}} - {{replace-emoji - (concat ":" emoji ":") - (hash lazy=true tabIndex="0" class="recent-emoji") - }} - {{/each}} -
-
- {{/if}} - - - - {{#each-in this.customEmojis as |group emojis|}} -
-
- - {{i18n - (concat "emoji_picker." group) - translatedFallback=group - }} - -
- {{#if emojis.length}} -
- {{#each emojis as |emoji|}} - - - - {{/each}} -
- {{/if}} -
- {{/each-in}} -
-
- - -
-
- - {{#if this.site.mobileView}} -
- {{/if}} -{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js deleted file mode 100644 index 8d234b83598..00000000000 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ /dev/null @@ -1,467 +0,0 @@ -import Component from "@ember/component"; -import { action, computed } from "@ember/object"; -import { schedule } from "@ember/runloop"; -import { service } from "@ember/service"; -import { underscore } from "@ember/string"; -import { htmlSafe } from "@ember/template"; -import { tagName } from "@ember-decorators/component"; -import { observes } from "@ember-decorators/object"; -import { createPopper } from "@popperjs/core"; -import { - emojiSearch, - extendedEmojiList, - isSkinTonableEmoji, -} from "pretty-text/emoji"; -import { emojiUnescape, emojiUrlFor } from "discourse/lib/text"; -import { escapeExpression } from "discourse/lib/utilities"; -import discourseLater from "discourse-common/lib/later"; -import discourseComputed, { bind } from "discourse-common/utils/decorators"; - -function customEmojis() { - const groups = []; - for (const [code, emoji] of extendedEmojiList()) { - groups[emoji.group] ||= []; - groups[emoji.group].push({ code, src: emojiUrlFor(code) }); - } - return groups; -} - -@tagName("") -export default class EmojiPicker extends Component { - @service emojiStore; - - customEmojis = customEmojis(); - recentEmojis = null; - hoveredEmoji = null; - isActive = false; - usePopper = true; - placement = "auto"; // one of popper.js' placements, see https://popper.js.org/docs/v2/constructors/#options - initialFilter = ""; - - elements = { - searchInput: ".emoji-picker-search-container input", - picker: ".emoji-picker-emoji-area", - }; - - didInsertElement() { - super.didInsertElement(...arguments); - this._sectionObserver = this._setupSectionObserver(); - this.appEvents.on("emoji-picker:close", this, "onClose"); - } - - // `readOnly` may seem like a better choice here, but the computed property - // provides caching (emojiStore.diversity is a simple getter) - @discourseComputed("emojiStore.diversity") - selectedDiversity(diversity) { - return diversity; - } - - // didReceiveAttrs would be a better choice here, but this is sadly causing - // too many unexpected reloads as it's triggered for other reasons than a mutation - // of isActive - @observes("isActive") - _setup() { - if (this.isActive) { - this.onShow(); - } else { - this.onClose(); - } - } - - willDestroyElement() { - super.willDestroyElement(...arguments); - this._sectionObserver?.disconnect(); - this.appEvents.off("emoji-picker:close", this, "onClose"); - } - - @action - onShow() { - this.set("recentEmojis", this.emojiStore.favorites); - - schedule("afterRender", () => { - this._applyFilter(this.initialFilter); - - const emojiPicker = document.querySelector(".emoji-picker"); - if (!emojiPicker) { - return; - } - - document.addEventListener("click", this.handleOutsideClick); - - const popperAnchor = this._getPopperAnchor(); - - if (!this.site.isMobileDevice && this.usePopper && popperAnchor) { - const modifiers = [ - { - name: "preventOverflow", - }, - { - name: "offset", - options: { - offset: [5, 5], - }, - }, - ]; - - if ( - this.placement === "auto" && - window.innerWidth < popperAnchor.clientWidth * 2 - ) { - modifiers.push({ - name: "computeStyles", - enabled: true, - fn({ state }) { - state.styles.popper = { - ...state.styles.popper, - position: "fixed", - left: `${(window.innerWidth - state.rects.popper.width) / 2}px`, - top: "50%", - transform: "translateY(-50%)", - }; - - return state; - }, - }); - } - - this._popper = createPopper(popperAnchor, emojiPicker, { - placement: this.placement, - }); - } - - // this is a low-tech trick to prevent appending hundreds of emojis - // of blocking the rendering of the picker - discourseLater(() => { - schedule("afterRender", () => { - if (!this.site.isMobileDevice || this.isEditorFocused) { - emojiPicker.querySelector("input.filter")?.focus(); - - if (this._sectionObserver) { - emojiPicker - .querySelectorAll(".emojis-container .section .section-header") - .forEach((p) => this._sectionObserver.observe(p)); - } - } - - if (this.selectedDiversity !== 0) { - this._applyDiversity(this.selectedDiversity); - } - }); - }, 50); - }); - } - - @action - onClose(event) { - event?.stopPropagation(); - document.removeEventListener("click", this.handleOutsideClick); - this.onEmojiPickerClose?.(event); - } - - @computed("selectedDiversity") - get diversityScales() { - return [ - "default", - "light", - "medium-light", - "medium", - "medium-dark", - "dark", - ].map((name, index) => { - return { - name, - title: `emoji_picker.${underscore(name)}_tone`, - icon: index + 1 === this.selectedDiversity ? "check" : "", - }; - }); - } - - @action - onClearRecent() { - this.emojiStore.favorites = []; - this.set("recentEmojis", []); - } - - @action - onDiversitySelection(index) { - const scale = index + 1; - this.emojiStore.diversity = scale; - - this._applyDiversity(scale); - } - - @action - onEmojiHover(event) { - const img = event.target; - if (!img.classList.contains("emoji") || img.tagName !== "IMG") { - return false; - } - - this._updateEmojiPreview(event.target.title); - } - - @action - onEmojiSelection(event) { - const img = event.target; - - if (!img.classList.contains("emoji") || img.tagName !== "IMG") { - return false; - } - - let code = event.target.title; - code = this._codeWithDiversity(code, this.selectedDiversity); - - this.emojiSelected(code); - - this._trackEmojiUsage(code, { - refresh: !img.parentNode.parentNode.classList.contains("recent"), - }); - - if (this.site.isMobileDevice) { - this.onClose(event); - } - } - - @action - onCategorySelection(sectionName, event) { - event?.preventDefault(); - document - .querySelector( - `.emoji-picker-emoji-area .section[data-section="${sectionName}"]` - ) - ?.scrollIntoView(); - } - - @action - keydown(event) { - const arrowKeys = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"]; - const emojis = document.querySelectorAll(".emoji-picker-emoji-area .emoji"); - - let currentEmoji; - - if ( - event.key === "ArrowDown" && - this._focusedOn(this.elements.searchInput) - ) { - this._updateEmojiPreview(emojis[0].title); - emojis[0].focus(); - event.preventDefault(); - return false; - } - - if (event.key === "Escape") { - this.onClose(event); - const path = event.path || event.composedPath?.(); - - const fromChatComposer = path.find((e) => - e?.classList?.contains("chat-composer-container") - ); - - const fromTopicComposer = path.find((e) => - e?.classList?.contains("d-editor") - ); - - if (fromTopicComposer) { - document.querySelector(".d-editor-input")?.focus(); - } else if (fromChatComposer) { - document.querySelector(".chat-composer__input")?.focus(); - } else { - document.querySelector("textarea")?.focus(); - } - - return false; - } - - if (arrowKeys.includes(event.key)) { - if (!this._focusedOn(this.elements.picker)) { - return; - } - - Array.from(emojis).find((e, index) => { - currentEmoji = index; - return e.isEqualNode(event.target); - }); - - if (event.key === "ArrowRight") { - let nextEmoji = currentEmoji + 1; - - if (nextEmoji < emojis.length) { - this._updateEmojiPreview(emojis[nextEmoji].title); - emojis[nextEmoji].focus(); - } else if (nextEmoji >= emojis.length) { - this._updateEmojiPreview(emojis[0].title); - emojis[0].focus(); - } - } - - if (event.key === "ArrowLeft") { - const previousEmoji = currentEmoji - 1; - if (currentEmoji > 0) { - this._updateEmojiPreview(emojis[previousEmoji].title); - emojis[previousEmoji].focus(); - } - } - - const active = emojis[currentEmoji]; - - if (event.key === "ArrowDown") { - // source: https://stackoverflow.com/a/49090383/349424 - // look for same element type with - // - higher offsetTop - // - same offsetLeft - const emojiBelow = [...emojis] - .filter((c) => c.offsetTop > active.offsetTop) - .find((c) => c.offsetLeft === active.offsetLeft); - if (emojiBelow) { - this._updateEmojiPreview(emojiBelow.title); - emojiBelow.focus(); - } - } - - if (event.key === "ArrowUp") { - // look for same element type with - // - lower offsetTop - // - same offsetLeft - const emojiAbove = [...emojis] - .reverse() - .filter((c) => c.offsetTop < active.offsetTop) - .find((c) => c.offsetLeft === active.offsetLeft); - - if (emojiAbove) { - this._updateEmojiPreview(emojiAbove.title); - emojiAbove.focus(); - } else { - this.set("hoveredEmoji", null); - document.querySelector(this.elements.searchInput).focus(); - } - } - - event.preventDefault(); - return false; - } - - if (event.key === "Enter") { - if (!this._focusedOn(".emoji")) { - return; - } - this.onEmojiSelection(event); - this.onClose(event); - event.preventDefault(); - return false; - } - } - - @action - onFilterChange(event) { - this._applyFilter(event.target.value); - } - - _focusedOn(item) { - // returns the item currently being focused on - return document.activeElement.closest(item) ? document.activeElement : null; - } - - _applyFilter(filter) { - const emojiPicker = document.querySelector(".emoji-picker"); - const results = document.querySelector(".emoji-picker-emoji-area .results"); - results.innerHTML = ""; - - if (filter) { - results.innerHTML = emojiSearch(filter.toLowerCase(), { - diversity: this.emojiStore.diversity, - exclude: this.site.denied_emojis, - }) - .map(this._replaceEmoji) - .join(""); - - emojiPicker.classList.add("has-filter"); - results.scrollIntoView(); - } else { - emojiPicker.classList.remove("has-filter"); - } - } - - _trackEmojiUsage(code, options = {}) { - this.emojiStore.track(code); - - if (options.refresh) { - this.set("recentEmojis", [...this.emojiStore.favorites]); - } - } - - _replaceEmoji(code) { - const escaped = emojiUnescape(`:${escapeExpression(code)}:`, { - lazy: true, - tabIndex: "0", - }); - return htmlSafe(escaped); - } - - _codeWithDiversity(code, selectedDiversity) { - if (/:t\d/.test(code)) { - return code; - } else if (selectedDiversity > 1 && isSkinTonableEmoji(code)) { - return `${code}:t${selectedDiversity}`; - } else { - return code; - } - } - - _applyDiversity(diversity) { - const emojiPickerArea = document.querySelector(".emoji-picker-emoji-area"); - emojiPickerArea?.querySelectorAll(".emoji.diversity").forEach((img) => { - img.src = emojiUrlFor(this._codeWithDiversity(img.title, diversity)); - }); - } - - _setupSectionObserver() { - return new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const sectionName = entry.target.parentNode.dataset.section; - const categoryButtons = document.querySelector( - ".emoji-picker .emoji-picker-category-buttons" - ); - - if (!categoryButtons) { - return; - } - - categoryButtons - .querySelectorAll(".category-button") - .forEach((b) => b.classList.remove("current")); - - categoryButtons - .querySelector(`.category-button[data-section="${sectionName}"]`) - ?.classList?.add("current"); - } - }); - }, - { threshold: 1 } - ); - } - - _getPopperAnchor() { - // .d-editor-textarea-wrapper is only for backward compatibility here - // in new code use .emoji-picker-anchor - return ( - document.querySelector(".emoji-picker-anchor") ?? - document.querySelector(".d-editor-textarea-wrapper") - ); - } - - _updateEmojiPreview(title) { - return this.set( - "hoveredEmoji", - this._codeWithDiversity(title, this.selectedDiversity) - ); - } - - @bind - handleOutsideClick(event) { - if (!event.target.closest(".emoji-picker")) { - this.onClose(event); - } - } -} diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker/content.gjs b/app/assets/javascripts/discourse/app/components/emoji-picker/content.gjs new file mode 100644 index 00000000000..6d81cec5b49 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/emoji-picker/content.gjs @@ -0,0 +1,620 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { concat, fn, hash } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action, get } from "@ember/object"; +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 { eq, gt, includes, notEq } from "truth-helpers"; +import DButton from "discourse/components/d-button"; +import FilterInput from "discourse/components/filter-input"; +import concatClass from "discourse/helpers/concat-class"; +import noop from "discourse/helpers/noop"; +import replaceEmoji from "discourse/helpers/replace-emoji"; +import withEventValue from "discourse/helpers/with-event-value"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { + disableBodyScroll, + enableBodyScroll, +} from "discourse/lib/body-scroll-lock"; +import { emojiUrlFor } from "discourse/lib/text"; +import { INPUT_DELAY } from "discourse-common/config/environment"; +import discourseDebounce from "discourse-common/lib/debounce"; +import { makeArray } from "discourse-common/lib/helpers"; +import { bind } from "discourse-common/utils/decorators"; +import { i18n } from "discourse-i18n"; +import DiversityMenu from "./diversity-menu"; + +const DEFAULT_VISIBLE_SECTIONS = ["favorites", "smileys_&_emotion"]; +const DEFAULT_LAST_SECTION = "favorites"; + +const tonableEmojiTitle = (emoji, diversity) => { + if (!emoji.tonable || diversity === 1) { + return `:${emoji.name}:`; + } + + return `:${emoji.name}:t${diversity}:`; +}; + +const tonableEmojiUrl = (emoji, scale) => { + if (!emoji.tonable || scale === 1) { + return emoji.url; + } + + return emoji.url.split(".png")[0] + `/${scale}.png`; +}; + +export default class EmojiPicker extends Component { + @service emojiStore; + @service capabilities; + @service site; + + @tracked filteredEmojis = null; + @tracked scrollObserverEnabled = true; + @tracked scrollDirection = "up"; + @tracked emojis = null; + @tracked visibleSections = DEFAULT_VISIBLE_SECTIONS; + @tracked lastVisibleSection = DEFAULT_LAST_SECTION; + @tracked term = this.args.term; + + prevYPosition = 0; + + scrollableNode; + + setupSectionsNavScroll = modifierFn((element) => { + disableBodyScroll(element); + + return () => { + enableBodyScroll(element); + }; + }); + + scrollListener = modifierFn((element) => { + this.scrollableNode = element; + disableBodyScroll(element); + element.addEventListener("scroll", this._handleScroll); + + return () => { + this.scrollableNode = null; + element.removeEventListener("scroll", this._handleScroll); + enableBodyScroll(element); + }; + }); + + addVisibleSections(sections) { + this.visibleSections = makeArray(this.visibleSections) + .concat(makeArray(sections)) + .uniq(); + } + + get sections() { + return !this.loading && this.emojiStore.list + ? Object.keys(this.emojiStore.list) + : []; + } + + get groups() { + const favorites = { + favorites: this.emojiStore + .favoritesForContext(this.args.context) + .filter((f) => !this.site.denied_emojis?.includes(f)) + .map((emoji) => { + return { + name: emoji, + group: "favorites", + url: emojiUrlFor(emoji), + }; + }), + }; + + return { + ...favorites, + ...this.emojiStore.list, + }; + } + + get flatEmojis() { + if (!this.emojiStore.list) { + return []; + } + + // eslint-disable-next-line no-unused-vars + let { favorites, ...rest } = this.emojiStore.list; + return Object.values(rest).flat(); + } + + @action + registerFilterInput(element) { + this.filterInput = element; + } + + @action + clearFavorites() { + this.emojiStore.resetContext(this.args.context); + } + + @action + trapKeyDownEvents(event) { + if (event.key === "ArrowUp") { + event.stopPropagation(); + } + + if (event.key === "ArrowDown" && event.target === this.filterInput) { + event.stopPropagation(); + event.preventDefault(); + + this.scrollableNode.querySelector(`.emoji[tabindex="0"]`)?.focus(); + } + } + + @action + didInputFilter(value) { + if (!value?.length) { + cancel(this.debouncedFilterHandler); + this.filteredEmojis = null; + return; + } + + this.debouncedFilterHandler = discourseDebounce( + this, + this.debouncedDidInputFilter, + value, + INPUT_DELAY + ); + } + + @action + focusFilter(target) { + target?.focus({ preventScroll: true }); + } + + 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) + ) + ); + + schedule("afterRender", () => { + if (this.scrollableNode) { + this.scrollableNode.scrollTop = 0; + } + }); + } + + @action + onSectionsKeyDown(event) { + if (event.key === "Enter") { + this.didSelectEmoji(event); + } else { + this.didNavigateSection(event); + } + } + + @action + didNavigateSection(event) { + const sectionsEmojis = (section) => [...section.querySelectorAll(".emoji")]; + const focusSectionsLastEmoji = (section) => { + const emojis = sectionsEmojis(section); + return emojis[emojis.length - 1].focus(); + }; + const focusSectionsFirstEmoji = (section) => { + sectionsEmojis(section)[0].focus(); + }; + const currentSection = event.target.closest(".emoji-picker__section"); + const focusFilter = () => { + this.filterInput?.focus(); + }; + const allEmojis = () => [ + ...document.querySelectorAll( + ".emoji-picker__section:not(.hidden) .emoji" + ), + ]; + + if (event.key === "ArrowRight") { + event.preventDefault(); + const nextEmoji = event.target.nextElementSibling; + + if (nextEmoji) { + nextEmoji.focus(); + } else { + const nextSection = currentSection.nextElementSibling; + if (nextSection) { + focusSectionsFirstEmoji(nextSection); + } + } + } + + if (event.key === "ArrowLeft") { + event.preventDefault(); + const prevEmoji = event.target.previousElementSibling; + + if (prevEmoji) { + prevEmoji.focus(); + } else { + const prevSection = currentSection.previousElementSibling; + if (prevSection) { + focusSectionsLastEmoji(prevSection); + } else { + focusFilter(); + } + } + } + + if (event.key === "ArrowDown") { + event.preventDefault(); + event.stopPropagation(); + + const nextEmoji = allEmojis() + .filter((c) => c.offsetTop > event.target.offsetTop) + .findBy("offsetLeft", event.target.offsetLeft); + + if (nextEmoji) { + nextEmoji.focus(); + } else { + // for perf reason all emojis might not be loaded at this point + // but the first one will always be + const nextSection = currentSection.nextElementSibling; + if (nextSection) { + focusSectionsFirstEmoji(nextSection); + } + } + } + + if (event.key === "ArrowUp") { + event.preventDefault(); + event.stopPropagation(); + + const prevEmoji = allEmojis() + .reverse() + .filter((c) => c.offsetTop < event.target.offsetTop) + .findBy("offsetLeft", event.target.offsetLeft); + + if (prevEmoji) { + prevEmoji.focus(); + } else { + focusFilter(); + } + } + } + + @action + async didSelectEmoji(event) { + if (!event.target.classList.contains("emoji")) { + return; + } + + if (event.type === "click" || event.key === "Enter") { + event.preventDefault(); + event.stopPropagation(); + let emoji = event.target.dataset.emoji; + const tonable = event.target.dataset.tonable; + const diversity = this.emojiStore.diversity; + if (tonable && diversity > 1) { + emoji = `${emoji}:t${diversity}`; + } + + this.emojiStore.trackEmojiForContext(emoji, this.args.context); + + this.args.didSelectEmoji?.(emoji); + + await this.args.close?.(); + } + } + + @action + didRequestSection(section) { + this.term = ""; + this.didInputFilter(null); + + // we disable scroll listener during requesting section + // to avoid it from detecting another section during scroll to requested section + this.scrollObserverEnabled = false; + this.addVisibleSections(this._getSectionsUpTo(section)); + this.lastVisibleSection = section; + + // iOS hack to avoid blank div when requesting section during momentum + if (this.scrollableNode && this.capabilities.isIOS) { + this.scrollableNode.style.overflow = "hidden"; + } + + next(() => { + schedule("afterRender", () => { + const targetEmoji = document.querySelector( + `.emoji-picker__section[data-section="${section}"]` + ); + targetEmoji.scrollIntoView({ block: "start" }); + + // iOS hack to avoid blank div when requesting section during momentum + if (this.scrollableNode && this.capabilities.isIOS) { + this.scrollableNode.style.overflow = "scroll"; + } + + this.scrollObserverEnabled = true; + }); + }); + } + + @action + async loadEmojis() { + if (this.emojiStore.list) { + this.didInputFilter(this.term); + return; + } + + this.loading = true; + + try { + this.emojiStore.list = await ajax("/emojis.json"); + + // we cant filter an empty list so have to wait for it + this.didInputFilter(this.term); + } catch (error) { + popupAjaxError(error); + } finally { + this.loading = false; + } + } + + @bind + _handleScroll(event) { + if (!this.scrollObserverEnabled) { + return; + } + + this._setScrollDirection(event.target); + + const visibleSections = [ + ...document.querySelectorAll(".emoji-picker__section"), + ].filter((sectionElement) => + this._isSectionVisibleInPicker(sectionElement, event.target) + ); + + if (visibleSections?.length) { + let sectionElement; + + if (this.scrollDirection === "up" || this.prevYPosition < 50) { + sectionElement = visibleSections.firstObject; + } else { + sectionElement = visibleSections.lastObject; + } + + this.lastVisibleSection = sectionElement.dataset.section; + this.addVisibleSections(visibleSections.map((s) => s.dataset.section)); + + document + .querySelector(".emoji-picker__section-btn.active") + ?.scrollIntoView({ + block: "nearest", + inline: "start", + }); + } + } + + _setScrollDirection(target) { + if (target.scrollTop > this.prevYPosition) { + this.scrollDirection = "down"; + } else { + this.scrollDirection = "up"; + } + + this.prevYPosition = target.scrollTop; + } + + _isSectionVisibleInPicker(section, picker) { + const { bottom, height, top } = section.getBoundingClientRect(); + const containerRect = picker.getBoundingClientRect(); + + return top <= containerRect.top + ? containerRect.top - top <= height + : bottom - containerRect.bottom <= height; + } + + _getSectionsUpTo(section) { + const sections = []; + for (const sectionNode of document.querySelectorAll( + ".emoji-picker__section" + )) { + const sectionName = sectionNode.dataset.section; + sections.push(sectionNode.dataset.section); + if (sectionName === section) { + break; + } + } + return sections; + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker/detached.gjs b/app/assets/javascripts/discourse/app/components/emoji-picker/detached.gjs new file mode 100644 index 00000000000..e2aa71308e3 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/emoji-picker/detached.gjs @@ -0,0 +1,12 @@ +import EmojiPickerContent from "discourse/components/emoji-picker/content"; + +const EmojiPickerDetached = ; + +export default EmojiPickerDetached; diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker/diversity-menu.gjs b/app/assets/javascripts/discourse/app/components/emoji-picker/diversity-menu.gjs new file mode 100644 index 00000000000..44abc6257cc --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/emoji-picker/diversity-menu.gjs @@ -0,0 +1,71 @@ +import Component from "@glimmer/component"; +import { concat, fn } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { eq } from "truth-helpers"; +import DButton from "discourse/components/d-button"; +import DropdownMenu from "discourse/components/dropdown-menu"; +import replaceEmoji from "discourse/helpers/replace-emoji"; +import DMenu from "float-kit/components/d-menu"; + +export const FITZPATRICK_MODIFIERS = [ + { scale: 1, modifier: null }, + { scale: 2, modifier: ":t2" }, + { scale: 3, modifier: ":t3" }, + { scale: 4, modifier: ":t4" }, + { scale: 5, modifier: ":t5" }, + { scale: 6, modifier: ":t6" }, +]; + +export default class EmojiPicker extends Component { + @service emojiStore; + + fitzpatrickModifiers = FITZPATRICK_MODIFIERS; + + @action + didRequestFitzpatrickScale(scale) { + this.emojiStore.diversity = scale; + this.api.close(); + } + + @action + registerApi(api) { + this.api = api; + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker/index.gjs b/app/assets/javascripts/discourse/app/components/emoji-picker/index.gjs new file mode 100644 index 00000000000..f3da70e563e --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/emoji-picker/index.gjs @@ -0,0 +1,59 @@ +import Component from "@glimmer/component"; +import { concat } from "@ember/helper"; +import { action } from "@ember/object"; +import EmojiPickerContent from "discourse/components/emoji-picker/content"; +import concatClass from "discourse/helpers/concat-class"; +import replaceEmoji from "discourse/helpers/replace-emoji"; +import icon from "discourse-common/helpers/d-icon"; +import DMenu from "float-kit/components/d-menu"; + +export default class EmojiPicker extends Component { + @action + onRegisterMenu(api) { + this.menu = api; + } + + get icon() { + return this.args.icon ?? "discourse-emojis"; + } + + get context() { + return this.args.context ?? "topic"; + } + + +} diff --git a/plugins/chat/assets/javascripts/discourse/components/dc-filter-input.gjs b/app/assets/javascripts/discourse/app/components/filter-input.gjs similarity index 91% rename from plugins/chat/assets/javascripts/discourse/components/dc-filter-input.gjs rename to app/assets/javascripts/discourse/app/components/filter-input.gjs index be0c5b23626..5a8002d0254 100644 --- a/plugins/chat/assets/javascripts/discourse/components/dc-filter-input.gjs +++ b/app/assets/javascripts/discourse/app/components/filter-input.gjs @@ -7,7 +7,7 @@ import concatClass from "discourse/helpers/concat-class"; import noop from "discourse/helpers/noop"; import icon from "discourse-common/helpers/d-icon"; -export default class DcFilterInput extends Component { +export default class FilterInput extends Component { @tracked isFocused = false; focusState = modifier((element) => { @@ -31,7 +31,7 @@ export default class DcFilterInput extends Component {
@@ -43,7 +43,7 @@ export default class DcFilterInput extends Component { {{this.focusState}} {{on "input" (if @filterAction @filterAction (noop))}} @value={{@value}} - class="dc-filter-input" + class="filter-input" ...attributes /> diff --git a/app/assets/javascripts/discourse/app/components/user-status-picker.gjs b/app/assets/javascripts/discourse/app/components/user-status-picker.gjs index 848b551c605..30eb093ad66 100644 --- a/app/assets/javascripts/discourse/app/components/user-status-picker.gjs +++ b/app/assets/javascripts/discourse/app/components/user-status-picker.gjs @@ -2,9 +2,6 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; -import { scheduleOnce } from "@ember/runloop"; -import { htmlSafe } from "@ember/template"; -import DButton from "discourse/components/d-button"; import EmojiPicker from "discourse/components/emoji-picker"; import concatClass from "discourse/helpers/concat-class"; import { emojiUnescape } from "discourse/lib/text"; @@ -14,16 +11,11 @@ import { i18n } from "discourse-i18n"; export default class UserStatusPicker extends Component { @tracked isFocused = false; - @tracked emojiPickerIsActive = false; get emojiHtml() { return emojiUnescape(escapeExpression(`:${this.args.status.emoji}:`)); } - focusEmojiButton() { - document.querySelector(".user-status-picker .btn-emoji")?.focus(); - } - @action blur() { this.isFocused = false; @@ -32,9 +24,6 @@ export default class UserStatusPicker extends Component { @action emojiSelected(emoji) { this.args.status.emoji = emoji; - this.emojiPickerIsActive = false; - - scheduleOnce("afterRender", this, this.focusEmojiButton); } @action @@ -42,22 +31,12 @@ export default class UserStatusPicker extends Component { this.isFocused = true; } - @action - onEmojiPickerOutsideClick() { - this.emojiPickerIsActive = false; - } - @action updateDescription(event) { this.args.status.description = event.target.value; this.args.status.emoji ||= "speech_balloon"; } - @action - toggleEmojiPicker() { - this.emojiPickerIsActive = !this.emojiPickerIsActive; - } - } diff --git a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/menu.gjs b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/menu.gjs index 591ce872653..ca4d39064f6 100644 --- a/app/assets/javascripts/discourse/app/form-kit/components/fk/control/menu.gjs +++ b/app/assets/javascripts/discourse/app/form-kit/components/fk/control/menu.gjs @@ -22,7 +22,8 @@ export default class FKControlMenu extends Component { ); - assert.dom(".form-kit__control-menu").hasAttribute("disabled"); + assert.dom(".form-kit__control-menu-trigger").hasAttribute("disabled"); }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-status-picker-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/user-status-picker-test.gjs index 1f929617182..9a4295b214e 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/user-status-picker-test.gjs +++ b/app/assets/javascripts/discourse/tests/integration/components/user-status-picker-test.gjs @@ -39,11 +39,11 @@ module("Integration | Component | user-status-picker", function (hooks) { await render(); await click(".btn-emoji"); - await fillIn(".emoji-picker-content .filter", "mega"); - await click(".results .emoji"); + await fillIn(".emoji-picker-content .filter-input", "raised"); + await click(".emoji-picker__sections .emoji"); - assert.dom(".emoji").hasAttribute("alt", "mega"); - assert.strictEqual(status.emoji, "mega"); + assert.dom(".emoji").hasAttribute("alt", "raised_hands"); + assert.strictEqual(status.emoji, "raised_hands"); }); test("it sets default emoji when user starts typing a description", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/unit/services/emoji-store-test.js b/app/assets/javascripts/discourse/tests/unit/services/emoji-store-test.js index bfd5a5199a8..621dd9f863b 100644 --- a/app/assets/javascripts/discourse/tests/unit/services/emoji-store-test.js +++ b/app/assets/javascripts/discourse/tests/unit/services/emoji-store-test.js @@ -1,40 +1,132 @@ import { getOwner } from "@ember/owner"; import { setupTest } from "ember-qunit"; import { module, test } from "qunit"; +import KeyValueStore from "discourse/lib/key-value-store"; +import { + MAX_DISPLAYED_EMOJIS, + MAX_TRACKED_EMOJIS, + SKIN_TONE_STORE_KEY, + STORE_NAMESPACE, + USER_EMOJIS_STORE_KEY, +} from "discourse/services/emoji-store"; module("Unit | Service | emoji-store", function (hooks) { setupTest(hooks); hooks.beforeEach(function () { this.emojiStore = getOwner(this).lookup("service:emoji-store"); - this.emojiStore.reset(); }); hooks.afterEach(function () { this.emojiStore.reset(); }); - test("defaults", function (assert) { - assert.deepEqual(this.emojiStore.favorites, []); - assert.strictEqual(this.emojiStore.diversity, 1); + test(".favoritesForContext", function (assert) { + assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [ + "+1", + "heart", + "tada", + ]); }); - test("diversity", function (assert) { + test(".trackEmojiForContext", function (assert) { + this.emojiStore.trackEmojiForContext("grinning", "topic"); + const storedEmojis = new KeyValueStore(STORE_NAMESPACE).getObject( + `topic_${USER_EMOJIS_STORE_KEY}` + ); + + assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [ + "grinning", + "+1", + "heart", + "tada", + ]); + assert.deepEqual( + storedEmojis, + ["grinning", "+1", "heart", "tada"], + "it persists the tracked emojis" + ); + }); + + test("limits the maximum number of tracked emojis", function (assert) { + let trackedEmojis; + Array.from({ length: 45 }).forEach(() => { + trackedEmojis = this.emojiStore.trackEmojiForContext("grinning", "topic"); + }); + + assert.strictEqual(trackedEmojis.length, MAX_TRACKED_EMOJIS); + }); + + test("limits the maximum number of favorites emojis", function (assert) { + Array.from({ length: 25 }).forEach((_, i) => { + this.emojiStore.trackEmojiForContext(`emoji_${i}`, "topic"); + }); + + assert.strictEqual( + this.emojiStore.favoritesForContext("topic").length, + MAX_DISPLAYED_EMOJIS + ); + }); + + test("support for multiple contexts", function (assert) { + this.emojiStore.trackEmojiForContext("grinning", "topic"); + + 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", + ]); + }); + + test(".resetContext", function (assert) { + this.emojiStore.trackEmojiForContext("grinning", "topic"); + + this.emojiStore.resetContext("topic"); + + assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [ + "+1", + "heart", + "tada", + ]); + }); + + test(".diversity", function (assert) { + assert.deepEqual(this.emojiStore.diversity, 1); + }); + + test(".diversity=", function (assert) { this.emojiStore.diversity = 2; - assert.strictEqual(this.emojiStore.diversity, 2); + const storedDiversity = new KeyValueStore(STORE_NAMESPACE).getObject( + SKIN_TONE_STORE_KEY + ); + + assert.deepEqual(this.emojiStore.diversity, 2); + assert.deepEqual(storedDiversity, 2, "it persists the diversity value"); }); - test("favorites", function (assert) { - this.emojiStore.favorites = ["smile"]; - assert.deepEqual(this.emojiStore.favorites, ["smile"]); - }); + test("sort emojis by frequency", function (assert) { + this.emojiStore.trackEmojiForContext("cat", "topic"); + this.emojiStore.trackEmojiForContext("cat", "topic"); + this.emojiStore.trackEmojiForContext("cat", "topic"); + this.emojiStore.trackEmojiForContext("dog", "topic"); + this.emojiStore.trackEmojiForContext("dog", "topic"); - test("track", function (assert) { - this.emojiStore.track("woman:t4"); - assert.deepEqual(this.emojiStore.favorites, ["woman:t4"]); - - this.emojiStore.track("otter"); - this.emojiStore.track(":otter:"); - assert.deepEqual(this.emojiStore.favorites, ["otter", "woman:t4"]); + assert.deepEqual(this.emojiStore.favoritesForContext("topic"), [ + "cat", + "dog", + "+1", + "heart", + "tada", + ]); }); }); diff --git a/app/assets/javascripts/float-kit/addon/components/d-float-body.gjs b/app/assets/javascripts/float-kit/addon/components/d-float-body.gjs index bb344a4228b..6d61a80edbe 100644 --- a/app/assets/javascripts/float-kit/addon/components/d-float-body.gjs +++ b/app/assets/javascripts/float-kit/addon/components/d-float-body.gjs @@ -9,6 +9,7 @@ import DFloatPortal from "float-kit/components/d-float-portal"; import { getScrollParent } from "float-kit/lib/get-scroll-parent"; import FloatKitApplyFloatingUi from "float-kit/modifiers/apply-floating-ui"; import FloatKitCloseOnEscape from "float-kit/modifiers/close-on-escape"; +import and from "truth-helpers/helpers/and"; export default class DFloatBody extends Component { closeOnScroll = modifierFn(() => { @@ -25,6 +26,18 @@ export default class DFloatBody extends Component { }; }); + trapPointerDown = modifierFn((element) => { + const handler = (event) => { + event.stopPropagation(); + }; + + element.addEventListener("pointerdown", handler); + + return () => { + element.removeEventListener("pointerdown", handler); + }; + }); + get supportsCloseOnClickOutside() { return this.args.instance.expanded && this.options.closeOnClickOutside; } @@ -66,9 +79,10 @@ export default class DFloatBody extends Component { aria-expanded={{if @instance.expanded "true" "false"}} role={{@role}} {{FloatKitApplyFloatingUi this.trigger this.options @instance}} + {{this.trapPointerDown}} {{(if @trapTab (modifier TrapTab autofocus=this.options.autofocus))}} {{(if - this.supportsCloseOnClickOutside + (and @instance.expanded this.supportsCloseOnClickOutside) (modifier closeOnClickOutside (fn @instance.close (hash focusTrigger=false)) diff --git a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs index 4cbbf092576..3534ef336e3 100644 --- a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs +++ b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs @@ -3,8 +3,6 @@ import { concat } from "@ember/helper"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; import { getOwner } from "@ember/owner"; -import didInsert from "@ember/render-modifiers/modifiers/did-insert"; -import willDestroy from "@ember/render-modifiers/modifiers/will-destroy"; import { service } from "@ember/service"; import { modifier } from "ember-modifier"; import { and } from "truth-helpers"; @@ -35,10 +33,13 @@ export default class DMenu extends Component { }; }); - @action - registerFloatBody(element) { + registerFloatBody = modifier((element) => { this.body = element; - } + + return () => { + this.body = null; + }; + }); @action teardownFloatBody() { @@ -156,8 +157,7 @@ export default class DMenu extends Component { @innerClass="fk-d-menu__inner-content" @role="dialog" @inline={{this.options.inline}} - {{didInsert this.registerFloatBody}} - {{willDestroy this.teardownFloatBody}} + {{this.registerFloatBody}} > {{#if (has-block)}} {{yield this.componentArgs}} diff --git a/app/assets/javascripts/float-kit/addon/lib/constants.js b/app/assets/javascripts/float-kit/addon/lib/constants.js index 9f68c8d3ed3..c1d63c27607 100644 --- a/app/assets/javascripts/float-kit/addon/lib/constants.js +++ b/app/assets/javascripts/float-kit/addon/lib/constants.js @@ -71,6 +71,7 @@ export const MENU = { modalForMobile: false, inline: null, groupIdentifier: null, + parentIdentifier: null, triggerClass: null, contentClass: null, class: null, diff --git a/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js b/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js index 33653217177..949c2837e67 100644 --- a/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js +++ b/app/assets/javascripts/float-kit/addon/lib/d-menu-instance.js @@ -70,6 +70,8 @@ export default class DMenuInstance extends FloatKitInstance { if (options.focusTrigger) { this.trigger?.focus?.(); } + + await this.options.onClose?.(this); } @action diff --git a/app/assets/javascripts/float-kit/addon/modifiers/close-on-escape.js b/app/assets/javascripts/float-kit/addon/modifiers/close-on-escape.js index e90433dffbd..e19aa016df7 100644 --- a/app/assets/javascripts/float-kit/addon/modifiers/close-on-escape.js +++ b/app/assets/javascripts/float-kit/addon/modifiers/close-on-escape.js @@ -1,11 +1,8 @@ import { registerDestructor } from "@ember/destroyable"; -import { service } from "@ember/service"; import Modifier from "ember-modifier"; import { bind } from "discourse-common/utils/decorators"; export default class FloatKitCloseOnEscape extends Modifier { - @service menu; - constructor(owner, args) { super(owner, args); registerDestructor(this, (instance) => instance.cleanup()); diff --git a/app/assets/javascripts/pretty-text/addon/emoji.js b/app/assets/javascripts/pretty-text/addon/emoji.js index 75f663f1298..78c29b30ec6 100644 --- a/app/assets/javascripts/pretty-text/addon/emoji.js +++ b/app/assets/javascripts/pretty-text/addon/emoji.js @@ -15,10 +15,6 @@ export function registerEmoji(code, url, group) { extendedEmojiMap.set(code, { url, group }); } -export function extendedEmojiList() { - return extendedEmojiMap; -} - const emojiMap = new Map(); // Regex from https://github.com/mathiasbynens/emoji-test-regex-pattern/blob/main/dist/latest/javascript.txt diff --git a/app/assets/stylesheets/common/base/emoji.scss b/app/assets/stylesheets/common/base/emoji.scss index 70f86f07c7e..ad204857418 100644 --- a/app/assets/stylesheets/common/base/emoji.scss +++ b/app/assets/stylesheets/common/base/emoji.scss @@ -35,230 +35,3 @@ sup img.emoji { height: 1.1em; width: 1.1em; } - -.emoji-picker { - width: 100%; - color: var(--primary); - background-color: var(--secondary); - border: 1px solid var(--primary-low); - display: flex; - box-sizing: border-box; - background-clip: padding-box; - z-index: z("modal", "content"); - flex-direction: row; - height: 320px; - max-height: 50vh; - max-width: 420px; - - img.emoji { - // custom emojis might import images of various sizes - // we don't want them to be deformed in the picker - width: 20px !important; - height: 20px !important; - } - - .emoji-picker-content { - display: flex; - flex-direction: column; - flex: 20; - } - - .emoji-picker-emoji-area { - overflow-y: scroll; - -webkit-overflow-scrolling: touch; - width: 100%; - box-sizing: border-box; - padding: 0.25em; - padding-left: 0.75em; - height: 100%; - background: var(--secondary); - - .section { - margin: 0 0 1em; - - .trash-recent { - background: none; - font-size: var(--font-down-1); - - &:hover .d-icon { - color: var(--danger-hover); - } - } - } - - .section-header { - font-weight: 900; - padding: 0.25em 0; - display: flex; - justify-content: space-between; - align-items: center; - } - - .section-group, - .results { - display: flex; - flex-wrap: wrap; - img.emoji { - padding: 0.25em 0.28em; - cursor: pointer; - margin: 0; - display: inline-flex; - - &:hover, - &:focus { - background: var(--tertiary-low); - border-radius: 3px; - } - } - } - - .results { - padding: 0.25em 0; - - &:empty { - display: none; - } - } - } - - .emoji-picker-category-buttons { - overflow-y: auto; - width: 50px; - padding-left: 0.5em; - display: flex; - flex-wrap: wrap; - border-right: 1px solid var(--primary-low); - - .category-button { - background: none; - border: none; - padding: 0.5em; - outline: none; - - .emoji { - pointer-events: none; - filter: grayscale(100%); - } - - &:hover .emoji, - &.current .emoji { - filter: grayscale(0); - } - } - } - - &.has-filter { - .emojis-container { - visibility: hidden; - height: 0px; - overflow: hidden; - } - - .emoji-picker-category-buttons { - pointer-events: none; - opacity: 0.5; - .category-button.current .emoji { - filter: grayscale(100%); - } - } - } -} - -.emoji-picker-search-container { - display: flex; - width: 100%; - position: relative; - padding: 0.75em; - border-bottom: 1px solid var(--primary-low); - box-sizing: border-box; - align-items: center; - - .filter { - flex: 1 0 auto; - margin: 0; - width: calc(100% - 50px); - margin-right: 0.5em; - } - - .d-icon { - color: var(--primary-medium); - cursor: pointer; - padding: 0.25em; - &:hover { - color: var(--tertiary); - } - } -} - -.emoji-picker-footer { - display: flex; - justify-content: space-between; - align-items: center; - border-top: 1px solid var(--primary-low); -} - -.emoji-picker-emoji-info { - display: flex; - align-items: center; - padding-left: 0.5em; - - img.emoji { - height: 32px !important; - width: 32px !important; - } -} - -.emoji-picker-diversity-picker { - border: 0; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - padding: 0.5em; - - .diversity-scale { - display: flex; - align-items: center; - justify-content: center; - min-height: auto; - border-radius: 3px; - margin: 0.15em; - height: 24px; - width: 24px; - - .d-icon { - color: #fff; - filter: drop-shadow(0.5px 1.5px 0 rgba(0, 0, 0, 0.3)); - } - } - - .diversity-scale.default { - background: #ffcc4d; - } - .diversity-scale.light { - background: #f7dece; - } - .diversity-scale.medium-light { - background: #f3d2a2; - } - .diversity-scale.medium { - background: #d5ab88; - } - .diversity-scale.medium-dark { - background: #af7e57; - } - .diversity-scale.dark { - background: #7c533e; - } -} - -.emoji-picker-modal-overlay { - z-index: z("modal", "overlay"); - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - opacity: 0.8; - background-color: var(--primary); -} diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss index d8eb9592fb6..07df54c0354 100644 --- a/app/assets/stylesheets/common/components/_index.scss +++ b/app/assets/stylesheets/common/components/_index.scss @@ -58,3 +58,6 @@ @import "widget-dropdown"; @import "welcome-header"; @import "notifications-tracking"; +@import "emoji-picker"; +@import "filter-input"; +@import "dropdown-menu"; diff --git a/app/assets/stylesheets/common/components/dropdown-menu.scss b/app/assets/stylesheets/common/components/dropdown-menu.scss new file mode 100644 index 00000000000..9a4c754825b --- /dev/null +++ b/app/assets/stylesheets/common/components/dropdown-menu.scss @@ -0,0 +1,18 @@ +.dropdown-menu { + padding: 0; + margin: 0; + + &__item { + list-style: none; + + .btn { + padding: 0.65rem 1rem; + width: 100%; + justify-content: flex-start; + } + } + + &__divider { + margin: 0rem; + } +} diff --git a/plugins/chat/assets/stylesheets/common/chat-emoji-picker.scss b/app/assets/stylesheets/common/components/emoji-picker.scss similarity index 61% rename from plugins/chat/assets/stylesheets/common/chat-emoji-picker.scss rename to app/assets/stylesheets/common/components/emoji-picker.scss index 9c51a474b45..30fdcae5897 100644 --- a/plugins/chat/assets/stylesheets/common/chat-emoji-picker.scss +++ b/app/assets/stylesheets/common/components/emoji-picker.scss @@ -1,16 +1,36 @@ -.chat-emoji-picker { - border-top: 1px solid var(--primary-low); - transition: height 125ms ease; +.fk-d-menu[data-content][data-identifier="emoji-picker"] { + z-index: z("modal", "dialog"); +} + +.emoji-picker-trigger { + .d-icon + .d-button-label { + margin-left: 0.25em; + } +} + +.emoji-picker { display: flex; flex-direction: column; height: 300px; - overflow: hidden; - background: var(--secondary); + width: 500px; + max-width: 100vw; + + .spinner-container { + height: 100%; + } + + &__diversity-menu.fk-d-menu { + z-index: z("modal", "dialog"); + + .dropdown-menu { + min-width: auto; + } + } .emoji { padding: 6px; - width: 32px; - height: 32px; + width: 24px; + height: 24px; image-rendering: -webkit-optimize-contrast; cursor: pointer; @@ -23,20 +43,19 @@ } &__filter-container { - top: 0; - position: sticky; - background: var(--secondary); + background: var(--primary-very-low); display: flex; height: 50px; } &__filter { width: 100%; - padding: 0.5rem; - margin: 0.25rem; + margin: 0.5rem; + border-radius: var(--d-border-radius); + box-sizing: border-box; input { - background: none; + background-color: transparent !important; width: 100%; } @@ -44,40 +63,51 @@ color: var(--primary-medium); } - &.dc-filter-input-container { + &.filter-input-container { border-color: transparent; - background: var(--primary-very-low); + background: var(--secondary); } } + &__content { + display: flex; + flex-direction: row; + height: 250px; + } + &__scrollable-content { - height: 100%; + width: 100%; overflow-y: scroll; - text-transform: capitalize; - @include chat-scrollbar(); - margin: 1px; + overscroll-behavior: contain; + height: 100%; } &__no-results { - padding: 1em; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + font-weight: 700; + padding: 2em; } &__sections-nav { - top: 0; - position: sticky; - background: var(--secondary); - border-bottom: 1px solid var(--primary-low); - height: 50px; + height: 100%; display: flex; - align-items: center; + flex-direction: column; + min-width: 40px; + overflow-y: auto; + overflow-x: hidden; + height: 250px; + background: var(--primary-low); + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + overscroll-behavior: contain; + } - &__indicator { - background: var(--tertiary); - height: 4px; - transition: transform 0.3s cubic-bezier(0.1, 0.82, 0.25, 1); - position: absolute; - bottom: 0; - } + &__sections-nav::-webkit-scrollbar { + display: none; } &__section-btn { @@ -94,6 +124,10 @@ background: none; } + &.active { + scale: 1.4; + } + .emoji { width: 21px; height: 21px; @@ -114,18 +148,23 @@ right: 0; } + &__section-title-container { + display: flex; + position: sticky; + top: -1px; // to avoid an ugly sub 1px gap on retina + background: rgba(var(--secondary-rgb), 0.95); + z-index: 2; + } + &__section-title { margin: 0; padding: 0.5rem; - color: var(--primary-very-high); - font-size: var(--font-up-0); + color: var(--primary-high); + font-size: var(--font-down-2); font-weight: 700; - background: rgba(var(--secondary-rgb), 0.95); - position: sticky; - top: 0; - z-index: 1; width: 100%; box-sizing: border-box; + text-transform: uppercase; } &__fitzpatrick-modifier-btn { @@ -198,18 +237,3 @@ align-items: center; } } - -.chat-channel-message-emoji-picker-connector { - position: relative; - - .chat-emoji-picker { - border: 1px solid var(--primary-low); - width: 320px; - z-index: z("header") + 1; - - .emoji { - width: 22px; - height: 22px; - } - } -} diff --git a/plugins/chat/assets/stylesheets/common/dc-filter-input.scss b/app/assets/stylesheets/common/components/filter-input.scss similarity index 86% rename from plugins/chat/assets/stylesheets/common/dc-filter-input.scss rename to app/assets/stylesheets/common/components/filter-input.scss index bf6dd2fbe4c..98ca3461f93 100644 --- a/plugins/chat/assets/stylesheets/common/dc-filter-input.scss +++ b/app/assets/stylesheets/common/components/filter-input.scss @@ -1,4 +1,4 @@ -.dc-filter-input-container { +.filter-input-container { display: flex; align-items: center; justify-content: space-between; @@ -9,8 +9,8 @@ border: 1px solid var(--tertiary); } - .dc-filter-input, - .dc-filter-input:focus { + .filter-input, + .filter-input:focus { width: 100%; margin: 0; border: none; diff --git a/app/assets/stylesheets/common/components/user-status-picker.scss b/app/assets/stylesheets/common/components/user-status-picker.scss index 05819079ae5..2ebb7670f24 100644 --- a/app/assets/stylesheets/common/components/user-status-picker.scss +++ b/app/assets/stylesheets/common/components/user-status-picker.scss @@ -8,9 +8,10 @@ width: 100%; border: 1px solid var(--primary-medium); - .btn-emoji { + .emoji-picker-trigger { margin: 3px; - width: 2.3em; + width: 38px; + height: 38px; text-align: center; } diff --git a/app/assets/stylesheets/common/form-kit/_control-menu.scss b/app/assets/stylesheets/common/form-kit/_control-menu.scss index 1ad96915607..8503e08643d 100644 --- a/app/assets/stylesheets/common/form-kit/_control-menu.scss +++ b/app/assets/stylesheets/common/form-kit/_control-menu.scss @@ -1,4 +1,15 @@ -.form-kit__control-menu { +.form-kit__control-menu-trigger { @include default-input; justify-content: space-between; } + +.form-kit__control-menu-content { + .dropdown-menu { + min-width: 200px; + } + .dropdown-menu__item { + &:hover { + background: var(--d-hover); + } + } +} diff --git a/app/assets/stylesheets/mobile/_index.scss b/app/assets/stylesheets/mobile/_index.scss index 696fa9ce9ab..d5f687dce87 100644 --- a/app/assets/stylesheets/mobile/_index.scss +++ b/app/assets/stylesheets/mobile/_index.scss @@ -12,7 +12,6 @@ @import "directory"; @import "discourse"; @import "edit-category"; -@import "emoji"; @import "header"; @import "invite-signup"; @import "lightbox"; diff --git a/app/assets/stylesheets/mobile/components/_index.scss b/app/assets/stylesheets/mobile/components/_index.scss index c6f42c21789..133fb5db2e6 100644 --- a/app/assets/stylesheets/mobile/components/_index.scss +++ b/app/assets/stylesheets/mobile/components/_index.scss @@ -4,3 +4,6 @@ @import "user-card"; @import "user-stream-item"; @import "welcome-header"; +@import "more-topics"; +@import "bookmark-menu"; +@import "emoji-picker"; diff --git a/app/assets/stylesheets/mobile/components/emoji-picker.scss b/app/assets/stylesheets/mobile/components/emoji-picker.scss new file mode 100644 index 00000000000..068e1d11f19 --- /dev/null +++ b/app/assets/stylesheets/mobile/components/emoji-picker.scss @@ -0,0 +1,57 @@ +.fk-d-menu-modal.emoji-picker-content { + .emoji-picker { + height: auto; + } + + html:not(.keyboard-visible.mobile-view) & .d-modal__container { + height: calc(var(--composer-vh, var(--1dvh)) * 100); + max-height: 100%; + + .emoji-picker__content { + height: calc( + var(--composer-vh, var(--1dvh)) * 100 - 50px - + env(safe-area-inset-bottom) + ); + } + } + + html.keyboard-visible.mobile-view & .d-modal__container { + .emoji-picker__content { + height: calc(var(--composer-vh, var(--1dvh)) * 100 - 50px); + } + } + + .d-modal__body { + padding: 0; + } + + .d-modal__container { + align-self: flex-start; + } + + .emoji-picker__sections-nav { + order: 1; + height: 50px; + width: 100%; + flex-direction: row; + display: flex; + box-sizing: border-box; + overflow-y: hidden; + overflow-x: auto; + } + + .emoji-picker__content { + flex-direction: column; + padding-top: 1em; + box-sizing: border-box; + } + + .emoji-picker__scrollable-content { + height: 100%; + } + + .emoji-picker__close-btn { + margin-left: 0.25em; + padding-left: 0.75em; + } +} diff --git a/app/assets/stylesheets/mobile/emoji.scss b/app/assets/stylesheets/mobile/emoji.scss deleted file mode 100644 index ee41bc80954..00000000000 --- a/app/assets/stylesheets/mobile/emoji.scss +++ /dev/null @@ -1,18 +0,0 @@ -.emoji-picker { - border: none; - position: fixed; - width: 100%; - max-width: 100vh; - top: 0; - left: 0; - border-bottom: 1px solid var(--primary-low); - min-height: 50vh; - .emoji-picker-emoji-area { - img.emoji { - // custom emojis might import images of various sizes - // we don't want them to be deformed in the picker - width: 28px !important; - height: 28px !important; - } - } -} diff --git a/app/assets/stylesheets/mobile/float-kit/d-menu.scss b/app/assets/stylesheets/mobile/float-kit/d-menu.scss index b315d85581a..c9ebac78145 100644 --- a/app/assets/stylesheets/mobile/float-kit/d-menu.scss +++ b/app/assets/stylesheets/mobile/float-kit/d-menu.scss @@ -10,9 +10,6 @@ max-width: 100px; border-radius: 10px; } - .d-modal__body { - padding: 1em 0; - } h3 { padding-top: 0.25em; diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb new file mode 100644 index 00000000000..0217d8d8d14 --- /dev/null +++ b/app/controllers/emojis_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class EmojisController < ApplicationController + def index + emojis = Emoji.allowed.group_by(&:group) + render json: MultiJson.dump(emojis) + end +end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 840774add95..ac316fa1cd1 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2603,6 +2603,8 @@ en: disable_mailing_list_mode: "Disallow users from enabling mailing list mode (prevents any mailing list emails from being sent.)" default_email_previous_replies: "Include previous replies in emails by default." + default_emoji_reactions: "Default favorites emoji reactions. Add up to 5 emojis for quick reaction." + default_email_in_reply_to: "Include excerpt of replied to post in emails by default." default_hide_profile: "Hide user public profile by default." @@ -2977,6 +2979,7 @@ en: default_email_mailing_list_mode_frequency: "" default_email_messages_level: "" default_email_previous_replies: "" + default_emoji_reactions: "" default_hide_presence: "" default_hide_profile: "" default_include_tl0_in_digests: "" diff --git a/config/routes.rb b/config/routes.rb index b9178f85dce..f8480e6a819 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1711,6 +1711,8 @@ Discourse::Application.routes.draw do get "/form-templates/:id" => "form_templates#show" get "/form-templates" => "form_templates#index" + get "/emojis" => "emojis#index" + if Rails.env.test? # Routes that are only used for testing get "/test_net_http_timeouts" => "test_requests#test_net_http_timeouts" diff --git a/config/site_settings.yml b/config/site_settings.yml index 881eb95d5e0..85799e5a258 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -959,6 +959,10 @@ posting: max_emojis_in_title: default: 1 area: "emojis" + default_emoji_reactions: + type: emoji_list + default: +1|heart|tada + client: true allow_uncategorized_topics: client: true default: false diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake index a794bef55c5..08ae71850b4 100644 --- a/lib/tasks/javascript.rake +++ b/lib/tasks/javascript.rake @@ -189,20 +189,6 @@ task "javascript:update_constants" => :environment do write_template("pretty-text/addon/emoji/version.js", task_name, <<~JS) export const IMAGE_VERSION = "#{Emoji::EMOJI_VERSION}"; JS - - groups_json = JSON.parse(File.read("lib/emoji/groups.json")) - - emoji_buttons = groups_json.map { |group| <<~HTML } - - HTML - - emoji_sections = groups_json.map { |group| html_for_section(group) } - - components_dir = "discourse/app/components" - write_hbs_template("#{components_dir}/emoji-group-buttons.hbs", task_name, emoji_buttons.join) - write_hbs_template("#{components_dir}/emoji-group-sections.hbs", task_name, emoji_sections.join) end task "javascript:update" => "clean_up" do diff --git a/plugins/chat/admin/assets/javascripts/admin/components/chat-incoming-webhook-edit-form.gjs b/plugins/chat/admin/assets/javascripts/admin/components/chat-incoming-webhook-edit-form.gjs index 13f23a054c6..69fbad0f0cc 100644 --- a/plugins/chat/admin/assets/javascripts/admin/components/chat-incoming-webhook-edit-form.gjs +++ b/plugins/chat/admin/assets/javascripts/admin/components/chat-incoming-webhook-edit-form.gjs @@ -1,5 +1,4 @@ import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; import { fn } from "@ember/helper"; import { action } from "@ember/object"; import { service } from "@ember/service"; @@ -17,8 +16,6 @@ export default class ChatIncomingWebhookEditForm extends Component { @service toasts; @service router; - @tracked emojiPickerIsActive = false; - get formData() { return { name: this.args.webhook?.name, @@ -32,7 +29,6 @@ export default class ChatIncomingWebhookEditForm extends Component { @action emojiSelected(setData, emoji) { setData("emoji", `:${emoji}:`); - this.emojiPickerIsActive = false; } @action @@ -129,6 +125,7 @@ export default class ChatIncomingWebhookEditForm extends Component { @name="emoji" @title={{i18n "chat.incoming_webhooks.emoji"}} @description={{i18n "chat.incoming_webhooks.emoji_instructions"}} + @size="large" as |field| > @@ -140,33 +137,19 @@ export default class ChatIncomingWebhookEditForm extends Component { {{/if}} - - - {{#unless this.emojiPickerIsActive}} - - - - - - - - - {{/unless}} - + + + + + + + + diff --git a/plugins/chat/app/controllers/chat/emojis_controller.rb b/plugins/chat/app/controllers/chat/emojis_controller.rb deleted file mode 100644 index e27f8ae537e..00000000000 --- a/plugins/chat/app/controllers/chat/emojis_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module Chat - class EmojisController < ::Chat::BaseController - def index - emojis = Emoji.allowed.group_by(&:group) - render json: MultiJson.dump(emojis) - end - end -end diff --git a/plugins/chat/assets/javascripts/discourse/components/browse-channels.gjs b/plugins/chat/assets/javascripts/discourse/components/browse-channels.gjs index fa3014af898..d91502ad623 100644 --- a/plugins/chat/assets/javascripts/discourse/components/browse-channels.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/browse-channels.gjs @@ -8,13 +8,13 @@ import { schedule } from "@ember/runloop"; import { service } from "@ember/service"; import { eq } from "truth-helpers"; import DButton from "discourse/components/d-button"; +import FilterInput from "discourse/components/filter-input"; import { INPUT_DELAY } from "discourse-common/config/environment"; import discourseDebounce from "discourse-common/lib/debounce"; import { i18n } from "discourse-i18n"; import List from "discourse/plugins/chat/discourse/components/chat/list"; import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message"; import ChatChannelCard from "discourse/plugins/chat/discourse/components/chat-channel-card"; -import DcFilterInput from "discourse/plugins/chat/discourse/components/dc-filter-input"; const ARCHIVED = "archived"; const ALL = "all"; @@ -89,11 +89,10 @@ export default class BrowseChannels extends Component { -
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-message-emoji-picker.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-message-emoji-picker.gjs deleted file mode 100644 index 0d83337d0a0..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-message-emoji-picker.gjs +++ /dev/null @@ -1,77 +0,0 @@ -import Component from "@glimmer/component"; -import { action } from "@ember/object"; -import { service } from "@ember/service"; -import { createPopper } from "@popperjs/core"; -import { modifier } from "ember-modifier"; -import { headerOffset } from "discourse/lib/offset-calculator"; -import ChatEmojiPicker from "discourse/plugins/chat/discourse/components/chat-emoji-picker"; - -export default class ChatChannelMessageEmojiPicker extends Component { - @service site; - @service chatEmojiPickerManager; - - context = "chat-channel-message"; - - listenToBodyScroll = modifier(() => { - const handler = () => { - this.chatEmojiPickerManager.close(); - }; - - document.addEventListener("scroll", handler); - - return () => { - document.removeEventListener("scroll", handler); - }; - }); - - @action - willDestroy() { - super.willDestroy(...arguments); - this._popper?.destroy(); - } - - @action - didSelectEmoji(emoji) { - this.chatEmojiPickerManager.picker?.didSelectEmoji(emoji); - this.chatEmojiPickerManager.close(); - } - - @action - didInsert(element) { - if (this.site.mobileView) { - element.classList.remove("hidden"); - return; - } - - this._popper = createPopper( - this.chatEmojiPickerManager.picker?.trigger, - element, - { - placement: "top", - modifiers: [ - { - name: "eventListeners", - options: { scroll: false, resize: false }, - }, - { - name: "flip", - options: { padding: { top: headerOffset() } }, - }, - ], - } - ); - - element.classList.remove("hidden"); - } - - -} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs index 1cb2efe7895..cfef3ffdd4e 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs @@ -55,7 +55,6 @@ export default class ChatChannel extends Component { @service chatApi; @service chatChannelsManager; @service chatDraftsManager; - @service chatEmojiPickerManager; @service chatStateManager; @service chatChannelScrollPositions; @service("chat-channel-composer") composer; diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-dropdown.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-composer-dropdown.gjs index e57e0e1d8c4..dc1e9cccfc7 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-composer-dropdown.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-dropdown.gjs @@ -8,8 +8,9 @@ import DMenu from "float-kit/components/d-menu"; export default class ChatComposerDropdown extends Component { @action - onButtonClick(button, closeFn) { - closeFn({ focusTrigger: false }); + async onButtonClick(button, closeFn) { + await closeFn({ focusTrigger: false }); + button.action(); } @@ -27,6 +28,7 @@ export default class ChatComposerDropdown extends Component { @arrow={{true}} @placements={{array "top" "bottom"}} @identifier="chat-composer-dropdown__menu" + @modalForMobile={{true}} ...attributes as |menu| > @@ -39,6 +41,7 @@ export default class ChatComposerDropdown extends Component { @label={{button.label}} class={{concatClass "chat-composer-dropdown__action-btn" + "btn-transparent" button.id }} /> diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs index cffa9b679ae..23ab4d29a19 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs @@ -79,9 +79,13 @@ /> {{/each}} - {{/if}} + + {{#if this.site.desktopView}} - - \ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js index 5dcc8ca5afa..5da6a1a5ec4 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js @@ -9,6 +9,7 @@ import $ from "jquery"; import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji"; import { translations } from "pretty-text/emoji/data"; import { Promise } from "rsvp"; +import EmojiPickerDetached from "discourse/components/emoji-picker/detached"; import InsertHyperlink from "discourse/components/modal/insert-hyperlink"; import { SKIP } from "discourse/lib/autocomplete"; import { @@ -23,6 +24,8 @@ import { initUserStatusHtml, renderUserStatusHtml, } from "discourse/lib/user-status-on-autocomplete"; +import virtualElementFromTextRange from "discourse/lib/virtual-element-from-text-range"; +import { waitForClosedKeyboard } from "discourse/lib/wait-for-keyboard"; import { cloneJSON } from "discourse-common/lib/object"; import { findRawTemplate } from "discourse-common/lib/raw-templates"; import { i18n } from "discourse-i18n"; @@ -41,12 +44,12 @@ export default class ChatComposer extends Component { @service composerPresenceManager; @service chatComposerWarningsTracker; @service appEvents; - @service chatEmojiReactionStore; - @service chatEmojiPickerManager; + @service emojiStore; @service currentUser; @service chatApi; @service chatDraftsManager; @service modal; + @service menu; @tracked isFocused = false; @tracked inProgressUploadsCount = 0; @@ -375,14 +378,10 @@ export default class ChatComposer extends Component { @action onSelectEmoji(emoji) { - const code = `:${emoji}:`; - this.chatEmojiReactionStore.track(code); this.composer.textarea.emojiSelected(emoji); if (this.site.desktopView) { this.composer.focus(); - } else { - this.chatEmojiPickerManager.close(); } } @@ -490,16 +489,33 @@ export default class ChatComposer extends Component { return [matches[1]]; } }, - transformComplete: (v) => { + transformComplete: async (v) => { if (v.code) { - this.chatEmojiReactionStore.track(v.code); return `${v.code}:`; } else { $textarea.autocomplete({ cancel: true }); - this.chatEmojiPickerManager.open({ - context: this.context, - initialFilter: v.term, - }); + + const menuOptions = { + identifier: "emoji-picker", + groupIdentifier: "emoji-picker", + component: EmojiPickerDetached, + context: `channel_${this.args.channel.id}`, + modalForMobile: true, + data: { + didSelectEmoji: (emoji) => { + this.onSelectEmoji(emoji); + }, + term: v.term, + context: "chat", + }, + }; + + // Close the keyboard before showing the emoji picker + // it avoids a whole range of bugs on iOS + await waitForClosedKeyboard(this); + + const virtualElement = virtualElementFromTextRange(); + this.menuInstance = await this.menu.show(virtualElement, menuOptions); return ""; } }, @@ -529,8 +545,9 @@ export default class ChatComposer extends Component { } if (term === "") { - if (this.chatEmojiReactionStore.favorites.length) { - return resolve(this.chatEmojiReactionStore.favorites.slice(0, 5)); + const favorites = this.emojiStore.favoritesForContext("chat"); + if (favorites.length > 0) { + return resolve(favorites.slice(0, 5)); } else { return resolve([ "slight_smile", @@ -571,7 +588,7 @@ export default class ChatComposer extends Component { const options = emojiSearch(term, { maxResults: 5, - diversity: this.chatEmojiReactionStore.diversity, + diversity: this.emojiStore.diversity, exclude: emojiDenied, }); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.hbs deleted file mode 100644 index 0c85fef2300..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.hbs +++ /dev/null @@ -1,246 +0,0 @@ -{{! template-lint-disable no-invalid-interactive }} -{{! template-lint-disable no-nested-interactive }} -{{! template-lint-disable no-pointer-down-event-binding }} - -{{#if (eq this.chatEmojiPickerManager.picker.context @context)}} -
-
- - - -
- - {{#if this.chatEmojiPickerManager.sections.length}} - {{#if (eq this.filteredEmojis null)}} -
-
- - {{#each-in this.groups as |section emojis|}} - - {{#if (eq section "favorites")}} - {{replace-emoji ":star:"}} - {{else}} - - {{/if}} - - {{/each-in}} -
- {{/if}} - -
-
- {{#if (not-eq this.filteredEmojis null)}} -
- {{#each this.filteredEmojis as |emoji|}} - {{emoji.name}} - {{else}} -

- {{i18n "chat.emoji_picker.no_results"}} -

- {{/each}} -
- {{/if}} - - {{#each-in this.groups as |section emojis|}} -
-

- {{i18n - (concat "chat.emoji_picker." section) - translatedFallback=section - }} -

-
- {{! we always want the first emoji for tabbing}} - {{#let (get emojis "0") as |emoji|}} - {{emoji.name}} - {{/let}} - - {{#if - (includes this.chatEmojiPickerManager.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)}} - {{emoji.name}} - {{/if}} - {{/each}} - {{/if}} -
-
- {{/each-in}} -
-
- {{else}} -
- {{/if}} -
- - {{#if - (and - this.site.mobileView - (eq this.chatEmojiPickerManager.picker.context "chat-channel-message") - ) - }} -
- {{/if}} -{{/if}} \ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js b/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js deleted file mode 100644 index c64a3695c0c..00000000000 --- a/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js +++ /dev/null @@ -1,424 +0,0 @@ -import Component from "@glimmer/component"; -import { tracked } from "@glimmer/tracking"; -import { action } from "@ember/object"; -import { later, schedule } from "@ember/runloop"; -import { service } from "@ember/service"; -import { htmlSafe } from "@ember/template"; -import { emojiUrlFor } from "discourse/lib/text"; -import { INPUT_DELAY } from "discourse-common/config/environment"; -import discourseDebounce from "discourse-common/lib/debounce"; -import { bind } from "discourse-common/utils/decorators"; - -export const FITZPATRICK_MODIFIERS = [ - { - scale: 1, - modifier: null, - }, - { - scale: 2, - modifier: ":t2", - }, - { - scale: 3, - modifier: ":t3", - }, - { - scale: 4, - modifier: ":t4", - }, - { - scale: 5, - modifier: ":t5", - }, - { - scale: 6, - modifier: ":t6", - }, -]; - -export default class ChatEmojiPicker extends Component { - @service chatEmojiPickerManager; - @service emojiPickerScrollObserver; - @service chatEmojiReactionStore; - @service capabilities; - @service site; - - @tracked filteredEmojis = null; - @tracked isExpandedFitzpatrickScale = false; - - fitzpatrickModifiers = FITZPATRICK_MODIFIERS; - - get groups() { - const emojis = this.chatEmojiPickerManager.emojis; - const favorites = { - favorites: this.chatEmojiReactionStore.favorites - .filter((f) => !this.site.denied_emojis?.includes(f)) - .map((name) => { - return { - name, - group: "favorites", - url: emojiUrlFor(name), - }; - }), - }; - - return { - ...favorites, - ...emojis, - }; - } - - get flatEmojis() { - if (!this.chatEmojiPickerManager.emojis) { - return []; - } - - // eslint-disable-next-line no-unused-vars - let { favorites, ...rest } = this.chatEmojiPickerManager.emojis; - return Object.values(rest).flat(); - } - - get navIndicatorStyle() { - const section = this.chatEmojiPickerManager.lastVisibleSection; - const index = Object.keys(this.groups).indexOf(section); - - return htmlSafe( - `width: ${ - 100 / Object.keys(this.groups).length - }%; transform: translateX(${index * 100}%);` - ); - } - - get navBtnStyle() { - return htmlSafe(`width: ${100 / Object.keys(this.groups).length}%;`); - } - - @action - trapKeyDownEvents(event) { - if (event.key === "Escape") { - this.chatEmojiPickerManager.close(); - } - - if (event.key === "ArrowUp") { - event.stopPropagation(); - } - - if ( - event.key === "ArrowDown" && - event.target.classList.contains("dc-filter-input") - ) { - event.stopPropagation(); - event.preventDefault(); - - document - .querySelector( - `.chat-emoji-picker__scrollable-content .emoji[tabindex="0"]` - ) - ?.focus(); - } - } - - @action - didNavigateFitzpatrickScale(event) { - if (event.type !== "keyup") { - return; - } - - const scaleNodes = - event.target - .closest(".chat-emoji-picker__fitzpatrick-scale") - ?.querySelectorAll(".chat-emoji-picker__fitzpatrick-modifier-btn") || - []; - - const scales = [...scaleNodes]; - - if (event.key === "ArrowRight") { - event.preventDefault(); - - if (event.target === scales[scales.length - 1]) { - scales[0].focus(); - } else { - event.target.nextElementSibling?.focus(); - } - } - - if (event.key === "ArrowLeft") { - event.preventDefault(); - - if (event.target === scales[0]) { - scales[scales.length - 1].focus(); - } else { - event.target.previousElementSibling?.focus(); - } - } - } - - @action - didToggleFitzpatrickScale(event) { - if (event.type === "keyup") { - if (event.key === "Escape") { - event.preventDefault(); - this.isExpandedFitzpatrickScale = false; - return; - } - - if (event.key !== "Enter") { - return; - } - } - - this.isExpandedFitzpatrickScale = !this.isExpandedFitzpatrickScale; - } - - @action - didRequestFitzpatrickScale(scale, event) { - if (event.type === "keyup") { - if (event.key === "Escape") { - event.preventDefault(); - event.stopPropagation(); - this.isExpandedFitzpatrickScale = false; - this._focusCurrentFitzpatrickScale(); - return; - } - - if (event.key !== "Enter") { - return; - } - } - - event.preventDefault(); - event.stopPropagation(); - - this.isExpandedFitzpatrickScale = false; - this.chatEmojiReactionStore.diversity = scale; - this._focusCurrentFitzpatrickScale(); - } - - _focusCurrentFitzpatrickScale() { - schedule("afterRender", () => { - document - .querySelector(".chat-emoji-picker__fitzpatrick-modifier-btn.current") - ?.focus(); - }); - } - - @action - didInputFilter(value) { - if (!value?.length) { - this.filteredEmojis = null; - return; - } - - discourseDebounce(this, this.debouncedDidInputFilter, value, INPUT_DELAY); - } - - @action - focusFilter(target) { - schedule("afterRender", () => { - target?.focus({ preventScroll: true }); - }); - } - - 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) - ) - ); - - schedule("afterRender", () => { - const scrollableContent = document.querySelector( - ".chat-emoji-picker__scrollable-content" - ); - - if (scrollableContent) { - scrollableContent.scrollTop = 0; - } - }); - } - - @action - onSectionsKeyDown(event) { - if (event.key === "Enter") { - this.didSelectEmoji(event); - } else { - this.didNavigateSection(event); - } - } - - @action - didNavigateSection(event) { - const sectionsEmojis = (section) => [...section.querySelectorAll(".emoji")]; - const focusSectionsLastEmoji = (section) => { - const emojis = sectionsEmojis(section); - return emojis[emojis.length - 1].focus(); - }; - const focusSectionsFirstEmoji = (section) => { - sectionsEmojis(section)[0].focus(); - }; - const currentSection = event.target.closest(".chat-emoji-picker__section"); - const focusFilter = () => { - document.querySelector(".dc-filter-input")?.focus(); - }; - const allEmojis = () => [ - ...document.querySelectorAll( - ".chat-emoji-picker__section:not(.hidden) .emoji" - ), - ]; - - if (event.key === "ArrowRight") { - event.preventDefault(); - const nextEmoji = event.target.nextElementSibling; - - if (nextEmoji) { - nextEmoji.focus(); - } else { - const nextSection = currentSection.nextElementSibling; - if (nextSection) { - focusSectionsFirstEmoji(nextSection); - } - } - } - - if (event.key === "ArrowLeft") { - event.preventDefault(); - const prevEmoji = event.target.previousElementSibling; - - if (prevEmoji) { - prevEmoji.focus(); - } else { - const prevSection = currentSection.previousElementSibling; - if (prevSection) { - focusSectionsLastEmoji(prevSection); - } else { - focusFilter(); - } - } - } - - if (event.key === "ArrowDown") { - event.preventDefault(); - event.stopPropagation(); - - const nextEmoji = allEmojis() - .filter((c) => c.offsetTop > event.target.offsetTop) - .findBy("offsetLeft", event.target.offsetLeft); - - if (nextEmoji) { - nextEmoji.focus(); - } else { - // for perf reason all emojis might not be loaded at this point - // but the first one will always be - const nextSection = currentSection.nextElementSibling; - if (nextSection) { - focusSectionsFirstEmoji(nextSection); - } - } - } - - if (event.key === "ArrowUp") { - event.preventDefault(); - event.stopPropagation(); - - const prevEmoji = allEmojis() - .reverse() - .filter((c) => c.offsetTop < event.target.offsetTop) - .findBy("offsetLeft", event.target.offsetLeft); - - if (prevEmoji) { - prevEmoji.focus(); - } else { - focusFilter(); - } - } - } - - @action - didSelectEmoji(event) { - if (!event.target.classList.contains("emoji")) { - return; - } - - if (event.type === "click" || event.key === "Enter") { - event.preventDefault(); - event.stopPropagation(); - let emoji = event.target.dataset.emoji; - const tonable = event.target.dataset.tonable; - const diversity = this.chatEmojiReactionStore.diversity; - if (tonable && diversity > 1) { - emoji = `${emoji}:t${diversity}`; - } - - this.args.didSelectEmoji?.(emoji); - } - } - - @action - didRequestSection(section) { - const scrollableContent = document.querySelector( - ".chat-emoji-picker__scrollable-content" - ); - - this.filteredEmojis = null; - - // we disable scroll listener during requesting section - // to avoid it from detecting another section during scroll to requested section - this.emojiPickerScrollObserver.enabled = false; - this.chatEmojiPickerManager.addVisibleSections([section]); - this.chatEmojiPickerManager.lastVisibleSection = section; - - // iOS hack to avoid blank div when requesting section during momentum - if (scrollableContent && this.capabilities.isIOS) { - document.querySelector( - ".chat-emoji-picker__scrollable-content" - ).style.overflow = "hidden"; - } - - schedule("afterRender", () => { - const firstEmoji = document.querySelector( - `.chat-emoji-picker__section[data-section="${section}"] .emoji:nth-child(1)` - ); - - const targetEmoji = - [ - ...document.querySelectorAll( - `.chat-emoji-picker__section[data-section="${section}"] .emoji` - ), - ].find((emoji) => emoji.offsetTop > firstEmoji.offsetTop) || firstEmoji; - - targetEmoji.focus(); - - later(() => { - // iOS hack to avoid blank div when requesting section during momentum - if (scrollableContent && this.capabilities.isIOS) { - document.querySelector( - ".chat-emoji-picker__scrollable-content" - ).style.overflow = "scroll"; - } - - this.emojiPickerScrollObserver.enabled = true; - }, 200); - }); - } - - @action - addClickOutsideEventListener() { - document.addEventListener("click", this.didClickOutside); - } - - @action - removeClickOutsideEventListener() { - document.removeEventListener("click", this.didClickOutside); - } - - @bind - didClickOutside(event) { - if (!event.target.closest(".chat-emoji-picker")) { - this.chatEmojiPickerManager.close(); - } - } -} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.gjs index 26c4e31a6fb..8c269d5540c 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.gjs @@ -18,6 +18,7 @@ import DropdownSelectBox from "select-kit/components/dropdown-select-box"; import ChatMessageReaction from "discourse/plugins/chat/discourse/components/chat-message-reaction"; import chatMessageContainer from "discourse/plugins/chat/discourse/lib/chat-message-container"; import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor"; +import ChatMessageReactionModel from "discourse/plugins/chat/discourse/models/chat-message-reaction"; const MSG_ACTIONS_VERTICAL_PADDING = -10; const FULL = "full"; @@ -26,13 +27,24 @@ const REDUCED_WIDTH_THRESHOLD = 500; export default class ChatMessageActionsDesktop extends Component { @service chat; - @service chatEmojiPickerManager; @service site; + @service emojiStore; @tracked size = FULL; popper = null; + get favoriteReactions() { + return this.emojiStore + .favoritesForContext(`channel_${this.message.channel.id}`) + .slice(0, 3) + .map( + (emoji) => + this.message.reactions.find((reaction) => reaction.emoji === emoji) || + ChatMessageReactionModel.create({ emoji }) + ); + } + get message() { return this.chat.activeMessage.model; } @@ -53,6 +65,16 @@ export default class ChatMessageActionsDesktop extends Component { return this.size === FULL; } + get messageContainer() { + return chatMessageContainer(this.message.id, this.context); + } + + @action + openEmojiPicker(_, event) { + event.preventDefault(); + this.messageInteractor.openEmojiPicker(event.target); + } + @action onWheel() { // prevents menu to stop scroll on the list of messages @@ -64,24 +86,19 @@ export default class ChatMessageActionsDesktop extends Component { this.popper?.destroy(); schedule("afterRender", () => { - const messageContainer = chatMessageContainer( - this.message.id, - this.context - ); - - if (!messageContainer) { + if (!this.messageContainer) { return; } - const viewport = messageContainer.closest(".popper-viewport"); + const viewport = this.messageContainer.closest(".popper-viewport"); this.size = viewport.clientWidth < REDUCED_WIDTH_THRESHOLD ? REDUCED : FULL; - if (!messageContainer) { + if (!this.messageContainer) { return; } - this.popper = createPopper(messageContainer, element, { + this.popper = createPopper(this.messageContainer, element, { placement: "top-end", strategy: "fixed", modifiers: [ @@ -133,10 +150,7 @@ export default class ChatMessageActionsDesktop extends Component { }} > {{#if this.shouldRenderFavoriteReactions}} - {{#each - this.messageInteractor.emojiReactions key="emoji" - as |reaction| - }} + {{#each this.favoriteReactions as |reaction|}} {{/if}} diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-mobile.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-mobile.gjs index 4b37aa8b26b..af5a679318c 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-mobile.gjs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-mobile.gjs @@ -10,6 +10,7 @@ import { and, or } from "truth-helpers"; import BookmarkIcon from "discourse/components/bookmark-icon"; import DButton from "discourse/components/d-button"; import DModal from "discourse/components/d-modal"; +import EmojiPickerDetached from "discourse/components/emoji-picker/detached"; import concatClass from "discourse/helpers/concat-class"; import ChatMessageReaction from "discourse/plugins/chat/discourse/components/chat-message-reaction"; import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar"; @@ -19,6 +20,8 @@ export default class ChatMessageActionsMobile extends Component { @service chat; @service site; @service capabilities; + @service modal; + @service menu; @tracked hasExpandedReply = false; @@ -70,9 +73,19 @@ export default class ChatMessageActionsMobile extends Component { } @action - openEmojiPicker(_, event) { - this.args.closeModal(); - this.messageInteractor.openEmojiPicker(_, event); + async openEmojiPicker(_, event) { + await this.args.closeModal(); + + await this.menu.show(event.target, { + identifier: "emoji-picker", + groupIdentifier: "emoji-picker", + component: EmojiPickerDetached, + modalForMobile: true, + data: { + context: "chat", + didSelectEmoji: this.messageInteractor.selectReaction, + }, + }); }