From c5f6c00a813f7bcaaa518dfceb25806aec83bebd Mon Sep 17 00:00:00 2001 From: Daniil Fadeev Date: Wed, 13 Sep 2023 12:59:24 +0400 Subject: [PATCH] popovers: Extract user info popovers into their own module. The intent behind this commit is to tidy up how we handle user info popovers. The first step is to move everything related to them into its own module. This commit should not have any functional changes. --- tools/test-js-with-node | 1 + web/src/hotkey.js | 25 +- web/src/popovers.js | 773 +---------------- web/src/recent_view_ui.js | 3 +- web/src/ui_init.js | 2 + web/src/user_card_popover.js | 785 ++++++++++++++++++ web/tests/hotkey.test.js | 2 +- ...vers.test.js => user_card_popover.test.js} | 10 +- 8 files changed, 816 insertions(+), 785 deletions(-) create mode 100644 web/src/user_card_popover.js rename web/tests/{popovers.test.js => user_card_popover.test.js} (96%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index e1d2e93401..813fea9fca 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -219,6 +219,7 @@ EXEMPT_FILES = make_set( "web/src/upload.js", "web/src/upload_widget.ts", "web/src/url-template.d.ts", + "web/src/user_card_popover.js", "web/src/user_group_create.js", "web/src/user_group_create_members.js", "web/src/user_group_create_members_data.ts", diff --git a/web/src/hotkey.js b/web/src/hotkey.js index 923c974446..6fc688b705 100644 --- a/web/src/hotkey.js +++ b/web/src/hotkey.js @@ -50,6 +50,7 @@ import * as stream_popover from "./stream_popover"; import * as stream_settings_ui from "./stream_settings_ui"; import * as topic_zoom from "./topic_zoom"; import * as unread_ops from "./unread_ops"; +import * as user_card_popover from "./user_card_popover"; import * as user_group_popover from "./user_group_popover"; import {user_settings} from "./user_settings"; import * as user_topics_ui from "./user_topics_ui"; @@ -264,8 +265,8 @@ export function process_escape_key(e) { } if (popovers.any_active()) { - if (popovers.user_info_manage_menu_popped()) { - popovers.hide_user_info_popover_manage_menu(); + if (user_card_popover.user_info_manage_menu_popped()) { + user_card_popover.hide_user_info_popover_manage_menu(); $("#user_info_popover .user_info_popover_manage_menu_btn").trigger("focus"); return true; } @@ -369,23 +370,23 @@ function handle_popover_events(event_name) { return true; } - if (popovers.user_info_manage_menu_popped()) { - popovers.user_info_popover_manage_menu_handle_keyboard(event_name); + if (user_card_popover.user_info_manage_menu_popped()) { + user_card_popover.user_info_popover_manage_menu_handle_keyboard(event_name); return true; } - if (popovers.message_info_popped()) { - popovers.user_info_popover_for_message_handle_keyboard(event_name); + if (user_card_popover.message_info_popped()) { + user_card_popover.user_info_popover_for_message_handle_keyboard(event_name); return true; } - if (popovers.user_info_popped()) { - popovers.user_info_popover_handle_keyboard(event_name); + if (user_card_popover.user_info_popped()) { + user_card_popover.user_info_popover_handle_keyboard(event_name); return true; } - if (popovers.user_sidebar_popped()) { - popovers.user_sidebar_popover_handle_keyboard(event_name); + if (user_card_popover.user_sidebar_popped()) { + user_card_popover.user_sidebar_popover_handle_keyboard(event_name); return true; } @@ -735,7 +736,7 @@ export function process_hotkey(e, hotkey) { return false; } - if (overlays.settings_open() && !popovers.user_info_popped()) { + if (overlays.settings_open() && !user_card_popover.user_info_popped()) { return false; } @@ -1027,7 +1028,7 @@ export function process_hotkey(e, hotkey) { lightbox.show_from_selected_message(); return true; case "show_sender_info": - popovers.show_sender_info(); + user_card_popover.show_sender_info(); return true; // ':': open reactions to message case "toggle_reactions_popover": { diff --git a/web/src/popovers.js b/web/src/popovers.js index bb69937090..b7d8979556 100644 --- a/web/src/popovers.js +++ b/web/src/popovers.js @@ -1,73 +1,26 @@ -import ClipboardJS from "clipboard"; -import {parseISO} from "date-fns"; import $ from "jquery"; -import tippy, {hideAll} from "tippy.js"; +import {hideAll} from "tippy.js"; import url_template_lib from "url-template"; -import render_no_arrow_popover from "../templates/no_arrow_popover.hbs"; import render_playground_links_popover_content from "../templates/playground_links_popover_content.hbs"; -import render_user_info_popover_content from "../templates/user_info_popover_content.hbs"; -import render_user_info_popover_manage_menu from "../templates/user_info_popover_manage_menu.hbs"; -import render_user_info_popover_title from "../templates/user_info_popover_title.hbs"; import * as blueslip from "./blueslip"; -import * as buddy_data from "./buddy_data"; -import * as channel from "./channel"; -import * as compose_actions from "./compose_actions"; -import * as compose_state from "./compose_state"; -import * as compose_ui from "./compose_ui"; -import * as dialog_widget from "./dialog_widget"; import * as emoji_picker from "./emoji_picker"; -import * as hash_util from "./hash_util"; -import {$t, $t_html} from "./i18n"; -import * as message_lists from "./message_lists"; import * as message_viewport from "./message_viewport"; -import * as muted_users from "./muted_users"; -import * as muted_users_ui from "./muted_users_ui"; -import * as narrow from "./narrow"; import * as overlays from "./overlays"; -import {page_params} from "./page_params"; -import * as people from "./people"; import * as popover_menus from "./popover_menus"; import * as realm_playground from "./realm_playground"; import * as resize from "./resize"; -import * as rows from "./rows"; -import * as settings_config from "./settings_config"; -import * as settings_users from "./settings_users"; import * as stream_popover from "./stream_popover"; -import * as timerender from "./timerender"; -import * as ui_report from "./ui_report"; +import * as user_card_popover from "./user_card_popover"; import * as user_group_popover from "./user_group_popover"; -import * as user_profile from "./user_profile"; -import {user_settings} from "./user_settings"; -import * as user_status from "./user_status"; -import * as user_status_ui from "./user_status_ui"; -let $current_message_info_popover_elem; -let $current_user_info_popover_elem; -let $current_user_info_popover_manage_menu; let $current_playground_links_popover_elem; -let userlist_placement = "right"; - let list_of_popovers = []; export function clear_for_testing() { - $current_message_info_popover_elem = undefined; - $current_user_info_popover_elem = undefined; - $current_user_info_popover_manage_menu = undefined; $current_playground_links_popover_elem = undefined; list_of_popovers.length = 0; - userlist_placement = "right"; -} - -export function clipboard_enable(arg) { - // arg is a selector or element - // We extract this function for testing purpose. - return new ClipboardJS(arg); -} - -export function elem_to_user_id($elem) { - return Number.parseInt($elem.attr("data-user-id"), 10); } // this utilizes the proxy pattern to intercept all calls to $.fn.popover @@ -86,347 +39,6 @@ $.fn.popover = Object.assign(function (...args) { } }, old_popover); -function copy_email_handler(e) { - const $email_el = $(e.trigger.parentElement); - const $copy_icon = $email_el.find("i"); - - // only change the parent element's text back to email - // and not overwrite the tooltip. - const email_textnode = $email_el[0].childNodes[2]; - - $email_el.addClass("email_copied"); - email_textnode.nodeValue = $t({defaultMessage: "Email copied"}); - - setTimeout(() => { - $email_el.removeClass("email_copied"); - email_textnode.nodeValue = $copy_icon.attr("data-clipboard-text"); - }, 1500); - e.clearSelection(); -} - -function init_email_clipboard() { - /* - This shows (and enables) the copy-text icon for folks - who have names that would overflow past the right - edge of our user mention popup. - */ - $(".user_popover_email").each(function () { - if (this.clientWidth < this.scrollWidth) { - const $email_el = $(this); - const $copy_email_icon = $email_el.find("i"); - - /* - For deactivated users, the copy-email icon will - not even be present in the HTML, so we don't do - anything. We don't reveal emails for deactivated - users. - */ - if ($copy_email_icon[0]) { - $copy_email_icon.removeClass("hide_copy_icon"); - const copy_email_clipboard = clipboard_enable($copy_email_icon[0]); - copy_email_clipboard.on("success", copy_email_handler); - } - } - }); -} - -function init_email_tooltip(user) { - /* - This displays the email tooltip for folks - who have names that would overflow past the right - edge of our user mention popup. - */ - - $(".user_popover_email").each(function () { - if (this.clientWidth < this.scrollWidth) { - tippy(this, { - placement: "bottom", - content: people.get_visible_email(user), - interactive: true, - }); - } - }); -} - -function load_medium_avatar(user, $elt) { - const user_avatar_url = people.medium_avatar_url_for_person(user); - const sender_avatar_medium = new Image(); - - sender_avatar_medium.src = user_avatar_url; - $(sender_avatar_medium).on("load", function () { - $elt.css("background-image", "url(" + $(this).attr("src") + ")"); - }); -} - -export function hide_user_info_popover_manage_menu() { - if ($current_user_info_popover_manage_menu !== undefined) { - $current_user_info_popover_manage_menu.popover("destroy"); - $current_user_info_popover_manage_menu = undefined; - } -} - -function show_user_info_popover_manage_menu(element, user) { - const $last_popover_elem = $current_user_info_popover_manage_menu; - hide_user_info_popover_manage_menu(); - if ($last_popover_elem !== undefined && $last_popover_elem.get()[0] === element) { - return; - } - - const is_me = people.is_my_user_id(user.user_id); - const is_muted = muted_users.is_user_muted(user.user_id); - const is_system_bot = user.is_system_bot; - const muting_allowed = !is_me; - - const args = { - can_mute: muting_allowed && !is_muted, - can_manage_user: page_params.is_admin && !is_me && !is_system_bot, - can_unmute: muting_allowed && is_muted, - is_active: people.is_active_user_for_popover(user.user_id), - is_bot: user.is_bot, - user_id: user.user_id, - }; - - const $popover_elt = $(element); - $popover_elt.popover({ - content: render_user_info_popover_manage_menu(args), - placement: "bottom", - html: true, - trigger: "manual", - fixed: true, - }); - - $popover_elt.popover("show"); - $current_user_info_popover_manage_menu = $popover_elt; -} - -function render_user_info_popover( - user, - popover_element, - is_sender_popover, - has_message_context, - private_msg_class, - template_class, - popover_placement, -) { - const is_me = people.is_my_user_id(user.user_id); - - let invisible_mode = false; - - if (is_me) { - invisible_mode = !user_settings.presence_enabled; - } - - const is_active = people.is_active_user_for_popover(user.user_id); - const is_system_bot = user.is_system_bot; - const status_text = user_status.get_status_text(user.user_id); - const status_emoji_info = user_status.get_status_emoji(user.user_id); - const spectator_view = page_params.is_spectator; - - const show_manage_menu = !spectator_view && !is_me; - - let date_joined; - - // Some users might not have `date_joined` field because of the missing server data. - // These users are added late in `people.js` via `extract_people_from_message`. - if (spectator_view && !user.is_missing_server_data) { - date_joined = timerender.get_localized_date_or_time_for_format( - parseISO(user.date_joined), - "dayofyear_year", - ); - } - // Filtering out only those profile fields that can be display in the popover and are not empty. - const field_types = page_params.custom_profile_field_types; - const display_profile_fields = page_params.custom_profile_fields - .map((f) => user_profile.get_custom_profile_field_data(user, f, field_types)) - .filter((f) => f.display_in_profile_summary && f.value !== undefined && f.value !== null); - - const args = { - invisible_mode, - can_send_private_message: - is_active && - !is_me && - page_params.realm_private_message_policy !== - settings_config.private_message_policy_values.disabled.code, - display_profile_fields, - has_message_context, - is_active, - is_bot: user.is_bot, - is_me, - is_sender_popover, - pm_with_url: hash_util.pm_with_url(user.email), - user_circle_class: buddy_data.get_user_circle_class(user.user_id), - private_message_class: private_msg_class, - sent_by_url: hash_util.by_sender_url(user.email), - show_manage_menu, - user_email: user.delivery_email, - user_full_name: user.full_name, - user_id: user.user_id, - user_last_seen_time_status: buddy_data.user_last_seen_time_status(user.user_id), - user_time: people.get_user_time(user.user_id), - user_type: people.get_user_type(user.user_id), - status_content_available: Boolean(status_text || status_emoji_info), - status_text, - status_emoji_info, - user_mention_syntax: people.get_mention_syntax(user.full_name, user.user_id), - date_joined, - spectator_view, - }; - - if (user.is_bot) { - const bot_owner_id = user.bot_owner_id; - if (is_system_bot) { - args.is_system_bot = is_system_bot; - } else if (bot_owner_id) { - const bot_owner = people.get_by_user_id(bot_owner_id); - args.bot_owner = bot_owner; - } - } - - const $popover_content = $(render_user_info_popover_content(args)); - popover_element.popover({ - content: $popover_content.get(0), - fixed: true, - placement: popover_placement, - template: render_no_arrow_popover({class: template_class}), - title: render_user_info_popover_title({ - // See the load_medium_avatar comment for important background. - user_avatar: people.small_avatar_url_for_person(user), - user_is_guest: user.is_guest, - }), - html: true, - trigger: "manual", - top_offset: $("#userlist-title").get_offset_to_window().top + 15, - fix_positions: true, - }); - popover_element.popover("show"); - - init_email_clipboard(); - init_email_tooltip(user); - const $user_name_element = $popover_content.find(".user_full_name"); - const $bot_owner_element = $popover_content.find(".bot_owner"); - if ($user_name_element.prop("clientWidth") < $user_name_element.prop("scrollWidth")) { - $user_name_element.addClass("tippy-zulip-tooltip"); - } - if ( - args.bot_owner && - $bot_owner_element.prop("clientWidth") < $bot_owner_element.prop("scrollWidth") - ) { - $bot_owner_element.addClass("tippy-zulip-tooltip"); - } - - // Note: We pass the normal-size avatar in initial rendering, and - // then query the server to replace it with the medium-size - // avatar. The purpose of this double-fetch approach is to take - // advantage of the fact that the browser should already have the - // low-resolution image cached and thus display a low-resolution - // avatar rather than a blank area during the network delay for - // fetching the medium-size one. - load_medium_avatar(user, $(".popover-avatar")); -} - -// 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_for_message(element, user, message) { - const $last_popover_elem = $current_message_info_popover_elem; - 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; - } - message_lists.current.select_id(message.id); - const $elt = $(element); - if ($elt.data("popover") === undefined) { - if (user === undefined) { - // This is never supposed to happen, not even for deactivated - // users, so we'll need to debug this error if it occurs. - blueslip.error("Bad sender in message", { - zid: message.id, - sender_id: message.sender_id, - }); - return; - } - - const is_sender_popover = message.sender_id === user.user_id; - render_user_info_popover( - user, - $elt, - is_sender_popover, - true, - "respond_personal_button", - "message-info-popover", - "right", - ); - - $current_message_info_popover_elem = $elt; - } -} - -export function show_user_info_popover(element, user) { - const $last_popover_elem = $current_user_info_popover_elem; - hide_all(); - if ($last_popover_elem !== undefined && $last_popover_elem.get()[0] === element) { - return; - } - const $elt = $(element); - render_user_info_popover( - user, - $elt, - false, - false, - "compose_private_message", - "user-info-popover", - "right", - ); - $current_user_info_popover_elem = $elt; -} - -function get_user_info_popover_for_message_items() { - if (!$current_message_info_popover_elem) { - blueslip.error("Trying to get menu items when action popover is closed."); - return undefined; - } - - const popover_data = $current_message_info_popover_elem.data("popover"); - if (!popover_data) { - blueslip.error("Cannot find popover data for actions menu."); - return undefined; - } - - return $("li:not(.divider):visible a", popover_data.$tip); -} - -function get_user_info_popover_items() { - const $popover_elt = $("div.user-info-popover"); - if (!$current_user_info_popover_elem || !$popover_elt.length) { - blueslip.error("Trying to get menu items when action popover is closed."); - return undefined; - } - - if ($popover_elt.length >= 2) { - blueslip.error("More than one user info popovers cannot be opened at same time."); - return undefined; - } - - return $("li:not(.divider):visible a", $popover_elt); -} - -function get_user_info_popover_manage_menu_items() { - if (!$current_user_info_popover_manage_menu) { - blueslip.error("Trying to get menu items when action popover is closed."); - return undefined; - } - - const popover_data = $current_user_info_popover_manage_menu.data("popover"); - if (!popover_data) { - blueslip.error("Cannot find popover data for actions menu."); - return undefined; - } - - return $(".user_info_popover_manage_menu li:not(.divider):visible a", popover_data.$tip); -} - function get_action_menu_menu_items() { const $current_actions_popover_elem = $("[data-tippy-root] .actions_popover"); if (!$current_actions_popover_elem) { @@ -454,8 +66,8 @@ export function popover_items_handle_keyboard(key, $items) { if (key === "enter" && index >= 0 && index < $items.length) { $items[index].click(); - if (user_info_manage_menu_popped()) { - const $items = get_user_info_popover_manage_menu_items(); + if (user_card_popover.user_info_manage_menu_popped()) { + const $items = user_card_popover.get_user_info_popover_manage_menu_items(); focus_first_popover_item($items); } return; @@ -481,32 +93,6 @@ export function focus_first_action_popover_item() { focus_first_popover_item($items); } -export function message_info_popped() { - return $current_message_info_popover_elem !== undefined; -} - -export function hide_message_info_popover() { - if (message_info_popped()) { - $current_message_info_popover_elem.popover("destroy"); - $current_message_info_popover_elem = undefined; - } -} - -export function user_info_popped() { - return $current_user_info_popover_elem !== undefined; -} - -export function user_info_manage_menu_popped() { - return $current_user_info_popover_manage_menu !== undefined; -} - -export function hide_user_info_popover() { - if (user_info_popped()) { - $current_user_info_popover_elem.popover("destroy"); - $current_user_info_popover_elem = undefined; - } -} - export function hide_userlist_sidebar() { $(".app-main .column-right").removeClass("expanded"); } @@ -516,92 +102,6 @@ export function show_userlist_sidebar() { resize.resize_page_components(); } -let current_user_sidebar_user_id; -let current_user_sidebar_popover; - -export function user_sidebar_popped() { - return current_user_sidebar_popover !== undefined; -} - -export function hide_user_sidebar_popover() { - if (user_sidebar_popped()) { - // this hide_* method looks different from all the others since - // the presence list may be redrawn. Due to funkiness with jQuery's .data() - // this would confuse $.popover("destroy"), which looks at the .data() attached - // to a certain element. We thus save off the .data("popover") in the - // show_user_sidebar_popover and inject it here before calling destroy. - $("#user_presences").data("popover", current_user_sidebar_popover); - $("#user_presences").popover("destroy"); - current_user_sidebar_user_id = undefined; - current_user_sidebar_popover = undefined; - } -} - -function hide_all_user_info_popovers() { - hide_user_info_popover_manage_menu(); - hide_message_info_popover(); - hide_user_sidebar_popover(); - hide_user_info_popover(); -} - -function focus_user_info_popover_item() { - // For now I recommend only calling this when the user opens the menu with a hotkey. - // Our popup menus act kind of funny when you mix keyboard and mouse. - const $items = get_user_info_popover_for_message_items(); - - if ($(".user_info_popover_manage_menu_btn").is(":visible")) { - focus_first_popover_item($items, 1); - } else { - focus_first_popover_item($items); - } -} - -function get_user_sidebar_popover_items() { - if (!current_user_sidebar_popover) { - blueslip.error("Trying to get menu items when user sidebar popover is closed."); - return undefined; - } - - return $("li:not(.divider):visible a", current_user_sidebar_popover.$tip); -} - -export function user_sidebar_popover_handle_keyboard(key) { - const $items = get_user_sidebar_popover_items(); - popover_items_handle_keyboard(key, $items); -} - -export function user_info_popover_for_message_handle_keyboard(key) { - const $items = get_user_info_popover_for_message_items(); - popover_items_handle_keyboard(key, $items); -} - -export function user_info_popover_handle_keyboard(key) { - const $items = get_user_info_popover_items(); - popover_items_handle_keyboard(key, $items); -} - -export function user_info_popover_manage_menu_handle_keyboard(key) { - const $items = get_user_info_popover_manage_menu_items(); - popover_items_handle_keyboard(key, $items); -} - -export function show_sender_info() { - const $message = $(".selected_message"); - let $sender = $message.find(".message-avatar"); - if ($sender.length === 0) { - // Messages without an avatar have an invisible message_sender - // element that's roughly in the right place. - $sender = $message.find(".message_sender"); - } - - const message = message_lists.current.get(rows.id($message)); - const user = people.get_by_user_id(message.sender_id); - show_user_info_popover_for_message($sender[0], user, message); - if ($current_message_info_popover_elem && !page_params.is_spectator) { - focus_user_info_popover_item(); - } -} - // On mobile web, opening the keyboard can trigger a resize event // (which in turn can trigger a scroll event). This will have the // side effect of closing popovers, which we don't want. So we @@ -650,35 +150,6 @@ export function hide_playground_links_popover() { } export function register_click_handlers() { - $("#main_div").on("click", ".sender_name, .message-avatar", function (e) { - const $row = $(this).closest(".message_row"); - e.stopPropagation(); - const message = message_lists.current.get(rows.id($row)); - const user = people.get_by_user_id(message.sender_id); - show_user_info_popover_for_message(this, user, message); - }); - - $("#main_div").on("click", ".user-mention", function (e) { - const id_string = $(this).attr("data-user-id"); - // We fallback to email to handle legacy Markdown that was rendered - // before we cut over to using data-user-id - const email = $(this).attr("data-user-email"); - if (id_string === "*" || email === "*") { - return; - } - const $row = $(this).closest(".message_row"); - e.stopPropagation(); - const message = message_lists.current.get(rows.id($row)); - let user; - if (id_string) { - const user_id = Number.parseInt(id_string, 10); - user = people.get_by_user_id(user_id); - } else { - user = people.get_by_email(email); - } - show_user_info_popover_for_message(this, user, message); - }); - $("#main_div, #preview_content, #message-history").on( "click", ".code_external_link", @@ -715,214 +186,11 @@ export function register_click_handlers() { e.stopPropagation(); }); - $("body").on("click", ".info_popover_actions .narrow_to_private_messages", (e) => { - const user_id = elem_to_user_id($(e.target).parents("ul")); - const email = people.get_by_user_id(user_id).email; - hide_all(); - if (overlays.is_active()) { - overlays.close_active(); - } - narrow.by("dm", email, {trigger: "user sidebar popover"}); - e.stopPropagation(); - e.preventDefault(); - }); - - $("body").on("click", ".info_popover_actions .narrow_to_messages_sent", (e) => { - const user_id = elem_to_user_id($(e.target).parents("ul")); - const email = people.get_by_user_id(user_id).email; - hide_all(); - if (overlays.is_active()) { - overlays.close_active(); - } - narrow.by("sender", email, {trigger: "user sidebar popover"}); - e.stopPropagation(); - e.preventDefault(); - }); - - $("body").on("click", ".user_popover .mention_user", (e) => { - if (!compose_state.composing()) { - compose_actions.start("stream", {trigger: "sidebar user actions"}); - } - const user_id = elem_to_user_id($(e.target).parents("ul")); - const name = people.get_by_user_id(user_id).full_name; - const mention = people.get_mention_syntax(name, user_id); - compose_ui.insert_syntax_and_focus(mention); - hide_user_sidebar_popover(); - hide_userlist_sidebar(); - e.stopPropagation(); - e.preventDefault(); - }); - - $("body").on("click", ".message-info-popover .mention_user", (e) => { - if (!compose_state.composing()) { - compose_actions.respond_to_message({trigger: "user sidebar popover"}); - } - const user_id = elem_to_user_id($(e.target).parents("ul")); - const name = people.get_by_user_id(user_id).full_name; - const mention = people.get_mention_syntax(name, user_id); - compose_ui.insert_syntax_and_focus(mention); - hide_message_info_popover(); - e.stopPropagation(); - e.preventDefault(); - }); - - $("body").on("click", ".info_popover_actions .clear_status", (e) => { - e.preventDefault(); - const me = elem_to_user_id($(e.target).parents("ul")); - user_status.server_update_status({ - user_id: me, - status_text: "", - emoji_name: "", - emoji_code: "", - success() { - $(".info_popover_actions #status_message").empty(); - }, - }); - }); - - $("body").on("click", ".view_user_profile", (e) => { - const user_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); - const user = people.get_by_user_id(user_id); - show_user_info_popover(e.target, user); - e.stopPropagation(); - e.preventDefault(); - }); - - /* These click handlers are implemented as just deep links to the - * relevant part of the Zulip UI, so we don't want preventDefault, - * but we do want to close the modal when you click them. */ - - $("body").on("click", ".invisible_mode_turn_on", (e) => { - hide_all(); - user_status.server_invisible_mode_on(); - e.stopPropagation(); - e.preventDefault(); - }); - - $("body").on("click", ".invisible_mode_turn_off", (e) => { - hide_all(); - user_status.server_invisible_mode_off(); - e.stopPropagation(); - e.preventDefault(); - }); - - function open_user_status_modal(e) { - hide_all(); - - user_status_ui.open_user_status_modal(); - - e.stopPropagation(); - e.preventDefault(); - } - - $("body").on("click", ".update_status_text", open_user_status_modal); - - // Clicking on one's own status emoji should open the user status modal. - $("#user_presences").on( - "click", - ".user_sidebar_entry_me .status-emoji", - open_user_status_modal, - ); - - $("body").on("click", ".sidebar-popover-mute-user", (e) => { - const user_id = elem_to_user_id($(e.target).parents("ul")); - hide_all_user_info_popovers(); - e.stopPropagation(); - e.preventDefault(); - muted_users_ui.confirm_mute_user(user_id); - }); - - $("body").on("click", ".sidebar-popover-unmute-user", (e) => { - const user_id = elem_to_user_id($(e.target).parents("ul")); - hide_all_user_info_popovers(); - muted_users_ui.unmute_user(user_id); - e.stopPropagation(); - e.preventDefault(); - }); - - $("body").on("click", ".info_popover_actions .sidebar-popover-reactivate-user", (e) => { - const user_id = elem_to_user_id($(e.target).parents("ul")); - hide_all(); - e.stopPropagation(); - e.preventDefault(); - function handle_confirm() { - const url = "/json/users/" + encodeURIComponent(user_id) + "/reactivate"; - channel.post({ - url, - success() { - dialog_widget.close_modal(); - }, - error(xhr) { - ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $("#dialog_error")); - dialog_widget.hide_dialog_spinner(); - }, - }); - } - settings_users.confirm_reactivation(user_id, handle_confirm, true); - }); - - $("#user_presences").on("click", ".user-list-sidebar-menu-icon", function (e) { - e.stopPropagation(); - - const $target = $(this).closest("li"); - const user_id = elem_to_user_id($target.find("a")); - // Hiding popovers may mutate current_user_sidebar_user_id. - const previous_user_sidebar_id = current_user_sidebar_user_id; - - // Hide popovers, but we don't want to hide the sidebars on - // smaller browser windows. - hide_all_except_sidebars(); - - if (previous_user_sidebar_id === user_id) { - // If the popover is already shown, clicking again should toggle it. - return; - } - - const user = people.get_by_user_id(user_id); - const popover_placement = userlist_placement === "left" ? "right" : "left"; - - render_user_info_popover( - user, - $target, - false, - false, - "compose_private_message", - "user_popover", - popover_placement, - ); - - current_user_sidebar_user_id = user.user_id; - current_user_sidebar_popover = $target.data("popover"); - }); - $("body").on("click", ".flatpickr-calendar", (e) => { e.stopPropagation(); e.preventDefault(); }); - $("body").on("click", ".respond_personal_button, .compose_private_message", (e) => { - const user_id = elem_to_user_id($(e.target).parents("ul")); - const email = people.get_by_user_id(user_id).email; - compose_actions.start("private", { - trigger: "popover send private", - private_message_recipient: email, - }); - hide_all(); - if (overlays.is_active()) { - overlays.close_active(); - } - e.stopPropagation(); - e.preventDefault(); - }); - - clipboard_enable(".copy_mention_syntax"); - - $("body").on("click", ".copy_mention_syntax", (e) => { - hide_all(); - e.stopPropagation(); - e.preventDefault(); - }); - { let last_scroll = 0; @@ -945,29 +213,6 @@ export function register_click_handlers() { last_scroll = date; }); } - - $("body").on("click", ".sidebar-popover-manage-user", (e) => { - hide_all(); - const user_id = elem_to_user_id($(e.target).parents("ul")); - const user = people.get_by_user_id(user_id); - user_profile.show_user_profile(user, "manage-profile-tab"); - }); - - $("body").on("click", ".user_info_popover_manage_menu_btn", (e) => { - e.preventDefault(); - e.stopPropagation(); - const user_id = elem_to_user_id($(e.target).parents("ul")); - const user = people.get_by_user_id(user_id); - show_user_info_popover_manage_menu(e.target, user); - }); - - $("body").on("click", ".info_popover_actions .view_full_user_profile", (e) => { - const user_id = elem_to_user_id($(e.target).parents("ul")); - const user = people.get_by_user_id(user_id); - user_profile.show_user_profile(user); - e.stopPropagation(); - e.preventDefault(); - }); } export function any_active() { @@ -975,11 +220,11 @@ export function any_active() { // Expanded sidebars on mobile view count as popovers as well. return ( popover_menus.any_active() || - user_group_popover.is_open() || - user_sidebar_popped() || stream_popover.is_open() || - message_info_popped() || - user_info_popped() || + user_group_popover.is_open() || + user_card_popover.user_sidebar_popped() || + user_card_popover.message_info_popped() || + user_card_popover.user_info_popped() || emoji_picker.is_open() || $("[class^='column-'].expanded").length ); @@ -997,7 +242,7 @@ export function hide_all_except_sidebars(opts) { emoji_picker.hide_emoji_popover(); stream_popover.hide_stream_popover(); user_group_popover.hide(); - hide_all_user_info_popovers(); + user_card_popover.hide_all_user_info_popovers(); hide_playground_links_popover(); // look through all the popovers that have been added and removed. diff --git a/web/src/recent_view_ui.js b/web/src/recent_view_ui.js index 686923694d..85b8461aa2 100644 --- a/web/src/recent_view_ui.js +++ b/web/src/recent_view_ui.js @@ -42,6 +42,7 @@ import * as ui_util from "./ui_util"; import * as unread from "./unread"; import * as unread_ops from "./unread_ops"; import * as unread_ui from "./unread_ui"; +import * as user_card_popover from "./user_card_popover"; import * as user_status from "./user_status"; import * as user_topics from "./user_topics"; @@ -1282,7 +1283,7 @@ export function initialize() { const participant_user_id = Number.parseInt($(this).attr("data-user-id"), 10); e.stopPropagation(); const user = people.get_by_user_id(participant_user_id); - popovers.show_user_info_popover(this, user); + user_card_popover.show_user_info_popover(this, user); }); $("body").on( diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 1f277eb32f..444e37c3cf 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -116,6 +116,7 @@ import * as unread from "./unread"; import * as unread_ops from "./unread_ops"; import * as unread_ui from "./unread_ui"; import * as upload from "./upload"; +import * as user_card_popover from "./user_card_popover"; import * as user_group_edit from "./user_group_edit"; import * as user_group_edit_members from "./user_group_edit_members"; import * as user_group_popover from "./user_group_popover"; @@ -768,6 +769,7 @@ export function initialize_everything() { activity.initialize(); emoji_picker.initialize(); user_group_popover.initialize(); + user_card_popover.initialize(); pm_list.initialize(); topic_list.initialize({ on_topic_click(stream_id, topic) { diff --git a/web/src/user_card_popover.js b/web/src/user_card_popover.js new file mode 100644 index 0000000000..4ec3ad59a5 --- /dev/null +++ b/web/src/user_card_popover.js @@ -0,0 +1,785 @@ +import ClipboardJS from "clipboard"; +import {parseISO} from "date-fns"; +import $ from "jquery"; +import tippy from "tippy.js"; + +import render_no_arrow_popover from "../templates/no_arrow_popover.hbs"; +import render_user_info_popover_content from "../templates/user_info_popover_content.hbs"; +import render_user_info_popover_manage_menu from "../templates/user_info_popover_manage_menu.hbs"; +import render_user_info_popover_title from "../templates/user_info_popover_title.hbs"; + +import * as blueslip from "./blueslip"; +import * as buddy_data from "./buddy_data"; +import * as channel from "./channel"; +import * as compose_actions from "./compose_actions"; +import * as compose_state from "./compose_state"; +import * as compose_ui from "./compose_ui"; +import * as dialog_widget from "./dialog_widget"; +import * as hash_util from "./hash_util"; +import {$t, $t_html} from "./i18n"; +import * as message_lists from "./message_lists"; +import * as muted_users from "./muted_users"; +import * as muted_users_ui from "./muted_users_ui"; +import * as narrow from "./narrow"; +import * as overlays from "./overlays"; +import {page_params} from "./page_params"; +import * as people from "./people"; +import { + focus_first_popover_item, + hide_all, + hide_all_except_sidebars, + popover_items_handle_keyboard, +} from "./popovers"; +import * as popovers from "./popovers"; +import * as rows from "./rows"; +import * as settings_config from "./settings_config"; +import * as settings_users from "./settings_users"; +import * as timerender from "./timerender"; +import * as ui_report from "./ui_report"; +import * as user_profile from "./user_profile"; +import {user_settings} from "./user_settings"; +import * as user_status from "./user_status"; +import * as user_status_ui from "./user_status_ui"; + +let $current_message_info_popover_elem; +let $current_user_info_popover_elem; +let $current_user_info_popover_manage_menu; +let current_user_sidebar_popover; +let current_user_sidebar_user_id; + +let userlist_placement = "right"; + +export function hide_all_user_info_popovers() { + hide_user_info_popover_manage_menu(); + hide_message_info_popover(); + hide_user_sidebar_popover(); + hide_user_info_popover(); +} + +export function clear_for_testing() { + $current_message_info_popover_elem = undefined; + $current_user_info_popover_elem = undefined; + $current_user_info_popover_manage_menu = undefined; + userlist_placement = "right"; +} + +export function elem_to_user_id($elem) { + return Number.parseInt($elem.attr("data-user-id"), 10); +} + +function clipboard_enable(arg) { + // arg is a selector or element + // We extract this function for testing purpose. + return new ClipboardJS(arg); +} + +// user_info + +export function show_user_info_popover(element, user) { + const $last_popover_elem = $current_user_info_popover_elem; + hide_all(); + if ($last_popover_elem !== undefined && $last_popover_elem.get()[0] === element) { + return; + } + const $elt = $(element); + render_user_info_popover( + user, + $elt, + false, + false, + "compose_private_message", + "user-info-popover", + "right", + ); + $current_user_info_popover_elem = $elt; +} + +export function hide_user_info_popover() { + if (user_info_popped()) { + $current_user_info_popover_elem.popover("destroy"); + $current_user_info_popover_elem = undefined; + } +} + +export function user_info_popped() { + return $current_user_info_popover_elem !== undefined; +} + +export function user_info_popover_handle_keyboard(key) { + const $items = get_user_info_popover_items(); + popover_items_handle_keyboard(key, $items); +} + +function get_user_info_popover_items() { + const $popover_elt = $("div.user-info-popover"); + if (!$current_user_info_popover_elem || !$popover_elt.length) { + blueslip.error("Trying to get menu items when action popover is closed."); + return undefined; + } + + if ($popover_elt.length >= 2) { + blueslip.error("More than one user info popovers cannot be opened at same time."); + return undefined; + } + + return $("li:not(.divider):visible a", $popover_elt); +} + +function render_user_info_popover( + user, + popover_element, + is_sender_popover, + has_message_context, + private_msg_class, + template_class, + popover_placement, +) { + const is_me = people.is_my_user_id(user.user_id); + + let invisible_mode = false; + + if (is_me) { + invisible_mode = !user_settings.presence_enabled; + } + + const is_active = people.is_active_user_for_popover(user.user_id); + const is_system_bot = user.is_system_bot; + const status_text = user_status.get_status_text(user.user_id); + const status_emoji_info = user_status.get_status_emoji(user.user_id); + const spectator_view = page_params.is_spectator; + + const show_manage_menu = !spectator_view && !is_me; + + let date_joined; + + // Some users might not have `date_joined` field because of the missing server data. + // These users are added late in `people.js` via `extract_people_from_message`. + if (spectator_view && !user.is_missing_server_data) { + date_joined = timerender.get_localized_date_or_time_for_format( + parseISO(user.date_joined), + "dayofyear_year", + ); + } + // Filtering out only those profile fields that can be display in the popover and are not empty. + const field_types = page_params.custom_profile_field_types; + const display_profile_fields = page_params.custom_profile_fields + .map((f) => user_profile.get_custom_profile_field_data(user, f, field_types)) + .filter((f) => f.display_in_profile_summary && f.value !== undefined && f.value !== null); + + const args = { + invisible_mode, + can_send_private_message: + is_active && + !is_me && + page_params.realm_private_message_policy !== + settings_config.private_message_policy_values.disabled.code, + display_profile_fields, + has_message_context, + is_active, + is_bot: user.is_bot, + is_me, + is_sender_popover, + pm_with_url: hash_util.pm_with_url(user.email), + user_circle_class: buddy_data.get_user_circle_class(user.user_id), + private_message_class: private_msg_class, + sent_by_url: hash_util.by_sender_url(user.email), + show_manage_menu, + user_email: user.delivery_email, + user_full_name: user.full_name, + user_id: user.user_id, + user_last_seen_time_status: buddy_data.user_last_seen_time_status(user.user_id), + user_time: people.get_user_time(user.user_id), + user_type: people.get_user_type(user.user_id), + status_content_available: Boolean(status_text || status_emoji_info), + status_text, + status_emoji_info, + user_mention_syntax: people.get_mention_syntax(user.full_name, user.user_id), + date_joined, + spectator_view, + }; + + if (user.is_bot) { + const bot_owner_id = user.bot_owner_id; + if (is_system_bot) { + args.is_system_bot = is_system_bot; + } else if (bot_owner_id) { + const bot_owner = people.get_by_user_id(bot_owner_id); + args.bot_owner = bot_owner; + } + } + + const $popover_content = $(render_user_info_popover_content(args)); + popover_element.popover({ + content: $popover_content.get(0), + fixed: true, + placement: popover_placement, + template: render_no_arrow_popover({class: template_class}), + title: render_user_info_popover_title({ + // See the load_medium_avatar comment for important background. + user_avatar: people.small_avatar_url_for_person(user), + user_is_guest: user.is_guest, + }), + html: true, + trigger: "manual", + top_offset: $("#userlist-title").get_offset_to_window().top + 15, + fix_positions: true, + }); + popover_element.popover("show"); + + init_email_clipboard(); + init_email_tooltip(user); + const $user_name_element = $popover_content.find(".user_full_name"); + const $bot_owner_element = $popover_content.find(".bot_owner"); + if ($user_name_element.prop("clientWidth") < $user_name_element.prop("scrollWidth")) { + $user_name_element.addClass("tippy-zulip-tooltip"); + } + if ( + args.bot_owner && + $bot_owner_element.prop("clientWidth") < $bot_owner_element.prop("scrollWidth") + ) { + $bot_owner_element.addClass("tippy-zulip-tooltip"); + } + + // Note: We pass the normal-size avatar in initial rendering, and + // then query the server to replace it with the medium-size + // avatar. The purpose of this double-fetch approach is to take + // advantage of the fact that the browser should already have the + // low-resolution image cached and thus display a low-resolution + // avatar rather than a blank area during the network delay for + // fetching the medium-size one. + load_medium_avatar(user, $(".popover-avatar")); +} + +function copy_email_handler(e) { + const $email_el = $(e.trigger.parentElement); + const $copy_icon = $email_el.find("i"); + + // only change the parent element's text back to email + // and not overwrite the tooltip. + const email_textnode = $email_el[0].childNodes[2]; + + $email_el.addClass("email_copied"); + email_textnode.nodeValue = $t({defaultMessage: "Email copied"}); + + setTimeout(() => { + $email_el.removeClass("email_copied"); + email_textnode.nodeValue = $copy_icon.attr("data-clipboard-text"); + }, 1500); + e.clearSelection(); +} + +function init_email_clipboard() { + /* + This shows (and enables) the copy-text icon for folks + who have names that would overflow past the right + edge of our user mention popup. + */ + $(".user_popover_email").each(function () { + if (this.clientWidth < this.scrollWidth) { + const $email_el = $(this); + const $copy_email_icon = $email_el.find("i"); + + /* + For deactivated users, the copy-email icon will + not even be present in the HTML, so we don't do + anything. We don't reveal emails for deactivated + users. + */ + if ($copy_email_icon[0]) { + $copy_email_icon.removeClass("hide_copy_icon"); + const copy_email_clipboard = clipboard_enable($copy_email_icon[0]); + copy_email_clipboard.on("success", copy_email_handler); + } + } + }); +} + +function init_email_tooltip(user) { + /* + This displays the email tooltip for folks + who have names that would overflow past the right + edge of our user mention popup. + */ + + $(".user_popover_email").each(function () { + if (this.clientWidth < this.scrollWidth) { + tippy(this, { + placement: "bottom", + content: people.get_visible_email(user), + interactive: true, + }); + } + }); +} + +function load_medium_avatar(user, $elt) { + const user_avatar_url = people.medium_avatar_url_for_person(user); + const sender_avatar_medium = new Image(); + + sender_avatar_medium.src = user_avatar_url; + $(sender_avatar_medium).on("load", function () { + $elt.css("background-image", "url(" + $(this).attr("src") + ")"); + }); +} + +// user_info_manage_menu + +function show_user_info_popover_manage_menu(element, user) { + const $last_popover_elem = $current_user_info_popover_manage_menu; + hide_user_info_popover_manage_menu(); + if ($last_popover_elem !== undefined && $last_popover_elem.get()[0] === element) { + return; + } + + const is_me = people.is_my_user_id(user.user_id); + const is_muted = muted_users.is_user_muted(user.user_id); + const is_system_bot = user.is_system_bot; + const muting_allowed = !is_me; + + const args = { + can_mute: muting_allowed && !is_muted, + can_manage_user: page_params.is_admin && !is_me && !is_system_bot, + can_unmute: muting_allowed && is_muted, + is_active: people.is_active_user_for_popover(user.user_id), + is_bot: user.is_bot, + user_id: user.user_id, + }; + + const $popover_elt = $(element); + $popover_elt.popover({ + content: render_user_info_popover_manage_menu(args), + placement: "bottom", + html: true, + trigger: "manual", + fixed: true, + }); + + $popover_elt.popover("show"); + $current_user_info_popover_manage_menu = $popover_elt; +} + +export function hide_user_info_popover_manage_menu() { + if ($current_user_info_popover_manage_menu !== undefined) { + $current_user_info_popover_manage_menu.popover("destroy"); + $current_user_info_popover_manage_menu = undefined; + } +} + +export function user_info_manage_menu_popped() { + return $current_user_info_popover_manage_menu !== undefined; +} + +export function user_info_popover_manage_menu_handle_keyboard(key) { + const $items = get_user_info_popover_manage_menu_items(); + popover_items_handle_keyboard(key, $items); +} + +export function get_user_info_popover_manage_menu_items() { + if (!$current_user_info_popover_manage_menu) { + blueslip.error("Trying to get menu items when action popover is closed."); + return undefined; + } + + const popover_data = $current_user_info_popover_manage_menu.data("popover"); + if (!popover_data) { + blueslip.error("Cannot find popover data for actions menu."); + return undefined; + } + + return $(".user_info_popover_manage_menu li:not(.divider):visible a", popover_data.$tip); +} + +// message_info + +// 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_for_message(element, user, message) { + const $last_popover_elem = $current_message_info_popover_elem; + 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; + } + message_lists.current.select_id(message.id); + const $elt = $(element); + if ($elt.data("popover") === undefined) { + if (user === undefined) { + // This is never supposed to happen, not even for deactivated + // users, so we'll need to debug this error if it occurs. + blueslip.error("Bad sender in message", { + zid: message.id, + sender_id: message.sender_id, + }); + return; + } + + const is_sender_popover = message.sender_id === user.user_id; + render_user_info_popover( + user, + $elt, + is_sender_popover, + true, + "respond_personal_button", + "message-info-popover", + "right", + ); + + $current_message_info_popover_elem = $elt; + } +} + +// triggered from keyboard shortcut +export function show_sender_info() { + const $message = $(".selected_message"); + let $sender = $message.find(".message-avatar"); + if ($sender.length === 0) { + // Messages without an avatar have an invisible message_sender + // element that's roughly in the right place. + $sender = $message.find(".message_sender"); + } + + const message = message_lists.current.get(rows.id($message)); + const user = people.get_by_user_id(message.sender_id); + show_user_info_popover_for_message($sender[0], user, message); + if ($current_message_info_popover_elem && !page_params.is_spectator) { + focus_user_info_popover_item(); + } +} + +function focus_user_info_popover_item() { + // For now I recommend only calling this when the user opens the menu with a hotkey. + // Our popup menus act kind of funny when you mix keyboard and mouse. + const $items = get_user_info_popover_for_message_items(); + + if ($(".user_info_popover_manage_menu_btn").is(":visible")) { + focus_first_popover_item($items, 1); + } else { + focus_first_popover_item($items); + } +} + +export function message_info_popped() { + return $current_message_info_popover_elem !== undefined; +} + +export function hide_message_info_popover() { + if (message_info_popped()) { + $current_message_info_popover_elem.popover("destroy"); + $current_message_info_popover_elem = undefined; + } +} + +export function user_info_popover_for_message_handle_keyboard(key) { + const $items = get_user_info_popover_for_message_items(); + popover_items_handle_keyboard(key, $items); +} + +function get_user_info_popover_for_message_items() { + if (!$current_message_info_popover_elem) { + blueslip.error("Trying to get menu items when action popover is closed."); + return undefined; + } + + const popover_data = $current_message_info_popover_elem.data("popover"); + if (!popover_data) { + blueslip.error("Cannot find popover data for actions menu."); + return undefined; + } + + return $("li:not(.divider):visible a", popover_data.$tip); +} + +// user_sidebar + +export function user_sidebar_popped() { + return current_user_sidebar_popover !== undefined; +} + +export function hide_user_sidebar_popover() { + if (user_sidebar_popped()) { + // this hide_* method looks different from all the others since + // the presence list may be redrawn. Due to funkiness with jQuery's .data() + // this would confuse $.popover("destroy"), which looks at the .data() attached + // to a certain element. We thus save off the .data("popover") in the + // show_user_sidebar_popover and inject it here before calling destroy. + $("#user_presences").data("popover", current_user_sidebar_popover); + $("#user_presences").popover("destroy"); + current_user_sidebar_user_id = undefined; + current_user_sidebar_popover = undefined; + } +} + +function get_user_sidebar_popover_items() { + if (!current_user_sidebar_popover) { + blueslip.error("Trying to get menu items when user sidebar popover is closed."); + return undefined; + } + + return $("li:not(.divider):visible a", current_user_sidebar_popover.$tip); +} + +export function user_sidebar_popover_handle_keyboard(key) { + const $items = get_user_sidebar_popover_items(); + popover_items_handle_keyboard(key, $items); +} + +export function register_click_handlers() { + $("#main_div").on("click", ".sender_name, .message-avatar", function (e) { + const $row = $(this).closest(".message_row"); + e.stopPropagation(); + const message = message_lists.current.get(rows.id($row)); + const user = people.get_by_user_id(message.sender_id); + show_user_info_popover_for_message(this, user, message); + }); + + $("#main_div").on("click", ".user-mention", function (e) { + const id_string = $(this).attr("data-user-id"); + // We fallback to email to handle legacy Markdown that was rendered + // before we cut over to using data-user-id + const email = $(this).attr("data-user-email"); + if (id_string === "*" || email === "*") { + return; + } + const $row = $(this).closest(".message_row"); + e.stopPropagation(); + const message = message_lists.current.get(rows.id($row)); + let user; + if (id_string) { + const user_id = Number.parseInt(id_string, 10); + user = people.get_by_user_id(user_id); + } else { + user = people.get_by_email(email); + } + show_user_info_popover_for_message(this, user, message); + }); + + $("body").on("click", ".info_popover_actions .narrow_to_private_messages", (e) => { + const user_id = elem_to_user_id($(e.target).parents("ul")); + const email = people.get_by_user_id(user_id).email; + hide_all(); + if (overlays.is_active()) { + overlays.close_active(); + } + narrow.by("dm", email, {trigger: "user sidebar popover"}); + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".info_popover_actions .narrow_to_messages_sent", (e) => { + const user_id = elem_to_user_id($(e.target).parents("ul")); + const email = people.get_by_user_id(user_id).email; + hide_all(); + if (overlays.is_active()) { + overlays.close_active(); + } + narrow.by("sender", email, {trigger: "user sidebar popover"}); + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".info_popover_actions .clear_status", (e) => { + e.preventDefault(); + const me = elem_to_user_id($(e.target).parents("ul")); + user_status.server_update_status({ + user_id: me, + status_text: "", + emoji_name: "", + emoji_code: "", + success() { + $(".info_popover_actions #status_message").empty(); + }, + }); + }); + + $("body").on("click", ".info_popover_actions .sidebar-popover-reactivate-user", (e) => { + const user_id = elem_to_user_id($(e.target).parents("ul")); + hide_all(); + e.stopPropagation(); + e.preventDefault(); + function handle_confirm() { + const url = "/json/users/" + encodeURIComponent(user_id) + "/reactivate"; + channel.post({ + url, + success() { + dialog_widget.close_modal(); + }, + error(xhr) { + ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $("#dialog_error")); + dialog_widget.hide_dialog_spinner(); + }, + }); + } + settings_users.confirm_reactivation(user_id, handle_confirm, true); + }); + + $("body").on("click", ".info_popover_actions .view_full_user_profile", (e) => { + const user_id = elem_to_user_id($(e.target).parents("ul")); + const user = people.get_by_user_id(user_id); + user_profile.show_user_profile(user); + e.stopPropagation(); + e.preventDefault(); + }); + $("body").on("click", ".user_popover .mention_user", (e) => { + if (!compose_state.composing()) { + compose_actions.start("stream", {trigger: "sidebar user actions"}); + } + const user_id = elem_to_user_id($(e.target).parents("ul")); + const name = people.get_by_user_id(user_id).full_name; + const mention = people.get_mention_syntax(name, user_id); + compose_ui.insert_syntax_and_focus(mention); + hide_user_sidebar_popover(); + popovers.hide_userlist_sidebar(); + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".message-info-popover .mention_user", (e) => { + if (!compose_state.composing()) { + compose_actions.respond_to_message({trigger: "user sidebar popover"}); + } + const user_id = elem_to_user_id($(e.target).parents("ul")); + const name = people.get_by_user_id(user_id).full_name; + const mention = people.get_mention_syntax(name, user_id); + compose_ui.insert_syntax_and_focus(mention); + hide_message_info_popover(); + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".view_user_profile", (e) => { + const user_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); + const user = people.get_by_user_id(user_id); + show_user_info_popover(e.target, user); + e.stopPropagation(); + e.preventDefault(); + }); + + /* These click handlers are implemented as just deep links to the + * relevant part of the Zulip UI, so we don't want preventDefault, + * but we do want to close the modal when you click them. */ + + $("body").on("click", ".invisible_mode_turn_on", (e) => { + hide_all(); + user_status.server_invisible_mode_on(); + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".invisible_mode_turn_off", (e) => { + hide_all(); + user_status.server_invisible_mode_off(); + e.stopPropagation(); + e.preventDefault(); + }); + + function open_user_status_modal(e) { + hide_all(); + + user_status_ui.open_user_status_modal(); + + e.stopPropagation(); + e.preventDefault(); + } + + $("body").on("click", ".update_status_text", open_user_status_modal); + + // Clicking on one's own status emoji should open the user status modal. + $("#user_presences").on( + "click", + ".user_sidebar_entry_me .status-emoji", + open_user_status_modal, + ); + + $("#user_presences").on("click", ".user-list-sidebar-menu-icon", function (e) { + e.stopPropagation(); + + const $target = $(this).closest("li"); + const user_id = elem_to_user_id($target.find("a")); + // Hiding popovers may mutate current_user_sidebar_user_id. + const previous_user_sidebar_id = current_user_sidebar_user_id; + + // Hide popovers, but we don't want to hide the sidebars on + // smaller browser windows. + hide_all_except_sidebars(); + + if (previous_user_sidebar_id === user_id) { + // If the popover is already shown, clicking again should toggle it. + return; + } + + const user = people.get_by_user_id(user_id); + const popover_placement = userlist_placement === "left" ? "right" : "left"; + + render_user_info_popover( + user, + $target, + false, + false, + "compose_private_message", + "user_popover", + popover_placement, + ); + + current_user_sidebar_user_id = user.user_id; + current_user_sidebar_popover = $target.data("popover"); + }); + + $("body").on("click", ".sidebar-popover-mute-user", (e) => { + const user_id = elem_to_user_id($(e.target).parents("ul")); + hide_all_user_info_popovers(); + e.stopPropagation(); + e.preventDefault(); + muted_users_ui.confirm_mute_user(user_id); + }); + + $("body").on("click", ".sidebar-popover-unmute-user", (e) => { + const user_id = elem_to_user_id($(e.target).parents("ul")); + hide_all_user_info_popovers(); + muted_users_ui.unmute_user(user_id); + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".respond_personal_button, .compose_private_message", (e) => { + const user_id = elem_to_user_id($(e.target).parents("ul")); + const email = people.get_by_user_id(user_id).email; + compose_actions.start("private", { + trigger: "popover send private", + private_message_recipient: email, + }); + hide_all(); + if (overlays.is_active()) { + overlays.close_active(); + } + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".copy_mention_syntax", (e) => { + hide_all(); + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".sidebar-popover-manage-user", (e) => { + hide_all(); + const user_id = elem_to_user_id($(e.target).parents("ul")); + const user = people.get_by_user_id(user_id); + user_profile.show_user_profile(user, "manage-profile-tab"); + }); + + $("body").on("click", ".user_info_popover_manage_menu_btn", (e) => { + e.preventDefault(); + e.stopPropagation(); + const user_id = elem_to_user_id($(e.target).parents("ul")); + const user = people.get_by_user_id(user_id); + show_user_info_popover_manage_menu(e.target, user); + }); +} + +export function initialize() { + register_click_handlers(); + clipboard_enable(".copy_mention_syntax"); +} diff --git a/web/tests/hotkey.test.js b/web/tests/hotkey.test.js index c44979b8ca..0958fab897 100644 --- a/web/tests/hotkey.test.js +++ b/web/tests/hotkey.test.js @@ -65,7 +65,7 @@ const overlays = mock_esm("../src/overlays", { active_modal: () => undefined, is_overlay_or_modal_open: () => overlays.is_modal_open() || overlays.is_active(), }); -const popovers = mock_esm("../src/popovers", { +const popovers = mock_esm("../src/user_card_popover", { user_info_manage_menu_popped: () => false, message_info_popped: () => false, user_sidebar_popped: () => false, diff --git a/web/tests/popovers.test.js b/web/tests/user_card_popover.test.js similarity index 96% rename from web/tests/popovers.test.js rename to web/tests/user_card_popover.test.js index bc5e9ee33b..e9587ac5d7 100644 --- a/web/tests/popovers.test.js +++ b/web/tests/user_card_popover.test.js @@ -33,13 +33,9 @@ mock_esm("../src/stream_popover", { hide_streamlist_sidebar: noop, }); -set_global("document", { - to_$: () => $("document-stub"), -}); - const people = zrequire("people"); const user_status = zrequire("user_status"); -const popovers = zrequire("popovers"); +const user_card_popover = zrequire("user_card_popover"); const alice = { email: "alice@example.com", @@ -101,8 +97,8 @@ function test_ui(label, f) { run_test(label, (handlers) => { page_params.is_admin = false; page_params.custom_profile_fields = []; - popovers.clear_for_testing(); - popovers.register_click_handlers(); + user_card_popover.clear_for_testing(); + user_card_popover.initialize(); f(handlers); }); }