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); }); }