diff --git a/app/assets/javascripts/admin/addon/components/emoji-value-list.js b/app/assets/javascripts/admin/addon/components/emoji-value-list.js
new file mode 100644
index 00000000000..e7096a71dad
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/components/emoji-value-list.js
@@ -0,0 +1,168 @@
+import Component from "@ember/component";
+import I18n from "I18n";
+import discourseComputed from "discourse-common/utils/decorators";
+import { emojiUrlFor } from "discourse/lib/text";
+import { action, set, setProperties } from "@ember/object";
+import { later, schedule } from "@ember/runloop";
+
+export default Component.extend({
+ classNameBindings: [":value-list", ":emoji-list"],
+ values: null,
+ validationMessage: null,
+ emojiPickerIsActive: false,
+ isEditorFocused: false,
+
+ @discourseComputed("values")
+ collection(values) {
+ values = values || "";
+
+ return values
+ .split("|")
+ .filter(Boolean)
+ .map((value) => {
+ return {
+ isEditable: true,
+ isEditing: false,
+ value,
+ emojiUrl: emojiUrlFor(value),
+ };
+ });
+ },
+
+ @action
+ closeEmojiPicker() {
+ this.collection.setEach("isEditing", false);
+ this.set("emojiPickerIsActive", false);
+ this.set("isEditorFocused", false);
+ },
+
+ @action
+ emojiSelected(code) {
+ if (!this._validateInput(code)) {
+ return;
+ }
+
+ const item = this.collection.findBy("isEditing");
+ if (item) {
+ setProperties(item, {
+ value: code,
+ emojiUrl: emojiUrlFor(code),
+ isEditing: false,
+ });
+
+ this._saveValues();
+ } else {
+ const newCollectionValue = {
+ value: code,
+ emojiUrl: emojiUrlFor(code),
+ isEditable: true,
+ isEditing: false,
+ };
+ this.collection.addObject(newCollectionValue);
+ this._saveValues();
+ }
+
+ this.set("emojiPickerIsActive", false);
+ this.set("isEditorFocused", false);
+ },
+
+ @discourseComputed("collection")
+ showUpDownButtons(collection) {
+ return collection.length - 1 ? true : false;
+ },
+
+ _splitValues(values) {
+ if (values && values.length) {
+ const emojiList = [];
+ const emojis = values.split("|").filter(Boolean);
+ emojis.forEach((emojiName) => {
+ const emoji = {
+ isEditable: true,
+ isEditing: false,
+ };
+ emoji.value = emojiName;
+ emoji.emojiUrl = emojiUrlFor(emojiName);
+
+ emojiList.push(emoji);
+ });
+
+ return emojiList;
+ } else {
+ return [];
+ }
+ },
+
+ @action
+ editValue(index) {
+ this.closeEmojiPicker();
+ schedule("afterRender", () => {
+ if (parseInt(index, 10) >= 0) {
+ const item = this.collection[index];
+ if (item.isEditable) {
+ set(item, "isEditing", true);
+ }
+ }
+
+ this.set("isEditorFocused", true);
+ later(() => {
+ if (this.element && !this.isDestroying && !this.isDestroyed) {
+ this.set("emojiPickerIsActive", true);
+ }
+ }, 100);
+ });
+ },
+
+ @action
+ removeValue(value) {
+ this._removeValue(value);
+ },
+
+ @action
+ shift(operation, index) {
+ let futureIndex = index + operation;
+
+ if (futureIndex > this.collection.length - 1) {
+ futureIndex = 0;
+ } else if (futureIndex < 0) {
+ futureIndex = this.collection.length - 1;
+ }
+
+ const shiftedEmoji = this.collection[index];
+ this.collection.removeAt(index);
+ this.collection.insertAt(futureIndex, shiftedEmoji);
+
+ this._saveValues();
+ },
+
+ _validateInput(input) {
+ this.set("validationMessage", null);
+
+ if (!emojiUrlFor(input)) {
+ this.set(
+ "validationMessage",
+ I18n.t("admin.site_settings.emoji_list.invalid_input")
+ );
+ return false;
+ }
+
+ return true;
+ },
+
+ _removeValue(value) {
+ this.collection.removeObject(value);
+ this._saveValues();
+ },
+
+ _replaceValue(index, newValue) {
+ const item = this.collection[index];
+ if (item.value === newValue) {
+ return;
+ }
+ set(item, "value", newValue);
+ this._saveValues();
+ },
+
+ _saveValues() {
+ this.set("values", this.collection.mapBy("value").join("|"));
+ },
+});
diff --git a/app/assets/javascripts/admin/addon/mixins/setting-component.js b/app/assets/javascripts/admin/addon/mixins/setting-component.js
index d96bd40c22b..fc6c6c24d5f 100644
--- a/app/assets/javascripts/admin/addon/mixins/setting-component.js
+++ b/app/assets/javascripts/admin/addon/mixins/setting-component.js
@@ -27,6 +27,7 @@ const CUSTOM_TYPES = [
"tag_list",
"color",
"simple_list",
+ "emoji_list",
];
const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"];
diff --git a/app/assets/javascripts/admin/addon/templates/components/emoji-value-list.hbs b/app/assets/javascripts/admin/addon/templates/components/emoji-value-list.hbs
new file mode 100644
index 00000000000..aeed56f4586
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/templates/components/emoji-value-list.hbs
@@ -0,0 +1,53 @@
+{{#if collection}}
+
+ {{#each collection as |data index|}}
+ -
+ {{#if data.isEditable}}
+ {{d-button
+ action=(action "removeValue")
+ actionParam=data
+ icon="times"
+ class="remove-value-btn btn-small"
+ }}
+ {{/if}}
+
+
+
+ {{#if showUpDownButtons}}
+ {{d-button
+ action=(action "shift" -1 index)
+ icon="arrow-up"
+ class="shift-up-value-btn btn-small"
+ }}
+ {{d-button
+ action=(action "shift" 1 index)
+ icon="arrow-down"
+ class="shift-down-value-btn btn-small"
+ }}
+ {{/if}}
+
+ {{/each}}
+
+{{/if}}
+
+
+ {{d-button
+ action=(action "editValue")
+ actionParam=data
+ icon="emoji-icon"
+ class="add-emoji-button d-editor-textarea-wrapper"
+ label="admin.site_settings.emoji_list.add_emoji_button.label"
+ }}
+
+
+{{emoji-picker
+ isActive=emojiPickerIsActive
+ isEditorFocused=isEditorFocused
+ emojiSelected=(action "emojiSelected")
+ onEmojiPickerClose=(action "closeEmojiPicker")
+}}
+
+{{setting-validation-message message=validationMessage}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/site-settings/emoji-list.hbs b/app/assets/javascripts/admin/addon/templates/components/site-settings/emoji-list.hbs
new file mode 100644
index 00000000000..50d1d18898b
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/templates/components/site-settings/emoji-list.hbs
@@ -0,0 +1,3 @@
+{{emoji-value-list setting=setting values=value}}
+{{html-safe setting.description}}
+{{setting-validation-message message=validationMessage}}
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index a65d7ff0add..e920bb76c90 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -884,6 +884,47 @@ table#user-badges {
}
}
+.emoji-value-list {
+ margin-left: 0;
+
+ .emoji-details {
+ display: flex;
+ align-items: center;
+ min-height: 30px;
+ padding: $input-padding;
+ line-height: 1;
+ color: var(--primary);
+ border: 1px solid var(--primary-low);
+
+ .emoji-name {
+ margin-left: 0.5em;
+ }
+
+ &:not(.can-edit) {
+ pointer-events: none;
+ background-color: var(--primary-very-low);
+ }
+ }
+
+ .value-input {
+ flex-direction: row;
+ }
+}
+
+.value .add-emoji-button {
+ display: block;
+ background-color: var(--primary-low);
+ border: none;
+}
+
+.value .add-value-btn,
+.shift-up-value-btn,
+.shift-down-value-btn {
+ @include value-btn;
+ margin-right: 0 !important;
+ margin-left: 0.25em;
+}
+
.secret-value-list {
.value {
flex-flow: row wrap;
diff --git a/app/assets/stylesheets/desktop.scss b/app/assets/stylesheets/desktop.scss
index dfae3eca774..d302363926b 100644
--- a/app/assets/stylesheets/desktop.scss
+++ b/app/assets/stylesheets/desktop.scss
@@ -4,3 +4,6 @@
// Import all component-specific files
@import "desktop/components/_index";
+
+// Import all admin-specific files
+@import "desktop/admin/_index";
diff --git a/app/assets/stylesheets/desktop/admin/_index.scss b/app/assets/stylesheets/desktop/admin/_index.scss
new file mode 100644
index 00000000000..b45777ec742
--- /dev/null
+++ b/app/assets/stylesheets/desktop/admin/_index.scss
@@ -0,0 +1 @@
+@import "admin_base";
diff --git a/app/assets/stylesheets/desktop/admin/admin_base.scss b/app/assets/stylesheets/desktop/admin/admin_base.scss
new file mode 100644
index 00000000000..c014649b9e0
--- /dev/null
+++ b/app/assets/stylesheets/desktop/admin/admin_base.scss
@@ -0,0 +1,15 @@
+.emoji-value-list {
+ .value {
+ .shift-up-value-btn,
+ .shift-down-value-btn {
+ display: none;
+ }
+
+ &:hover {
+ .shift-up-value-btn,
+ .shift-down-value-btn {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 31de720fd34..9201bb510bb 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -5010,6 +5010,10 @@ en:
reset: "reset"
none: "none"
site_settings:
+ emoji_list:
+ invalid_input: "Emoji list should only contain valid emoji names, eg: hugs"
+ add_emoji_button:
+ label: "Add Emoji"
title: "Settings"
no_results: "No results found."
more_than_30_results: "There are more than 30 results. Please refine your search or select a category."
diff --git a/lib/site_settings/type_supervisor.rb b/lib/site_settings/type_supervisor.rb
index f5bb727d356..ee78697ebf2 100644
--- a/lib/site_settings/type_supervisor.rb
+++ b/lib/site_settings/type_supervisor.rb
@@ -35,7 +35,8 @@ class SiteSettings::TypeSupervisor
group_list: 20,
tag_list: 21,
color: 22,
- simple_list: 23
+ simple_list: 23,
+ emoji_list: 24
)
end
diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb
index 61aa7c3df30..9a2d18b9831 100644
--- a/lib/svg_sprite/svg_sprite.rb
+++ b/lib/svg_sprite/svg_sprite.rb
@@ -66,6 +66,7 @@ module SvgSprite
"download",
"ellipsis-h",
"ellipsis-v",
+ "emoji-icon",
"envelope",
"envelope-square",
"exchange-alt",
diff --git a/spec/components/site_settings/type_supervisor_spec.rb b/spec/components/site_settings/type_supervisor_spec.rb
index 86fd7267ed2..36c6f82a197 100644
--- a/spec/components/site_settings/type_supervisor_spec.rb
+++ b/spec/components/site_settings/type_supervisor_spec.rb
@@ -91,6 +91,9 @@ describe SiteSettings::TypeSupervisor do
it "'simple_list' should be at the right position" do
expect(SiteSettings::TypeSupervisor.types[:simple_list]).to eq(23)
end
+ it "'emoji_list' should be at the right position" do
+ expect(SiteSettings::TypeSupervisor.types[:emoji_list]).to eq(24)
+ end
end
end
diff --git a/vendor/assets/svg-icons/emoji.svg b/vendor/assets/svg-icons/emoji.svg
new file mode 100644
index 00000000000..9b715176936
--- /dev/null
+++ b/vendor/assets/svg-icons/emoji.svg
@@ -0,0 +1,7 @@
+
+
+