mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	We scan a tooltip for any required windows-to-mac hotkey conversions from the list of attributes supplied to the hotkey_hints helper. If we find any, we add/modify the hotkyes in the hotkey hints list to match the mac-style key combinations and then return back the modified list of hotkey hints to be displayed in the tooltip. We also rename the "adjust_mac_shortcuts" function, used for the keyboard shortcuts menu and help center documnets, to "adjust_mac_kbd_tags" to avoid any ambiguity with the adjust_mac_tooltip_keys funtion which is used for tooltip hotkeys.
		
			
				
	
	
		
			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_kbd_tags(".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;
 | 
						|
        },
 | 
						|
    });
 | 
						|
}
 |