mirror of
https://github.com/zulip/zulip.git
synced 2025-10-25 09:03:57 +00:00
If the selected message was different from the message where actions popover was present, it resulted in emoji popover being attached to the selected message instead of the one where actions popover was present.
445 lines
18 KiB
JavaScript
445 lines
18 KiB
JavaScript
/* Module for popovers that have been ported to the modern
|
|
TippyJS/Popper popover library from the legacy Bootstrap
|
|
popovers system in popovers.js. */
|
|
|
|
import ClipboardJS from "clipboard";
|
|
import $ from "jquery";
|
|
import tippy, {delegate} from "tippy.js";
|
|
|
|
import render_actions_popover_content from "../templates/actions_popover_content.hbs";
|
|
import render_compose_control_buttons_popover from "../templates/compose_control_buttons_popover.hbs";
|
|
import render_compose_select_enter_behaviour_popover from "../templates/compose_select_enter_behaviour_popover.hbs";
|
|
import render_left_sidebar_stream_setting_popover from "../templates/left_sidebar_stream_setting_popover.hbs";
|
|
import render_mobile_message_buttons_popover_content from "../templates/mobile_message_buttons_popover_content.hbs";
|
|
|
|
import * as channel from "./channel";
|
|
import * as common from "./common";
|
|
import * as compose_actions from "./compose_actions";
|
|
import * as condense from "./condense";
|
|
import * as emoji_picker from "./emoji_picker";
|
|
import * as giphy from "./giphy";
|
|
import {$t} from "./i18n";
|
|
import * as message_edit from "./message_edit";
|
|
import * as message_edit_history from "./message_edit_history";
|
|
import * as message_lists from "./message_lists";
|
|
import * as narrow_state from "./narrow_state";
|
|
import * as popover_menus_data from "./popover_menus_data";
|
|
import * as popovers from "./popovers";
|
|
import * as read_receipts from "./read_receipts";
|
|
import * as rows from "./rows";
|
|
import * as settings_data from "./settings_data";
|
|
import * as stream_popover from "./stream_popover";
|
|
import {parse_html} from "./ui_util";
|
|
import * as unread_ops from "./unread_ops";
|
|
import {user_settings} from "./user_settings";
|
|
|
|
let left_sidebar_stream_setting_popover_displayed = false;
|
|
let compose_mobile_button_popover_displayed = false;
|
|
export let compose_enter_sends_popover_displayed = false;
|
|
let compose_control_buttons_popover_instance;
|
|
let message_actions_popover_displayed = false;
|
|
let message_actions_popover_keyboard_toggle = false;
|
|
|
|
export function actions_popped() {
|
|
return message_actions_popover_displayed;
|
|
}
|
|
|
|
export function get_compose_control_buttons_popover() {
|
|
return compose_control_buttons_popover_instance;
|
|
}
|
|
|
|
const default_popover_props = {
|
|
delay: 0,
|
|
appendTo: () => document.body,
|
|
trigger: "click",
|
|
interactive: true,
|
|
hideOnClick: true,
|
|
/* The light-border TippyJS theme is a bit of a misnomer; it
|
|
is a popover styling similar to Bootstrap. We've also customized
|
|
its CSS to support Zulip's dark theme. */
|
|
theme: "light-border",
|
|
touch: true,
|
|
/* Don't use allow-HTML here since it is unsafe. Instead, use `parse_html`
|
|
to generate the required html */
|
|
};
|
|
|
|
export function any_active() {
|
|
return (
|
|
left_sidebar_stream_setting_popover_displayed ||
|
|
compose_mobile_button_popover_displayed ||
|
|
compose_control_buttons_popover_instance ||
|
|
compose_enter_sends_popover_displayed ||
|
|
message_actions_popover_displayed
|
|
);
|
|
}
|
|
|
|
function on_show_prep(instance) {
|
|
$(instance.popper).on("click", (e) => {
|
|
// Popover is not hidden on click inside it unless the click handler for the
|
|
// element explicitly hides the popover when handling the event.
|
|
// `stopPropagation` is required here to avoid global click handlers from
|
|
// being triggered.
|
|
e.stopPropagation();
|
|
});
|
|
$(instance.popper).one("click", ".navigate_and_close_popover", (e) => {
|
|
// Handler for links inside popover which don't need a special click handler.
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
popovers.hide_all_except_sidebars();
|
|
}
|
|
|
|
function tippy_no_propagation(target, popover_props) {
|
|
// For some elements, such as the click target to open the message
|
|
// actions menu, we want to avoid propagating the click event to
|
|
// parent elements. Tippy's built-in `delegate` method does not
|
|
// have an option to do stopPropagation, so we use this method to
|
|
// open the Tippy popovers associated with such elements.
|
|
//
|
|
// A click on the click target will close the menu; for this to
|
|
// work correctly without leaking, all callers need call
|
|
// `instance.destroy()` inside their `onHidden` handler.
|
|
//
|
|
// TODO: Should we instead we wrap the caller's `onHidden` hook,
|
|
// if any, to add `instance.destroy()`?
|
|
$("body").on("click", target, (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const instance = e.currentTarget._tippy;
|
|
|
|
if (instance) {
|
|
instance.hide();
|
|
return;
|
|
}
|
|
|
|
tippy(e.currentTarget, {
|
|
...default_popover_props,
|
|
showOnCreate: true,
|
|
...popover_props,
|
|
});
|
|
});
|
|
}
|
|
|
|
export function toggle_message_actions_menu(message) {
|
|
if (message.locally_echoed) {
|
|
// Don't open the popup for locally echoed messages for now.
|
|
// It creates bugs with things like keyboard handlers when
|
|
// we get the server response.
|
|
return true;
|
|
}
|
|
|
|
const $popover_reference = $(".selected_message .actions_hover .zulip-icon-ellipsis-v-solid");
|
|
message_actions_popover_keyboard_toggle = true;
|
|
$popover_reference.trigger("click");
|
|
return true;
|
|
}
|
|
|
|
export function initialize() {
|
|
tippy_no_propagation("#streams_inline_icon", {
|
|
onShow(instance) {
|
|
const can_create_streams =
|
|
settings_data.user_can_create_private_streams() ||
|
|
settings_data.user_can_create_public_streams() ||
|
|
settings_data.user_can_create_web_public_streams();
|
|
on_show_prep(instance);
|
|
|
|
if (!can_create_streams) {
|
|
// If the user can't create streams, we directly
|
|
// navigate them to the Manage streams subscribe UI.
|
|
window.location.assign("#streams/all");
|
|
// Returning false from an onShow handler cancels the show.
|
|
return false;
|
|
}
|
|
|
|
instance.setContent(parse_html(render_left_sidebar_stream_setting_popover()));
|
|
left_sidebar_stream_setting_popover_displayed = true;
|
|
return true;
|
|
},
|
|
onHidden(instance) {
|
|
instance.destroy();
|
|
left_sidebar_stream_setting_popover_displayed = false;
|
|
},
|
|
});
|
|
|
|
// compose box buttons popover shown on mobile widths.
|
|
// We want this click event to propagate and hide other popovers
|
|
// that could possibly obstruct user from using this popover.
|
|
delegate("body", {
|
|
...default_popover_props,
|
|
target: ".compose_mobile_button",
|
|
placement: "top",
|
|
onShow(instance) {
|
|
on_show_prep(instance);
|
|
instance.setContent(
|
|
parse_html(
|
|
render_mobile_message_buttons_popover_content({
|
|
is_in_private_narrow: narrow_state.narrowed_to_pms(),
|
|
}),
|
|
),
|
|
);
|
|
compose_mobile_button_popover_displayed = true;
|
|
},
|
|
onMount(instance) {
|
|
const $popper = $(instance.popper);
|
|
$popper.one("click", ".compose_mobile_stream_button", (e) => {
|
|
compose_actions.start("stream", {trigger: "new topic button"});
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
$popper.one("click", ".compose_mobile_private_button", (e) => {
|
|
compose_actions.start("private");
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
},
|
|
onHidden(instance) {
|
|
// Destroy instance so that event handlers
|
|
// are destroyed too.
|
|
instance.destroy();
|
|
compose_mobile_button_popover_displayed = false;
|
|
},
|
|
});
|
|
|
|
// Click event handlers for it are handled in `compose_ui` and
|
|
// we don't want to close this popover on click inside it but
|
|
// only if user clicked outside it.
|
|
tippy_no_propagation(".compose_control_menu_wrapper", {
|
|
placement: "top",
|
|
onShow(instance) {
|
|
instance.setContent(
|
|
parse_html(
|
|
render_compose_control_buttons_popover({
|
|
giphy_enabled: giphy.is_giphy_enabled(),
|
|
}),
|
|
),
|
|
);
|
|
compose_control_buttons_popover_instance = instance;
|
|
popovers.hide_all_except_sidebars();
|
|
},
|
|
onHidden(instance) {
|
|
instance.destroy();
|
|
compose_control_buttons_popover_instance = undefined;
|
|
},
|
|
});
|
|
|
|
tippy_no_propagation(".enter_sends", {
|
|
placement: "top",
|
|
onShow(instance) {
|
|
on_show_prep(instance);
|
|
instance.setContent(
|
|
parse_html(
|
|
render_compose_select_enter_behaviour_popover({
|
|
enter_sends_true: user_settings.enter_sends,
|
|
}),
|
|
),
|
|
);
|
|
compose_enter_sends_popover_displayed = true;
|
|
},
|
|
onMount(instance) {
|
|
common.adjust_mac_shortcuts(".enter_sends_choices kbd");
|
|
|
|
$(instance.popper).one("click", ".enter_sends_choice", (e) => {
|
|
let selected_behaviour = $(e.currentTarget)
|
|
.find("input[type='radio']")
|
|
.attr("value");
|
|
selected_behaviour = selected_behaviour === "true"; // Convert to bool
|
|
user_settings.enter_sends = selected_behaviour;
|
|
$(`.enter_sends_${!selected_behaviour}`).hide();
|
|
$(`.enter_sends_${selected_behaviour}`).show();
|
|
|
|
// Refocus in the content box so you can continue typing or
|
|
// press Enter to send.
|
|
$("#compose-textarea").trigger("focus");
|
|
|
|
channel.patch({
|
|
url: "/json/settings",
|
|
data: {enter_sends: selected_behaviour},
|
|
});
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
},
|
|
onHidden(instance) {
|
|
instance.destroy();
|
|
compose_enter_sends_popover_displayed = false;
|
|
},
|
|
});
|
|
|
|
tippy_no_propagation(".actions_hover .zulip-icon-ellipsis-v-solid", {
|
|
// The is our minimum supported width for mobile. We shouldn't
|
|
// make the popover wider than this.
|
|
maxWidth: "320px",
|
|
placement: "bottom",
|
|
popperOptions: {
|
|
modifiers: [
|
|
{
|
|
// The placement is set to bottom, but if that placement does not fit,
|
|
// the opposite top placement will be used.
|
|
name: "flip",
|
|
options: {
|
|
fallbackPlacements: ["top", "left"],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
onShow(instance) {
|
|
on_show_prep(instance);
|
|
const $row = $(instance.reference).closest(".message_row");
|
|
const message_id = rows.id($row);
|
|
message_lists.current.select_id(message_id);
|
|
const args = popover_menus_data.get_actions_popover_content_context(message_id);
|
|
instance.setContent(parse_html(render_actions_popover_content(args)));
|
|
$row.addClass("has_popover has_actions_popover");
|
|
message_actions_popover_displayed = true;
|
|
},
|
|
onMount(instance) {
|
|
if (message_actions_popover_keyboard_toggle) {
|
|
popovers.focus_first_action_popover_item();
|
|
}
|
|
message_actions_popover_keyboard_toggle = false;
|
|
|
|
// We want click events to propagate to `instance` so that
|
|
// instance.hide gets called.
|
|
const $popper = $(instance.popper);
|
|
$popper.one("click", ".respond_button", (e) => {
|
|
// Arguably, we should fetch the message ID to respond to from
|
|
// e.target, but that should always be the current selected
|
|
// message in the current message list (and
|
|
// compose_actions.respond_to_message doesn't take a message
|
|
// argument).
|
|
compose_actions.quote_and_reply({trigger: "popover respond"});
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".popover_edit_message, .popover_view_source", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
const $row = message_lists.current.get_row(message_id);
|
|
message_edit.start($row);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".popover_move_message", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
const message = message_lists.current.get(message_id);
|
|
stream_popover.build_move_topic_to_stream_popover(
|
|
message.stream_id,
|
|
message.topic,
|
|
message,
|
|
);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".mark_as_unread", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
unread_ops.mark_as_unread_from_here(message_id);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".popover_toggle_collapse", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
const $row = message_lists.current.get_row(message_id);
|
|
const message = message_lists.current.get(rows.id($row));
|
|
if ($row) {
|
|
if (message.collapsed) {
|
|
condense.uncollapse($row);
|
|
} else {
|
|
condense.collapse($row);
|
|
}
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".rehide_muted_user_message", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
const $row = message_lists.current.get_row(message_id);
|
|
const message = message_lists.current.get(rows.id($row));
|
|
const message_container = message_lists.current.view.message_containers.get(
|
|
message.id,
|
|
);
|
|
if ($row && !message_container.is_hidden) {
|
|
message_lists.current.view.hide_revealed_message(message_id);
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".view_edit_history", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
const $row = message_lists.current.get_row(message_id);
|
|
const message = message_lists.current.get(rows.id($row));
|
|
message_edit_history.show_history(message);
|
|
$("#message-history-cancel").trigger("focus");
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".view_read_receipts", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
read_receipts.show_user_list(message_id);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".delete_message", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
message_edit.delete_message(message_id);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
instance.hide();
|
|
});
|
|
|
|
$popper.one("click", ".reaction_button", (e) => {
|
|
const message_id = $(e.currentTarget).data("message-id");
|
|
// Don't propagate the click event since `toggle_emoji_popover` opens a
|
|
// emoji_picker which we don't want to hide after actions popover is hidden.
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
emoji_picker.toggle_emoji_popover(
|
|
instance.reference.parentElement,
|
|
message_id,
|
|
true,
|
|
);
|
|
instance.hide();
|
|
});
|
|
|
|
new ClipboardJS($popper.find(".copy_link")[0]).on("success", (e) => {
|
|
// e.trigger returns the DOM element triggering the copy action
|
|
const message_id = e.trigger.dataset.messageId;
|
|
const $row = $(`[zid='${CSS.escape(message_id)}']`);
|
|
$row.find(".alert-msg")
|
|
.text($t({defaultMessage: "Copied!"}))
|
|
.css("display", "block")
|
|
.delay(1000)
|
|
.fadeOut(300);
|
|
|
|
setTimeout(() => {
|
|
// The Clipboard library works by focusing to a hidden textarea.
|
|
// We unfocus this so keyboard shortcuts, etc., will work again.
|
|
$(":focus").trigger("blur");
|
|
}, 0);
|
|
instance.hide();
|
|
});
|
|
},
|
|
onHidden(instance) {
|
|
const $row = $(instance.reference).closest(".message_row");
|
|
$row.removeClass("has_popover has_actions_popover");
|
|
instance.destroy();
|
|
message_actions_popover_displayed = false;
|
|
message_actions_popover_keyboard_toggle = false;
|
|
},
|
|
});
|
|
}
|