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:
parent
fa5ba6beb8
commit
0996c3b7b3
|
@ -1,37 +1,74 @@
|
|||
import { sort } from "@ember/object/computed";
|
||||
import EmberObject from "@ember/object";
|
||||
import EmberObject, { action, computed } from "@ember/object";
|
||||
import Controller from "@ember/controller";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
const ALL_FILTER = "all";
|
||||
|
||||
export default Controller.extend({
|
||||
sortedEmojis: sort("model", "emojiSorting"),
|
||||
filter: null,
|
||||
sorting: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.emojiSorting = ["name"];
|
||||
this.setProperties({
|
||||
filter: ALL_FILTER,
|
||||
sorting: ["group", "name"]
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
emojiUploaded(emoji) {
|
||||
emoji.url += "?t=" + new Date().getTime();
|
||||
this.model.pushObject(EmberObject.create(emoji));
|
||||
},
|
||||
sortedEmojis: sort("filteredEmojis.[]", "sorting"),
|
||||
|
||||
destroy(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
emojiGroups: computed("model", {
|
||||
get() {
|
||||
return this.model.mapBy("group").uniq();
|
||||
}
|
||||
}),
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,35 +1,49 @@
|
|||
<div class='emoji'>
|
||||
<h2>{{i18n 'admin.emoji.title'}}</h2>
|
||||
<div class='admin-emojis'>
|
||||
<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}}
|
||||
<div>
|
||||
<table id="custom_emoji">
|
||||
<thead>
|
||||
<table id="custom_emoji">
|
||||
<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>
|
||||
<th>{{i18n "admin.emoji.image"}}</th>
|
||||
<th>{{i18n "admin.emoji.name"}}</th>
|
||||
<th></th>
|
||||
<th><img class="emoji emoji-custom" src={{e.url}} title={{e.name}}></th>
|
||||
<th>:{{e.name}}:</th>
|
||||
<th>{{e.group}}</th>
|
||||
<th>
|
||||
{{d-button
|
||||
action=(action "destroyEmoji" e)
|
||||
class="btn-danger"
|
||||
icon="far-trash-alt"
|
||||
}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each sortedEmojis as |e|}}
|
||||
<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>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -14,9 +14,26 @@ import ENV, { INPUT_DELAY } from "discourse-common/config/environment";
|
|||
const { run } = Ember;
|
||||
|
||||
const PER_ROW = 11;
|
||||
const customEmojis = _.keys(extendedEmojiList()).map(code => {
|
||||
return { code, src: emojiUrlFor(code) };
|
||||
});
|
||||
function customEmojis() {
|
||||
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({
|
||||
automaticPositioning: true,
|
||||
|
@ -35,7 +52,9 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
show() {
|
||||
const template = findRawTemplate("emoji-picker")({ customEmojis });
|
||||
const template = findRawTemplate("emoji-picker")({
|
||||
customEmojis: customEmojis()
|
||||
});
|
||||
this.$picker.html(template);
|
||||
|
||||
this.$filter = this.$picker.find(".filter");
|
||||
|
@ -579,7 +598,7 @@ export default Component.extend({
|
|||
this.$picker.width() -
|
||||
this.$picker.find(".categories-column").width() -
|
||||
this.$picker.find(".diversity-picker").width() -
|
||||
32;
|
||||
60;
|
||||
this.$picker.find(".info").css("max-width", infoMaxWidth);
|
||||
},
|
||||
|
||||
|
|
|
@ -1,23 +1,53 @@
|
|||
import { notEmpty, not } from "@ember/object/computed";
|
||||
import { action } from "@ember/object";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import UploadMixin from "discourse/mixins/upload";
|
||||
|
||||
const DEFAULT_GROUP = "default";
|
||||
|
||||
export default Component.extend(UploadMixin, {
|
||||
type: "emoji",
|
||||
uploadUrl: "/admin/customize/emojis",
|
||||
hasName: notEmpty("name"),
|
||||
hasGroup: notEmpty("group"),
|
||||
addDisabled: not("hasName"),
|
||||
group: "default",
|
||||
emojiGroups: null,
|
||||
newEmojiGroups: null,
|
||||
tagName: null,
|
||||
|
||||
uploadOptions() {
|
||||
return {
|
||||
sequentialUploads: true
|
||||
};
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.set("newEmojiGroups", this.emojiGroups);
|
||||
},
|
||||
|
||||
@discourseComputed("hasName", "name")
|
||||
data(hasName, name) {
|
||||
return hasName ? { name } : {};
|
||||
uploadOptions() {
|
||||
return { sequentialUploads: true };
|
||||
},
|
||||
|
||||
@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() {
|
||||
|
@ -25,7 +55,7 @@ export default Component.extend(UploadMixin, {
|
|||
},
|
||||
|
||||
uploadDone(upload) {
|
||||
this.set("name", null);
|
||||
this.done(upload);
|
||||
this.done(upload, this.group);
|
||||
this.setProperties({ name: null, group: DEFAULT_GROUP });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ export default {
|
|||
});
|
||||
|
||||
(PreloadStore.get("customEmoji") || []).forEach(emoji =>
|
||||
registerEmoji(emoji.name, emoji.url)
|
||||
registerEmoji(emoji.name, emoji.url, emoji.group)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,11 +1,48 @@
|
|||
{{text-field name="name" placeholderKey="admin.emoji.name" value=name}}
|
||||
|
||||
<label class="btn 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>
|
||||
{{#conditional-loading-section isLoading=uploading}}
|
||||
<div class="emoji-uploader">
|
||||
<div class="control">
|
||||
<span class="label">
|
||||
{{i18n "admin.emoji.name"}}
|
||||
</span>
|
||||
<div class="input">
|
||||
{{input
|
||||
name="name"
|
||||
placeholderKey="admin.emoji.name"
|
||||
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}}
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if !Emoji.custom.blank? %>
|
||||
<div class='category-icon'>
|
||||
<button data-tabicon="<%= Emoji.custom.first.name %>" type="button" class="emoji" tabindex="-1" data-section="ungrouped" title="{{i18n 'emoji_picker.custom'}}"></button>
|
||||
</div>
|
||||
<% Emoji.custom.group_by { |emoji| emoji.group }.each do |group, emojis| %>
|
||||
<% if emojis.present? %>
|
||||
<div class='category-icon'>
|
||||
<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 %>
|
||||
</div>
|
||||
|
||||
|
@ -49,18 +51,22 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
{{#if customEmojis.length}}
|
||||
<div class='section' data-section='ungrouped'>
|
||||
<div class='section-header'>
|
||||
<span class="title">{{i18n 'emoji_picker.custom'}}</span>
|
||||
{{#each customEmojis as |emojis|}}
|
||||
{{#if emojis.length}}
|
||||
<div class='section' data-section='custom-{{emojis.firstObject.group}}'>
|
||||
<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 class='section-group'>
|
||||
{{#each customEmojis as |emoji|}}
|
||||
<button style="background-url: url("{{emoji.src}}")" type="button" class="emoji" tabindex="-1" title="{{emoji.code}}"></button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class='footer'>
|
||||
<div class='info'></div>
|
||||
|
|
|
@ -10,9 +10,9 @@ import { IMAGE_VERSION } from "pretty-text/emoji/version";
|
|||
|
||||
const extendedEmoji = {};
|
||||
|
||||
export function registerEmoji(code, url) {
|
||||
export function registerEmoji(code, url, group) {
|
||||
code = code.toLowerCase();
|
||||
extendedEmoji[code] = url;
|
||||
extendedEmoji[code] = { url, group };
|
||||
}
|
||||
|
||||
export function extendedEmojiList() {
|
||||
|
@ -92,7 +92,7 @@ function isReplacableInlineEmoji(string, index, inlineEmoji) {
|
|||
}
|
||||
|
||||
export function performEmojiUnescape(string, opts) {
|
||||
if (!string || typeof string !== "string") {
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -126,6 +126,8 @@ export function performEmojiUnescape(string, opts) {
|
|||
} alt='${emojiVal}' class='${classes}'>`
|
||||
: m;
|
||||
});
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
export function performEmojiEscape(string, opts) {
|
||||
|
@ -143,6 +145,8 @@ export function performEmojiEscape(string, opts) {
|
|||
|
||||
return m;
|
||||
});
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
export function isCustomEmoji(code, opts) {
|
||||
|
@ -157,11 +161,11 @@ export function buildEmojiUrl(code, opts) {
|
|||
let url;
|
||||
code = String(code).toLowerCase();
|
||||
if (extendedEmoji.hasOwnProperty(code)) {
|
||||
url = extendedEmoji[code];
|
||||
url = extendedEmoji[code].url;
|
||||
}
|
||||
|
||||
if (opts && opts.customEmoji && opts.customEmoji[code]) {
|
||||
url = opts.customEmoji[code];
|
||||
url = opts.customEmoji[code].url || opts.customEmoji[code];
|
||||
}
|
||||
|
||||
const noToneMatch = code.match(/([^:]+):?/);
|
||||
|
|
|
@ -979,3 +979,4 @@ a.inline-editable-field {
|
|||
@import "common/admin/admin_report_table";
|
||||
@import "common/admin/admin_report_inline_table";
|
||||
@import "common/admin/admin_intro";
|
||||
@import "common/admin/admin_emojis";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -556,7 +556,7 @@
|
|||
}
|
||||
|
||||
#custom_emoji {
|
||||
width: 27%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-body .inputs .branch {
|
||||
|
|
|
@ -45,6 +45,8 @@ sup img.emoji {
|
|||
justify-content: space-between;
|
||||
border-right: 1px solid $primary-low;
|
||||
min-width: 36px;
|
||||
overflow-y: auto;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.emoji-picker .category-icon {
|
||||
|
|
|
@ -6,3 +6,7 @@
|
|||
.emoji-picker .category-icon {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.emoji-picker .categories-column {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ class Admin::EmojisController < Admin::AdminController
|
|||
def create
|
||||
file = params[:file] || params[:files].first
|
||||
name = params[:name] || File.basename(file.original_filename, ".*")
|
||||
group = params[:group] ? params[:group].downcase : nil
|
||||
|
||||
hijack do
|
||||
# fix the name
|
||||
|
@ -26,11 +27,11 @@ class Admin::EmojisController < Admin::AdminController
|
|||
|
||||
data =
|
||||
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
|
||||
Emoji.clear_cache
|
||||
{ name: custom_emoji.name, url: custom_emoji.upload.url }
|
||||
{ name: custom_emoji.name, url: custom_emoji.upload.url, group: group }
|
||||
else
|
||||
good = false
|
||||
failed_json.merge(errors: custom_emoji.errors.full_messages)
|
||||
|
|
|
@ -14,6 +14,7 @@ end
|
|||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# upload_id :integer not null
|
||||
# group :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
|
|
@ -6,9 +6,11 @@ class Emoji
|
|||
|
||||
FITZPATRICK_SCALE ||= [ "1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff" ]
|
||||
|
||||
DEFAULT_GROUP ||= "default"
|
||||
|
||||
include ActiveModel::SerializerSupport
|
||||
|
||||
attr_accessor :name, :url, :tonable
|
||||
attr_accessor :name, :url, :tonable, :group
|
||||
|
||||
def self.all
|
||||
Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom }
|
||||
|
@ -104,15 +106,19 @@ class Emoji
|
|||
result << Emoji.new.tap do |e|
|
||||
e.name = emoji.name
|
||||
e.url = emoji.upload&.url
|
||||
e.group = emoji.group || DEFAULT_GROUP
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Plugin::CustomEmoji.emojis.each do |name, url|
|
||||
result << Emoji.new.tap do |e|
|
||||
e.name = name
|
||||
url = (Discourse.base_uri + url) if url[/^\/[^\/]/]
|
||||
e.url = url
|
||||
Plugin::CustomEmoji.emojis.each do |group, emojis|
|
||||
emojis.each do |name, url|
|
||||
result << Emoji.new.tap do |e|
|
||||
e.name = name
|
||||
url = (Discourse.base_uri + url) if url[/^\/[^\/]/]
|
||||
e.url = url
|
||||
e.group = group || DEFAULT_GROUP
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EmojiSerializer < ApplicationSerializer
|
||||
attributes :name, :url
|
||||
attributes :name, :url, :group
|
||||
end
|
||||
|
|
|
@ -1651,7 +1651,6 @@ en:
|
|||
objects: Objects
|
||||
symbols: Symbols
|
||||
flags: Flags
|
||||
custom: Custom emojis
|
||||
recent: Recently used
|
||||
default_tone: No skin tone
|
||||
light_tone: Light skin tone
|
||||
|
@ -1659,6 +1658,7 @@ en:
|
|||
medium_tone: Medium skin tone
|
||||
medium_dark_tone: Medium dark skin tone
|
||||
dark_tone: Dark skin tone
|
||||
default: Custom emojis
|
||||
|
||||
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)"
|
||||
add: "Add New Emoji"
|
||||
name: "Name"
|
||||
group: "Group"
|
||||
image: "Image"
|
||||
delete_confirm: "Are you sure you want to delete the :%{name}: emoji?"
|
||||
|
||||
|
|
|
@ -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
|
|
@ -29,17 +29,17 @@ class DbHelper
|
|||
|
||||
text_columns.each do |table, columns|
|
||||
set = columns.map do |column|
|
||||
replace = "REPLACE(#{column[:name]}, :from, :to)"
|
||||
replace = "REPLACE(\"#{column[:name]}\", :from, :to)"
|
||||
replace = truncate(replace, table, column)
|
||||
"#{column[:name]} = #{replace}"
|
||||
"\"#{column[:name]}\" = #{replace}"
|
||||
end.join(", ")
|
||||
|
||||
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 ")
|
||||
|
||||
rows = DB.exec(<<~SQL, from: from, to: to, like: like)
|
||||
UPDATE #{table}
|
||||
UPDATE \"#{table}\"
|
||||
SET #{set}
|
||||
WHERE #{where}
|
||||
SQL
|
||||
|
@ -55,17 +55,17 @@ class DbHelper
|
|||
|
||||
text_columns.each do |table, columns|
|
||||
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)
|
||||
"#{column[:name]} = #{replace}"
|
||||
"\"#{column[:name]}\" = #{replace}"
|
||||
end.join(", ")
|
||||
|
||||
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 ")
|
||||
|
||||
rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match)
|
||||
UPDATE #{table}
|
||||
UPDATE \"#{table}\"
|
||||
SET #{set}
|
||||
WHERE #{where}
|
||||
SQL
|
||||
|
@ -84,9 +84,9 @@ class DbHelper
|
|||
next if excluded_tables.include?(r.table_name)
|
||||
|
||||
rows = DB.query(<<~SQL, like: like)
|
||||
SELECT #{r.column_name}
|
||||
FROM #{r.table_name}
|
||||
WHERE #{r.column_name} LIKE :like
|
||||
SELECT \"#{r.column_name}\"
|
||||
FROM \"#{r.table_name}\"
|
||||
WHERE \""#{r.column_name}"\" LIKE :like
|
||||
SQL
|
||||
|
||||
if rows.size > 0
|
||||
|
|
|
@ -6,21 +6,29 @@ require_dependency 'plugin/metadata'
|
|||
require_dependency 'auth'
|
||||
|
||||
class Plugin::CustomEmoji
|
||||
CACHE_KEY ||= "plugin-emoji"
|
||||
def self.cache_key
|
||||
@@cache_key ||= "plugin-emoji"
|
||||
@@cache_key ||= CACHE_KEY
|
||||
end
|
||||
|
||||
def self.emojis
|
||||
@@emojis ||= {}
|
||||
end
|
||||
|
||||
def self.register(name, url)
|
||||
@@cache_key = Digest::SHA1.hexdigest(cache_key + name)[0..10]
|
||||
emojis[name] = url
|
||||
def self.clear_cache
|
||||
@@cache_key = CACHE_KEY
|
||||
@@emojis = {}
|
||||
end
|
||||
|
||||
def self.unregister(name)
|
||||
emojis.delete(name)
|
||||
def self.register(name, url, group = Emoji::DEFAULT_GROUP)
|
||||
@@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
|
||||
|
||||
def self.translations
|
||||
|
@ -471,8 +479,9 @@ class Plugin::Instance
|
|||
DiscoursePluginRegistry.register_seed_path_builder(&block)
|
||||
end
|
||||
|
||||
def register_emoji(name, url)
|
||||
Plugin::CustomEmoji.register(name, url)
|
||||
def register_emoji(name, url, group = Emoji::DEFAULT_GROUP)
|
||||
Plugin::CustomEmoji.register(name, url, group)
|
||||
Emoji.clear_cache
|
||||
end
|
||||
|
||||
def translate_emoji(from, to)
|
||||
|
|
|
@ -538,4 +538,30 @@ describe Plugin::Instance do
|
|||
expect(Reviewable.types).to match_array(current_list << new_element)
|
||||
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
|
||||
|
|
|
@ -59,22 +59,38 @@ RSpec.describe Admin::EmojisController do
|
|||
it 'should allow an admin to add a custom emoji' do
|
||||
Emoji.expects(:clear_cache)
|
||||
|
||||
post "/admin/customize/emojis.json", params: {
|
||||
name: 'test',
|
||||
file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png")
|
||||
}
|
||||
post "/admin/customize/emojis.json", params: {
|
||||
name: 'test',
|
||||
file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png")
|
||||
}
|
||||
|
||||
custom_emoji = CustomEmoji.last
|
||||
upload = custom_emoji.upload
|
||||
custom_emoji = CustomEmoji.last
|
||||
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)
|
||||
expect(data["errors"]).to eq(nil)
|
||||
expect(data["name"]).to eq(custom_emoji.name)
|
||||
expect(data["url"]).to eq(upload.url)
|
||||
it 'should allow an admin to add a custom emoji with a custom group' do
|
||||
Emoji.expects(:clear_cache)
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
deleteCachedInlineOnebox
|
||||
} from "pretty-text/inline-oneboxer";
|
||||
import { extractDataAttribute } from "pretty-text/engines/discourse-markdown-it";
|
||||
import { registerEmoji } from "pretty-text/emoji";
|
||||
|
||||
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 => {
|
||||
assert.deepEqual(extractDataAttribute("foo="), ["data-foo", ""]);
|
||||
assert.deepEqual(extractDataAttribute("foo=bar"), ["data-foo", "bar"]);
|
||||
|
|
Loading…
Reference in New Issue