FEATURE: allows multiple custom emoji groups (#9308)

Note: DBHelper would fail with a sql syntax error on columns like "group".

Co-authored-by: Jarek Radosz <jradosz@gmail.com>
This commit is contained in:
Joffrey JAFFEUX
2020-03-30 20:16:10 +02:00
committed by GitHub
parent fa5ba6beb8
commit 0996c3b7b3
24 changed files with 428 additions and 138 deletions

View File

@ -1,37 +1,74 @@
import { sort } from "@ember/object/computed"; import { sort } from "@ember/object/computed";
import EmberObject from "@ember/object"; import EmberObject, { action, computed } from "@ember/object";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
const ALL_FILTER = "all";
export default Controller.extend({ export default Controller.extend({
sortedEmojis: sort("model", "emojiSorting"), filter: null,
sorting: null,
init() { init() {
this._super(...arguments); this._super(...arguments);
this.emojiSorting = ["name"]; this.setProperties({
filter: ALL_FILTER,
sorting: ["group", "name"]
});
}, },
actions: { sortedEmojis: sort("filteredEmojis.[]", "sorting"),
emojiUploaded(emoji) {
emoji.url += "?t=" + new Date().getTime();
this.model.pushObject(EmberObject.create(emoji));
},
destroy(emoji) { emojiGroups: computed("model", {
return bootbox.confirm( get() {
I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }), return this.model.mapBy("group").uniq();
I18n.t("no_value"),
I18n.t("yes_value"),
destroy => {
if (destroy) {
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
type: "DELETE"
}).then(() => {
this.model.removeObject(emoji);
});
}
}
);
} }
}),
sortingGroups: computed("emojiGroups.[]", {
get() {
return [ALL_FILTER].concat(this.emojiGroups);
}
}),
filteredEmojis: computed("model.[]", "filter", {
get() {
if (!this.filter || this.filter === ALL_FILTER) {
return this.model;
} else {
return this.model.filterBy("group", this.filter);
}
}
}),
@action
filterGroups(value) {
this.set("filter", value);
},
@action
emojiUploaded(emoji, group) {
emoji.url += "?t=" + new Date().getTime();
emoji.group = group;
this.model.pushObject(EmberObject.create(emoji));
},
@action
destroyEmoji(emoji) {
return bootbox.confirm(
I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }),
I18n.t("no_value"),
I18n.t("yes_value"),
destroy => {
if (destroy) {
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
type: "DELETE"
}).then(() => {
this.model.removeObject(emoji);
});
}
}
);
} }
}); });

View File

@ -1,35 +1,49 @@
<div class='emoji'> <div class='admin-emojis'>
<h2>{{i18n 'admin.emoji.title'}}</h2> <h1>{{i18n 'admin.emoji.title'}}</h1>
<p class="desc">{{i18n 'admin.emoji.help'}}</p> <p class="desc">{{i18n "admin.emoji.help"}}</p>
<p>{{emoji-uploader done=(action "emojiUploaded")}}</p> {{emoji-uploader
emojiGroups=emojiGroups
done=(action "emojiUploaded")
}}
<hr>
{{#if sortedEmojis}} {{#if sortedEmojis}}
<div> <table id="custom_emoji">
<table id="custom_emoji"> <thead>
<thead> <tr>
<th>{{i18n "admin.emoji.image"}}</th>
<th>{{i18n "admin.emoji.name"}}</th>
<th>
{{combo-box
value=filter
content=sortingGroups
nameProperty=null
valueProperty=null
onChange=(action "filterGroups")
}}
</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each sortedEmojis as |e|}}
<tr> <tr>
<th>{{i18n "admin.emoji.image"}}</th> <th><img class="emoji emoji-custom" src={{e.url}} title={{e.name}}></th>
<th>{{i18n "admin.emoji.name"}}</th> <th>:{{e.name}}:</th>
<th></th> <th>{{e.group}}</th>
<th>
{{d-button
action=(action "destroyEmoji" e)
class="btn-danger"
icon="far-trash-alt"
}}
</th>
</tr> </tr>
</thead> {{/each}}
<tbody> </tbody>
{{#each sortedEmojis as |e|}} </table>
<tr>
<th><img class="emoji emoji-custom" src={{e.url}} title={{e.name}}></th>
<th>:{{e.name}}:</th>
<th>
{{d-button
action=(action "destroy" e)
class="btn-danger pull-right"
icon="far-trash-alt"}}
</th>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}} {{/if}}
</div> </div>

View File

@ -14,9 +14,26 @@ import ENV, { INPUT_DELAY } from "discourse-common/config/environment";
const { run } = Ember; const { run } = Ember;
const PER_ROW = 11; const PER_ROW = 11;
const customEmojis = _.keys(extendedEmojiList()).map(code => { function customEmojis() {
return { code, src: emojiUrlFor(code) }; const list = extendedEmojiList();
}); const emojis = Object.keys(list)
.map(code => {
const { group } = list[code];
return {
code,
src: emojiUrlFor(code),
group,
key: `emoji_picker.${group || "default"}`
};
})
.reduce((acc, curr) => {
if (!acc[curr.group]) acc[curr.group] = [];
acc[curr.group].push(curr);
return acc;
}, {});
return Object.values(emojis);
}
export default Component.extend({ export default Component.extend({
automaticPositioning: true, automaticPositioning: true,
@ -35,7 +52,9 @@ export default Component.extend({
}, },
show() { show() {
const template = findRawTemplate("emoji-picker")({ customEmojis }); const template = findRawTemplate("emoji-picker")({
customEmojis: customEmojis()
});
this.$picker.html(template); this.$picker.html(template);
this.$filter = this.$picker.find(".filter"); this.$filter = this.$picker.find(".filter");
@ -579,7 +598,7 @@ export default Component.extend({
this.$picker.width() - this.$picker.width() -
this.$picker.find(".categories-column").width() - this.$picker.find(".categories-column").width() -
this.$picker.find(".diversity-picker").width() - this.$picker.find(".diversity-picker").width() -
32; 60;
this.$picker.find(".info").css("max-width", infoMaxWidth); this.$picker.find(".info").css("max-width", infoMaxWidth);
}, },

View File

@ -1,23 +1,53 @@
import { notEmpty, not } from "@ember/object/computed"; import { notEmpty, not } from "@ember/object/computed";
import { action } from "@ember/object";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import UploadMixin from "discourse/mixins/upload"; import UploadMixin from "discourse/mixins/upload";
const DEFAULT_GROUP = "default";
export default Component.extend(UploadMixin, { export default Component.extend(UploadMixin, {
type: "emoji", type: "emoji",
uploadUrl: "/admin/customize/emojis", uploadUrl: "/admin/customize/emojis",
hasName: notEmpty("name"), hasName: notEmpty("name"),
hasGroup: notEmpty("group"),
addDisabled: not("hasName"), addDisabled: not("hasName"),
group: "default",
emojiGroups: null,
newEmojiGroups: null,
tagName: null,
uploadOptions() { didReceiveAttrs() {
return { this._super(...arguments);
sequentialUploads: true
}; this.set("newEmojiGroups", this.emojiGroups);
}, },
@discourseComputed("hasName", "name") uploadOptions() {
data(hasName, name) { return { sequentialUploads: true };
return hasName ? { name } : {}; },
@action
createEmojiGroup(group) {
this.setProperties({
newEmojiGroups: this.emojiGroups.concat([group]).uniq(),
group
});
},
@discourseComputed("hasName", "name", "hasGroup", "group")
data(hasName, name, hasGroup, group) {
const payload = {};
if (hasName) {
payload.name = name;
}
if (hasGroup && group !== DEFAULT_GROUP) {
payload.group = group;
}
return payload;
}, },
validateUploadedFilesOptions() { validateUploadedFilesOptions() {
@ -25,7 +55,7 @@ export default Component.extend(UploadMixin, {
}, },
uploadDone(upload) { uploadDone(upload) {
this.set("name", null); this.done(upload, this.group);
this.done(upload); this.setProperties({ name: null, group: DEFAULT_GROUP });
} }
}); });

View File

@ -24,7 +24,7 @@ export default {
}); });
(PreloadStore.get("customEmoji") || []).forEach(emoji => (PreloadStore.get("customEmoji") || []).forEach(emoji =>
registerEmoji(emoji.name, emoji.url) registerEmoji(emoji.name, emoji.url, emoji.group)
); );
} }
}; };

View File

@ -1,11 +1,48 @@
{{text-field name="name" placeholderKey="admin.emoji.name" value=name}} {{#conditional-loading-section isLoading=uploading}}
<div class="emoji-uploader">
<label class="btn btn-primary {{if addDisabled 'disabled'}}"> <div class="control">
{{d-icon "plus"}} <span class="label">
{{i18n "admin.emoji.add"}} {{i18n "admin.emoji.name"}}
<input </span>
class="hidden-upload-field" <div class="input">
disabled={{addDisabled}} {{input
type="file" name="name"
accept=".png,.gif"> placeholderKey="admin.emoji.name"
</label> value=(readonly name)
input=(action (mut name) value="target.value")
}}
</div>
</div>
<div class="control">
<span class="label">
{{i18n "admin.emoji.group"}}
</span>
<div class="input">
{{combo-box
name="group"
value=group
content=newEmojiGroups
onChange=(action "createEmojiGroup")
valueProperty=null
nameProperty=null
options=(hash
allowAny=true
)
}}
</div>
</div>
<div class="control">
<div class="input">
<label class="btn btn-default btn-primary {{if addDisabled 'disabled'}}">
{{d-icon "plus"}}
{{i18n "admin.emoji.add"}}
<input
class="hidden-upload-field"
disabled={{addDisabled}}
type="file"
accept=".png,.gif">
</label>
</div>
</div>
</div>
{{/conditional-loading-section}}

View File

@ -9,10 +9,12 @@
</div> </div>
<% end %> <% end %>
<% if !Emoji.custom.blank? %> <% Emoji.custom.group_by { |emoji| emoji.group }.each do |group, emojis| %>
<div class='category-icon'> <% if emojis.present? %>
<button data-tabicon="<%= Emoji.custom.first.name %>" type="button" class="emoji" tabindex="-1" data-section="ungrouped" title="{{i18n 'emoji_picker.custom'}}"></button> <div class='category-icon'>
</div> <button data-tabicon="<%= emojis.first.name %>" type="button" class="emoji" tabindex="-1" data-section="custom-<%= group %>" title="{{i18n 'emoji_picker.<%= group %>'}}"></button>
</div>
<% end %>
<% end %> <% end %>
</div> </div>
@ -49,18 +51,22 @@
</div> </div>
<% end %> <% end %>
{{#if customEmojis.length}} {{#each customEmojis as |emojis|}}
<div class='section' data-section='ungrouped'> {{#if emojis.length}}
<div class='section-header'> <div class='section' data-section='custom-{{emojis.firstObject.group}}'>
<span class="title">{{i18n 'emoji_picker.custom'}}</span> <div class='section-header'>
<span class="title">
{{i18n emojis.firstObject.key}}
</span>
</div>
<div class='section-group'>
{{#each emojis as |emoji|}}
<button style="background-url: url("{{emoji.src}}")" type="button" class="emoji" tabindex="-1" title="{{emoji.code}}"></button>
{{/each}}
</div>
</div> </div>
<div class='section-group'> {{/if}}
{{#each customEmojis as |emoji|}} {{/each}}
<button style="background-url: url("{{emoji.src}}")" type="button" class="emoji" tabindex="-1" title="{{emoji.code}}"></button>
{{/each}}
</div>
</div>
{{/if}}
</div> </div>
<div class='footer'> <div class='footer'>
<div class='info'></div> <div class='info'></div>

View File

@ -10,9 +10,9 @@ import { IMAGE_VERSION } from "pretty-text/emoji/version";
const extendedEmoji = {}; const extendedEmoji = {};
export function registerEmoji(code, url) { export function registerEmoji(code, url, group) {
code = code.toLowerCase(); code = code.toLowerCase();
extendedEmoji[code] = url; extendedEmoji[code] = { url, group };
} }
export function extendedEmojiList() { export function extendedEmojiList() {
@ -92,7 +92,7 @@ function isReplacableInlineEmoji(string, index, inlineEmoji) {
} }
export function performEmojiUnescape(string, opts) { export function performEmojiUnescape(string, opts) {
if (!string || typeof string !== "string") { if (!string) {
return; return;
} }
@ -126,6 +126,8 @@ export function performEmojiUnescape(string, opts) {
} alt='${emojiVal}' class='${classes}'>` } alt='${emojiVal}' class='${classes}'>`
: m; : m;
}); });
return string;
} }
export function performEmojiEscape(string, opts) { export function performEmojiEscape(string, opts) {
@ -143,6 +145,8 @@ export function performEmojiEscape(string, opts) {
return m; return m;
}); });
return string;
} }
export function isCustomEmoji(code, opts) { export function isCustomEmoji(code, opts) {
@ -157,11 +161,11 @@ export function buildEmojiUrl(code, opts) {
let url; let url;
code = String(code).toLowerCase(); code = String(code).toLowerCase();
if (extendedEmoji.hasOwnProperty(code)) { if (extendedEmoji.hasOwnProperty(code)) {
url = extendedEmoji[code]; url = extendedEmoji[code].url;
} }
if (opts && opts.customEmoji && opts.customEmoji[code]) { if (opts && opts.customEmoji && opts.customEmoji[code]) {
url = opts.customEmoji[code]; url = opts.customEmoji[code].url || opts.customEmoji[code];
} }
const noToneMatch = code.match(/([^:]+):?/); const noToneMatch = code.match(/([^:]+):?/);

View File

@ -979,3 +979,4 @@ a.inline-editable-field {
@import "common/admin/admin_report_table"; @import "common/admin/admin_report_table";
@import "common/admin/admin_report_inline_table"; @import "common/admin/admin_report_inline_table";
@import "common/admin/admin_intro"; @import "common/admin/admin_intro";
@import "common/admin/admin_emojis";

View File

@ -0,0 +1,50 @@
.admin-emojis {
#custom_emoji {
.select-kit {
width: 220px;
}
}
}
.emoji-uploader {
display: flex;
align-items: flex-end;
input,
.select-kit {
width: 220px;
margin: 0 1em 0 0;
}
.upload-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
}
.mobile-view {
.admin-emojis {
.emoji-uploader {
flex-direction: column;
.fields {
input,
.select-kit {
width: 100%;
}
}
.upload-container {
margin: 1em 0 0 0;
}
}
#custom_emoji {
.select-kit {
display: none;
}
}
}
}

View File

@ -556,7 +556,7 @@
} }
#custom_emoji { #custom_emoji {
width: 27%; width: 100%;
} }
.modal-body .inputs .branch { .modal-body .inputs .branch {

View File

@ -45,6 +45,8 @@ sup img.emoji {
justify-content: space-between; justify-content: space-between;
border-right: 1px solid $primary-low; border-right: 1px solid $primary-low;
min-width: 36px; min-width: 36px;
overflow-y: auto;
padding: 0.5em;
} }
.emoji-picker .category-icon { .emoji-picker .category-icon {

View File

@ -6,3 +6,7 @@
.emoji-picker .category-icon { .emoji-picker .category-icon {
margin: 2px; margin: 2px;
} }
.emoji-picker .categories-column {
padding: 0;
}

View File

@ -9,6 +9,7 @@ class Admin::EmojisController < Admin::AdminController
def create def create
file = params[:file] || params[:files].first file = params[:file] || params[:files].first
name = params[:name] || File.basename(file.original_filename, ".*") name = params[:name] || File.basename(file.original_filename, ".*")
group = params[:group] ? params[:group].downcase : nil
hijack do hijack do
# fix the name # fix the name
@ -26,11 +27,11 @@ class Admin::EmojisController < Admin::AdminController
data = data =
if upload.persisted? if upload.persisted?
custom_emoji = CustomEmoji.new(name: name, upload: upload) custom_emoji = CustomEmoji.new(name: name, upload: upload, group: group)
if custom_emoji.save if custom_emoji.save
Emoji.clear_cache Emoji.clear_cache
{ name: custom_emoji.name, url: custom_emoji.upload.url } { name: custom_emoji.name, url: custom_emoji.upload.url, group: group }
else else
good = false good = false
failed_json.merge(errors: custom_emoji.errors.full_messages) failed_json.merge(errors: custom_emoji.errors.full_messages)

View File

@ -14,6 +14,7 @@ end
# id :integer not null, primary key # id :integer not null, primary key
# name :string not null # name :string not null
# upload_id :integer not null # upload_id :integer not null
# group :string
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# #

View File

@ -6,9 +6,11 @@ class Emoji
FITZPATRICK_SCALE ||= [ "1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff" ] FITZPATRICK_SCALE ||= [ "1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff" ]
DEFAULT_GROUP ||= "default"
include ActiveModel::SerializerSupport include ActiveModel::SerializerSupport
attr_accessor :name, :url, :tonable attr_accessor :name, :url, :tonable, :group
def self.all def self.all
Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom } Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom }
@ -104,15 +106,19 @@ class Emoji
result << Emoji.new.tap do |e| result << Emoji.new.tap do |e|
e.name = emoji.name e.name = emoji.name
e.url = emoji.upload&.url e.url = emoji.upload&.url
e.group = emoji.group || DEFAULT_GROUP
end end
end end
end end
Plugin::CustomEmoji.emojis.each do |name, url| Plugin::CustomEmoji.emojis.each do |group, emojis|
result << Emoji.new.tap do |e| emojis.each do |name, url|
e.name = name result << Emoji.new.tap do |e|
url = (Discourse.base_uri + url) if url[/^\/[^\/]/] e.name = name
e.url = url url = (Discourse.base_uri + url) if url[/^\/[^\/]/]
e.url = url
e.group = group || DEFAULT_GROUP
end
end end
end end

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
class EmojiSerializer < ApplicationSerializer class EmojiSerializer < ApplicationSerializer
attributes :name, :url attributes :name, :url, :group
end end

View File

@ -1651,7 +1651,6 @@ en:
objects: Objects objects: Objects
symbols: Symbols symbols: Symbols
flags: Flags flags: Flags
custom: Custom emojis
recent: Recently used recent: Recently used
default_tone: No skin tone default_tone: No skin tone
light_tone: Light skin tone light_tone: Light skin tone
@ -1659,6 +1658,7 @@ en:
medium_tone: Medium skin tone medium_tone: Medium skin tone
medium_dark_tone: Medium dark skin tone medium_dark_tone: Medium dark skin tone
dark_tone: Dark skin tone dark_tone: Dark skin tone
default: Custom emojis
shared_drafts: shared_drafts:
title: "Shared Drafts" title: "Shared Drafts"
@ -4556,6 +4556,7 @@ en:
help: "Add new emoji that will be available to everyone. (PROTIP: drag & drop multiple files at once)" help: "Add new emoji that will be available to everyone. (PROTIP: drag & drop multiple files at once)"
add: "Add New Emoji" add: "Add New Emoji"
name: "Name" name: "Name"
group: "Group"
image: "Image" image: "Image"
delete_confirm: "Are you sure you want to delete the :%{name}: emoji?" delete_confirm: "Are you sure you want to delete the :%{name}: emoji?"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddGroupToCustomEmojis < ActiveRecord::Migration[6.0]
def change
add_column :custom_emojis, :group, :string, null: true, limit: 20
end
end

View File

@ -29,17 +29,17 @@ class DbHelper
text_columns.each do |table, columns| text_columns.each do |table, columns|
set = columns.map do |column| set = columns.map do |column|
replace = "REPLACE(#{column[:name]}, :from, :to)" replace = "REPLACE(\"#{column[:name]}\", :from, :to)"
replace = truncate(replace, table, column) replace = truncate(replace, table, column)
"#{column[:name]} = #{replace}" "\"#{column[:name]}\" = #{replace}"
end.join(", ") end.join(", ")
where = columns.map do |column| where = columns.map do |column|
"#{column[:name]} IS NOT NULL AND #{column[:name]} LIKE :like" "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like"
end.join(" OR ") end.join(" OR ")
rows = DB.exec(<<~SQL, from: from, to: to, like: like) rows = DB.exec(<<~SQL, from: from, to: to, like: like)
UPDATE #{table} UPDATE \"#{table}\"
SET #{set} SET #{set}
WHERE #{where} WHERE #{where}
SQL SQL
@ -55,17 +55,17 @@ class DbHelper
text_columns.each do |table, columns| text_columns.each do |table, columns|
set = columns.map do |column| set = columns.map do |column|
replace = "REGEXP_REPLACE(#{column[:name]}, :pattern, :replacement, :flags)" replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)"
replace = truncate(replace, table, column) replace = truncate(replace, table, column)
"#{column[:name]} = #{replace}" "\"#{column[:name]}\" = #{replace}"
end.join(", ") end.join(", ")
where = columns.map do |column| where = columns.map do |column|
"#{column[:name]} IS NOT NULL AND #{column[:name]} #{match} :pattern" "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern"
end.join(" OR ") end.join(" OR ")
rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match) rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match)
UPDATE #{table} UPDATE \"#{table}\"
SET #{set} SET #{set}
WHERE #{where} WHERE #{where}
SQL SQL
@ -84,9 +84,9 @@ class DbHelper
next if excluded_tables.include?(r.table_name) next if excluded_tables.include?(r.table_name)
rows = DB.query(<<~SQL, like: like) rows = DB.query(<<~SQL, like: like)
SELECT #{r.column_name} SELECT \"#{r.column_name}\"
FROM #{r.table_name} FROM \"#{r.table_name}\"
WHERE #{r.column_name} LIKE :like WHERE \""#{r.column_name}"\" LIKE :like
SQL SQL
if rows.size > 0 if rows.size > 0

View File

@ -6,21 +6,29 @@ require_dependency 'plugin/metadata'
require_dependency 'auth' require_dependency 'auth'
class Plugin::CustomEmoji class Plugin::CustomEmoji
CACHE_KEY ||= "plugin-emoji"
def self.cache_key def self.cache_key
@@cache_key ||= "plugin-emoji" @@cache_key ||= CACHE_KEY
end end
def self.emojis def self.emojis
@@emojis ||= {} @@emojis ||= {}
end end
def self.register(name, url) def self.clear_cache
@@cache_key = Digest::SHA1.hexdigest(cache_key + name)[0..10] @@cache_key = CACHE_KEY
emojis[name] = url @@emojis = {}
end end
def self.unregister(name) def self.register(name, url, group = Emoji::DEFAULT_GROUP)
emojis.delete(name) @@cache_key = Digest::SHA1.hexdigest(cache_key + name + group)[0..10]
new_group = emojis[group] || {}
new_group[name] = url
emojis[group] = new_group
end
def self.unregister(name, group = Emoji::DEFAULT_GROUP)
emojis[group].delete(name)
end end
def self.translations def self.translations
@ -471,8 +479,9 @@ class Plugin::Instance
DiscoursePluginRegistry.register_seed_path_builder(&block) DiscoursePluginRegistry.register_seed_path_builder(&block)
end end
def register_emoji(name, url) def register_emoji(name, url, group = Emoji::DEFAULT_GROUP)
Plugin::CustomEmoji.register(name, url) Plugin::CustomEmoji.register(name, url, group)
Emoji.clear_cache
end end
def translate_emoji(from, to) def translate_emoji(from, to)

View File

@ -538,4 +538,30 @@ describe Plugin::Instance do
expect(Reviewable.types).to match_array(current_list << new_element) expect(Reviewable.types).to match_array(current_list << new_element)
end end
end end
describe '#register_emoji' do
before do
Plugin::CustomEmoji.clear_cache
end
it 'allows to register an emoji' do
Plugin::Instance.new.register_emoji("foo", "/foo/bar.png")
custom_emoji = Emoji.custom.first
expect(custom_emoji.name).to eq("foo")
expect(custom_emoji.url).to eq("/foo/bar.png")
expect(custom_emoji.group).to eq(Emoji::DEFAULT_GROUP)
end
it 'allows to register an emoji with a group' do
Plugin::Instance.new.register_emoji("bar", "/baz/bar.png", "baz")
custom_emoji = Emoji.custom.first
expect(custom_emoji.name).to eq("bar")
expect(custom_emoji.url).to eq("/baz/bar.png")
expect(custom_emoji.group).to eq("baz")
end
end
end end

View File

@ -59,22 +59,38 @@ RSpec.describe Admin::EmojisController do
it 'should allow an admin to add a custom emoji' do it 'should allow an admin to add a custom emoji' do
Emoji.expects(:clear_cache) Emoji.expects(:clear_cache)
post "/admin/customize/emojis.json", params: { post "/admin/customize/emojis.json", params: {
name: 'test', name: 'test',
file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png")
} }
custom_emoji = CustomEmoji.last custom_emoji = CustomEmoji.last
upload = custom_emoji.upload upload = custom_emoji.upload
expect(upload.original_filename).to eq('logo.png') expect(upload.original_filename).to eq('logo.png')
data = JSON.parse(response.body) data = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(data["errors"]).to eq(nil)
expect(data["name"]).to eq(custom_emoji.name)
expect(data["url"]).to eq(upload.url)
expect(custom_emoji.group).to eq(nil)
end
expect(response.status).to eq(200) it 'should allow an admin to add a custom emoji with a custom group' do
expect(data["errors"]).to eq(nil) Emoji.expects(:clear_cache)
expect(data["name"]).to eq(custom_emoji.name)
expect(data["url"]).to eq(upload.url) post "/admin/customize/emojis.json", params: {
name: 'test',
group: 'Foo',
file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png")
}
custom_emoji = CustomEmoji.last
data = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(custom_emoji.group).to eq("foo")
end end
end end

View File

@ -8,6 +8,7 @@ import {
deleteCachedInlineOnebox deleteCachedInlineOnebox
} from "pretty-text/inline-oneboxer"; } from "pretty-text/inline-oneboxer";
import { extractDataAttribute } from "pretty-text/engines/discourse-markdown-it"; import { extractDataAttribute } from "pretty-text/engines/discourse-markdown-it";
import { registerEmoji } from "pretty-text/emoji";
QUnit.module("lib:pretty-text"); QUnit.module("lib:pretty-text");
@ -1519,6 +1520,24 @@ QUnit.test("emoji - emojiSet", assert => {
); );
}); });
QUnit.test("emoji - registerEmoji", assert => {
registerEmoji("foo", "/foo.png");
assert.cookedOptions(
":foo:",
{},
`<p><img src="/foo.png?v=${v}" title=":foo:" class="emoji emoji-custom only-emoji" alt=":foo:"></p>`
);
registerEmoji("bar", "/bar.png", "baz");
assert.cookedOptions(
":bar:",
{},
`<p><img src="/bar.png?v=${v}" title=":bar:" class="emoji emoji-custom only-emoji" alt=":bar:"></p>`
);
});
QUnit.test("extractDataAttribute", assert => { QUnit.test("extractDataAttribute", assert => {
assert.deepEqual(extractDataAttribute("foo="), ["data-foo", ""]); assert.deepEqual(extractDataAttribute("foo="), ["data-foo", ""]);
assert.deepEqual(extractDataAttribute("foo=bar"), ["data-foo", "bar"]); assert.deepEqual(extractDataAttribute("foo=bar"), ["data-foo", "bar"]);