From 786bda674cddff3b22194120dac4137663338ae8 Mon Sep 17 00:00:00 2001 From: Eric Eslinger Date: Tue, 13 Feb 2018 20:21:46 -0800 Subject: [PATCH] popovers: Add a popover for group mentions. This adds a click handler to `.user-group-mention` which works in a fashion that is quite similar to `.user-mention`. It generates and displays a popover. The popover has a list of members, their online status (if they are not bots) or their bot status if they are bots (it's not clear whether ultimately bots should be able to be members of usergroups, but I'm able to add one, so I thought it would be worth supporting). The popover's `UL` element has max-height and overflow-y atttributes so large groups will grow a scrollbar. Fixes #8300. --- frontend_tests/node_tests/templates.js | 47 ++++++++ static/js/popovers.js | 111 ++++++++++++++++-- static/styles/popovers.css | 23 ++++ .../user_group_info_popover.handlebars | 7 ++ ...user_group_info_popover_content.handlebars | 30 +++++ 5 files changed, 205 insertions(+), 13 deletions(-) create mode 100644 static/templates/user_group_info_popover.handlebars create mode 100644 static/templates/user_group_info_popover_content.handlebars diff --git a/frontend_tests/node_tests/templates.js b/frontend_tests/node_tests/templates.js index 27ffb1a4ad..ad635adaa7 100644 --- a/frontend_tests/node_tests/templates.js +++ b/frontend_tests/node_tests/templates.js @@ -1350,6 +1350,53 @@ function render(template_name, args) { }()); +(function user_group_info_popover() { + var html = render('user_group_info_popover'); + global.write_handlebars_output("user_group_info_popover", html); + + $(html).hasClass('popover message-info-popover group-info-popover'); +}()); + +(function user_group_info_popover_content() { + var args = { + group_name: 'groupName', + group_description: 'groupDescription', + members: [ + { + presence_status: 'active', + full_name: 'Active Alice', + user_last_seen_time_status: 'time', + is_bot: false, + }, + { + presence_status: 'offline', + full_name: 'Bot Bob', + user_last_seen_time_status: 'time', + is_bot: true, + }, + { + presence_status: 'offline', + full_name: 'Inactive Imogen', + user_last_seen_time_status: 'time', + is_bot: false, + }, + ], + }; + + var html = render('user_group_info_popover_content', args); + global.write_handlebars_output("user_group_info_popover_content", html); + + var allUsers = $(html).find("li"); + assert.equal(allUsers[0].classList.contains("user_active"), true); + assert.equal(allUsers[2].classList.contains("user_offline"), true); + assert.equal($(allUsers[0]).text().trim(), 'Active Alice'); + assert.equal($(allUsers[1]).text().trim(), 'Bot Bob'); + assert.equal($(allUsers[2]).text().trim(), 'Inactive Imogen'); + + assert.equal($(html).find('.group-name').text().trim(), 'groupName'); + assert.equal($(html).find('.group-description').text().trim(), 'groupDescription'); +}()); + (function user_info_popover() { var html = render('user_info_popover', {class: 'message-info-popover'}); global.write_handlebars_output("user_info_popover", html); diff --git a/static/js/popovers.js b/static/js/popovers.js index 18a390bd19..b7aa99fc07 100644 --- a/static/js/popovers.js +++ b/static/js/popovers.js @@ -67,11 +67,28 @@ function user_last_seen_time_status(user_id) { return timerender.last_seen_status_from_date(last_active_date.clone()); } +function calculate_info_popover_placement(size, elt) { + var ypos = elt.offset().top; + + if (!((ypos + (size / 2) < message_viewport.height()) && + (ypos > (size / 2)))) { + if (((ypos + size) < message_viewport.height())) { + return 'bottom'; + } else if (ypos > size) { + return 'top'; + } + } +} + +// exporting for testability +exports._test_calculate_info_popover_placement = calculate_info_popover_placement; + // element is the target element to pop off of // user is the user whose profile to show // message is the message containing it, which should be selected function show_user_info_popover(element, user, message) { var last_popover_elem = current_message_info_popover_elem; + var popover_size = 428; // hardcoded pixel height of the popover popovers.hide_all(); if (last_popover_elem !== undefined && last_popover_elem.get()[0] === element) { @@ -104,21 +121,9 @@ function show_user_info_popover(element, user, message) { is_bot: people.get_person_from_user_id(user.user_id).is_bot, }; - var ypos = elt.offset().top; - var popover_size = 428; - var placement = "right"; - - if (!((ypos + (popover_size / 2) < message_viewport.height()) && - (ypos > (popover_size / 2)))) { - if (((ypos + popover_size) < message_viewport.height())) { - placement = "bottom"; - } else if (ypos > popover_size) { - placement = "top"; - } - } elt.popover({ - placement: placement, + placement: calculate_info_popover_placement(popover_size, elt), template: templates.render('user_info_popover', {class: "message-info-popover"}), title: templates.render('user_info_popover_title', {user_avatar: "avatar/" + user.email}), @@ -133,6 +138,73 @@ function show_user_info_popover(element, user, message) { } } +function fetch_group_members(member_ids) { + return member_ids + .map(function (m) { + return people.get_person_from_user_id(m); + }) + .filter(function (m) { + return m !== undefined; + }) + .map(function (p) { + return Object.assign({}, p, { + presence_status: presence.get_status(p.user_id), + is_active: people.is_active_user_for_popover(p.user_id), + user_last_seen_time_status: user_last_seen_time_status(p.user_id), + }); + }); +} + +function sort_group_members(members) { + return members + .sort(function (a, b) { + return a.full_name.localeCompare(b.full_name); + }); +} + +// exporting these functions for testing purposes +exports._test_fetch_group_members = fetch_group_members; +exports._test_sort_group_members = sort_group_members; + +// element is the target element to pop off of +// user is the user whose profile to show +// message is the message containing it, which should be selected +function show_user_group_info_popover(element, group, message) { + var last_popover_elem = current_message_info_popover_elem; + // hardcoded pixel height of the popover + // note that the actual size varies (in group size), but this is about as big as it gets + var popover_size = 390; + 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(message.id); + var elt = $(element); + if (elt.data('popover') === undefined) { + var args = { + group_name: group.name, + group_description: group.description, + members: sort_group_members(fetch_group_members(group.members)), + }; + elt.popover({ + placement: calculate_info_popover_placement(popover_size, elt), + template: templates.render('user_group_info_popover', {class: "message-info-popover"}), + content: templates.render('user_group_info_popover_content', args), + trigger: "manual", + }); + elt.popover("show"); + $('.nav.nav-list.member-list').perfectScrollbar({ + suppressScrollX: true, + useKeyboard: false, + wheelSpeed: 0.5, + }); + current_message_info_popover_elem = elt; + } +} + exports.toggle_actions_popover = function (element, id) { var last_popover_elem = current_actions_popover_elem; popovers.hide_all(); @@ -449,6 +521,19 @@ exports.register_click_handlers = function () { show_user_info_popover(this, user, message); }); + $("#main_div").on("click", ".user-group-mention", function (e) { + var id = $(this).attr('data-user-group-id'); + var row = $(this).closest(".message_row"); + e.stopPropagation(); + var message = current_msg_list.get(rows.id(row)); + var group = user_groups.get_user_group_from_id(id); + if (group === undefined) { + blueslip.error('Unable to find user group in message' + message.sender_id); + } else { + show_user_group_info_popover(this, group, message); + } + }); + $('body').on('click', '.user_popover .narrow_to_private_messages', function (e) { var user_id = $(e.target).parents('ul').attr('data-user-id'); var email = people.get_person_from_user_id(user_id).email; diff --git a/static/styles/popovers.css b/static/styles/popovers.css index 1b01d18622..bd08af51c5 100644 --- a/static/styles/popovers.css +++ b/static/styles/popovers.css @@ -96,6 +96,29 @@ ul.remind_me_popover .remind_icon { width: 240px; } +.group-info-popover .group-info { + text-align: center; +} + +.group-info-popover .group-info .group-name { + font-weight: bold; +} + +.group-info-popover .member-list { + max-height: 300px; + overflow-y: auto; + list-style: none; + margin-left: 0; +} + +.group-info-popover .member-list .bot { + color: hsl(180, 5%, 74%); + vertical-align: top; + width: 20px; + padding-top: 3.5px; + text-align: center; +} + .user_popover .popover-title { padding: 0; } diff --git a/static/templates/user_group_info_popover.handlebars b/static/templates/user_group_info_popover.handlebars new file mode 100644 index 0000000000..c2e44b5bd7 --- /dev/null +++ b/static/templates/user_group_info_popover.handlebars @@ -0,0 +1,7 @@ +
+
+
+
+
+
+
diff --git a/static/templates/user_group_info_popover_content.handlebars b/static/templates/user_group_info_popover_content.handlebars new file mode 100644 index 0000000000..6e2c327e5f --- /dev/null +++ b/static/templates/user_group_info_popover_content.handlebars @@ -0,0 +1,30 @@ +{{! Contents of the "user group info" popup }} +
+
{{group_name}}
+
+ {{group_description}} +
+
+
+ +