diff --git a/.eslintrc.json b/.eslintrc.json
index cf10b34040..646c2d1345 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -78,6 +78,7 @@
"activity": false,
"invite": false,
"colorspace": false,
+ "reactions": false,
"tutorial": false,
"templates": false,
"alert_words": false,
diff --git a/frontend_tests/node_tests/templates.js b/frontend_tests/node_tests/templates.js
index d259b729db..c2e30262f2 100644
--- a/frontend_tests/node_tests/templates.js
+++ b/frontend_tests/node_tests/templates.js
@@ -532,6 +532,21 @@ function render(template_name, args) {
assert($(html).text().trim(), "Message to stream devel");
}());
+(function message_reaction() {
+ var args = {
+ emoji_name: 'smile',
+ message_id: '1'
+ };
+
+ var html = '';
+ html += '
';
+ html += render('message_reaction', args);
+ html += '
';
+
+ global.write_handlebars_output("message_reaction", html);
+ assert($(html).find(".message_reaction").has(".emoji .emoji-smile"));
+}());
+
(function new_stream_users() {
var args = {
users: [
@@ -576,6 +591,22 @@ function render(template_name, args) {
assert.equal(button_area.find(".no_propagate_notifications").text().trim(), 'No');
}());
+(function reaction_popover_content() {
+ var args = {
+ search: 'Search',
+ message_id: 1,
+ emojis: global.emoji.emojis_name_to_css_class,
+ };
+
+ var html = '';
+ html += render('reaction_popover_content', args);
+ html += "
";
+ // test to make sure the first emoji is present in the popover
+ var emoji_key = $(html).find(".emoji-100").attr('title');
+ assert.equal(emoji_key, ':100:');
+ global.write_handlebars_output("reaction_popover_content", html);
+}());
+
(function settings_tab() {
var page_param_checkbox_options = {
stream_desktop_notifications_enabled: true,
diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js
index 393c8ceadc..4dbe4eb7c7 100644
--- a/static/js/click_handlers.js
+++ b/static/js/click_handlers.js
@@ -83,6 +83,13 @@ $(function () {
toggle_star(rows.id($(this).closest(".message_row")));
});
+ $("#main_div").on("click", ".message_reaction", function (e) {
+ e.stopPropagation();
+ var emoji_name = $(this).attr('data-emoji-name');
+ var message_id = $(this).parent().attr('data-message-id');
+ reactions.message_reaction_on_click(message_id, emoji_name);
+ });
+
$("#main_div").on("click", "a.stream", function (e) {
e.preventDefault();
var stream = stream_data.get_sub_by_id($(this).attr('data-stream-id'));
diff --git a/static/js/emoji.js b/static/js/emoji.js
index daf053f248..ed49ef6e4f 100644
--- a/static/js/emoji.js
+++ b/static/js/emoji.js
@@ -3,6 +3,7 @@ var emoji = (function () {
var exports = {};
exports.emojis = [];
+exports.realm_emojis = {};
exports.emojis_by_name = {};
exports.emojis_name_to_css_class = {};
exports.emojis_by_unicode = {};
@@ -15,6 +16,10 @@ var emoji_names = ["+1", "-1", "100", "1234", "8ball", "a", "a_button", "ab", "a
var unicode_emoji_names = ["1f198", "1f3ed", "0034", "1f341", "1f3d7", "26f9", "1f32c", "1f314", "1f199", "1f6b2", "267b", "270c", "1f622", "1f4ad", "1f698", "1f618", "1f3a8", "1f3eb", "1f3ae", "1f45f", "1f624", "1f437", "1f6b2", "1f193", "1f69f", "1f564", "1f4a9", "1f335", "1f69d", "1f498", "1f373", "1f195", "262e", "1f33f", "1f63e", "1f499", "1f4af", "1f343", "1f3a2", "1f432", "1f6b8", "1f69a", "2195", "1f5fb", "1f51f", "1f637", "1f4b7", "1f621", "1f250", "1f697", "1f51d", "1f3e5", "1f534", "1f5fa", "1f51a", "1f6e5", "1f1ee", "260e", "1f573", "1f45d", "1f3ee", "1f535", "1f004", "2199", "1f3b2", "1f4cc", "1f21a", "1f42c", "1f303", "25fd", "1f61a", "1f30e", "1f51a", "23ea", "1f355", "1f4bc", "1f63c", "1f6c3", "1f371", "1f497", "1f387", "2728", "261d", "1f337", "1f5e3", "1f691", "2614", "1f3e2", "1f3ac", "1f606", "1f5fe", "1f3e4", "1f635", "1f47f", "1f458", "1f194", "1f3ee", "1f55d", "2615", "1f461", "1f519", "1f62e", "1f4c3", "1f3e6", "1f35e", "1f506", "1f447", "1f694", "2651", "1f356", "1f5fc", "1f55b", "1f3a3", "1f44e", "1f51e", "1f52d", "1f915", "1f577", "1f21a", "1f4f8", "1f360", "1f50f", "1f1f7", "1f62f", "1f6c4", "1f338", "2747", "1f4a6", "1f449", "1f3b7", "1f3a3", "1f3b4", "1f423", "1f193", "1f4a8", "1f684", "1f357", "1f347", "1f63c", "1f36d", "25fe", "1f3e7", "1f4d4", "1f44d", "1f49d", "2702", "1f3b0", "1f3c0", "1f51d", "1f561", "1f6e4", "1f485", "1f38c", "1f606", "271d", "1f690", "1f6bf", "1f3bc", "1f415", "1f50a", "1f54b", "1f3c3", "1f6f3", "270d", "1f400", "1f391", "1f30c", "1f454", "1f63d", "2744", "1f58c", "1f52e", "1f201", "1f444", "2611", "1f55a", "24c2", "1f415", "1f5fe", "1f44e", "1f34d", "1f631", "1f4a3", "1f4e1", "1f4fb", "1f984", "1f237", "1f17f", "1f498", "1f31a", "1f192", "1f35a", "1f42f", "1f576", "1f22f", "1f1e9", "231a", "1f626", "1f349", "1f492", "1f232", "1f49d", "1f52c", "2049", "1f479", "1f473", "262a", "1f309", "1f6ad", "1f528", "1f61b", "1f4ee", "24c2", "1f6be", "2652", "1f629", "1f340", "1f55c", "1f37e", "1f404", "2755", "2b1c", "1f61a", "1f43d", "26f8", "1f643", "1f329", "1f624", "1f37a", "1f3df", "1f6eb", "1f436", "2797", "1f503", "1f344", "23fa", "1f644", "1f41c", "1f605", "1f390", "1f4fd", "2693", "0037", "1f363", "1f5c3", "1f46b", "1f4ab", "25b6", "1f3bb", "1f401", "1f194", "23eb", "1f49f", "1f313", "1f570", "1f6f0", "270b", "1f384", "1f381", "1f494", "1f61d", "1f30a", "2665", "1f614", "26c4", "2b06", "1f4b4", "1f4cf", "1f33e", "1f62a", "1f34f", "1f4c8", "25fb", "1f33b", "1f642", "1f61f", "1f629", "1f607", "1f639", "1f54e", "1f33d", "262f", "1f325", "1f55c", "1f6ec", "1f4c6", "1f381", "1f4ff", "1f61b", "1f50e", "1f4a9", "1f4ed", "1f451", "1f617", "1f496", "2663", "1f46e", "1f646", "1f64e", "1f32b", "1f361", "1f536", "1f197", "261d", "1f33d", "2733", "1f3c6", "1f4b0", "1f55f", "25aa", "2b55", "1f515", "1f35b", "1f62d", "1f312", "1f405", "0032", "1f198", "1f51c", "1f5dc", "2716", "1f3be", "1f493", "1f386", "1f632", "1f4f2", "3297", "1f647", "2754", "2196", "1f619", "23eb", "1f6a9", "1f34e", "1f604", "264a", "1f6a2", "1f4a9", "1f3a5", "1f505", "1f196", "1f332", "1f3c8", "2649", "1f69b", "1f51b", "1f693", "1f50f", "1f633", "2660", "1f636", "1f4fc", "1f377", "1f563", "1f608", "1f3bf", "1f3ec", "1f40a", "1f533", "27b0", "1f6a0", "1f348", "1f623", "1f531", "1f233", "1f6e0", "1f192", "1f506", "1f913", "1f333", "1f4ae", "00ae", "1f52b", "1f68f", "1f500", "1f55e", "2b05", "1f5dd", "1f538", "25ab", "1f52a", "1f5c2", "1f4c1", "1f522", "1f608", "1f3fa", "1f366", "26be", "1f466", "1f64c", "2795", "1f647", "1f688", "1f486", "1f239", "1f6a8", "1f4e4", "1f55e", "1f376", "1f616", "1f620", "1f4f6", "1f38d", "1f319", "1f605", "2648", "1f33e", "1f401", "1f47c", "1f482", "2709", "1f4b8", "1f37b", "1f645", "1f620", "1f399", "1f4a5", "1f697", "1f408", "0033", "1f3bd", "26f4", "2764", "1f4c8", "1f49a", "1f615", "1f475", "264f", "26f5", "1f4ec", "1f418", "1f4d6", "1f625", "1f238", "1f6e3", "1f31e", "1f382", "1f50d", "1f4c5", "1f54a", "1f468", "1f419", "267f", "1f69a", "1f202", "1f6e1", "1f487", "1f561", "2b07", "1f31c", "1f3f5", "1f4b1", "1f4ed", "1f5de", "23f3", "1f6c0", "1f42d", "1f564", "1f3b3", "1f422", "1f6af", "1f477", "1f911", "1f3bd", "1f513", "1f41e", "1f472", "1f3cb", "1f1ea", "2757", "1f30a", "1f17e", "1f3ce", "1f642", "1f486", "1f4d8", "1f397", "1f62c", "1f509", "1f171", "1f3b5", "1f460", "1f4d7", "1f3a7", "1f47e", "23f9", "270a", "1f48b", "1f60b", "1f63a", "26ce", "1f49e", "0031", "1f48d", "26a1", "1f411", "1f516", "267b", "1f578", "1f440", "1f4f1", "1f3f3", "1f4dd", "1f4a6", "1f595", "1f4fa", "1f53d", "1f332", "1f43e", "1f640", "1f562", "23f3", "1f3a9", "1f567", "1f69c", "1f236", "1f237", "1f63f", "1f47c", "1f17f", "1f4a8", "1f42e", "1f481", "1f4a2", "1f4ec", "270f", "1f50d", "1f609", "1f306", "1f321", "1f5a8", "1f32a", "1f392", "1f607", "1f63b", "1f4b3", "1f3c1", "1f4df", "1f364", "1f307", "26ab", "1f575", "1f6b6", "1f537", "1f45e", "1f306", "1f6cd", "2b06", "1f328", "1f4b9", "1f48e", "274e", "1f467", "1f602", "1f4e7", "1f234", "1f61f", "1f0cf", "274e", "1f4b5", "1f4aa", "1f462", "1f649", "1f3ea", "1f4ba", "1f6af", "2653", "1f4c5", "1f625", "1f4e2", "1f60d", "1f3d5", "1f6b4", "1f3f7", "2666", "1f64b", "26bd", "1f1ec", "1f336", "1f1ef", "1f603", "1f3b8", "1f326", "1f372", "1f379", "1f502", "1f6bb", "1f320", "1f42c", "23e9", "264b", "1f456", "1f52a", "1f417", "26f5", "1f983", "1f471", "1f430", "1f685", "1f61d", "1f681", "1f39b", "1f3ad", "1f405", "1f63d", "1f301", "1f509", "1f421", "1f4ac", "1f331", "1f4e9", "1f3c8", "2b1b", "1f234", "1f4f0", "1f236", "1f41a", "1f45b", "260e", "1f634", "1f433", "1f54d", "1f3d8", "1f639", "1f6b5", "1f686", "1f6ce", "1f4a0", "1f3c3", "1f488", "1f368", "1f523", "1f32f", "1f579", "1f695", "21aa", "1f238", "1f46f", "1f42b", "1f3c2", "1f339", "23f1", "1f48a", "26f7", "1f4d9", "1f3af", "1f61e", "1f601", "1f6d0", "1f47a", "1f504", "1f567", "1f606", "1f488", "1f44f", "2194", "1f3ef", "1f63b", "1f371", "1f314", "1f38b", "1f17e", "1f52a", "1f30b", "1f618", "1f51b", "1f549", "1f197", "1f307", "1f4e6", "27a1", "1f4c9", "1f43a", "1f402", "1f5e1", "1f550", "1f910", "1f489", "1f362", "1f391", "1f382", "2705", "1f613", "1f320", "1f3c1", "1f518", "2935", "1f621", "1f40b", "269b", "1f4fc", "1f353", "1f6b1", "1f31f", "1f638", "1f6bd", "1f18e", "1f202", "1f3a6", "1f170", "1f191", "1f4be", "1f455", "1f4de", "1f614", "1f32d", "1f64f", "1f565", "1f633", "1f4a9", "203c", "1f350", "1f6e2", "1f637", "1f60f", "1f304", "26c5", "1f58b", "1f4b5", "1f4a1", "1f6b3", "1f472", "1f4fa", "1f450", "1f6a8", "303d", "2122", "1f604", "1f535", "1f4e0", "1f469", "1f3ab", "1f35c", "1f500", "1f378", "1f5ef", "1f44e", "1f682", "1f327", "1f641", "1f375", "0030", "1f380", "1f524", "1f49c", "1f44d", "1f530", "0023", "1f631", "1f64e", "1f3c4", "1f68f", "1f311", "26a1", "1f44d", "1f61c", "26d4", "1f4db", "1f3db", "1f439", "26cf", "269c", "1f46a", "1f358", "1f4e5", "23ed", "1f62b", "1f3a0", "1f441", "1f429", "25c0", "1f4e3", "1f330", "1f6aa", "1f324", "1f4ea", "1f6b9", "1f383", "1f3da", "0039", "1f36b", "270c", "1f354", "1f251", "1f4a1", "1f60c", "2708", "1f457", "1f6a4", "26c4", "1f4d2", "1f410", "23f8", "1f1eb", "1f51c", "1f531", "1f374", "23e9", "1f404", "1f53b", "1f170", "1f3d0", "1f409", "1f527", "1f446", "1f373", "1f53a", "1f5b1", "1f64f", "1f3c9", "1f557", "26bd", "23ef", "1f38e", "1f435", "1f4ca", "1f3f0", "1f396", "1f60f", "1f5e8", "1f359", "1f68e", "1f475", "1f3ee", "2139", "1f4ef", "1f3e0", "1f41f", "1f470", "270a", "1f628", "1f484", "26f2", "1f300", "1f4a0", "1f6a5", "1f501", "1f36a", "1f632", "1f611", "1f493", "1f3f0", "1f60a", "1f19a", "1f3c5", "1f692", "1f43e", "1f40e", "1f5ff", "1f33c", "1f697", "1f689", "1f562", "1f455", "1f34c", "1f60c", "1f3e8", "1f559", "1f6a1", "1f43c", "1f171", "1f18e", "1f52f", "1f367", "1f43f", "1f6b6", "1f626", "26f0", "1f428", "1f52f", "1f425", "1f23a", "1f310", "1f3e0", "1f439", "1f4b9", "1f201", "1f3ac", "1f45e", "1f1f0", "26e9", "1f250", "26f3", "1f4bd", "1f58d", "1f447", "1f30e", "00a9", "1f465", "1f3a9", "1f485", "23f0", "1f48f", "1f3aa", "2600", "1f4e8", "1f552", "1f49b", "1f622", "1f4b4", "1f35b", "1f50a", "274c", "1f62e", "1f563", "1f53c", "1f3a8", "1f389", "1f393", "1f553", "1f33a", "1f0cf", "270b", "1f636", "1f39f", "1f43b", "1f6ab", "1f474", "1f5ff", "1f3f4", "1f4eb", "1f5fd", "1f4e3", "1f6c0", "1f346", "1f370", "1f43a", "1f514", "1f50b", "1f5d1", "1f483", "1f4c4", "26ea", "1f515", "1f51e", "3299", "1f55f", "1f4eb", "1f40f", "1f232", "1f4b7", "1f525", "1f630", "1f60d", "1f30d", "21aa", "1f35f", "1f302", "1f459", "1f6a6", "1f617", "27bf", "1f590", "1f46d", "1f4b2", "1f5d3", "26d1", "1f455", "1f554", "27a1", "1f6c5", "1f41d", "1f448", "1f513", "1f37d", "2934", "1f40c", "1f53d", "1f406", "1f566", "1f3d9", "1f555", "1f4b6", "1f37b", "1f638", "1f4d0", "1f551", "1f552", "1f38f", "1f553", "1f60e", "1f3e9", "1f64a", "1f453", "27bf", "1f558", "1f42a", "1f452", "1f461", "1f38f", "1f352", "1f315", "1f4c9", "1f46b", "1f5f3", "1f45a", "203c", "1f61c", "1f38d", "1f004", "1f474", "1f699", "1f316", "1f519", "1f444", "1f56f", "1f916", "1f58a", "23ee", "2796", "1f443", "1f44a", "1f4a4", "1f3b6", "1f372", "1f385", "1f4a2", "1f420", "1f507", "1f3d1", "1f251", "1f392", "1f3e2", "1f44a", "1f6ba", "1f6bc", "1f424", "1f6b1", "1f317", "1f389", "1f560", "2753", "1f6a9", "1f560", "1f565", "1f4f1", "263a", "1f39a", "1f517", "1f427", "1f50c", "1f480", "1f35f", "1f199", "1f1fa", "1f45f", "1f615", "1f4b6", "1f425", "2712", "26a0", "1f3f9", "1f308", "1f34b", "1f351", "1f682", "1f53a", "1f68d", "1f522", "1f603", "1f235", "25fc", "1f4d5", "1f3dc", "1f611", "1f4c0", "1f50e", "23ea", "1f3dd", "1f4dc", "1f407", "1f60b", "1f3e4", "1f6be", "1f47d", "1f31b", "1f1ec", "1f38e", "1f3cc", "1f627", "1f54c", "2734", "1f44b", "1f6e9", "1f504", "1f683", "1f3b6", "1f645", "1f6ae", "1f5b2", "1f35d", "1f233", "1f48c", "1f4cb", "1f37c", "1f426", "1f6cb", "1f521", "1f4c7", "1f44a", "264c", "1f3e1", "1f648", "1f687", "1f37f", "1f375", "2b55", "263a", "1f34e", "1f60a", "1f55d", "1f6b9", "2601", "1f36f", "1f438", "1f4f7", "1f980", "1f4f9", "1f634", "270f", "1f6b5", "1f34a", "1f6ba", "1f686", "1f403", "1f476", "1f334", "21a9", "1f62a", "1f520", "1f6ae", "26b0", "1f521", "1f512", "1f416", "1f3ba", "1f431", "1f55b", "0036", "21a9", "1f30f", "2714", "1f448", "1f4d3", "1f32e", "1f345", "1f507", "26b1", "1f523", "1f3cd", "1f623", "1f4ce", "1f4b0", "1f31d", "1f610", "1f31f", "1f4f6", "1f40d", "1f48f", "1f699", "1f38a", "1f37a", "2b50", "1f4bf", "1f68a", "1f524", "1f502", "1f63a", "1f453", "1f530", "1f4f4", "1f4da", "1f64c", "1f3b1", "1f4af", "1f378", "1f35c", "2614", "1f35a", "1f478", "1f235", "1f6c2", "1f539", "1f48e", "1f490", "1f365", "1f551", "1f62d", "1f3de", "1f412", "1f510", "1f914", "1f627", "1f41a", "1f53b", "1f529", "1f47a", "1f3af", "1f612", "26fd", "1f6cf", "1f41d", "1f4cd", "1f4d1", "1f68b", "1f3a4", "25b6", "1f3b1", "1f446", "231b", "1f449", "0038", "1f3e3", "1f45c", "1f503", "1f63e", "1f62c", "1f31a", "2757", "1f195", "1f3c7", "1f414", "26f1", "2198", "1f982", "1f318", "264d", "264e", "1f61e", "2650", "1f616", "1f6a5", "1f43b", "1f4f5", "1f46f", "1f4f2", "1f696", "1f479", "1f4bb", "2b07", "1f3a1", "1f601", "23ec", "1f408", "26c8", "1f366", "1f30f", "274c", "1f52b", "2709", "1f9c0", "26fa", "1f602", "1f5c4", "1f511", "1f491", "1f505", "1f385", "1f646", "1f47e", "1f1e8", "1f191", "1f30d", "1f4bf", "1f360", "1f3ca", "3030", "1f684", "1f46e", "1f495", "1f6ac", "1f4c2", "1f628", "1f600", "2b05", "1f6c1", "1f3d3", "23f2", "1f41e", "1f239", "1f413", "1f19a", "1f596", "1f5bc", "1f685", "26aa", "1f196", "1f388", "1f343", "1f508", "1f6cc", "1f22f", "23ec", "1f40b", "1f62f", "1f612", "1f497", "1f3f8", "1f4aa", "1f5d2", "1f680", "1f42a", "1f462", "26f3", "1f526", "1f460", "1f5a5", "1f3bf", "1f3b9", "1f4a5", "1f6b0", "26c5", "1f550", "1f912", "1f917", "0035", "1f554", "1f555", "1f556", "1f557", "1f558", "1f369", "1f3e7", "1f587", "1f36c", "1f46c", "2668", "1f23a", "1f4e0", "1f464", "1f4e7", "26d3", "1f918", "1f619", "1f365", "1f6ab", "1f6b7", "25c0", "1f4ea", "1f39e", "1f559", "1f55a", "1f613", "1f69e", "1f445", "1f532", "1f3a7", "1f630", "1f4bb", "1f44c", "1f36e", "1f6a3", "26fd", "1f609", "1f44f", "1f981", "1f3cf", "1f68c", "1f6a7", "1f342", "1f438", "1f442", "1f41b", "1f64b", "1f3d4", "1f556", "23cf", "1f370", "1f393", "1f44b", "1f416", "1f520", "1f3d2", "1f64d", "1f434", "2197", "1f4d6", "1f566", "1f305", "1f40e", "1f3d6", "1f501", "2b50", "1f407", "1f463", "1f47b", "1f4a7", "1f4f3", "1f640", "1f600", "1f574"];
emoji_names.push("zulip");
+exports.realm_emojis.zulip = {
+ emoji_name: 'zulip',
+ emoji_url: '/static/third/gemoji/images/emoji/zulip.png'
+};
_.each(emoji_names, function (value) {
default_emojis.push({emoji_name: value, emoji_url: "/static/generated/emoji/images/emoji/" + value + ".png"});
@@ -24,20 +29,26 @@ _.each(unicode_emoji_names, function (value) {
default_unicode_emojis.push({emoji_name: value, emoji_url: "/static/generated/emoji/images/emoji/unicode/" + value + ".png"});
});
+exports.emoji_name_to_css_class = function (emoji_name) {
+ if (emoji_name.indexOf("+") >= 0) {
+ return emoji_name.replace("+", "");
+ }
+ return emoji_name;
+};
+
exports.update_emojis = function update_emojis(realm_emojis) {
// Copy the default emoji list and add realm-specific emoji to it
exports.emojis = default_emojis.slice(0);
_.each(realm_emojis, function (data, name) {
- exports.emojis.push({emoji_name:name, emoji_url: data.display_url});
+ exports.emojis.push({emoji_name: name, emoji_url: data.display_url});
+ exports.realm_emojis[name] = {emoji_name: name, emoji_url: data.display_url};
});
exports.emojis_by_name = {};
exports.emojis_name_to_css_class = {};
_.each(exports.emojis, function (emoji) {
+ var css_class = exports.emoji_name_to_css_class(emoji.emoji_name);
+ exports.emojis_name_to_css_class[emoji.emoji_name] = css_class;
exports.emojis_by_name[emoji.emoji_name] = emoji.emoji_url;
- exports.emojis_name_to_css_class[emoji.emoji_name] = emoji.emoji_name;
- if (emoji.emoji_name.indexOf("+") >= 0) {
- exports.emojis_name_to_css_class[emoji.emoji_name] = emoji.emoji_name.replace("+", "");
- }
});
exports.emojis_by_unicode = {};
_.each(default_unicode_emojis, function (emoji) {
diff --git a/static/js/message_list_view.js b/static/js/message_list_view.js
index 88ebaf5314..3cd5d81986 100644
--- a/static/js/message_list_view.js
+++ b/static/js/message_list_view.js
@@ -162,6 +162,8 @@ MessageListView.prototype = {
}
_.each(message_containers, function (message_container) {
+ var message_reactions = reactions.get_message_reactions(message_container.msg);
+ message_container.msg.message_reactions = message_reactions;
message_container.include_recipient = false;
message_container.include_footer = false;
@@ -490,7 +492,11 @@ MessageListView.prototype = {
if (message_actions.rerender_messages.length > 0) {
_.each(message_actions.rerender_messages, function (message_container) {
var old_row = self.get_row(message_container.msg.id);
- var msg_to_render = _.extend(message_container, {table_name: this.table_name});
+ var msg_reactions = reactions.get_message_reactions(message_container.msg);
+ message_container.msg.message_reactions = msg_reactions;
+ var msg_to_render = _.extend(message_container, {
+ table_name: this.table_name,
+ });
var row = $(templates.render('single_message', msg_to_render));
self._post_process_dom_messages(row.get());
old_row.replaceWith(row);
@@ -504,7 +510,11 @@ MessageListView.prototype = {
last_message_row = table.find('.message_row:last').expectOne();
last_group_row = rows.get_message_recipient_row(last_message_row);
dom_messages = $(_.map(message_actions.append_messages, function (message_container) {
- var msg_to_render = _.extend(message_container, {table_name: this.table_name});
+ var msg_reactions = reactions.get_message_reactions(message_container.msg);
+ message_container.msg.message_reactions = msg_reactions;
+ var msg_to_render = _.extend(message_container, {
+ table_name: this.table_name,
+ });
return templates.render('single_message', msg_to_render);
}).join('')).filter('.message_row');
@@ -783,7 +793,11 @@ MessageListView.prototype = {
this._add_msg_timestring(message_container);
this._maybe_format_me_message(message_container);
- var msg_to_render = _.extend(message_container, {table_name: this.table_name});
+ var msg_reactions = reactions.get_message_reactions(message_container.msg);
+ message_container.msg.message_reactions = msg_reactions;
+ var msg_to_render = _.extend(message_container, {
+ table_name: this.table_name,
+ });
var rendered_msg = $(templates.render('single_message', msg_to_render));
this._post_process_dom_messages(rendered_msg.get());
row.replaceWith(rendered_msg);
diff --git a/static/js/message_store.js b/static/js/message_store.js
index 7f4425c40a..891c42eb76 100644
--- a/static/js/message_store.js
+++ b/static/js/message_store.js
@@ -133,6 +133,9 @@ function add_message_metadata(message) {
}
alert_words.process_message(message);
+ if (!message.reactions) {
+ message.reactions = [];
+ }
stored_messages[message.id] = message;
return message;
}
diff --git a/static/js/popovers.js b/static/js/popovers.js
index 1f6010f52e..e7747ace3a 100644
--- a/static/js/popovers.js
+++ b/static/js/popovers.js
@@ -4,6 +4,7 @@ var exports = {};
var current_actions_popover_elem;
var current_message_info_popover_elem;
+var current_message_reactions_popover_elem;
var emoji_map_is_open = false;
var userlist_placement = "right";
@@ -43,6 +44,78 @@ function show_message_info_popover(element, id) {
}
}
+exports.toggle_reactions_popover = function (element, id) {
+ var last_popover_elem = current_message_reactions_popover_elem;
+ popovers.hide_all();
+ if (last_popover_elem !== undefined
+ && last_popover_elem.get()[0] === element) {
+ // We want it to be the case that a user can dismiss a popover
+ // by clicking on the same element that caused the popover.
+ return;
+ }
+
+ current_msg_list.select_id(id);
+ var elt = $(element);
+ if (elt.data('popover') === undefined) {
+ var emojis = _.clone(emoji.emojis_name_to_css_class);
+ var emojis_used = reactions.get_emojis_used_by_user_for_message_id(id);
+ var realm_emojis = emoji.realm_emojis;
+ _.each(realm_emojis, function (realm_emoji, realm_emoji_name) {
+ emojis[realm_emoji_name] = {
+ name: realm_emoji_name,
+ is_realm_emoji: true,
+ url: realm_emoji.emoji_url
+ };
+ });
+ _.each(emojis_used, function (emoji_name) {
+ var is_realm_emoji = emojis[emoji_name].is_realm_emoji;
+ var url = emojis[emoji_name].url;
+ emojis[emoji_name] = {
+ name: emoji_name,
+ has_reacted: true,
+ css_class: emoji.emoji_name_to_css_class(emoji_name),
+ is_realm_emoji: is_realm_emoji,
+ url: url
+ };
+ });
+
+ var args = {
+ message_id: id,
+ emojis: emojis,
+ };
+
+ var approx_popover_height = 400;
+ var approx_popover_width = 400;
+ var distance_from_bottom = viewport.height() - elt.offset().top;
+ var distance_from_right = viewport.width() - elt.offset().left;
+ var will_extend_beyond_bottom_of_viewport = distance_from_bottom < approx_popover_height;
+ var will_extend_beyond_top_of_viewport = elt.offset().top < approx_popover_height;
+ var will_extend_beyond_left_of_viewport = elt.offset().left < (approx_popover_width / 2);
+ var will_extend_beyond_right_of_viewport = distance_from_right < (approx_popover_width / 2);
+ var placement = 'bottom';
+ if (will_extend_beyond_bottom_of_viewport && !will_extend_beyond_top_of_viewport) {
+ placement = 'top';
+ }
+ if (will_extend_beyond_right_of_viewport && !will_extend_beyond_left_of_viewport) {
+ placement = 'left';
+ }
+ if (will_extend_beyond_left_of_viewport && !will_extend_beyond_right_of_viewport) {
+ placement = 'right';
+ }
+ elt.prop('title', '');
+ elt.popover({
+ placement: placement,
+ title: "",
+ content: templates.render('reaction_popover_content', args),
+ trigger: "manual"
+ });
+ elt.popover("show");
+ elt.prop('title', 'Add reaction...');
+ $('.reaction-popover-filter').focus();
+ current_message_reactions_popover_elem = elt;
+ }
+};
+
exports.toggle_actions_popover = function (element, id) {
var last_popover_elem = current_actions_popover_elem;
popovers.hide_all();
@@ -85,6 +158,7 @@ exports.toggle_actions_popover = function (element, id) {
editability_menu_item: editability_menu_item,
can_mute_topic: can_mute_topic,
can_unmute_topic: can_unmute_topic,
+ should_display_add_reaction_option: message.sent_by_me,
conversation_time_uri: narrow.by_conversation_and_time_uri(message),
narrowed: narrow.active()
};
@@ -170,6 +244,10 @@ function message_info_popped() {
return current_message_info_popover_elem !== undefined;
}
+function reaction_popped() {
+ return current_message_reactions_popover_elem !== undefined;
+}
+
exports.hide_message_info_popover = function () {
if (message_info_popped()) {
current_message_info_popover_elem.popover("destroy");
@@ -177,6 +255,14 @@ exports.hide_message_info_popover = function () {
}
};
+exports.hide_reactions_popover = function () {
+ if (reaction_popped()) {
+ $('.popover').remove();
+ current_message_reactions_popover_elem.popover("destroy");
+ current_message_reactions_popover_elem = undefined;
+ }
+};
+
exports.hide_userlist_sidebar = function () {
$(".app-main .column-right").removeClass("expanded");
};
@@ -280,6 +366,19 @@ exports.register_click_handlers = function () {
popovers.toggle_actions_popover(this, rows.id(row));
});
+ $("#main_div").on("click", ".reaction_button", function (e) {
+ var row = $(this).closest(".message_row");
+ e.stopPropagation();
+ popovers.toggle_reactions_popover(this, rows.id(row));
+ });
+
+
+ $("body").on("click", ".reaction_button", function (e) {
+ var msgid = $(e.currentTarget).data('msgid');
+ e.stopPropagation();
+ popovers.toggle_reactions_popover(this, msgid);
+ });
+
$("#main_div").on("click", ".sender_info_hover", function (e) {
var row = $(this).closest(".message_row");
e.stopPropagation();
@@ -720,12 +819,14 @@ exports.register_click_handlers = function () {
exports.any_active = function () {
// True if any popover (that this module manages) is currently shown.
return popovers.actions_popped() || user_sidebar_popped() || stream_sidebar_popped() ||
- topic_sidebar_popped() || message_info_popped() || emoji_map_is_open;
+ topic_sidebar_popped() || message_info_popped() || emoji_map_is_open ||
+ reaction_popped();
};
exports.hide_all = function () {
popovers.hide_actions_popover();
popovers.hide_message_info_popover();
+ popovers.hide_reactions_popover();
popovers.hide_stream_sidebar_popover();
popovers.hide_topic_sidebar_popover();
popovers.hide_user_sidebar_popover();
diff --git a/static/js/reactions.js b/static/js/reactions.js
new file mode 100644
index 0000000000..343f7b889f
--- /dev/null
+++ b/static/js/reactions.js
@@ -0,0 +1,198 @@
+var reactions = (function () {
+var exports = {};
+
+function send_reaction_ajax(message_id, emoji_name, operation) {
+ if (!emoji.emojis_by_name[emoji_name]) {
+ // Emoji doesn't exist
+ return;
+ }
+ var args = {
+ url: '/json/messages/' + message_id + '/emoji_reactions/' + encodeURIComponent(emoji_name),
+ data: {},
+ success: function () {},
+ error: function (xhr) {
+ var response = channel.xhr_error_message("Error sending reaction", xhr);
+ blueslip.error(response);
+ }
+ };
+ if (operation === 'add') {
+ channel.put(args);
+ } else if (operation === 'remove') {
+ channel.del(args);
+ }
+}
+
+function get_user_list_for_message_reaction(message_id, emoji_name) {
+ var message = message_store.get(message_id);
+ var matching_reactions = message.reactions.filter(function (reaction) {
+ return reaction.emoji_name === emoji_name;
+ });
+ return matching_reactions.map(function (reaction) {
+ return reaction.user.id;
+ });
+}
+
+exports.message_reaction_on_click = function (message_id, emoji_name) {
+ // When a message's reaction is clicked,
+ // if the user has reacted to this message with this emoji
+ // the reaction is removed
+ // otherwise, the reaction is added
+ var user_list = get_user_list_for_message_reaction(message_id, emoji_name);
+ var operation = 'remove';
+ if (user_list.indexOf(page_params.user_id) === -1) {
+ // User hasn't reacted with this emoji to this message
+ operation = 'add';
+ }
+ send_reaction_ajax(message_id, emoji_name, operation);
+};
+
+function reaction_popover_reaction_on_click() {
+ // When an emoji is clicked in the popover,
+ // if the user has reacted to this message with this emoji
+ // the reaction is removed
+ // otherwise, the reaction is added
+ var emoji_name = this.title;
+ var message_id = $(this).parent().attr('data-message-id');
+ var user_list = get_user_list_for_message_reaction(message_id, emoji_name);
+ var operation = 'add';
+ if (user_list.indexOf(page_params.user_id) !== -1) {
+ // User has reacted with this emoji to this message
+ $(this).removeClass('reacted');
+ operation = 'remove';
+ }
+ send_reaction_ajax(message_id, emoji_name, operation);
+ popovers.hide_all();
+}
+
+function filter_emojis() {
+ var search = $(".reaction-popover-filter");
+ var search_term = search.expectOne().val().trim();
+ var reaction_list = $(".reaction-popover-reaction");
+ if (search_term !== '') {
+ reaction_list.filter(function () {
+ return this.title.indexOf(search_term) === -1;
+ }).css("display", "none");
+ reaction_list.filter(function () {
+ return this.title.indexOf(search_term) !== -1;
+ }).css("display", "block");
+ } else {
+ reaction_list.css("display", "block");
+ }
+}
+
+$(document).on('click', '.reaction-popover-reaction', reaction_popover_reaction_on_click);
+$(document).on('input', '.reaction-popover-filter', filter_emojis);
+
+function full_name(user_id) {
+ if (user_id === page_params.user_id) {
+ return 'You (click to remove)';
+ }
+ return people.get_person_from_user_id(user_id).full_name;
+}
+
+function generate_title(emoji_name, user_ids) {
+ var i = user_ids.indexOf(page_params.user_id);
+ if (i !== -1) {
+ // Move current user's id to start of list
+ user_ids.splice(i, 1);
+ user_ids.unshift(page_params.user_id);
+ }
+ var reacted_with_string = ' reacted with :' + emoji_name + ':';
+ var user_names = user_ids.map(full_name);
+ if (user_names.length === 1) {
+ return user_names[0] + reacted_with_string;
+ }
+ return _.initial(user_names).join(', ') + ' and ' + _.last(user_names) + reacted_with_string;
+}
+
+exports.add_reaction = function (event) {
+ event.emoji_name_css_class = emoji.emoji_name_to_css_class(event.emoji_name);
+ event.user.id = event.user.user_id;
+ message_store.get(event.message_id).reactions.push(event);
+ var message_element = $('.message_table').find("[zid='" + event.message_id + "']");
+ var message_reactions_element = message_element.find('.message_reactions');
+ var user_list = get_user_list_for_message_reaction(event.message_id, event.emoji_name);
+ var new_title = generate_title(event.emoji_name, user_list);
+ if (user_list.length === 1) {
+ if (emoji.realm_emojis[event.emoji_name]) {
+ event.is_realm_emoji = true;
+ event.url = emoji.realm_emojis[event.emoji_name].emoji_url;
+ }
+ event.count = 1;
+ event.title = new_title;
+ var reaction_button_element = message_reactions_element.find('.reaction_button');
+ $(templates.render('message_reaction', event)).insertBefore(reaction_button_element);
+ } else {
+ var reaction = message_reactions_element.find("[data-emoji-name='" + event.emoji_name + "']");
+ var count_element = reaction.find('.message_reaction_count');
+ count_element.html(user_list.length);
+ reaction.prop('title', new_title);
+ }
+};
+
+exports.remove_reaction = function (event) {
+ var emoji_name = event.emoji_name;
+ var message_id = event.message_id;
+ var user_id = event.user.user_id;
+ var message = message_store.get(message_id);
+ var i = -1;
+ _.each(message.reactions, function (reaction, index) {
+ if (reaction.emoji_name === emoji_name && reaction.user.id === user_id) {
+ i = index;
+ }
+ });
+ if (i !== -1) {
+ message.reactions.splice(i, 1);
+ }
+ var user_list = get_user_list_for_message_reaction(message_id, emoji_name);
+ var new_title = generate_title(emoji_name, user_list);
+ var message_element = $('.message_table').find("[zid='" + message_id + "']");
+ var message_reactions_element = message_element.find('.message_reactions');
+ var matching_reactions = message_reactions_element.find('[data-emoji-name="' + emoji_name + '"]');
+ var count_element = matching_reactions.find('.message_reaction_count');
+ matching_reactions.prop('title', new_title);
+ count_element.html(user_list.length);
+ if (user_list.length === 0) {
+ matching_reactions.remove();
+ }
+};
+
+exports.get_emojis_used_by_user_for_message_id = function (message_id) {
+ var user_id = page_params.user_id;
+ var message = message_store.get(message_id);
+ var reactions_by_user = message.reactions.filter(function (reaction) {
+ return reaction.user.id === user_id;
+ });
+ return reactions_by_user.map(function (reaction) {
+ return reaction.emoji_name;
+ });
+};
+
+exports.get_message_reactions = function (message) {
+ var message_reactions = new Dict();
+ _.each(message.reactions, function (reaction) {
+ var user_list = message_reactions.setdefault(reaction.emoji_name, []);
+ user_list.push(reaction.user.id);
+ });
+ var reactions = message_reactions.items().map(function (item) {
+ var reaction = {
+ emoji_name: item[0],
+ emoji_name_css_class: emoji.emoji_name_to_css_class(item[0]),
+ count: item[1].length,
+ title: generate_title(item[0], item[1])
+ };
+ if (emoji.realm_emojis[reaction.emoji_name]) {
+ reaction.is_realm_emoji = true;
+ reaction.url = emoji.realm_emojis[reaction.emoji_name].emoji_url;
+ }
+ return reaction;
+ });
+ return reactions;
+};
+
+return exports;
+}());
+
+if (typeof module !== 'undefined') {
+ module.exports = reactions;
+}
diff --git a/static/js/server_events.js b/static/js/server_events.js
index 0111b408b8..f938cb2b15 100644
--- a/static/js/server_events.js
+++ b/static/js/server_events.js
@@ -45,6 +45,14 @@ function dispatch_normal_event(event) {
reload.initiate(reload_options);
break;
+ case 'reaction':
+ if (event.op === 'add') {
+ reactions.add_reaction(event);
+ } else if (event.op === 'remove') {
+ reactions.remove_reaction(event);
+ }
+ break;
+
case 'realm':
if (event.op === 'update' && event.property === 'name') {
page_params.realm_name = event.value;
diff --git a/static/js/ui.js b/static/js/ui.js
index 192e1c6d68..ca90d33d17 100644
--- a/static/js/ui.js
+++ b/static/js/ui.js
@@ -124,7 +124,7 @@ function message_hover(message_row) {
!message.status_message) {
message_row.find(".edit_content").html('');
} else {
- message_row.find(".edit_content").html('');
+ message_row.find(".edit_content").html('');
}
current_message_hover = message_row;
}
diff --git a/static/styles/reactions.css b/static/styles/reactions.css
new file mode 100644
index 0000000000..f08bc878d9
--- /dev/null
+++ b/static/styles/reactions.css
@@ -0,0 +1,133 @@
+.message_reactions .reaction_button {
+ border-radius: 0.5em;
+ display: none;
+ margin: 0.2em;
+ padding: 0.2em;
+ padding-left: 0.3em;
+ padding-right: 0.3em;
+ float: left;
+}
+
+.message_reactions i {
+ font-size: 1.3em;
+ float: left;
+ color: #555;
+}
+
+.reaction_button .message_reaction_count {
+ font-size: 1.1em;
+ color: #555;
+}
+
+.message_reactions:hover .reaction_button {
+ display: block;
+ background-color: #fafafa;
+ border: thin solid #bbb;
+ color: #bbb;
+}
+
+.message_reactions .reaction_button:hover {
+ border: thin solid #add8e6;
+ background-color: #eef7fa;
+}
+
+.reaction_button:hover i {
+ color: #0088CC;
+}
+
+.reaction_button:hover .message_reaction_count {
+ color: #0088CC;
+}
+
+.reaction_button:hover {
+ cursor: pointer;
+ opacity: 1.0;
+ color: #0088CC;
+}
+
+.reaction-popover {
+ display: inline-block;
+ width: 20em;
+ margin-right: 0px;
+ margin-left: -14px;
+ margin-top: -9px;
+ margin-bottom: -9px;
+}
+
+.reaction-popover-top {
+ padding: 0.8em;
+ padding-bottom: 0;
+ width: 100%;
+}
+
+.reaction-popover-top .icon-vector-search {
+ position: absolute;
+ right: 2.3em;
+ top: 1.3em;
+ color: #bbb;
+}
+
+.reaction-popover-filter {
+ width: 87%;
+ margin: auto;
+ padding-left: 3em;
+}
+
+.reaction-popover-emoji-map {
+ margin: 0;
+ padding: 0.5em;
+ padding-top: 0;
+ overflow: hidden;
+ overflow-y: scroll;
+ display: inline-block;
+ height: 16.5em;
+ width: 100%;
+}
+
+.reaction-popover-reaction {
+ float: left;
+ margin: 0.1em;
+ padding: 0.3em;
+ cursor: pointer;
+ border: thin solid white;
+ border-radius: 0.5em;
+}
+
+.reaction-popover .reacted {
+ background-color: #eef7fa;
+ border-color: #add8e6;
+}
+
+.message_reactions {
+ margin-left: -0.2em;
+ padding-left: 46px;
+ overflow: auto;
+}
+
+.message_reaction {
+ float: left;
+ margin: 0.2em;
+ padding: 0.4em;
+ padding-left: 0.3em;
+ padding-right: 0.3em;
+ cursor: pointer;
+ background-color: #eef7fa;
+ border: thin solid #add8e6;
+ border-radius: 0.5em;
+}
+
+.message_reaction .emoji {
+ float: left;
+ zoom: 0.80;
+ -moz-transform: scale(0.80);
+ -moz-transform-origin: 0 0;
+}
+
+.message_reaction_count {
+ font-weight: bold;
+ font-size: 0.8em;
+ float: left;
+ color: #0088CC;
+ margin-left: 0.1em;
+ line-height: 1em;
+}
diff --git a/static/templates/actions_popover_content.handlebars b/static/templates/actions_popover_content.handlebars
index e07a23b7d1..1129375f54 100644
--- a/static/templates/actions_popover_content.handlebars
+++ b/static/templates/actions_popover_content.handlebars
@@ -38,6 +38,15 @@
{{/if}}
+ {{#if should_display_add_reaction_option}}
+
+
+
+ {{#tr message}}Add emoji reaction{{/tr}}
+
+
+ {{/if}}
+
{{t "Link to this conversation" }}
diff --git a/static/templates/message_reaction.handlebars b/static/templates/message_reaction.handlebars
new file mode 100644
index 0000000000..712b1df579
--- /dev/null
+++ b/static/templates/message_reaction.handlebars
@@ -0,0 +1,8 @@
+
+ {{#if this.is_realm_emoji}}
+
+ {{else}}
+
+ {{/if}}
+
{{this.count}}
+
diff --git a/static/templates/message_reactions.handlebars b/static/templates/message_reactions.handlebars
new file mode 100644
index 0000000000..1ec58fc969
--- /dev/null
+++ b/static/templates/message_reactions.handlebars
@@ -0,0 +1,7 @@
+{{#each this/msg/message_reactions}}
+ {{partial "message_reaction"}}
+{{/each}}
+
diff --git a/static/templates/reaction_popover_content.handlebars b/static/templates/reaction_popover_content.handlebars
new file mode 100644
index 0000000000..a0cca0c807
--- /dev/null
+++ b/static/templates/reaction_popover_content.handlebars
@@ -0,0 +1,18 @@
+{{! Contents of the "reaction emoji map" popup }}
+
+
+
+
+
+
+ {{#each emojis}}
+
+ {{#if this/is_realm_emoji}}
+
+ {{else}}
+
+ {{/if}}
+
+ {{/each}}
+
+
diff --git a/static/templates/single_message.handlebars b/static/templates/single_message.handlebars
index c159726444..4f50382fe1 100644
--- a/static/templates/single_message.handlebars
+++ b/static/templates/single_message.handlebars
@@ -60,6 +60,7 @@
{{t "[More...]" }}
{{t "[Condense this message]" }}
+ {{ partial "message_reactions" }}
diff --git a/zproject/settings.py b/zproject/settings.py
index 2e5d2824c2..24335a12dd 100644
--- a/zproject/settings.py
+++ b/zproject/settings.py
@@ -679,6 +679,7 @@ PIPELINE = {
'styles/settings.css',
'styles/subscriptions.css',
'styles/compose.css',
+ 'styles/reactions.css',
'styles/left-sidebar.css',
'styles/overlay.css',
'styles/pygments.css',
@@ -698,6 +699,7 @@ PIPELINE = {
'styles/settings.css',
'styles/subscriptions.css',
'styles/compose.css',
+ 'styles/reactions.css',
'styles/left-sidebar.css',
'styles/overlay.css',
'styles/pygments.css',
@@ -838,6 +840,7 @@ JS_SPECS = {
'js/referral.js',
'js/custom_markdown.js',
'js/bot_data.js',
+ 'js/reactions.js',
# JS bundled by webpack is also included here if PIPELINE_ENABLED setting is true
],
'output_filename': 'min/app.js'