FEATURE: custom emojis

This commit is contained in:
Régis Hanol 2014-12-23 01:12:26 +01:00
parent 6e1601c10d
commit 45dbdb6896
3536 changed files with 477 additions and 245 deletions

View file

@ -0,0 +1,20 @@
export default Ember.ArrayController.extend({
sortProperties: ["name"],
actions: {
emojiUploaded: function (emoji) {
this.pushObject(emoji);
},
destroy: function(emoji) {
var self = this;
return bootbox.confirm(I18n.t("admin.emoji.delete_confirm", { name: emoji.name }), I18n.t("no_value"), I18n.t("yes_value"), function (destroy) {
if (destroy) {
return Discourse.ajax("/admin/customize/emojis/" + emoji.name, { type: "DELETE" }).then(function() {
self.removeObject(emoji);
});
}
});
}
}
});

View file

@ -0,0 +1,7 @@
export default Discourse.Route.extend({
model: function() {
return Discourse.ajax("/admin/customize/emojis.json").then(function(emojis) {
return emojis.map(function (emoji) { return Ember.Object.create(emoji); });
});
}
});

View file

@ -18,8 +18,8 @@ Discourse.Route.buildRoutes(function() {
this.resource('adminSiteText', { path: '/site_text' }, function() {
this.route('edit', {path: '/:text_type'});
});
this.resource('adminUserFields', { path: '/user_fields' }, function() {
});
this.resource('adminUserFields', { path: '/user_fields' });
this.resource('adminEmojis', { path: '/emojis' });
});
this.route('api');

View file

@ -5,6 +5,7 @@
<li>{{#link-to 'adminCustomize.css_html'}}{{i18n 'admin.customize.css_html.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminSiteText'}}{{i18n 'admin.site_text.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminUserFields'}}{{i18n 'admin.user_fields.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminEmojis'}}{{i18n 'admin.emoji.title'}}{{/link-to}}</li>
</ul>
</div>
</div>

View file

@ -0,0 +1,30 @@
<div class='emoji'>
<h2>{{i18n 'admin.emoji.title'}}</h2>
<p class="desc">{{i18n 'admin.emoji.help'}}</p>
<p>{{emoji-uploader done="emojiUploaded"}}</p>
{{#if controller}}
<div class="span8">
<table id="custom_emoji">
<thead>
<tr>
<th>{{i18n "admin.emoji.image"}}</th>
<th>{{i18n "admin.emoji.name"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each e in controller}}
<tr>
<th><img class="emoji" src="{{unbound e.url}}" title="{{unbound e.name}}"></th>
<th>:{{e.name}}:</th>
<th><button {{action "destroy" e}} class='btn btn-danger no-text pull-right'>{{fa-icon 'trash-o'}} </button></th>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/if}}
</div>

View file

@ -38,8 +38,7 @@
<div class='form-display'><strong>{{f.name}}</strong></div>
<div class='form-display'>{{{f.description}}}</div>
<div class='form-display'>{{f.fieldName}}</div>
<div class='form-display'>
</div>
<div class='form-display'></div>
<div class='form-element controls'>
<button {{action "edit"}}class='btn btn-default'>{{fa-icon 'pencil'}} {{i18n 'admin.user_fields.edit'}}</button>
<button {{action "destroy"}}class='btn btn-danger'>{{fa-icon 'trash-o'}} {{i18n 'admin.user_fields.delete'}}</button>

View file

@ -0,0 +1,19 @@
import UploadMixin from 'discourse/mixins/upload';
export default Em.Component.extend(UploadMixin, {
type: "emoji",
uploadUrl: "/admin/customize/emojis",
hasName: Em.computed.notEmpty("name"),
addDisabled: Em.computed.not("hasName"),
data: function() {
return Ember.isBlank(this.get("name")) ? {} : { name: this.get("name") };
}.property("name"),
uploadDone: function (data) {
this.set("name", null);
this.sendAction("done", data.result);
}
});

View file

@ -1,4 +1,3 @@
// TODO: Make this a proper ES6 import
var ComposerView = require('discourse/views/composer').default;

View file

@ -1,5 +1,4 @@
var _groups = [
var groups = [
{
name: "emoticons",
icons: ["smile","smiley","grinning","blush","relaxed","wink","heart_eyes","kissing_heart","kissing_closed_eyes","kissing","kissing_smiling_eyes","stuck_out_tongue_winking_eye","stuck_out_tongue_closed_eyes","stuck_out_tongue","flushed","grin","pensive","relieved","unamused","disappointed","persevere","cry","joy","sob","sleepy","disappointed_relieved","cold_sweat","sweat_smile","sweat","weary","tired_face","fearful","scream","angry","rage","triumph","confounded","laughing","yum","mask","sunglasses","sleeping","dizzy_face","astonished","worried","frowning","anguished","smiling_imp","imp","open_mouth","grimacing","neutral_face","confused","hushed","no_mouth","innocent","smirk","expressionless","man_with_gua_pi_mao","man_with_turban","cop","construction_worker","guardsman","baby","boy","girl","man","woman","older_man","older_woman","person_with_blond_hair","angel","princess","smiley_cat","smile_cat","heart_eyes_cat","kissing_cat","smirk_cat","scream_cat","crying_cat_face","joy_cat","pouting_cat","japanese_ogre","japanese_goblin","see_no_evil","hear_no_evil","speak_no_evil","skull","alien","poop","fire","sparkles","star2","dizzy","boom","anger","sweat_drops","droplet","zzz","dash","ear","eyes","nose","tongue","lips","thumbsup","thumbsdown","ok_hand","punch","fist","v","wave","raised_hand","open_hands","point_up_2","point_down","point_right","point_left","raised_hands","pray","point_up","clap","muscle","walking","runner","dancer","couple","family","two_men_holding_hands","two_women_holding_hands","couplekiss","couple_with_heart","dancers","ok_woman","no_good","information_desk_person","raising_hand","massage","haircut","nail_care","bride_with_veil","person_with_pouting_face","person_frowning","bow","tophat","crown","womans_hat","athletic_shoe","mans_shoe","sandal","high_heel","boot","shirt","necktie","womans_clothes","dress","running_shirt_with_sash","jeans","kimono","bikini","briefcase","handbag","pouch","purse","eyeglasses","ribbon","closed_umbrella","lipstick","yellow_heart","blue_heart","purple_heart","green_heart","heart","broken_heart","heartpulse","heartbeat","two_hearts","sparkling_heart","revolving_hearts","cupid","love_letter","kiss","ring","gem","bust_in_silhouette","busts_in_silhouette","speech_balloon","footprints","thought_balloon"]
@ -27,29 +26,29 @@ var _groups = [
];
// scrub groups
_groups.forEach(function(group){
groups.forEach(function(group){
group.icons = _.reject(group.icons, function(obj){
return !Discourse.Emoji.exists(obj);
});
});
// export so others can modify
Discourse.Emoji.groups = _groups;
Discourse.Emoji.groups = groups;
var closeSelector = function(){
$('.emoji-modal, .emoji-modal-wrapper').remove();
$('body, textarea').off('keydown.emoji');
};
var _ungroupedIcons;
var ungroupedIcons;
var toolbar = function(selected){
if(!_ungroupedIcons){
_ungroupedIcons = [];
if(!ungroupedIcons){
ungroupedIcons = [];
var groupedIcons = {};
_.each(_groups, function(group){
_.each(groups, function(group){
_.each(group.icons, function(icon){
groupedIcons[icon] = true;
});
@ -58,16 +57,16 @@ var toolbar = function(selected){
var emojis = Discourse.Emoji.list();
_.each(emojis,function(emoji){
if(groupedIcons[emoji] !== true){
_ungroupedIcons.push(emoji);
ungroupedIcons.push(emoji);
}
});
if(_ungroupedIcons.length > 0){
_groups.push({name: 'ungrouped', icons: _ungroupedIcons});
if(ungroupedIcons.length > 0){
groups.push({name: 'ungrouped', icons: ungroupedIcons});
}
}
return _.map(_groups, function(g, i){
return _.map(groups, function(g, i){
var row = {src: Discourse.Emoji.urlFor(g.icons[0]), groupId: i};
if(i===selected){
row.selected = true;
@ -111,9 +110,10 @@ var bindEvents = function(page,offset){
var render = function(page, offset){
var rows = [];
var row = [];
var icons = _groups[page].icons;
var icons = groups[page].icons;
var max = offset + PER_PAGE;
for(var i=offset; i<(offset+PER_PAGE); i++){
for(var i=offset; i<max; i++){
if(!icons[i]){ break; }
if(row.length === PER_ROW){
rows.push(row);
@ -127,14 +127,14 @@ var render = function(page, offset){
toolbarItems: toolbar(page),
rows: rows,
prevDisabled: offset === 0,
nextDisabled: (offset + PER_PAGE + 1) > icons.length
nextDisabled: (max + 1) > icons.length
};
$('body .emoji-modal').remove();
var rendered = Ember.TEMPLATES["javascripts/emoji-toolbar.raw"](model);
var rendered = Ember.TEMPLATES["emoji-toolbar.raw"](model);
$('body').append(rendered);
bindEvents(page,offset);
bindEvents(page, offset);
};
var showSelector = function(){

View file

@ -3,47 +3,45 @@ Discourse.Emoji = {};
// bump up this number to expire all emojis
Discourse.Emoji.ImageVersion = "0"
var _emoji = <%= Emoji.all.map { |e| e["aliases"] }.flatten.inspect %>;
var emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
var _extendedEmoji = {};
var extendedEmoji = {};
Discourse.Dialect.registerEmoji = function(code, url) {
_extendedEmoji[code] = url;
extendedEmoji[code] = url;
};
Discourse.Emoji.list = function(){
var copy = _emoji.slice(0);
_.each(_extendedEmoji, function(v,k){
copy.push(k);
});
return copy;
var list = emoji.slice(0);
_.each(extendedEmoji, function(v,k){ list.push(k); });
return list;
};
var _toSearch;
var toSearch;
var search = function(term, options) {
var maxResults = (options && options["maxResults"]) || -1;
_toSearch = _toSearch || _emoji.concat(Object.keys(_extendedEmoji));
toSearch = toSearch || emoji.concat(Object.keys(extendedEmoji));
if(maxResults === 0) { return []; }
if (maxResults === 0) { return []; }
var i, results = [];
var done = function(){
var done = function() {
return maxResults > 0 && results.length >= maxResults;
}
for (i=0; i < _toSearch.length; i++) {
if (_toSearch[i].indexOf(term) === 0) {
results.push(_toSearch[i]);
for (i=0; i < toSearch.length; i++) {
if (toSearch[i].indexOf(term) === 0) {
results.push(toSearch[i]);
if(done()) { break; }
}
}
if(!done()){
for (i=0; i < _toSearch.length; i++) {
if (_toSearch[i].indexOf(term) > 0) {
results.push(_toSearch[i]);
for (i=0; i < toSearch.length; i++) {
if (toSearch[i].indexOf(term) > 0) {
results.push(toSearch[i]);
if(done()) { break; }
}
}
@ -54,20 +52,20 @@ var search = function(term, options) {
Discourse.Emoji.search = search;
var _emojiHash = {};
_emoji.forEach(function(code){
_emojiHash[code] = true;
var emojiHash = {};
emoji.forEach(function(code){
emojiHash[code] = true;
});
var urlFor = function(code) {
var url, set = Discourse.SiteSettings.emoji_set;
if(_extendedEmoji.hasOwnProperty(code)) {
url = _extendedEmoji[code];
if(extendedEmoji.hasOwnProperty(code)) {
url = extendedEmoji[code];
}
if(!url && _emojiHash.hasOwnProperty(code)) {
url = Discourse.getURL('/plugins/emoji/images/' + set + '/' + code + '.png');
if(!url && emojiHash.hasOwnProperty(code)) {
url = Discourse.getURL('/images/emoji/' + set + '/' + code + '.png');
}
if(url && url[0] !== 'h' && Discourse.CDN) {
@ -84,7 +82,7 @@ var urlFor = function(code) {
Discourse.Emoji.urlFor = urlFor;
Discourse.Emoji.exists = function(code){
return !!(_extendedEmoji.hasOwnProperty(code) || _emojiHash.hasOwnProperty(code));
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
}
function imageFor(code) {

View file

@ -297,7 +297,11 @@ Discourse.Utilities = {
// the error message is provided by the server
case 422:
bootbox.alert(data.jqXHR.responseJSON.join("\n"));
if (data.jqXHR.responseJSON.message) {
bootbox.alert(data.jqXHR.responseJSON.message);
} else {
bootbox.alert(data.jqXHR.responseJSON.join("\n"));
}
return;
}
}

View file

@ -20,15 +20,17 @@ export default Em.Mixin.create({
url: this.get('uploadUrl'),
dataType: "json",
fileInput: $upload,
formData: { image_type: this.get('type') },
dropZone: this.$(),
pasteZone: this.$()
});
$upload.on('fileuploadsubmit', function (e, data) {
var result = Discourse.Utilities.validateUploadedFiles(data.files, true);
self.setProperties({ uploadProgress: 0, uploading: result });
return result;
var isValid = Discourse.Utilities.validateUploadedFiles(data.files, true);
var form = { image_type: self.get('type') };
if (self.get("data")) { form = $.extend(form, self.get("data")); }
data.formData = form;
self.setProperties({ uploadProgress: 0, uploading: isValid });
return isValid;
});
$upload.on("fileuploadprogressall", function(e, data) {
@ -40,7 +42,11 @@ export default Em.Mixin.create({
if(data.result.url) {
self.uploadDone(data);
} else {
bootbox.alert(I18n.t('post.errors.upload'));
if (data.result.message) {
bootbox.alert(data.result.message);
} else {
bootbox.alert(I18n.t('post.errors.upload'));
}
}
});

View file

@ -0,0 +1,6 @@
{{text-field name="name" placeholderKey="admin.emoji.name" value=name}}
<input type="file" accept=".png,.gif" style="display:none" />
<button {{bind-attr disabled="addDisabled"}} {{action "selectFile"}} class='btn btn-primary'>
{{fa-icon "plus"}}
{{i18n 'admin.emoji.add'}}
</button>

View file

@ -1,10 +1,10 @@
<input type="file" accept="image/*" style="display:none" />
<div class="uploaded-image-preview" class="input-xxlarge" {{bind-attr style="backgroundStyle"}}>
<div class="image-upload-controls">
<button {{action "selectFile"}} class="btn pad-left no-text"><i class="fa fa-picture-o"></i></button>
{{#if backgroundStyle}}
<button {{action "trash"}} class="btn btn-danger pad-left no-text"><i class="fa fa-trash-o"></i></button>
{{/if}}
<span {{bind-attr class=":btn uploading::hidden"}}>{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%</span>
<button {{action "selectFile"}} class="btn pad-left no-text">{{fa-icon "picture-o"}}</button>
{{#if backgroundStyle}}
<button {{action "trash"}} class="btn btn-danger pad-left no-text">{{fa-icon "trash-o"}}</button>
{{/if}}
<span {{bind-attr class=":btn uploading::hidden"}}>{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%</span>
</div>
</div>

View file

@ -12,14 +12,15 @@
// Stuff we need to load first
//= require ./discourse/helpers/i18n
//= require ./discourse/lib/ember_compat_handlebars
//= require ./discourse/helpers/register-unbound
//= require ./discourse/lib/computed
//= require ./discourse/helpers/register-unbound
//= require ./discourse/mixins/scrolling
//= require_tree ./discourse/mixins
//= require ./discourse/lib/markdown
//= require ./discourse/lib/search-for-term
//= require ./discourse/views/view
//= require ./discourse/views/container
//= require ./discourse/lib/user-search
//= require ./discourse/lib/autocomplete
//= require ./discourse/lib/after-transition
//= require ./discourse/lib/debounce
//= require ./discourse/models/model
//= require ./discourse/models/user_action
@ -30,6 +31,8 @@
//= require ./discourse/controllers/discovery-sortable
//= require ./discourse/controllers/object
//= require ./discourse/controllers/navigation/default
//= require ./discourse/views/view
//= require ./discourse/views/container
//= require ./discourse/views/modal_body_view
//= require ./discourse/views/flag
//= require ./discourse/views/combo-box
@ -38,6 +41,7 @@
//= require ./discourse/views/notifications-button
//= require ./discourse/views/topic-notifications-button
//= require ./discourse/views/pagedown-preview
//= require ./discourse/views/composer
//= require ./discourse/routes/discourse_route
//= require ./discourse/routes/build-topic-route
//= require ./discourse/routes/restricted-user
@ -52,8 +56,9 @@
//= require ./discourse/helpers/cold-age-class
//= require ./discourse/helpers/loading-spinner
//= require ./discourse/helpers/category-link
//= require ./discourse/dialects/dialect
//= require ./discourse/lib/emoji/emoji
//= require_tree ./discourse/dialects
//= require_tree ./discourse/controllers
//= require_tree ./discourse/lib

View file

@ -0,0 +1,36 @@
class Admin::EmojisController < Admin::AdminController
def index
render_serialized(Emoji.custom, EmojiSerializer, root: false)
end
def create
file = params[:file] || params[:files].first
name = params[:name] || File.basename(file.original_filename, ".*")
# fix the name
name = name.gsub(/[^a-z0-9]+/i, '_')
.gsub(/_{2,}/, '_')
.downcase
# check the name doesn't already exist
if Emoji.all.detect { |e| e.name == name }
render json: failed_json.merge(message: I18n.t("emoji.errors.name_already_exists", name: name)), status: 422
else
if emoji = Emoji.create_for(file, name)
render_serialized(emoji, EmojiSerializer, root: false)
else
render json: failed_json.merge(message: I18n.t("emoji.errors.error_while_storing_emoji")), status: 422
end
end
end
def destroy
name = params.require(:id)
Emoji.custom.detect { |e| e.name == name }.try(:remove)
render nothing: true
end
end

View file

@ -255,6 +255,7 @@ class ApplicationController < ActionController::Base
store_preloaded("siteSettings", SiteSetting.client_settings_json)
store_preloaded("customHTML", custom_html_json)
store_preloaded("banner", banner_json)
store_preloaded("customEmoji", custom_emoji)
end
def preload_current_user_data
@ -281,7 +282,6 @@ class ApplicationController < ActionController::Base
end
def banner_json
json = ApplicationController.banner_json_cache["json"]
unless json
@ -293,6 +293,11 @@ class ApplicationController < ActionController::Base
json
end
def custom_emoji
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
MultiJson.dump(serializer)
end
def render_json_error(obj)
render json: MultiJson.dump(create_errors_json(obj)), status: 422
end

92
app/models/emoji.rb Normal file
View file

@ -0,0 +1,92 @@
class Emoji
include ActiveModel::SerializerSupport
attr_reader :path
attr_accessor :name, :url
# whitelist emojis so that new user can post emojis
Post::white_listed_image_classes << "emoji"
def initialize(path = nil)
@path = path
end
def remove
return if path.blank?
if File.exists?(path)
File.delete(path) rescue nil
Emoji.clear_cache
end
end
def self.all
@all ||= standard | custom
end
def self.standard
@standard ||= load_standard
end
def self.custom
@custom ||= load_custom
end
def self.create_from_path(path)
extension = File.extname(path)
Emoji.new(path).tap do |e|
e.name = File.basename(path, ".*")
e.url = "/#{base_url}/#{e.name}#{extension}"
end
end
def self.create_from_db_item(emoji)
name = emoji["aliases"].first
filename = "#{name}.png"
Emoji.new.tap do |e|
e.name = name
e.url = "/images/emoji/#{SiteSetting.emoji_set}/#{filename}"
end
end
def self.create_for(file, name)
extension = File.extname(file.original_filename)
path = "#{Emoji.base_directory}/#{name}#{extension}"
# store the emoji
FileUtils.mkdir_p(Pathname.new(path).dirname)
File.open(path, "wb") { |f| f << file.tempfile.read }
# clear the cache
Emoji.clear_cache
# return created emoji
Emoji.custom.detect { |e| e.name == name }
end
def self.clear_cache
@custom = nil
@all = nil
end
def self.db_file
"lib/emoji/db.json"
end
def self.load_standard
File.open(db_file, "r:UTF-8") { |f| JSON.parse(f.read) }
.map { |emoji| Emoji.create_from_db_item(emoji) }
end
def self.load_custom
Dir.glob(File.join(Emoji.base_directory, "*.{png,gif}"))
.sort
.map { |emoji| Emoji.create_from_path(emoji) }
end
def self.base_directory
"public/#{base_url}"
end
def self.base_url
db = RailsMultisite::ConnectionManagement.current_db
"uploads/#{db}/_emoji"
end
end

View file

@ -0,0 +1,38 @@
require 'enum_site_setting'
class EmojiSetSiteSetting < EnumSiteSetting
# fix the URLs when changing the site setting
DiscourseEvent.on(:site_setting_saved) do |site_setting|
if site_setting.name.to_s == "emoji_set" && site_setting.value_changed?
before = "/images/emoji/#{site_setting.value_was}/"
after = "/images/emoji/#{site_setting.value}/"
Scheduler::Defer.later("Fix Emoji Links") do
Post.exec_sql("UPDATE posts SET cooked = REPLACE(cooked, :before, :after) WHERE cooked LIKE :like",
before: before,
after: after,
like: "%#{before}%"
)
end
end
end
def self.valid_value?(val)
values.any? { |v| v[:value] == val.to_s }
end
def self.values
@values ||= [
{ name: 'apple_international', value: 'apple' },
{ name: 'google', value: 'google' },
{ name: 'twitter', value: 'twitter' },
{ name: 'emoji_one', value: 'emoji_one' },
]
end
def self.translate_names?
true
end
end

View file

@ -0,0 +1,3 @@
class EmojiSerializer < ApplicationSerializer
attributes :name, :url
end

View file

@ -36,6 +36,9 @@
Discourse.Environment = '<%= Rails.env %>';
Discourse.SiteSettings = PreloadStore.get('siteSettings');
Discourse.LetterAvatarVersion = <%= LetterAvatar::VERSION %>;
PreloadStore.get("customEmoji").forEach(function(emoji) {
Discourse.Dialect.registerEmoji(emoji.name, emoji.url);
});
Discourse.Router = Ember.Router.extend({ location: 'discourse-location' });
Discourse.Route.mapRoutes();
Discourse.start();

View file

@ -646,7 +646,14 @@ en:
title: "with GitHub"
message: "Authenticating with GitHub (make sure pop up blockers are not enabled)"
apple_international: "Apple/International"
google: "Google"
twitter: "Twitter"
emoji_one: "Emoji One"
composer:
emoji: "Emoji :smile:"
add_warning: "This is an official warning."
posting_not_on_topic: "Which topic do you want to reply to?"
saving_draft_tip: "saving"
@ -2179,6 +2186,14 @@ en:
with_post_time: <span class="username">%{username}</span> for post in %{link} at <span class="time">%{time}</span>
with_time: <span class="username">%{username}</span> at <span class="time">%{time}</span>
emoji:
title: "Emoji"
help: "Add new emoji that will be available to everyone. (PROTIP: drag & drop multiple files at once)"
add: "Add New Emoji"
name: "Name"
image: "Image"
delete_confirm: "Are you sure you want to delete the :%{name}: emoji?"
lightbox:
download: "download"

View file

@ -1029,6 +1029,9 @@ en:
prevent_anons_from_downloading_files: "Prevent anonymous users from downloading attachments. WARNING: this will prevent any non-image site assets posted as attachments from working."
enable_emoji: "Enable emoji"
emoji_set: "How would you like your emoji?"
errors:
invalid_email: "Invalid email address."
invalid_username: "There's no user with that username."
@ -1089,6 +1092,11 @@ en:
change_owner:
post_revision_text: "Ownership transferred from %{old_user} to %{new_user}"
emoji:
errors:
name_already_exists: "Sorry, the name '%{name}' is already used by another emoji."
error_while_storing_emoji: "Sorry, there has been an error while storing the emoji."
topic_statuses:
archived_enabled: "This topic is now archived. It is frozen and cannot be changed in any way."
archived_disabled: "This topic is now unarchived. It is no longer frozen, and can be changed."

View file

@ -125,6 +125,7 @@ Discourse::Application.routes.draw do
resources :site_text, constraints: AdminConstraint.new
resources :site_text_types, constraints: AdminConstraint.new
resources :user_fields, constraints: AdminConstraint.new
resources :emojis, constraints: AdminConstraint.new
end
resources :color_schemes, constraints: AdminConstraint.new

View file

@ -389,6 +389,13 @@ posting:
default: ''
refresh: true
type: list
enable_emoji:
default: true
client: true
emoji_set:
default: 'emoji_one'
client: true
enum: 'EmojiSetSiteSetting'
email:
email_time_window_mins:

View file

@ -0,0 +1,20 @@
class FixEmojiPathTake2 < ActiveRecord::Migration
OLD_URL = '/plugins/emoji/images/'
NEW_URL = '/images/emoji/'
def up
execute <<-SQL
UPDATE posts
SET cooked = REPLACE(cooked, '#{OLD_URL}', '#{NEW_URL}')
WHERE cooked LIKE '%#{OLD_URL}%'
SQL
end
def down
execute <<-SQL
UPDATE posts
SET cooked = REPLACE(cooked, '#{NEW_URL}', '#{OLD_URL}')
WHERE cooked LIKE '%#{NEW_URL}%'
SQL
end
end

View file

@ -268,8 +268,8 @@ module BackupRestore
end
def extract_uploads
log "Extracting uploads..."
if `tar --list --file #{@tar_filename} | grep 'uploads/'`.present?
log "Extracting uploads..."
FileUtils.cd(File.join(Rails.root, "public")) do
`tar --extract --keep-newer-files --file #{@tar_filename} uploads/`
end

View file

@ -1,7 +1,6 @@
# Like a hash, just does its best to stay in sync across the farm
# On boot all instances are blank, but they populate as various processes
# fill it up
#
require 'weakref'
@ -31,9 +30,9 @@ class DistributedCache
hash = current.hash(message.site_id)
case payload["op"]
when "set" then hash[payload["key"]] = payload["value"]
when "set" then hash[payload["key"]] = payload["value"]
when "delete" then hash.delete(payload["key"])
when "clear" then hash.clear
when "clear" then hash.clear
end
rescue WeakRef::RefError
@ -64,7 +63,7 @@ class DistributedCache
def self.publish(hash, message)
message[:origin] = hash.object_id
message[:hash_key] = hash.key
MessageBus.publish(channel_name, message, {user_ids: [-1]})
MessageBus.publish(channel_name, message, { user_ids: [-1] })
end
def self.set(hash, key, value)
@ -72,11 +71,11 @@ class DistributedCache
end
def self.delete(hash, key)
publish(hash, { op: :delete, key: key})
publish(hash, { op: :delete, key: key })
end
def self.clear(hash)
publish(hash, {op: :clear})
publish(hash, { op: :clear })
end
def self.register(hash)
@ -93,7 +92,6 @@ class DistributedCache
@data = {}
end
def []=(k,v)
k = k.to_s if Symbol === k
DistributedCache.set(self, k, v)
@ -116,7 +114,6 @@ class DistributedCache
hash.clear
end
def hash(db = nil)
db ||= RailsMultisite::ConnectionManagement.current_db
@data[db] ||= ThreadSafe::Hash.new

View file

@ -163,7 +163,7 @@ module Email
img.remove
end
if img['src'] =~ /plugins\/emoji/
if img['src'] =~ /images\/emoji/
img.replace img['title']
end
end

View file

@ -9,7 +9,7 @@ end
def download_emojis_for(set, url_template, options={})
puts "Downloading emojis for #{set}..."
path = "plugins/emoji/public/images/#{set}"
path = "public/images/emoji/#{set}"
FileUtils.rm_rf(path) rescue nil
FileUtils.mkdir_p(path) rescue nil
@ -41,7 +41,7 @@ GOOGLE_EMOJIS = {35=>1, 48=>2, 49=>3, 50=>4, 51=>5, 52=>6, 53=>7, 54=>8, 55=>9,
def download_google_emojis(url_template)
puts "Downloading emojis for google..."
path = "plugins/emoji/public/images/google"
path = "public/images/google"
FileUtils.rm_rf(path) rescue nil
FileUtils.mkdir_p(path) rescue nil

View file

@ -1,3 +0,0 @@
# Discourse Emoji Gem
Adds Emoji support to discourse. Thanks to the gemoji gem for the assets.

View file

@ -1,22 +0,0 @@
require 'enum_site_setting'
class EmojiSetSiteSetting < EnumSiteSetting
def self.valid_value?(val)
values.any? { |v| v[:value] == val.to_s }
end
def self.values
@values ||= [
{ name: 'apple_international', value: 'apple' },
{ name: 'google', value: 'google' },
{ name: 'twitter', value: 'twitter' },
{ name: 'emoji_one', value: 'emoji_one' },
]
end
def self.translate_names?
true
end
end

View file

@ -1,8 +0,0 @@
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/emoji/engine', __FILE__)
require 'rails/all'
require 'rails/engine/commands'

View file

@ -1,6 +0,0 @@
de:
admin_js:
admin:
site_settings:
categories:
plugins: "Plug-ins"

View file

@ -1,15 +0,0 @@
en:
js:
composer:
emoji: "Emoji :smile:"
apple_international: "Apple/International"
google: "Google"
twitter: "Twitter"
emoji_one: "Emoji One"
admin_js:
admin:
site_settings:
categories:
plugins: "Plugins"

View file

@ -1,6 +0,0 @@
pl_PL:
admin_js:
admin:
site_settings:
categories:
plugins: "Wtyczki"

View file

@ -1,6 +0,0 @@
ru:
admin_js:
admin:
site_settings:
categories:
plugins: "Плагины"

View file

@ -1,6 +0,0 @@
zh_CN:
admin_js:
admin:
site_settings:
categories:
plugins: "插件"

View file

@ -1,3 +0,0 @@
de:
site_settings:
enable_emoji: "das Emoji-Plug-in aktivieren"

View file

@ -1,4 +0,0 @@
en:
site_settings:
enable_emoji: "Enable the emoji plugin"
emoji_set: "How would you like your emoji?"

View file

@ -1,3 +0,0 @@
pl_PL:
site_settings:
enable_emoji: "Włącz wyświetlanie Emoji"

View file

@ -1,3 +0,0 @@
ru:
site_settings:
enable_emoji: "Включить плагин emoji"

View file

@ -1,3 +0,0 @@
zh_CN:
site_settings:
enable_emoji: "启用绘文字emoji插件"

View file

@ -1,8 +0,0 @@
plugins:
enable_emoji:
default: true
client: true
emoji_set:
default: 'emoji_one'
client: true
enum: 'EmojiSetSiteSetting'

View file

@ -1,4 +0,0 @@
require "emoji/engine"
module Emoji
end

View file

@ -1,20 +0,0 @@
module Emoji
class Engine < ::Rails::Engine
isolate_namespace Emoji
end
def self.all
return @all if defined?(@all)
@all = parse_db
end
def self.db_file
File.expand_path('../../../db.json', __FILE__)
end
private
def self.parse_db
File.open(db_file, "r:UTF-8") { |f| JSON.parse(f.read) }
end
end

View file

@ -1,3 +0,0 @@
module Emoji
VERSION = "0.0.1"
end

View file

@ -1,33 +0,0 @@
# name: emoji
# about: emoji support for Discourse
# version: 0.2
# authors: Sam Saffron, Robin Ward, Régis Hanol
load File.expand_path('../lib/emoji/engine.rb', __FILE__)
register_asset('javascripts/emoji.js.erb', :server_side)
register_asset('javascripts/emoji-autocomplete.js', :composer)
register_asset('javascripts/discourse/templates/emoji-toolbar.raw.hbs', :composer)
register_asset('javascripts/emoji-toolbar.js', :composer)
register_asset('stylesheets/emoji.css')
def site_setting_saved(site_setting)
return unless site_setting.name.to_s == "emoji_set"
return unless site_setting.value_changed?
before = "/plugins/emoji/images/#{site_setting.value_was}/"
after = "/plugins/emoji/images/#{site_setting.value}/"
Scheduler::Defer.later "Fix Emoji Links" do
Post.exec_sql("UPDATE posts SET cooked = REPLACE(cooked, :before, :after) WHERE cooked LIKE :like",
before: before,
after: after,
like: "%#{before}%"
)
end
end
listen_for(:site_setting_saved)
after_initialize do
# whitelist emojis so that new user can post emojis
Post::white_listed_image_classes << "emoji"
end

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Some files were not shown because too many files have changed in this diff Show more