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'