emoji: Migrate bugdown emoji to use sprite sheets.

This commit switches to use sprite sheets for rendering emojis
in all the remaining places, i.e., message bodies and composebox
typeahead. This commit also includes some changes to notifications.py
file so that the spans used for rendering emojis can be converted
to corresponding image tags so that we don't break the emoji rendering
in missed message emails since we can't use sprite sheets there.

As part of switching the bugdown system to use sprite sheets, we need
to switch the name_to_codepoint mappings to match the new sprite
sheets.  This has the side effect of fixing a bunch of emoji like
numbers and flag emoji in the emoji pickers.

Fixes: #3895.
Fixes: #3972.
This commit is contained in:
Harshit Bansal 2017-09-27 17:39:42 +00:00 committed by Tim Abbott
parent d3bfc132fb
commit 5b5bcce098
18 changed files with 165 additions and 85 deletions

View File

@ -44,7 +44,10 @@ set_global('$', global.make_zjquery());
set_global('page_params', {}); set_global('page_params', {});
set_global('channel', {}); set_global('channel', {});
set_global('emoji', {emojis: emoji_list}); set_global('emoji', {
active_realm_emojis: {},
emojis: emoji_list,
});
set_global('pygments_data', {langs: set_global('pygments_data', {langs:
{python: 0, javscript: 1, html: 2, css: 3}, {python: 0, javscript: 1, html: 2, css: 3},
}); });
@ -832,14 +835,13 @@ global.people.add(deactivated_user);
(function test_content_highlighter() { (function test_content_highlighter() {
var fake_this = { completing: 'emoji' }; var fake_this = { completing: 'emoji' };
var item = { emoji_name: 'person shrugging', emoji_url: '¯\_(ツ)_/¯' }; var emoji = { emoji_name: 'person shrugging', emoji_url: '¯\_(ツ)_/¯' };
var th_render_typeahead_item_called = false; var th_render_typeahead_item_called = false;
typeahead_helper.render_typeahead_item = function (item) { typeahead_helper.render_emoji = function (item) {
assert.equal(item.primary, 'person shrugging'); assert.deepEqual(item, emoji);
assert.equal(item.img_src, '¯\_(ツ)_/¯');
th_render_typeahead_item_called = true; th_render_typeahead_item_called = true;
}; };
ct.content_highlighter.call(fake_this, item); ct.content_highlighter.call(fake_this, emoji);
fake_this = { completing: 'mention' }; fake_this = { completing: 'mention' };
var th_render_person_called = false; var th_render_person_called = false;

View File

@ -229,9 +229,9 @@ var bugdown_data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../zerver
{input: 'mmm...:burrito:s', {input: 'mmm...:burrito:s',
expected: '<p>mmm...<img alt=":burrito:" class="emoji" src="/static/generated/emoji/images/emoji/burrito.png" title="burrito">s</p>'}, expected: '<p>mmm...<img alt=":burrito:" class="emoji" src="/static/generated/emoji/images/emoji/burrito.png" title="burrito">s</p>'},
{input: 'This is an :poop: message', {input: 'This is an :poop: message',
expected: '<p>This is an <img alt=":poop:" class="emoji" src="/static/generated/emoji/images/emoji/unicode/1f4a9.png" title="poop"> message</p>'}, expected: '<p>This is an <span class="emoji emoji-1f4a9" title="poop">:poop:</span> message</p>'},
{input: "\ud83d\udca9", {input: "\ud83d\udca9",
expected: '<p><img alt=":poop:" class="emoji" src="/static/generated/emoji/images/emoji/unicode/1f4a9.png" title="poop"></p>'}, expected: '<p><span class="emoji emoji-1f4a9" title="poop">:poop:</span></p>'},
{input: '\u{1f937}', {input: '\u{1f937}',
expected: '<p>\u{1f937}</p>' }, expected: '<p>\u{1f937}</p>' },
// Test only those realm filters which don't return True for // Test only those realm filters which don't return True for

View File

@ -38,6 +38,12 @@ set_global('page_params', {user_id: 5});
set_global('channel', {}); set_global('channel', {});
set_global('templates', {}); set_global('templates', {});
set_global('emoji_codes', {
name_to_codepoint: {
alien: '1f47d',
smile: '1f604',
},
});
set_global('emoji_picker', { set_global('emoji_picker', {
hide_emoji_popover: function () {}, hide_emoji_popover: function () {},
}); });

View File

@ -1176,6 +1176,7 @@ function render(template_name, args) {
primary: 'primary-text', primary: 'primary-text',
secondary: 'secondary-text', secondary: 'secondary-text',
img_src: 'https://zulip.org', img_src: 'https://zulip.org',
is_emoji: true,
has_image: true, has_image: true,
has_secondary: true, has_secondary: true,
}; };

View File

@ -441,6 +441,57 @@ _.each(matches, function (person) {
assert(rendered); assert(rendered);
}()); }());
(function test_render_emoji() {
// Test render_emoji with normal emoji.
var rendered = false;
var emoji = {
emoji_name: 'thumbs_up',
codepoint: '1f44d',
};
set_global('emoji', {
active_realm_emojis: {
realm_emoji: 'TBD',
},
});
global.templates.render = function (template_name, args) {
assert.equal(template_name, 'typeahead_list_item');
assert.deepEqual(args, {
primary: 'thumbs up',
codepoint: '1f44d',
is_emoji: true,
has_image: false,
has_secondary: false,
});
rendered = true;
return 'typeahead-item-stub';
};
assert.equal(th.render_emoji(emoji), 'typeahead-item-stub');
assert(rendered);
// Test render_emoji with normal emoji.
rendered = false;
emoji = {
emoji_name: 'realm_emoji',
emoji_url: 'TBD',
};
global.templates.render = function (template_name, args) {
assert.equal(template_name, 'typeahead_list_item');
assert.deepEqual(args, {
primary: 'realm emoji',
img_src: 'TBD',
is_emoji: true,
has_image: true,
has_secondary: false,
});
rendered = true;
return 'typeahead-item-stub';
};
assert.equal(th.render_emoji(emoji), 'typeahead-item-stub');
assert(rendered);
}());
(function test_sort_emojis() { (function test_sort_emojis() {
var emoji_list = [ var emoji_list = [
{ emoji_name: '+1' }, { emoji_name: '+1' },

View File

@ -314,10 +314,7 @@ exports.compose_content_begins_typeahead = function (query) {
exports.content_highlighter = function (item) { exports.content_highlighter = function (item) {
if (this.completing === 'emoji') { if (this.completing === 'emoji') {
return typeahead_helper.render_typeahead_item({ return typeahead_helper.render_emoji(item);
primary: item.emoji_name.split("_").join(" "),
img_src: item.emoji_url,
});
} else if (this.completing === 'mention') { } else if (this.completing === 'mention') {
return typeahead_helper.render_person(item); return typeahead_helper.render_person(item);
} else if (this.completing === 'stream') { } else if (this.completing === 'stream') {

View File

@ -71,8 +71,7 @@ exports.initialize = function initialize() {
_.each(emoji_codes.names, function (value) { _.each(emoji_codes.names, function (value) {
var base_name = emoji_codes.name_to_codepoint[value]; var base_name = emoji_codes.name_to_codepoint[value];
default_emojis.push({emoji_name: value, default_emojis.push({emoji_name: value,
codepoint: emoji_codes.name_to_codepoint[value], codepoint: emoji_codes.name_to_codepoint[value]});
emoji_url: "/static/generated/emoji/images/emoji/unicode/" + base_name + ".png"});
if (exports.default_emoji_aliases.hasOwnProperty(base_name)) { if (exports.default_emoji_aliases.hasOwnProperty(base_name)) {
exports.default_emoji_aliases[base_name].push(value); exports.default_emoji_aliases[base_name].push(value);
@ -82,8 +81,7 @@ exports.initialize = function initialize() {
}); });
_.each(emoji_codes.codepoints, function (value) { _.each(emoji_codes.codepoints, function (value) {
default_unicode_emojis.push({emoji_name: value, default_unicode_emojis.push({emoji_name: value,
codepoint: value, codepoint: value});
emoji_url: "/static/generated/emoji/images/emoji/unicode/" + value + ".png"});
}); });
exports.update_emojis(page_params.realm_emoji); exports.update_emojis(page_params.realm_emoji);

View File

@ -117,35 +117,33 @@ function escape(html, encode) {
} }
function handleUnicodeEmoji(unicode_emoji) { function handleUnicodeEmoji(unicode_emoji) {
var hex_value = unicode_emoji.codePointAt(0).toString(16); var codepoint = unicode_emoji.codePointAt(0).toString(16);
if (emoji.emojis_by_unicode.hasOwnProperty(hex_value)) { if (emoji_codes.codepoint_to_name.hasOwnProperty(codepoint)) {
var emoji_url = emoji.emojis_by_unicode[hex_value]; var emoji_name = emoji_codes.codepoint_to_name[codepoint];
var emoji_name = emoji_codes.codepoint_to_name[hex_value];
var alt_text = ':' + emoji_name + ':'; var alt_text = ':' + emoji_name + ':';
var title = emoji_name.split("_").join(" "); var title = emoji_name.split("_").join(" ");
return '<img alt="' + alt_text + '"' + return '<span class="emoji emoji-' + codepoint + '"' +
' class="emoji" src="' + emoji_url + '"' + ' title="' + title + '">' + alt_text +
' title="' + title + '">'; '</span>';
} }
return unicode_emoji; return unicode_emoji;
} }
function handleEmoji(emoji_name) { function handleEmoji(emoji_name) {
var input_emoji = ':' + emoji_name + ':'; var alt_text = ':' + emoji_name + ':';
var title = emoji_name.split("_").join(" "); var title = emoji_name.split("_").join(" ");
var emoji_url;
if (emoji.active_realm_emojis.hasOwnProperty(emoji_name)) { if (emoji.active_realm_emojis.hasOwnProperty(emoji_name)) {
emoji_url = emoji.active_realm_emojis[emoji_name].emoji_url; var emoji_url = emoji.active_realm_emojis[emoji_name].emoji_url;
return '<img alt="' + input_emoji + '"' + return '<img alt="' + alt_text + '"' +
' class="emoji" src="' + emoji_url + '"' +
' title="' + title + '">';
} else if (emoji.emojis_by_name.hasOwnProperty(emoji_name)) {
emoji_url = emoji.emojis_by_name[emoji_name];
return '<img alt="' + input_emoji + '"' +
' class="emoji" src="' + emoji_url + '"' + ' class="emoji" src="' + emoji_url + '"' +
' title="' + title + '">'; ' title="' + title + '">';
} else if (emoji_codes.name_to_codepoint.hasOwnProperty(emoji_name)) {
var codepoint = emoji_codes.name_to_codepoint[emoji_name];
return '<span class="emoji emoji-' + codepoint + '"' +
' title="' + title + '">' + alt_text +
'</span>';
} }
return input_emoji; return alt_text;
} }
function handleAvatar(email) { function handleAvatar(email) {

View File

@ -14,7 +14,7 @@ exports.open_reactions_popover = function () {
}; };
function send_reaction_ajax(message_id, emoji_name, operation) { function send_reaction_ajax(message_id, emoji_name, operation) {
if (!emoji.emojis_by_name[emoji_name] && !emoji.active_realm_emojis[emoji_name]) { if (!emoji_codes.name_to_codepoint[emoji_name] && !emoji.active_realm_emojis[emoji_name]) {
// Emoji doesn't exist // Emoji doesn't exist
return; return;
} }

View File

@ -114,6 +114,19 @@ exports.render_stream = function (stream) {
return html; return html;
}; };
exports.render_emoji = function (item) {
var args = {
is_emoji: true,
primary: item.emoji_name.split("_").join(" "),
};
if (emoji.active_realm_emojis.hasOwnProperty(item.emoji_name)) {
args.img_src = item.emoji_url;
} else {
args.codepoint = item.codepoint;
}
return exports.render_typeahead_item(args);
};
exports.sorter = function (query, objs, get_item) { exports.sorter = function (query, objs, get_item) {
var results = util.prefix_sort(query, objs, get_item); var results = util.prefix_sort(query, objs, get_item);
return results.matches.concat(results.rest); return results.matches.concat(results.rest);

View File

@ -14,7 +14,7 @@ exports.actively_scrolling = function () {
exports.replace_emoji_with_text = function (element) { exports.replace_emoji_with_text = function (element) {
element.find(".emoji").replaceWith(function () { element.find(".emoji").replaceWith(function () {
return $(this).attr("alt"); return $(this).text();
}); });
}; };

View File

@ -1,7 +1,11 @@
{{#if has_image}} {{#if is_emoji}}
<img class="emoji" src="{{ img_src }}" /> {{#if has_image}}
&nbsp;&nbsp; <img class="emoji" src="{{ img_src }}" />
{{/if~}} {{else}}
<span class='emoji emoji-{{ codepoint }}' />
{{/if}}
&nbsp;&nbsp;
{{/if}}
<strong> <strong>
{{~ primary ~}} {{~ primary ~}}
</strong> </strong>

View File

@ -216,12 +216,13 @@ def generate_map_files(cache_path, emoji_map, emoji_data, emoji_catalog, unified
if name in emoji_map: if name in emoji_map:
patched_css_classes[str(name)] = str(emoji['unified'].lower()) patched_css_classes[str(name)] = str(emoji['unified'].lower())
name_to_codepoint = {name: unified_reactions_data[name] for name in names}
codepoint_to_name = generate_codepoint_to_name_map(names, unified_reactions_data) codepoint_to_name = generate_codepoint_to_name_map(names, unified_reactions_data)
emoji_codes_file.write(EMOJI_CODES_FILE_TEMPLATE % { emoji_codes_file.write(EMOJI_CODES_FILE_TEMPLATE % {
'names': names, 'names': names,
'codepoints': sorted([str(code_point) for code_point in set(emoji_map.values())]), 'codepoints': sorted([str(code_point) for code_point in set(emoji_map.values())]),
'name_to_codepoint': {str(key): str(emoji_map[key]) for key in emoji_map}, 'name_to_codepoint': name_to_codepoint,
'codepoint_to_name': codepoint_to_name, 'codepoint_to_name': codepoint_to_name,
'emoji_catalog': emoji_catalog, 'emoji_catalog': emoji_catalog,
'patched_css_classes': patched_css_classes 'patched_css_classes': patched_css_classes
@ -231,7 +232,7 @@ def generate_map_files(cache_path, emoji_map, emoji_data, emoji_catalog, unified
NAME_TO_CODEPOINT_PATH = os.path.join(cache_path, 'name_to_codepoint.json') NAME_TO_CODEPOINT_PATH = os.path.join(cache_path, 'name_to_codepoint.json')
name_to_codepoint_file = open(NAME_TO_CODEPOINT_PATH, 'w') name_to_codepoint_file = open(NAME_TO_CODEPOINT_PATH, 'w')
name_to_codepoint_file.write(ujson.dumps(emoji_map)) name_to_codepoint_file.write(ujson.dumps(name_to_codepoint))
name_to_codepoint_file.close() name_to_codepoint_file.close()
CODEPOINT_TO_NAME_PATH = os.path.join(cache_path, 'codepoint_to_name.json') CODEPOINT_TO_NAME_PATH = os.path.join(cache_path, 'codepoint_to_name.json')

View File

@ -243,13 +243,13 @@
{ {
"name": "many_emoji", "name": "many_emoji",
"input": "test :smile: again :poop:\n:) foo:)bar x::y::z :wasted waste: :fakeemojithisshouldnotrender:", "input": "test :smile: again :poop:\n:) foo:)bar x::y::z :wasted waste: :fakeemojithisshouldnotrender:",
"expected_output": "<p>test <img alt=\":smile:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f604.png\" title=\"smile\"> again <img alt=\":poop:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f4a9.png\" title=\"poop\"><br>\n:) foo:)bar x::y::z :wasted waste: :fakeemojithisshouldnotrender:</p>", "expected_output": "<p>test <span class=\"emoji emoji-1f604\" title=\"smile\">:smile:</span> again <span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span><br>\n:) foo:)bar x::y::z :wasted waste: :fakeemojithisshouldnotrender:</p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "random_emoji_1", "name": "random_emoji_1",
"input": ":airplane:", "input": ":airplane:",
"expected_output": "<p><img alt=\":airplane:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/2708.png\" title=\"airplane\"></p>", "expected_output": "<p><span class=\"emoji emoji-2708\" title=\"airplane\">:airplane:</span></p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
@ -261,19 +261,19 @@
{ {
"name": "random_emoji_2", "name": "random_emoji_2",
"input": ":poop:", "input": ":poop:",
"expected_output": "<p><img alt=\":poop:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f4a9.png\" title=\"poop\"></p>", "expected_output": "<p><span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span></p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "emojis_without_space", "name": "emojis_without_space",
"input": ":cat:hello:dog::rabbit:", "input": ":cat:hello:dog::rabbit:",
"expected_output": "<p><img alt=\":cat:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f408.png\" title=\"cat\">hello<img alt=\":dog:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f415.png\" title=\"dog\"><img alt=\":rabbit:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f407.png\" title=\"rabbit\"></p>", "expected_output": "<p><span class=\"emoji emoji-1f431\" title=\"cat\">:cat:</span>hello<span class=\"emoji emoji-1f436\" title=\"dog\">:dog:</span><span class=\"emoji emoji-1f430\" title=\"rabbit\">:rabbit:</span></p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "emojis_newline", "name": "emojis_newline",
"input": ":cat:\n:dog:", "input": ":cat:\n:dog:",
"expected_output": "<p><img alt=\":cat:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f408.png\" title=\"cat\"><br>\n<img alt=\":dog:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f415.png\" title=\"dog\"></p>", "expected_output": "<p><span class=\"emoji emoji-1f431\" title=\"cat\">:cat:</span><br>\n<span class=\"emoji emoji-1f436\" title=\"dog\">:dog:</span></p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
@ -285,61 +285,61 @@
{ {
"name": "unicode_emoji", "name": "unicode_emoji",
"input": "\ud83d\udca9", "input": "\ud83d\udca9",
"expected_output":"<p><img alt=\":poop:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f4a9.png\" title=\"poop\"><\/p>", "expected_output":"<p><span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span></p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "two_unicode_emoji", "name": "two_unicode_emoji",
"input": "\ud83d\udca9\ud83d\udca9", "input": "\ud83d\udca9\ud83d\udca9",
"expected_output":"<p><img alt=\":poop:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f4a9.png\" title=\"poop\"><img alt=\":poop:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f4a9.png\" title=\"poop\"><\/p>", "expected_output":"<p><span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span><span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span><\/p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "two_unicode_emoji_separated_by_text", "name": "two_unicode_emoji_separated_by_text",
"input": "\ud83d\udca9 word \ud83d\udca9", "input": "\ud83d\udca9 word \ud83d\udca9",
"expected_output":"<p><img alt=\":poop:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f4a9.png\" title=\"poop\"> word <img alt=\":poop:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f4a9.png\" title=\"poop\"><\/p>", "expected_output":"<p><span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span> word <span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span><\/p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "miscellaneous_symbols_and_pictographs", "name": "miscellaneous_symbols_and_pictographs",
"input": "Merry Christmas!!\ud83c\udf84", "input": "Merry Christmas!!\ud83c\udf84",
"expected_output":"<p>Merry Christmas!!<img alt=\":christmas_tree:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f384.png\" title=\"christmas tree\"><\/p>", "expected_output":"<p>Merry Christmas!!<span class=\"emoji emoji-1f384\" title=\"christmas tree\">:christmas_tree:</span><\/p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "miscellaneous_and_dingbats_emoji", "name": "miscellaneous_and_dingbats_emoji",
"input": "\u2693\u2797", "input": "\u2693\u2797",
"expected_output":"<p><img alt=\":anchor:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/2693.png\" title=\"anchor\"><img alt=\":heavy_division_sign:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/2797.png\" title=\"heavy division sign\"><\/p>", "expected_output":"<p><span class=\"emoji emoji-2693\" title=\"anchor\">:anchor:</span><span class=\"emoji emoji-2797\" title=\"heavy division sign\">:heavy_division_sign:</span><\/p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "supplemental_symbols_and_pictographs", "name": "supplemental_symbols_and_pictographs",
"input": "I am a robot \ud83e\udd16.", "input": "I am a robot \ud83e\udd16.",
"expected_output":"<p>I am a robot <img alt=\":robot_face:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f916.png\" title=\"robot face\">.<\/p>", "expected_output":"<p>I am a robot <span class=\"emoji emoji-1f916\" title=\"robot face\">:robot_face:</span>.<\/p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "miscellaneous_symbols_and_arrows", "name": "miscellaneous_symbols_and_arrows",
"input": "Black upward arrow \u2b06", "input": "Black upward arrow \u2b06",
"expected_output":"<p>Black upward arrow <img alt=\":arrow_up:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/2b06.png\" title=\"arrow up\"><\/p>", "expected_output":"<p>Black upward arrow <span class=\"emoji emoji-2b06\" title=\"arrow up\">:arrow_up:</span><\/p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "unicode_emoji_without_space", "name": "unicode_emoji_without_space",
"input": "Extra\ud83d\udc7dTerrestrial", "input": "Extra\ud83d\udc7dTerrestrial",
"expected_output":"<p>Extra<img alt=\":alien:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f47d.png\" title=\"alien\">Terrestrial<\/p>", "expected_output":"<p>Extra<span class=\"emoji emoji-1f47d\" title=\"alien\">:alien:</span>Terrestrial<\/p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "unicode_emojis_new_line", "name": "unicode_emojis_new_line",
"input": "\ud83d\udc7d\n\ud83d\udc7d", "input": "\ud83d\udc7d\n\ud83d\udc7d",
"expected_output":"<p><img alt=\":alien:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f47d.png\" title=\"alien\"><br>\n<img alt=\":alien:\" class=\"emoji\" src=\"\/static\/generated\/emoji\/images\/emoji\/unicode\/1f47d.png\" title=\"alien\"><\/p>", "expected_output":"<p><span class=\"emoji emoji-1f47d\" title=\"alien\">:alien:</span><br>\n<span class=\"emoji emoji-1f47d\" title=\"alien\">:alien:</span></p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {
"name": "emoji_alongside_punctuation", "name": "emoji_alongside_punctuation",
"input": ":smile:, :smile:; :smile:", "input": ":smile:, :smile:; :smile:",
"expected_output": "<p><img alt=\":smile:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f604.png\" title=\"smile\">, <img alt=\":smile:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f604.png\" title=\"smile\">; <img alt=\":smile:\" class=\"emoji\" src=\"/static/generated/emoji/images/emoji/unicode/1f604.png\" title=\"smile\"></p>", "expected_output": "<p><span class=\"emoji emoji-1f604\" title=\"smile\">:smile:</span>, <span class=\"emoji emoji-1f604\" title=\"smile\">:smile:</span>; <span class=\"emoji emoji-1f604\" title=\"smile\">:smile:</span></p>",
"bugdown_matches_marked": true "bugdown_matches_marked": true
}, },
{ {

View File

@ -802,16 +802,13 @@ unicode_emoji_regex = u'(?P<syntax>['\
def make_emoji(codepoint, display_string): def make_emoji(codepoint, display_string):
# type: (Text, Text) -> Element # type: (Text, Text) -> Element
src = '/static/generated/emoji/images/emoji/unicode/%s.png' % (codepoint,)
# Replace underscore in emoji's title with space # Replace underscore in emoji's title with space
title = display_string[1:-1].replace("_", " ") title = display_string[1:-1].replace("_", " ")
span = markdown.util.etree.Element('span')
elt = markdown.util.etree.Element('img') span.set('class', 'emoji emoji-%s' % (codepoint,))
elt.set('src', src) span.set('title', title)
elt.set('class', 'emoji') span.text = display_string
elt.set("alt", display_string) return span
elt.set("title", title)
return elt
def make_realm_emoji(src, display_string): def make_realm_emoji(src, display_string):
# type: (Text, Text) -> Element # type: (Text, Text) -> Element

View File

@ -89,11 +89,10 @@ def relative_to_full_url(base_url, content):
content = re.sub( content = re.sub(
r"<img src=(\S+)/user_uploads/(\S+)>", "", content) r"<img src=(\S+)/user_uploads/(\S+)>", "", content)
# URLs for emoji are of the form # Convert the zulip emoji's relative url to absolute one.
# "static/generated/emoji/images/emoji/snowflake.png".
content = re.sub( content = re.sub(
r"(?<=\=['\"])/static/generated/emoji/images/emoji/(?=[^<]+>)", r"(?<=\=['\"])/static/generated/emoji/images/emoji/unicode/zulip.png(?=[^<]+>)",
base_url + r"/static/generated/emoji/images/emoji/", base_url + r"/static/generated/emoji/images/emoji/unicode/zulip.png",
content) content)
# Realm emoji should use absolute URLs when referenced in missed-message emails. # Realm emoji should use absolute URLs when referenced in missed-message emails.
@ -110,6 +109,18 @@ def relative_to_full_url(base_url, content):
return content return content
def fix_emojis(content, base_url):
# type: (Text, Text) -> Text
# Convert the emoji spans to img tags.
content = re.sub(
r'<span class=\"emoji emoji-(\S+)\" title=\"([^\"]+)\">(\S+)</span>',
r'<img src="' + base_url + r'/static/generated/emoji/images-google-64/\1.png" ' +
r'title="\2" alt="\3" style="height: 20px;">',
content)
content = content.replace(' class="emoji"', ' style="height: 20px;"')
return content
def build_message_list(user_profile, messages): def build_message_list(user_profile, messages):
# type: (UserProfile, List[Message]) -> List[Dict[str, Any]] # type: (UserProfile, List[Message]) -> List[Dict[str, Any]]
""" """
@ -133,10 +144,6 @@ def build_message_list(user_profile, messages):
# with a simple hyperlink. # with a simple hyperlink.
return re.sub(r"\[(\S*)\]\((\S*)\)", r"\2", content) return re.sub(r"\[(\S*)\]\((\S*)\)", r"\2", content)
def fix_emojis(html):
# type: (Text) -> Text
return html.replace(' class="emoji"', ' height="20px" style="position: relative;top: 6px;"')
def build_message_payload(message): def build_message_payload(message):
# type: (Message) -> Dict[str, Text] # type: (Message) -> Dict[str, Text]
plain = message.content plain = message.content
@ -153,7 +160,7 @@ def build_message_list(user_profile, messages):
assert message.rendered_content is not None assert message.rendered_content is not None
html = message.rendered_content html = message.rendered_content
html = relative_to_full_url(user_profile.realm.uri, html) html = relative_to_full_url(user_profile.realm.uri, html)
html = fix_emojis(html) html = fix_emojis(html, user_profile.realm.uri)
return {'plain': plain, 'html': html} return {'plain': plain, 'html': html}

View File

@ -392,7 +392,7 @@ class BugdownTest(ZulipTestCase):
media_tweet_html = ('<a href="http://t.co/xo7pAhK6n3" target="_blank" title="http://t.co/xo7pAhK6n3">' media_tweet_html = ('<a href="http://t.co/xo7pAhK6n3" target="_blank" title="http://t.co/xo7pAhK6n3">'
'http://twitter.com/NEVNBoston/status/421654515616849920/photo/1</a>') 'http://twitter.com/NEVNBoston/status/421654515616849920/photo/1</a>')
emoji_in_tweet_html = """Zulip is <img alt=":hundred_points:" class="emoji" src="/static/generated/emoji/images/emoji/unicode/1f4af.png" title="hundred points">% open-source!""" emoji_in_tweet_html = """Zulip is <span class="emoji emoji-1f4af" title="hundred points">:hundred_points:</span>% open-source!"""
def make_inline_twitter_preview(url, tweet_html, image_html=''): def make_inline_twitter_preview(url, tweet_html, image_html=''):
# type: (Text, Text, Text) -> Text # type: (Text, Text, Text) -> Text
@ -541,11 +541,11 @@ class BugdownTest(ZulipTestCase):
# type: () -> None # type: () -> None
msg = u'\u2615' # ☕ msg = u'\u2615' # ☕
converted = bugdown_convert(msg) converted = bugdown_convert(msg)
self.assertEqual(converted, u'<p><img alt=":coffee:" class="emoji" src="/static/generated/emoji/images/emoji/unicode/2615.png" title="coffee"></p>') self.assertEqual(converted, u'<p><span class="emoji emoji-2615" title="coffee">:coffee:</span></p>')
msg = u'\u2615\u2615' # ☕☕ msg = u'\u2615\u2615' # ☕☕
converted = bugdown_convert(msg) converted = bugdown_convert(msg)
self.assertEqual(converted, u'<p><img alt=":coffee:" class="emoji" src="/static/generated/emoji/images/emoji/unicode/2615.png" title="coffee"><img alt=":coffee:" class="emoji" src="/static/generated/emoji/images/emoji/unicode/2615.png" title="coffee"></p>') self.assertEqual(converted, u'<p><span class="emoji emoji-2615" title="coffee">:coffee:</span><span class="emoji emoji-2615" title="coffee">:coffee:</span></p>')
def test_same_markup(self): def test_same_markup(self):
# type: () -> None # type: () -> None

View File

@ -13,8 +13,8 @@ from mock import patch, MagicMock
from six.moves import range from six.moves import range
from typing import Any, Dict, List, Text from typing import Any, Dict, List, Text
from zerver.lib.notifications import handle_missedmessage_emails, \ from zerver.lib.notifications import fix_emojis, \
relative_to_full_url handle_missedmessage_emails, relative_to_full_url
from zerver.lib.actions import do_update_message from zerver.lib.actions import do_update_message
from zerver.lib.message import access_message from zerver.lib.message import access_message
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
@ -338,7 +338,7 @@ class TestMissedMessages(ZulipTestCase):
msg_id = self.send_message(self.example_email('othello'), self.example_email('hamlet'), msg_id = self.send_message(self.example_email('othello'), self.example_email('hamlet'),
Recipient.PERSONAL, Recipient.PERSONAL,
'Extremely personal message with a realm emoji :green_tick:!') 'Extremely personal message with a realm emoji :green_tick:!')
body = '<img alt=":green_tick:" height="20px" style="position: relative;top: 6px;" src="http://zulip.testserver/user_avatars/1/emoji/green_tick.png" title="green tick">' body = '<img alt=":green_tick:" style="height: 20px;" src="http://zulip.testserver/user_avatars/1/emoji/green_tick.png" title="green tick">'
subject = 'Othello, the Moor of Venice sent you a message' subject = 'Othello, the Moor of Venice sent you a message'
self._test_cases(tokens, msg_id, body, subject, send_as_user=False, verify_html_body=True) self._test_cases(tokens, msg_id, body, subject, send_as_user=False, verify_html_body=True)
@ -403,12 +403,6 @@ class TestMissedMessages(ZulipTestCase):
# Specific test cases. # Specific test cases.
# A path to an emoji image
test_data = '<a href="/static/generated/emoji/images/emoji/">emoji</a>'
actual_output = relative_to_full_url("http://example.com", test_data)
expected_output = '<a href="http://example.com/static/generated/emoji/images/emoji/">emoji</a>'
self.assertEqual(actual_output, expected_output)
# A path similar to our emoji path, but not in a link: # A path similar to our emoji path, but not in a link:
test_data = "<p>Check out the file at: '/static/generated/emoji/images/emoji/'</p>" test_data = "<p>Check out the file at: '/static/generated/emoji/images/emoji/'</p>"
actual_output = relative_to_full_url("http://example.com", test_data) actual_output = relative_to_full_url("http://example.com", test_data)
@ -427,3 +421,14 @@ class TestMissedMessages(ZulipTestCase):
actual_output = relative_to_full_url("http://example.com", test_data) actual_output = relative_to_full_url("http://example.com", test_data)
expected_output = '<p>Set src="/avatar/username@example.com?s=30"</p>' expected_output = '<p>Set src="/avatar/username@example.com?s=30"</p>'
self.assertEqual(actual_output, expected_output) self.assertEqual(actual_output, expected_output)
def test_fix_emoji(self):
# type: () -> None
# An emoji.
test_data = '<p>See <span class="emoji emoji-26c8" title="cloud with lightning and rain">' + \
':cloud_with_lightning_and_rain:</span>.</p>'
actual_output = fix_emojis(test_data, "http://example.com")
expected_output = '<p>See <img src="http://example.com/static/generated/emoji/images-google-64/26c8.png" ' + \
'title="cloud with lightning and rain" alt=":cloud_with_lightning_and_rain:" ' + \
'style="height: 20px;">.</p>'
self.assertEqual(actual_output, expected_output)