Files
zulip/web/src/user_card_popover.ts
Anders Kaseorg ccfb50d4dd eslint: Enable no-jquery/no-sizzle.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-05-18 16:22:15 -07:00

979 lines
34 KiB
TypeScript

import ClipboardJS from "clipboard";
import {parseISO} from "date-fns";
import $ from "jquery";
import assert from "minimalistic-assert";
import * as tippy from "tippy.js";
import render_confirm_mute_user from "../templates/confirm_dialog/confirm_mute_user.hbs";
import render_user_card_popover from "../templates/popovers/user_card/user_card_popover.hbs";
import render_user_card_popover_for_unknown_user from "../templates/popovers/user_card/user_card_popover_for_unknown_user.hbs";
import * as blueslip from "./blueslip.ts";
import * as browser_history from "./browser_history.ts";
import * as buddy_data from "./buddy_data.ts";
import * as channel from "./channel.ts";
import * as compose_actions from "./compose_actions.ts";
import * as compose_reply from "./compose_reply.ts";
import * as compose_state from "./compose_state.ts";
import * as compose_ui from "./compose_ui.ts";
import * as confirm_dialog from "./confirm_dialog.ts";
import {show_copied_confirmation} from "./copied_tooltip.ts";
import * as dialog_widget from "./dialog_widget.ts";
import {is_overlay_hash} from "./hash_parser.ts";
import * as hash_util from "./hash_util.ts";
import {$t, $t_html} from "./i18n.ts";
import * as message_lists from "./message_lists.ts";
import {user_can_send_direct_message} from "./message_util.ts";
import * as message_view from "./message_view.ts";
import * as muted_users from "./muted_users.ts";
import * as overlays from "./overlays.ts";
import {page_params} from "./page_params.ts";
import type {User} from "./people.ts";
import * as people from "./people.ts";
import * as popover_menus from "./popover_menus.ts";
import * as popovers from "./popovers.ts";
import {hide_all} from "./popovers.ts";
import * as presence from "./presence.ts";
import * as rows from "./rows.ts";
import * as settings_panel_menu from "./settings_panel_menu.ts";
import * as sidebar_ui from "./sidebar_ui.ts";
import {current_user, realm} from "./state_data.ts";
import * as timerender from "./timerender.ts";
import * as ui_report from "./ui_report.ts";
import * as ui_util from "./ui_util.ts";
import * as user_deactivation_ui from "./user_deactivation_ui.ts";
import type {CustomProfileFieldData} from "./user_profile.ts";
import * as user_profile from "./user_profile.ts";
import {user_settings} from "./user_settings.ts";
import * as user_status from "./user_status.ts";
import * as user_status_ui from "./user_status_ui.ts";
import {the} from "./util.ts";
let current_user_sidebar_user_id: number | undefined;
export function confirm_mute_user(user_id: number): void {
function on_click(): void {
muted_users.mute_user(user_id);
}
const html_body = render_confirm_mute_user({
user_name: people.get_full_name(user_id),
});
confirm_dialog.launch({
html_heading: $t_html({defaultMessage: "Mute user"}),
help_link: "/help/mute-a-user",
html_body,
on_click,
});
}
class PopoverMenu {
instance: tippy.Instance | undefined;
constructor() {
this.instance = undefined;
}
is_open(): boolean {
return Boolean(this.instance);
}
hide(): void {
if (this.instance) {
this.instance.destroy();
this.instance = undefined;
}
}
handle_keyboard(key: string): void {
if (!this.instance) {
blueslip.error("Trying to get the items when popover is closed.");
return;
}
const $popover = $(this.instance.popper);
// eslint-disable-next-line no-jquery/no-sizzle
const $items = $("[tabindex='0']", $popover).filter(":visible");
popover_items_handle_keyboard_with_overrides(key, $items);
}
}
export const user_sidebar = new PopoverMenu();
export const message_user_card = new PopoverMenu();
export const user_card = new PopoverMenu();
function popover_items_handle_keyboard_with_overrides(key: string, $items: JQuery): void {
/* Variant of popover_items_handle_keyboard for focusing on the
user card popover menu options first, instead of other tabbable
buttons and links which can be distracting. */
const index = $items.index($items.filter(":focus"));
if (index === -1) {
const first_menu_option_index = $items.index(
$items.filter(".link-item .popover-menu-link"),
);
$items.eq(first_menu_option_index).trigger("focus");
return;
}
/* Otherwise, use the base implementation */
popover_menus.popover_items_handle_keyboard(key, $items);
}
function get_popover_classname(
popover: "user_sidebar" | "message_user_card" | "user_card",
): string {
const popovers = {
user_sidebar: "user-sidebar-popover-root",
message_user_card: "message-user-card-popover-root",
user_card: "user-card-popover-root",
};
return popovers[popover];
}
user_sidebar.hide = function () {
PopoverMenu.prototype.hide.call(this);
current_user_sidebar_user_id = undefined;
};
const user_card_popovers = {
user_sidebar,
message_user_card,
user_card,
};
export function any_active(): boolean {
return Object.values(user_card_popovers).some((instance) => instance.is_open());
}
export function hide_all_instances(): void {
for (const popover of Object.values(user_card_popovers)) {
popover.hide();
}
}
export function hide_all_user_card_popovers(): void {
hide_all_instances();
}
export function clear_for_testing(): void {
message_user_card.instance = undefined;
user_card.instance = undefined;
}
function elem_to_user_id($elem: JQuery): number {
return Number.parseInt($elem.attr("data-user-id")!, 10);
}
function clipboard_enable(arg: HTMLElement | string): ClipboardJS {
// arg is a selector or element
// We extract this function for testing purpose.
return new ClipboardJS(arg);
}
// Functions related to user card popover.
export function toggle_user_card_popover(element: HTMLElement, user: User): void {
show_user_card_popover(
user,
$(element),
false,
false,
"compose_private_message",
"user_card",
"right",
);
}
function toggle_user_card_popover_for_bot_owner(element: HTMLElement, user: User): void {
show_user_card_popover(
user,
$(element),
false,
false,
"compose_private_message",
"user_card",
"right",
true,
);
}
type UserCardPopoverData = {
invisible_mode: boolean;
can_send_private_message: boolean;
display_profile_fields: CustomProfileFieldData[];
has_message_context: boolean;
is_active: boolean;
is_bot: boolean;
is_me: boolean;
is_sender_popover: boolean;
pm_with_url: string;
user_circle_class: string;
private_message_class: string;
sent_by_url: string;
user_email: string | null;
user_full_name: string;
user_id: number;
user_last_seen_time_status: string;
user_time: string | undefined;
user_type: string | undefined;
status_content_available: boolean;
status_text: string | undefined;
status_emoji_info: user_status.UserStatusEmojiInfo | undefined;
show_placeholder_for_status_text: false | user_status.UserStatusEmojiInfo | undefined;
user_mention_syntax: string;
date_joined: string | undefined;
spectator_view: boolean;
should_add_guest_user_indicator: boolean;
user_avatar: string;
user_is_guest: boolean;
show_manage_section: boolean;
can_mute: boolean;
can_unmute: boolean;
can_manage_user: boolean;
is_system_bot?: boolean;
bot_owner?: User;
};
export let fetch_presence_for_popover = (user_id: number): void => {
if (page_params.is_spectator) {
return;
}
if (!people.is_active_user_for_popover(user_id) || people.get_by_user_id(user_id).is_bot) {
return;
}
const url = `json/users/${user_id}/presence`;
const selector_to_update = `#user_card_popover .popover-menu-list[data-user-id="${CSS.escape(user_id.toString())}"] .user-last-seen-time`;
channel.get({
url,
success(data: unknown) {
const parsed_data = presence.user_last_seen_response_schema.safeParse(data);
if (!parsed_data.success) {
blueslip.error("Failed to parse presence API response");
return;
}
const response = parsed_data.data;
if (response.result === "success" && response.presence) {
const {aggregated} = response.presence;
presence.presence_info.set(user_id, {
status: aggregated.status,
last_active: aggregated.timestamp,
});
// Update the user's last seen time in the user card
// popover once we have their presence information, if
// we still have that user card still open.
$(selector_to_update).text(buddy_data.user_last_seen_time_status(user_id));
}
},
error() {
// Fallback logic for users who haven't generated any presence data.
// A non-bot active user account might have no presence data either
// because they have always been in "invisible mode" or because the
// account was imported from another chat system.
//
// Store the fact that this user hasn't been online since
// account creation, to avoid uselessly asking the server
// again in this session.
const user = people.get_by_user_id(user_id);
presence.presence_info.set(user_id, {
status: "offline",
last_active: new Date(user.date_joined).getTime() / 1000,
});
$(selector_to_update).text(buddy_data.user_last_seen_time_status(user_id));
},
});
};
export function rewire_fetch_presence_for_popover(value: (user_id: number) => string): void {
fetch_presence_for_popover = value;
}
function get_user_card_popover_data(
user: User,
has_message_context: boolean,
is_sender_popover: boolean,
private_msg_class: string,
): UserCardPopoverData {
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_section = !spectator_view && !is_me;
const is_muted = muted_users.is_user_muted(user.user_id);
const muting_allowed = !is_me;
const can_mute = muting_allowed && !is_muted;
const can_unmute = muting_allowed && is_muted;
const can_manage_user = current_user.is_admin && !is_me && !is_system_bot;
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 = realm.custom_profile_field_types;
const display_profile_fields = realm.custom_profile_fields
.flatMap((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 user_id_string = user.user_id.toString();
const can_send_private_message =
user_can_send_direct_message(user_id_string) && is_active && !is_me;
const user_last_seen_time_status =
buddy_data.user_last_seen_time_status(user.user_id, fetch_presence_for_popover) ||
$t({defaultMessage: "Loading…"});
const args: UserCardPopoverData = {
invisible_mode,
can_send_private_message,
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),
user_email: user.delivery_email,
user_full_name: user.full_name,
user_id: user.user_id,
user_last_seen_time_status,
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,
show_placeholder_for_status_text: !status_text && status_emoji_info,
user_mention_syntax: people.get_mention_syntax(user.full_name, user.user_id, !is_active),
date_joined,
spectator_view,
should_add_guest_user_indicator: people.should_add_guest_user_indicator(user.user_id),
user_avatar: people.small_avatar_url_for_person(user),
user_is_guest: user.is_guest,
show_manage_section,
can_mute,
can_unmute,
can_manage_user,
};
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_user_by_id_assert_valid(bot_owner_id);
args.bot_owner = bot_owner;
}
}
return args;
}
function show_user_card_popover(
user: User,
$popover_element: JQuery,
is_sender_popover: boolean,
has_message_context: boolean,
private_msg_class: string,
template_class: "user_sidebar" | "message_user_card" | "user_card",
popover_placement: tippy.Placement,
show_as_overlay = false,
on_mount?: (instance: tippy.Instance) => void,
): void {
let popover_html;
let args;
if (user.is_inaccessible_user) {
const sent_by_url = hash_util.by_sender_url(user.email);
const user_avatar = people.small_avatar_url_for_person(user);
args = {
user_id: user.user_id,
sent_by_url,
user_avatar,
};
popover_html = render_user_card_popover_for_unknown_user(args);
} else {
args = get_user_card_popover_data(
user,
has_message_context,
is_sender_popover,
private_msg_class,
);
popover_html = render_user_card_popover(args);
}
popover_menus.toggle_popover_menu(
the($popover_element),
{
theme: "popover-menu",
placement: popover_placement,
onCreate(instance) {
instance.setContent(ui_util.parse_html(popover_html));
user_card_popovers[template_class].instance = instance;
const $popover = $(instance.popper);
$popover.addClass(get_popover_classname(template_class));
},
onHidden() {
user_card_popovers[template_class].hide();
},
onMount(instance) {
if (on_mount) {
on_mount(instance);
}
const $popover = $(instance.popper);
// 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.find(".popover-menu-user-avatar"));
init_email_clipboard();
init_email_tooltip(user);
},
},
{
show_as_overlay_on_mobile: true,
show_as_overlay_always: show_as_overlay,
},
);
}
function init_email_clipboard(): void {
/*
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).parent();
const $copy_email_icon = $email_el.find("#popover-menu-copy-email");
/*
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", (e) => {
show_copied_confirmation(e.trigger, {
show_check_icon: true,
});
});
}
}
});
}
function init_email_tooltip(user: User): void {
/*
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.default(this, {
content: people.get_visible_email(user),
appendTo: () => document.body,
});
}
});
}
function load_medium_avatar(user: User, $elt: JQuery): void {
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.attr("src", $(this).attr("src")!);
});
}
// Functions related to message user card popover.
// element is the target element to pop off of.
// user is the user whose profile to show.
// sender_id is the user id of the sender for the message we are
// showing the popover from.
function toggle_user_card_popover_for_message(
element: HTMLElement,
user: User,
sender_id: number,
has_message_context: boolean,
on_mount?: (instance: tippy.Instance) => void,
): void {
const $elt = $(element);
const is_sender_popover = sender_id === user.user_id;
show_user_card_popover(
user,
$elt,
is_sender_popover,
has_message_context,
"respond_personal_button",
"message_user_card",
"right",
false,
on_mount,
);
}
export function unsaved_message_user_mention_event_handler(
this: HTMLElement,
e: JQuery.ClickEvent,
): void {
e.stopPropagation();
const id_string = $(this).attr("data-user-id")!;
// Do not open popover for @all mention
if (id_string === "*") {
return;
}
const user_id = Number.parseInt(id_string, 10);
const user = people.get_by_user_id(user_id);
toggle_user_card_popover_for_message(this, user, current_user.user_id, false);
}
// This function serves as the entry point for toggling
// the user card popover via keyboard shortcut.
export function toggle_sender_info(): void {
if (message_user_card.is_open()) {
// We need to call the hide method here because
// the event wasn't triggered by the mouse.
// The Tippy unTrigger event wasn't called,
// so we have to manually hide the popover.
message_user_card.hide();
return;
}
// The "View user card" tooltip shown when hovering the avatar can
// block this from opening properly, so close it first.
//
// This isn't necessary for the click handler, because the click
// naturally closes the Tippy tooltip.
popovers.hide_all();
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");
}
assert(message_lists.current !== undefined);
const message = message_lists.current.get(rows.id($message));
assert(message !== undefined);
const user = people.get_by_user_id(message.sender_id);
toggle_user_card_popover_for_message(the($sender), user, message.sender_id, true, () => {
if (!page_params.is_spectator) {
focus_user_card_popover_item();
}
});
}
function focus_user_card_popover_item(): void {
// 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_card_popover_for_message_items();
popover_menus.focus_first_popover_item($items);
}
function get_user_card_popover_for_message_items(): JQuery | undefined {
if (!message_user_card.is_open()) {
blueslip.error("Trying to get menu items when message user card popover is closed.");
return undefined;
}
if (message_user_card.instance === undefined) {
blueslip.error("Cannot find popover data for message user card menu.");
return undefined;
}
const $popover = $(message_user_card.instance.popper);
// Return only the popover menu options that are visible, and not the
// copy buttons or the link items in the custom profile fields.
// eslint-disable-next-line no-jquery/no-sizzle
return $(".link-item .popover-menu-link", $popover).filter(":visible");
}
// Functions related to the user card popover in the user sidebar.
function toggle_sidebar_user_card_popover($target: JQuery): void {
const user_id = elem_to_user_id($target);
const user = people.get_by_user_id(user_id);
// Hiding popovers may mutate current_user_sidebar_user_id.
const previous_user_sidebar_id = current_user_sidebar_user_id;
// Hide popovers
hide_all();
if (previous_user_sidebar_id === user_id) {
// If the popover is already shown, clicking again should toggle it.
return;
}
show_user_card_popover(
user,
$target,
false,
false,
"compose_private_message",
"user_sidebar",
"left",
false,
(instance) => {
/* See comment in get_props_for_popover_centering for explanation of this. */
$(instance.popper).find(".tippy-box").addClass("show-when-reference-hidden");
},
);
current_user_sidebar_user_id = user.user_id;
}
function register_click_handlers(): void {
$("#main_div").on(
"click",
".sender_name, .inline-profile-picture-wrapper",
function (this: HTMLElement, e) {
const $row = $(this).closest(".message_row");
e.stopPropagation();
assert(message_lists.current !== undefined);
const message = message_lists.current.get(rows.id($row));
assert(message !== undefined);
const user = people.get_by_user_id(message.sender_id);
toggle_user_card_popover_for_message(this, user, message.sender_id, true);
},
);
$("#main_div").on("click", ".user-mention", function (this: HTMLElement, 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();
assert(message_lists.current !== undefined);
const message = message_lists.current.get(rows.id($row));
assert(message !== undefined);
let user;
if (id_string) {
const user_id = Number.parseInt(id_string, 10);
user = people.get_by_user_id(user_id);
} else {
user = email === undefined ? undefined : people.get_by_email(email);
if (user === undefined) {
// There can be a case when user is undefined if
// the user is an inaccessible user as we do not
// create the fake user objects for it because
// we do not have user ID. It is fine to not
// open popover for this case as such cases
// without user ID are rare and old.
return;
}
}
toggle_user_card_popover_for_message(this, user, message.sender_id, true);
});
// Note: Message feeds and drafts have their own direct event listeners
// that run before this one and call stopPropagation.
$("body").on("click", ".messagebox .user-mention", unsaved_message_user_mention_event_handler);
$("body").on("click", ".user-card-popover-actions .narrow_to_private_messages", function (e) {
const user_id = elem_to_user_id($(this).parents("ul"));
const email = people.get_by_user_id(user_id).email;
message_view.show(
[
{
operator: "dm",
operand: email,
},
],
{trigger: "user sidebar popover"},
);
hide_all();
if (overlays.any_active()) {
overlays.close_active();
}
e.stopPropagation();
e.preventDefault();
});
$("body").on("click", ".user-card-popover-actions .narrow_to_messages_sent", function (e) {
const user_id = elem_to_user_id($(this).parents("ul"));
const email = people.get_by_user_id(user_id).email;
message_view.show(
[
{
operator: "sender",
operand: email,
},
],
{trigger: "user sidebar popover"},
);
hide_all();
if (overlays.any_active()) {
overlays.close_active();
}
e.stopPropagation();
e.preventDefault();
});
$("body").on("click", ".user-card-popover-actions .user-card-clear-status-button", (e) => {
e.preventDefault();
user_status.server_update_status({
status_text: "",
emoji_name: "",
emoji_code: "",
success() {
hide_all_user_card_popovers();
},
});
});
$("body").on("click", ".sidebar-popover-reactivate-user", function (e) {
const user_id = elem_to_user_id($(this).parents("ul"));
hide_all();
e.stopPropagation();
e.preventDefault();
function handle_confirm(): void {
const url = "/json/users/" + encodeURIComponent(user_id) + "/reactivate";
channel.post({
url,
success() {
dialog_widget.close();
},
error(xhr) {
ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $("#dialog_error"));
dialog_widget.hide_dialog_spinner();
},
});
}
user_deactivation_ui.confirm_reactivation(user_id, handle_confirm, true);
});
$("body").on("click", ".user-card-popover-actions .view_full_user_profile", function (e) {
const user_id = elem_to_user_id($(this).parents("ul"));
const current_hash = window.location.hash;
// If any overlay is already open, we want the user profile to behave
// as a modal rather than an overlay.
if (is_overlay_hash(current_hash)) {
const user = people.get_by_user_id(user_id);
user_profile.show_user_profile(user);
} else {
browser_history.go_to_location(`user/${user_id}`);
}
e.stopPropagation();
e.preventDefault();
});
$("body").on("click", ".user-card-popover-root .mention_user", function (e) {
if (!compose_state.composing()) {
compose_actions.start({
message_type: "stream",
trigger: "sidebar user actions",
keep_composebox_empty: true,
});
}
const user_id = elem_to_user_id($(this).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);
user_sidebar.hide();
sidebar_ui.hide_userlist_sidebar();
e.stopPropagation();
e.preventDefault();
});
$("body").on("click", ".message-user-card-popover-root .mention_user", function (e) {
if (!compose_state.composing()) {
compose_reply.respond_to_message({
trigger: "user sidebar popover",
keep_composebox_empty: true,
});
}
const user_id = elem_to_user_id($(this).parents("ul"));
const name = people.get_by_user_id(user_id).full_name;
const is_active = people.is_active_user_for_popover(user_id);
const mention = people.get_mention_syntax(name, user_id, !is_active);
compose_ui.insert_syntax_and_focus(mention);
message_user_card.hide();
e.stopPropagation();
e.preventDefault();
});
$("body").on(
"click",
".view_user_profile, .person_picker .pill[data-user-id]",
function (this: HTMLElement, e) {
const user_id = Number.parseInt($(e.currentTarget).attr("data-user-id")!, 10);
const user = people.get_by_user_id(user_id);
if ($(this).closest(".user-card-popover-bot-owner-field").length > 0) {
hide_all_user_card_popovers();
toggle_user_card_popover_for_bot_owner(this, user);
} else {
toggle_user_card_popover(this, 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: JQuery.ClickEvent): void {
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.
$(".buddy-list-section").on(
"click",
".user_sidebar_entry_me .status-emoji",
open_user_status_modal,
);
$(".buddy-list-section").on("click", ".user-list-sidebar-menu-icon", (e) => {
e.stopPropagation();
const $target = $(e.currentTarget).closest("li");
toggle_sidebar_user_card_popover($target);
});
$(".buddy-list-section").on("click", ".user-profile-picture", (e) => {
e.stopPropagation();
const $target = $(e.currentTarget).closest("li");
toggle_sidebar_user_card_popover($target);
});
$("body").on("click", ".sidebar-popover-mute-user", function (e) {
const user_id = elem_to_user_id($(this).parents("ul"));
hide_all_user_card_popovers();
e.stopPropagation();
e.preventDefault();
confirm_mute_user(user_id);
});
$("body").on("click", ".sidebar-popover-unmute-user", function (e) {
const user_id = elem_to_user_id($(this).parents("ul"));
hide_all_user_card_popovers();
muted_users.unmute_user(user_id);
e.stopPropagation();
e.preventDefault();
});
$("body").on("click", ".respond_personal_button, .compose_private_message", function (e) {
const user_id = elem_to_user_id($(this).parents("ul"));
compose_actions.start({
message_type: "private",
trigger: "popover send private",
private_message_recipient_ids: [user_id],
});
hide_all();
if (overlays.any_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", function () {
hide_all();
const user_id = elem_to_user_id($(this).parents("ul"));
const user = people.get_by_user_id(user_id);
user_profile.show_user_profile(user, "manage-profile-tab");
});
$("body").on("click", ".edit-your-profile", () => {
hide_all();
window.location.hash = "#settings/profile";
settings_panel_menu.mobile_activate_section();
});
new ClipboardJS(".copy-custom-profile-field-link", {
text(trigger): string {
return $(trigger).parent().find(".custom-profile-field-link").attr("href")!;
},
}).on("success", (e) => {
show_copied_confirmation(e.trigger, {
show_check_icon: true,
});
});
}
export function initialize(): void {
register_click_handlers();
clipboard_enable(".copy_mention_syntax");
}