From 9c64a08cad2286b3a2c1bd90566c62709d0cef55 Mon Sep 17 00:00:00 2001 From: Arpith Siromoney Date: Fri, 2 Dec 2016 17:53:23 +0530 Subject: [PATCH] Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541. --- .eslintrc.json | 1 + frontend_tests/node_tests/templates.js | 31 +++ static/js/click_handlers.js | 7 + static/js/emoji.js | 21 +- static/js/message_list_view.js | 20 +- static/js/message_store.js | 3 + static/js/popovers.js | 103 ++++++++- static/js/reactions.js | 198 ++++++++++++++++++ static/js/server_events.js | 8 + static/js/ui.js | 2 +- static/styles/reactions.css | 133 ++++++++++++ .../actions_popover_content.handlebars | 9 + static/templates/message_reaction.handlebars | 8 + static/templates/message_reactions.handlebars | 7 + .../reaction_popover_content.handlebars | 18 ++ static/templates/single_message.handlebars | 1 + zproject/settings.py | 3 + 17 files changed, 563 insertions(+), 10 deletions(-) create mode 100644 static/js/reactions.js create mode 100644 static/styles/reactions.css create mode 100644 static/templates/message_reaction.handlebars create mode 100644 static/templates/message_reactions.handlebars create mode 100644 static/templates/reaction_popover_content.handlebars 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'