mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: custom emojis
This commit is contained in:
20
app/assets/javascripts/admin/controllers/admin-emojis.js.es6
Normal file
20
app/assets/javascripts/admin/controllers/admin-emojis.js.es6
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
7
app/assets/javascripts/admin/routes/admin-emojis.js.es6
Normal file
7
app/assets/javascripts/admin/routes/admin-emojis.js.es6
Normal 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); });
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
30
app/assets/javascripts/admin/templates/emojis.hbs
Normal file
30
app/assets/javascripts/admin/templates/emojis.hbs
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// TODO: Make this a proper ES6 import
|
||||
var ComposerView = require('discourse/views/composer').default;
|
||||
|
||||
ComposerView.on("initWmdEditor", function(){
|
||||
if (!Discourse.SiteSettings.enable_emoji) { return; }
|
||||
|
||||
var template = Handlebars.compile(
|
||||
"<div class='autocomplete'>" +
|
||||
"<ul>" +
|
||||
"{{#each options}}" +
|
||||
"<li>" +
|
||||
"<a href='#'><img src='{{src}}' class='emoji'> {{code}}</a>" +
|
||||
"</li>" +
|
||||
"{{/each}}" +
|
||||
"</ul>" +
|
||||
"</div>"
|
||||
);
|
||||
|
||||
$('#wmd-input').autocomplete({
|
||||
template: template,
|
||||
key: ":",
|
||||
transformComplete: function(v){ return v.code + ":"; },
|
||||
dataSource: function(term){
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
var full = ":" + term;
|
||||
term = term.toLowerCase();
|
||||
|
||||
if (term === "") {
|
||||
return resolve(["smile", "smiley", "wink", "sunny", "blush"]);
|
||||
}
|
||||
|
||||
if (Discourse.Emoji.translations[full]) {
|
||||
return resolve([Discourse.Emoji.translations[full]]);
|
||||
}
|
||||
|
||||
var options = Discourse.Emoji.search(term, {maxResults: 5});
|
||||
|
||||
return resolve(options);
|
||||
}).then(function(list) {
|
||||
return list.map(function(i) {
|
||||
return {code: i, src: Discourse.Emoji.urlFor(i)};
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
161
app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js
Normal file
161
app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js
Normal file
@@ -0,0 +1,161 @@
|
||||
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"]
|
||||
},
|
||||
{
|
||||
name: "nature",
|
||||
icons: ["dog","wolf","cat","mouse","hamster","rabbit","frog","tiger","koala","bear","pig","pig_nose","cow","boar","monkey_face","monkey","horse","sheep","elephant","panda_face","penguin","bird","baby_chick","hatched_chick","hatching_chick","chicken","snake","turtle","bug","bee","ant","beetle","snail","octopus","shell","tropical_fish","fish","dolphin","whale","whale2","cow2","ram","rat","water_buffalo","tiger2","rabbit2","dragon","racehorse","goat","rooster","dog2","pig2","mouse2","ox","dragon_face","blowfish","crocodile","camel","dromedary_camel","leopard","cat2","poodle","feet","bouquet","cherry_blossom","tulip","four_leaf_clover","rose","sunflower","hibiscus","maple_leaf","leaves","fallen_leaf","herb","ear_of_rice","mushroom","cactus","palm_tree","evergreen_tree","deciduous_tree","chestnut","seedling","blossom","globe_with_meridians","sun_with_face","full_moon_with_face","new_moon_with_face","new_moon","waxing_crescent_moon","first_quarter_moon","waxing_gibbous_moon","full_moon","waning_gibbous_moon","last_quarter_moon","waning_crescent_moon","last_quarter_moon_with_face","first_quarter_moon_with_face","crescent_moon","earth_africa","earth_americas","earth_asia","volcano","milky_way","stars","star","sunny","partly_sunny","cloud","zap","umbrella","snowflake","snowman","cyclone","foggy","rainbow","ocean"]
|
||||
},
|
||||
{
|
||||
name: "objects",
|
||||
icons: ["bamboo","gift_heart","dolls","school_satchel","mortar_board","flags","fireworks","sparkler","wind_chime","rice_scene","jack_o_lantern","ghost","santa","christmas_tree","gift","tanabata_tree","tada","confetti_ball","balloon","crossed_flags","crystal_ball","movie_camera","camera","video_camera","vhs","cd","dvd","minidisc","floppy_disk","computer","iphone","telephone","telephone_receiver","pager","fax","satellite","tv","radio","loud_sound","sound","speaker","mute","bell","no_bell","loudspeaker","mega","hourglass_flowing_sand","hourglass","alarm_clock","watch","unlock","lock","lock_with_ink_pen","closed_lock_with_key","key","mag_right","bulb","flashlight","high_brightness","low_brightness","electric_plug","battery","mag","bathtub","bath","shower","toilet","wrench","nut_and_bolt","hammer","door","smoking","bomb","gun","knife","pill","syringe","moneybag","yen","dollar","pound","euro","credit_card","money_with_wings","calling","e-mail","inbox_tray","outbox_tray","envelope","envelope_with_arrow","incoming_envelope","postal_horn","mailbox","mailbox_closed","mailbox_with_mail","mailbox_with_no_mail","postbox","package","pencil","page_facing_up","page_with_curl","bookmark_tabs","bar_chart","chart_with_upwards_trend","chart_with_downwards_trend","scroll","clipboard","date","calendar","card_index","file_folder","open_file_folder","scissors","pushpin","paperclip","black_nib","pencil2","straight_ruler","triangular_ruler","closed_book","green_book","blue_book","orange_book","notebook","notebook_with_decorative_cover","ledger","books","book","bookmark","name_badge","microscope","telescope","newspaper","art","clapper","microphone","headphones","musical_score","musical_note","notes","musical_keyboard","violin","trumpet","saxophone","guitar","space_invader","video_game","black_joker","flower_playing_cards","mahjong","game_die","dart","football","basketball","soccer","baseball","tennis","8ball","rugby_football","bowling","golf","mountain_bicyclist","bicyclist","checkered_flag","horse_racing","trophy","ski","snowboarder","swimmer","surfer","fishing_pole_and_fish"]
|
||||
},
|
||||
{
|
||||
name: "foods",
|
||||
icons: ["coffee","tea","sake","baby_bottle","beer","beers","cocktail","tropical_drink","wine_glass","fork_and_knife","pizza","hamburger","fries","poultry_leg","meat_on_bone","spaghetti","curry","fried_shrimp","bento","sushi","fish_cake","rice_ball","rice_cracker","rice","ramen","stew","oden","dango","egg","bread","doughnut","custard","icecream","ice_cream","shaved_ice","birthday","cake","cookie","chocolate_bar","candy","lollipop","honey_pot","apple","green_apple","tangerine","lemon","cherries","grapes","watermelon","strawberry","peach","melon","banana","pear","pineapple","sweet_potato","eggplant","tomato","corn"]
|
||||
},
|
||||
{
|
||||
name: "places",
|
||||
icons: ["house","house_with_garden","school","office","post_office","hospital","bank","convenience_store","love_hotel","hotel","wedding","church","department_store","european_post_office","city_sunset","city_dusk","japanese_castle","european_castle","tent","factory","tokyo_tower","japan","mount_fuji","sunrise_over_mountains","sunrise","night_with_stars","statue_of_liberty","bridge_at_night","carousel_horse","ferris_wheel","fountain","roller_coaster","ship","sailboat","speedboat","rowboat","anchor","rocket","airplane","seat","helicopter","steam_locomotive","tram","station","mountain_railway","train2","bullettrain_side","bullettrain_front","light_rail","metro","monorail","train","railway_car","trolleybus","bus","oncoming_bus","blue_car","oncoming_automobile","red_car","taxi","oncoming_taxi","articulated_lorry","truck","rotating_light","police_car","oncoming_police_car","fire_engine","ambulance","minibus","bike","aerial_tramway","suspension_railway","mountain_cableway","tractor","barber","busstop","ticket","vertical_traffic_light","traffic_light","warning","construction","beginner","fuelpump","izakaya_lantern","slot_machine","hotsprings","moyai","circus_tent","performing_arts","round_pushpin","triangular_flag_on_post","cn","us","in","jp","br","ru","de","ng","gb","fr","mx","kr","id","ph","eg","vn","tr","it","es","ca","pl","ar","co","ir","za","my","pk","au","th","ma","tw","nl","ua","sa","ke","ve","pe","ro","cl","uz","bd","kz","be","se","cz","sd","hu","pt","ch","at","tz"]
|
||||
},
|
||||
{
|
||||
name: "symbols",
|
||||
icons: ["hash","one","two","three","four","five","six","seven","eight","nine","zero","keycap_ten","1234","symbols","arrow_up","arrow_down","arrow_left","arrow_right","capital_abcd","abcd","abc","arrow_upper_right","arrow_upper_left","arrow_lower_right","arrow_lower_left","left_right_arrow","arrow_up_down","arrows_counterclockwise","arrow_backward","arrow_forward","arrow_up_small","arrow_down_small","leftwards_arrow_with_hook","arrow_right_hook","information_source","rewind","fast_forward","arrow_double_up","arrow_double_down","arrow_heading_down","arrow_heading_up","ok","twisted_rightwards_arrows","repeat","repeat_one","new","up","cool","free","ng","signal_strength","cinema","koko","u6307","u7a7a","u6e80","u5408","u7981","ideograph_advantage","u5272","u55b6","u6709","u7121","restroom","mens","womens","baby_symbol","wc","potable_water","put_litter_in_its_place","parking","wheelchair","no_smoking","u6708","u7533","sa","m","passport_control","baggage_claim","left_luggage","customs","accept","secret","congratulations","cl","sos","id","no_entry_sign","underage","no_mobile_phones","do_not_litter","non-potable_water","no_bicycles","no_pedestrians","children_crossing","no_entry","eight_spoked_asterisk","sparkle","negative_squared_cross_mark","white_check_mark","eight_pointed_black_star","heart_decoration","vs","vibration_mode","mobile_phone_off","a","b","ab","o2","diamond_shape_with_a_dot_inside","loop","recycle","aries","taurus","gemini","cancer","leo","virgo","libra","scorpius","sagittarius","capricorn","aquarius","pisces","ophiuchus","six_pointed_star","atm","chart","heavy_dollar_sign","currency_exchange","copyright","registered","tm","part_alternation_mark","wavy_dash","top","end","back","on","soon","x","o","exclamation","question","grey_exclamation","grey_question","bangbang","interrobang","arrows_clockwise","clock12","clock1230","clock1","clock130","clock2","clock230","clock3","clock330","clock4","clock430","clock5","clock530","clock6","clock7","clock8","clock9","clock10","clock11","clock630","clock730","clock830","clock930","clock1030","clock1130","heavy_multiplication_x","heavy_plus_sign","heavy_minus_sign","heavy_division_sign","spades","hearts","clubs","diamonds","white_flower","100","heavy_check_mark","ballot_box_with_check","radio_button","link","curly_loop","trident","black_square_button","white_square_button","black_medium_square","white_medium_square","black_medium_small_square","white_medium_small_square","black_small_square","white_small_square","small_red_triangle","white_large_square","black_large_square","black_circle","white_circle","red_circle","large_blue_circle","small_red_triangle_down","large_orange_diamond","large_blue_diamond","small_orange_diamond","small_blue_diamond"]
|
||||
}
|
||||
];
|
||||
|
||||
// scrub groups
|
||||
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;
|
||||
|
||||
var closeSelector = function(){
|
||||
$('.emoji-modal, .emoji-modal-wrapper').remove();
|
||||
$('body, textarea').off('keydown.emoji');
|
||||
};
|
||||
|
||||
var ungroupedIcons;
|
||||
|
||||
var toolbar = function(selected){
|
||||
|
||||
if(!ungroupedIcons){
|
||||
ungroupedIcons = [];
|
||||
var groupedIcons = {};
|
||||
|
||||
_.each(groups, function(group){
|
||||
_.each(group.icons, function(icon){
|
||||
groupedIcons[icon] = true;
|
||||
});
|
||||
});
|
||||
|
||||
var emojis = Discourse.Emoji.list();
|
||||
_.each(emojis,function(emoji){
|
||||
if(groupedIcons[emoji] !== true){
|
||||
ungroupedIcons.push(emoji);
|
||||
}
|
||||
});
|
||||
|
||||
if(ungroupedIcons.length > 0){
|
||||
groups.push({name: 'ungrouped', icons: ungroupedIcons});
|
||||
}
|
||||
}
|
||||
|
||||
return _.map(groups, function(g, i){
|
||||
var row = {src: Discourse.Emoji.urlFor(g.icons[0]), groupId: i};
|
||||
if(i===selected){
|
||||
row.selected = true;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
};
|
||||
|
||||
var PER_ROW = 12, PER_PAGE = 60;
|
||||
|
||||
var bindEvents = function(page,offset){
|
||||
var composerController = Discourse.__container__.lookup('controller:composer');
|
||||
|
||||
$('.emoji-page a').click(function(){
|
||||
composerController.appendTextAtCursor(":" + $(this).attr('title') + ":", {space: true});
|
||||
closeSelector();
|
||||
return false;
|
||||
}).hover(function(){
|
||||
var title = $(this).attr('title');
|
||||
var html = "<img src='" + Discourse.Emoji.urlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
|
||||
$('.emoji-modal .info').html(html);
|
||||
},function(){
|
||||
$('.emoji-modal .info').html("");
|
||||
});
|
||||
|
||||
$('.emoji-modal .nav .next a').click(function(){
|
||||
render(page, offset+PER_PAGE);
|
||||
});
|
||||
|
||||
$('.emoji-modal .nav .prev a').click(function(){
|
||||
render(page, offset-PER_PAGE);
|
||||
});
|
||||
|
||||
$('.emoji-modal .toolbar a').click(function(){
|
||||
var page = parseInt($(this).data('group-id'));
|
||||
render(page,0);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
var render = function(page, offset){
|
||||
var rows = [];
|
||||
var row = [];
|
||||
var icons = groups[page].icons;
|
||||
var max = offset + PER_PAGE;
|
||||
|
||||
for(var i=offset; i<max; i++){
|
||||
if(!icons[i]){ break; }
|
||||
if(row.length === PER_ROW){
|
||||
rows.push(row);
|
||||
row = [];
|
||||
}
|
||||
row.push({src: Discourse.Emoji.urlFor(icons[i]), title: icons[i]});
|
||||
}
|
||||
rows.push(row);
|
||||
|
||||
var model = {
|
||||
toolbarItems: toolbar(page),
|
||||
rows: rows,
|
||||
prevDisabled: offset === 0,
|
||||
nextDisabled: (max + 1) > icons.length
|
||||
};
|
||||
|
||||
$('body .emoji-modal').remove();
|
||||
var rendered = Ember.TEMPLATES["emoji-toolbar.raw"](model);
|
||||
$('body').append(rendered);
|
||||
|
||||
bindEvents(page, offset);
|
||||
};
|
||||
|
||||
var showSelector = function(){
|
||||
$('body').append('<div class="emoji-modal-wrapper"></div>');
|
||||
|
||||
$('.emoji-modal-wrapper').click(function(){
|
||||
closeSelector();
|
||||
});
|
||||
|
||||
render(0,0);
|
||||
|
||||
$('body, textarea').on('keydown.emoji', function(e){
|
||||
if(e.which === 27){
|
||||
closeSelector();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.PagedownCustom.appendButtons.push({
|
||||
id: 'wmd-emoji-button',
|
||||
description: I18n.t("composer.emoji"),
|
||||
execute: showSelector
|
||||
});
|
||||
189
app/assets/javascripts/discourse/lib/emoji/emoji.js.erb
Normal file
189
app/assets/javascripts/discourse/lib/emoji/emoji.js.erb
Normal file
@@ -0,0 +1,189 @@
|
||||
// TODO @robin to move this whole thing to es6
|
||||
Discourse.Emoji = {};
|
||||
// bump up this number to expire all emojis
|
||||
Discourse.Emoji.ImageVersion = "0"
|
||||
|
||||
var emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
|
||||
|
||||
var extendedEmoji = {};
|
||||
Discourse.Dialect.registerEmoji = function(code, url) {
|
||||
extendedEmoji[code] = url;
|
||||
};
|
||||
|
||||
Discourse.Emoji.list = function(){
|
||||
var list = emoji.slice(0);
|
||||
_.each(extendedEmoji, function(v,k){ list.push(k); });
|
||||
return list;
|
||||
};
|
||||
|
||||
var toSearch;
|
||||
|
||||
var search = function(term, options) {
|
||||
var maxResults = (options && options["maxResults"]) || -1;
|
||||
|
||||
toSearch = toSearch || emoji.concat(Object.keys(extendedEmoji));
|
||||
|
||||
if (maxResults === 0) { return []; }
|
||||
|
||||
var i, results = [];
|
||||
|
||||
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]);
|
||||
if(done()) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
if(!done()){
|
||||
for (i=0; i < toSearch.length; i++) {
|
||||
if (toSearch[i].indexOf(term) > 0) {
|
||||
results.push(toSearch[i]);
|
||||
if(done()) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Discourse.Emoji.search = search;
|
||||
|
||||
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(!url && emojiHash.hasOwnProperty(code)) {
|
||||
url = Discourse.getURL('/images/emoji/' + set + '/' + code + '.png');
|
||||
}
|
||||
|
||||
if(url && url[0] !== 'h' && Discourse.CDN) {
|
||||
url = Discourse.CDN + url;
|
||||
}
|
||||
|
||||
if(url){
|
||||
url = url + "?v=" + Discourse.Emoji.ImageVersion;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
Discourse.Emoji.urlFor = urlFor;
|
||||
|
||||
Discourse.Emoji.exists = function(code){
|
||||
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
|
||||
}
|
||||
|
||||
function imageFor(code) {
|
||||
var url = urlFor(code);
|
||||
if (url) {
|
||||
return ['img', { href: url, title: ':' + code + ':', 'class': 'emoji', alt: code }];
|
||||
}
|
||||
}
|
||||
|
||||
// Also support default emotions
|
||||
var translations = {
|
||||
':)' : 'smile',
|
||||
':-)' : 'smile',
|
||||
':(' : 'frowning',
|
||||
':-(' : 'frowning',
|
||||
';)' : 'wink',
|
||||
';-)' : 'wink',
|
||||
':\'(' : 'cry',
|
||||
':\'-(' : 'cry',
|
||||
':-\'(' : 'cry',
|
||||
':p' : 'stuck_out_tongue',
|
||||
':P' : 'stuck_out_tongue',
|
||||
':-P' : 'stuck_out_tongue',
|
||||
':O' : 'open_mouth',
|
||||
':-O' : 'open_mouth',
|
||||
':D' : 'smiley',
|
||||
':-D' : 'smiley',
|
||||
':|' : 'expressionless',
|
||||
':-|' : 'expressionless',
|
||||
";P" : 'stuck_out_tongue_winking_eye',
|
||||
";-P" : 'stuck_out_tongue_winking_eye',
|
||||
":$" : 'blush',
|
||||
":-$" : 'blush'
|
||||
};
|
||||
|
||||
Discourse.Emoji.translations = translations;
|
||||
|
||||
function checkPrev(prev) {
|
||||
if (prev && prev.length) {
|
||||
var lastToken = prev[prev.length-1];
|
||||
if (lastToken && lastToken.charAt) {
|
||||
var lastChar = lastToken.charAt(lastToken.length-1);
|
||||
if (lastChar !== ' ' && lastChar !== "\n") return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var translationsWithColon = {};
|
||||
Object.keys(translations).forEach(function (t) {
|
||||
if (t[0] === ':') {
|
||||
translationsWithColon[t] = translations[t];
|
||||
} else {
|
||||
var replacement = translations[t];
|
||||
Discourse.Dialect.inlineReplace(t, function (token, match, prev) {
|
||||
if (!Discourse.SiteSettings.enable_emoji) { return token; }
|
||||
return checkPrev(prev) ? imageFor(replacement) : token;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
var translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(function (t) {
|
||||
return "(" + escapeRegExp(t) + ")";
|
||||
}).join("|"));
|
||||
|
||||
Discourse.Dialect.registerInline(':', function(text, match, prev) {
|
||||
if (!Discourse.SiteSettings.enable_emoji) { return; }
|
||||
|
||||
var endPos = text.indexOf(':', 1),
|
||||
firstSpace = text.search(/\s/),
|
||||
contents;
|
||||
|
||||
if (!checkPrev(prev)) { return; }
|
||||
|
||||
// If there is no trailing colon, check our translations that begin with colons
|
||||
if (endPos === -1 || (firstSpace !== -1 && endPos > firstSpace)) {
|
||||
translationColonRegexp.lastIndex = 0;
|
||||
var m = translationColonRegexp.exec(text);
|
||||
if (m && m[0] && text.indexOf(m[0]) === 0) {
|
||||
|
||||
// Check outer edge
|
||||
var lastChar = text.charAt(m[0].length);
|
||||
if (lastChar && (lastChar !== ' ' && lastChar !== "\n")) return;
|
||||
contents = imageFor(translationsWithColon[m[0]]);
|
||||
if (contents) {
|
||||
return [m[0].length, contents];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple find and replace from our array
|
||||
var between = text.slice(1, endPos);
|
||||
contents = imageFor(between);
|
||||
if (contents) {
|
||||
return [endPos+1, contents];
|
||||
}
|
||||
});
|
||||
|
||||
Discourse.Markdown.whiteListTag('img', 'class', 'emoji');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class='emoji-modal'>
|
||||
<ul class='toolbar'>
|
||||
{{#each toolbarItems}}<li><a {{#if selected}}class='selected'{{/if}} data-group-id='{{groupId}}'><img src='{{src}}' class='emoji'></a></li>{{/each}}
|
||||
</ul>
|
||||
<div class='emoji-table-wrapper'>
|
||||
<table class='emoji-page'>
|
||||
{{#each rows}}
|
||||
<tr>
|
||||
{{#each this}}
|
||||
<td><a title='{{title}}'><img src='{{src}}' class='emoji'></a></td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='info'></div>
|
||||
<div class='nav'>
|
||||
<span class='prev'>
|
||||
{{#if prevDisabled}}
|
||||
<i class='fa fa-fast-backward'></i>
|
||||
{{else}}
|
||||
<a><i class='fa fa-fast-backward'></i></a>
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class='next'>
|
||||
{{#if nextDisabled}}
|
||||
<i class='fa fa-fast-forward'></i>
|
||||
{{else}}
|
||||
<a><i class='fa fa-fast-forward'></i></a>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
||||
@@ -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
|
||||
|
||||
106
app/assets/stylesheets/common/base/emoji.scss
Normal file
106
app/assets/stylesheets/common/base/emoji.scss
Normal file
@@ -0,0 +1,106 @@
|
||||
body img.emoji {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#wmd-emoji-button:before {
|
||||
content: "\f118";
|
||||
}
|
||||
|
||||
.emoji-modal {
|
||||
z-index: 10000;
|
||||
position: fixed;
|
||||
margin-left: -195px;
|
||||
margin-top: -100px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.emoji-page td {
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.emoji-page a {
|
||||
padding: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.emoji-page a:hover {
|
||||
background-color: rgb(210, 236, 252);
|
||||
}
|
||||
|
||||
.emoji-table-wrapper {
|
||||
min-width: 444px;
|
||||
min-height: 185px;
|
||||
}
|
||||
|
||||
.emoji-page {
|
||||
border-collapse: collapse;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.emoji-modal-wrapper {
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.8;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
|
||||
.emoji-modal .toolbar {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.emoji-modal .toolbar li {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.emoji-modal .toolbar li a {
|
||||
padding: 8px;
|
||||
background-color: #dadada;
|
||||
}
|
||||
|
||||
.emoji-modal .toolbar li a.selected {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.emoji-modal .info {
|
||||
height: 30px;
|
||||
margin-left: 8px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.emoji-modal .info span {
|
||||
margin-left: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.emoji-modal .info {
|
||||
float: left;
|
||||
}
|
||||
.emoji-modal .nav {
|
||||
float: right;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.emoji-modal .nav span {
|
||||
color: #aaa;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.emoji-modal .nav a {
|
||||
color: #333;
|
||||
}
|
||||
36
app/controllers/admin/emojis_controller.rb
Normal file
36
app/controllers/admin/emojis_controller.rb
Normal 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
|
||||
|
||||
@@ -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
92
app/models/emoji.rb
Normal 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
|
||||
38
app/models/emoji_set_site_setting.rb
Normal file
38
app/models/emoji_set_site_setting.rb
Normal 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
|
||||
3
app/serializers/emoji_serializer.rb
Normal file
3
app/serializers/emoji_serializer.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class EmojiSerializer < ApplicationSerializer
|
||||
attributes :name, :url
|
||||
end
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user