mirror of
https://github.com/zulip/zulip.git
synced 2025-11-13 18:36:36 +00:00
We no longer have "(no topic)" placeholder for empty string topic, so there's no need to push for configuring a topic.
982 lines
37 KiB
TypeScript
982 lines
37 KiB
TypeScript
// You won't find every click handler here, but it's a good place to start!
|
|
|
|
import $ from "jquery";
|
|
import assert from "minimalistic-assert";
|
|
import * as tippy from "tippy.js";
|
|
import {z} from "zod";
|
|
|
|
import render_buddy_list_tooltip_content from "../templates/buddy_list_tooltip_content.hbs";
|
|
|
|
import * as activity_ui from "./activity_ui.ts";
|
|
import * as browser_history from "./browser_history.ts";
|
|
import * as buddy_data from "./buddy_data.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 {media_breakpoints_num} from "./css_variables.ts";
|
|
import * as emoji_picker from "./emoji_picker.ts";
|
|
import * as hash_util from "./hash_util.ts";
|
|
import * as hashchange from "./hashchange.ts";
|
|
import * as message_edit from "./message_edit.ts";
|
|
import * as message_lists from "./message_lists.ts";
|
|
import * as message_store from "./message_store.ts";
|
|
import * as message_view from "./message_view.ts";
|
|
import * as narrow_state from "./narrow_state.ts";
|
|
import * as navigate from "./navigate.ts";
|
|
import {page_params} from "./page_params.ts";
|
|
import * as pm_list from "./pm_list.ts";
|
|
import * as popover_menus from "./popover_menus.ts";
|
|
import * as reactions from "./reactions.ts";
|
|
import * as recent_view_ui from "./recent_view_ui.ts";
|
|
import * as rows from "./rows.ts";
|
|
import * as settings_panel_menu from "./settings_panel_menu.ts";
|
|
import * as settings_preferences from "./settings_preferences.ts";
|
|
import * as settings_toggle from "./settings_toggle.ts";
|
|
import * as sidebar_ui from "./sidebar_ui.ts";
|
|
import * as spectators from "./spectators.ts";
|
|
import * as starred_messages_ui from "./starred_messages_ui.ts";
|
|
import * as stream_list from "./stream_list.ts";
|
|
import * as stream_popover from "./stream_popover.ts";
|
|
import * as topic_list from "./topic_list.ts";
|
|
import * as ui_util from "./ui_util.ts";
|
|
import {parse_html} from "./ui_util.ts";
|
|
import * as util from "./util.ts";
|
|
|
|
export function initialize(): void {
|
|
// MESSAGE CLICKING
|
|
|
|
function initialize_long_tap(): void {
|
|
const MS_DELAY = 750;
|
|
const meta: {touchdown: boolean; current_target: number | undefined; invalid?: boolean} = {
|
|
touchdown: false,
|
|
current_target: undefined,
|
|
};
|
|
|
|
$("#main_div").on("touchstart", ".messagebox", function () {
|
|
meta.touchdown = true;
|
|
meta.invalid = false;
|
|
const id = rows.id($(this).closest(".message_row"));
|
|
meta.current_target = id;
|
|
if (!id) {
|
|
return;
|
|
}
|
|
assert(message_lists.current !== undefined);
|
|
message_lists.current.select_id(id);
|
|
setTimeout(() => {
|
|
// The algorithm to trigger long tap is that first, we check
|
|
// whether the message is still touched after MS_DELAY ms and
|
|
// the user isn't scrolling the messages(see other touch event
|
|
// handlers to see how these meta variables are handled).
|
|
// Later we check whether after MS_DELAY the user is still
|
|
// long touching the same message as it can be possible that
|
|
// user touched another message within MS_DELAY period.
|
|
if (meta.touchdown && !meta.invalid && id === meta.current_target) {
|
|
$(this).trigger("longtap");
|
|
}
|
|
}, MS_DELAY);
|
|
});
|
|
|
|
$("#main_div").on("touchend", ".messagebox", () => {
|
|
meta.touchdown = false;
|
|
});
|
|
|
|
$("#main_div").on("touchmove", ".messagebox", () => {
|
|
meta.invalid = true;
|
|
});
|
|
|
|
$("#main_div").on("contextmenu", ".messagebox", (e) => {
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
|
|
// this initializes the trigger that will give off the longtap event, which
|
|
// there is no point in running if we are on desktop since this isn't a
|
|
// standard event that we would want to support.
|
|
if (util.is_mobile()) {
|
|
initialize_long_tap();
|
|
}
|
|
|
|
function is_clickable_message_element($target: JQuery<Element>): boolean {
|
|
// This function defines all the elements within a message
|
|
// body that have UI behavior other than starting a reply.
|
|
|
|
// Links should be handled by the browser.
|
|
if ($target.closest("a").length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Forms for message editing contain input elements
|
|
if ($target.is("textarea") || $target.is("input")) {
|
|
return true;
|
|
}
|
|
|
|
// Widget for adjusting the height of a message.
|
|
if ($target.is("button.message_expander") || $target.is("button.message_condenser")) {
|
|
return true;
|
|
}
|
|
|
|
// Inline image, video and twitter previews.
|
|
if (
|
|
$target.is("img.message_inline_image") ||
|
|
$target.is(".message_inline_animated_image_still") ||
|
|
$target.is("video") ||
|
|
$target.is(".message_inline_video") ||
|
|
$target.is("img.twitter-avatar")
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// UI elements for triggering message editing or viewing edit history.
|
|
if ($target.is("i.edit_message_button") || $target.is(".message_edit_notice")) {
|
|
return true;
|
|
}
|
|
|
|
// For spoilers, allow clicking either the header or elements within it
|
|
if ($target.is(".spoiler-header") || $target.parents(".spoiler-header").length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Ideally, this should be done via ClipboardJS, but it doesn't support
|
|
// feature of stopPropagation once clicked.
|
|
// See https://github.com/zenorocha/clipboard.js/pull/475
|
|
if ($target.is(".copy_codeblock") || $target.parents(".copy_codeblock").length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Don't select message on clicking message control buttons.
|
|
if ($target.parents(".message_controls").length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Allow toggling of tasks in todo widget
|
|
if (
|
|
$target.is(".todo-widget label.checkbox") ||
|
|
$target.parents(".todo-widget label.checkbox").length > 0
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const select_message_function = function (this: HTMLElement, e: JQuery.TriggeredEvent): void {
|
|
assert(e.target instanceof Element);
|
|
if (is_clickable_message_element($(e.target))) {
|
|
// If this click came from a hyperlink, don't trigger the
|
|
// reply action. The simple way of doing this is simply
|
|
// to call e.stopPropagation() from within the link's
|
|
// click handler.
|
|
//
|
|
// Unfortunately, on Firefox, this breaks Ctrl-click and
|
|
// Shift-click, because those are (apparently) implemented
|
|
// by adding an event listener on link clicks, and
|
|
// stopPropagation prevents them from being called.
|
|
return;
|
|
}
|
|
|
|
if (document.getSelection()?.type === "Range") {
|
|
// Drags on the message (to copy message text) shouldn't trigger a reply.
|
|
return;
|
|
}
|
|
|
|
const $row = $(this).closest(".message_row");
|
|
const id = rows.id($row);
|
|
|
|
assert(message_lists.current !== undefined);
|
|
message_lists.current.select_id(id);
|
|
|
|
if (message_edit.currently_editing_messages.has(id)) {
|
|
// Clicks on a message being edited shouldn't trigger a reply.
|
|
return;
|
|
}
|
|
|
|
// Clicks on a message from search results should bring the
|
|
// user to the message's near view instead of opening the
|
|
// compose box.
|
|
const current_filter = narrow_state.filter();
|
|
if (current_filter !== undefined && !current_filter.supports_collapsing_recipients()) {
|
|
const message = message_store.get(id);
|
|
|
|
if (message === undefined) {
|
|
// This might happen for locally echoed messages, for example.
|
|
return;
|
|
}
|
|
window.location.href = hash_util.by_conversation_and_time_url(message);
|
|
return;
|
|
}
|
|
|
|
if (page_params.is_spectator) {
|
|
return;
|
|
}
|
|
compose_reply.respond_to_message({trigger: "message click"});
|
|
e.stopPropagation();
|
|
};
|
|
|
|
// if on normal non-mobile experience, a `click` event should run the message
|
|
// selection function which will open the compose box and select the message.
|
|
if (!util.is_mobile()) {
|
|
$("#main_div").on("click", ".messagebox", select_message_function);
|
|
// on the other hand, on mobile it should be done with a long tap.
|
|
} else {
|
|
$("#main_div").on("longtap", ".messagebox", function (this: HTMLElement, e) {
|
|
const sel = window.getSelection();
|
|
// if one matches, remove the current selections.
|
|
// after a longtap that is valid, there should be no text selected.
|
|
if (sel) {
|
|
if (sel.removeAllRanges) {
|
|
sel.removeAllRanges();
|
|
} else if (sel.empty) {
|
|
sel.empty();
|
|
}
|
|
}
|
|
|
|
select_message_function.call(this, e);
|
|
});
|
|
}
|
|
|
|
$("#main_div").on("click", ".star_container", function (e) {
|
|
e.stopPropagation();
|
|
|
|
if (page_params.is_spectator) {
|
|
spectators.login_to_access();
|
|
return;
|
|
}
|
|
|
|
const message_id = rows.id($(this).closest(".message_row"));
|
|
const message = message_store.get(message_id);
|
|
assert(message !== undefined);
|
|
starred_messages_ui.toggle_starred_and_update_server(message);
|
|
});
|
|
|
|
$("#main_div").on("click", ".message_reaction", function (this: HTMLElement, e) {
|
|
e.stopPropagation();
|
|
|
|
if (page_params.is_spectator) {
|
|
spectators.login_to_access();
|
|
return;
|
|
}
|
|
|
|
emoji_picker.hide_emoji_popover();
|
|
const local_id = $(this).attr("data-reaction-id")!;
|
|
const message_id = rows.get_message_id(this);
|
|
reactions.process_reaction_click(message_id, local_id);
|
|
$(".tooltip").remove();
|
|
});
|
|
|
|
$("body").on("click", ".reveal_hidden_message", (e) => {
|
|
assert(message_lists.current !== undefined);
|
|
const message_id = rows.id($(e.currentTarget).closest(".message_row"));
|
|
message_lists.current.view.reveal_hidden_message(message_id);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
});
|
|
|
|
$("#main_div").on("click", "a.stream", function (this: HTMLAnchorElement, e) {
|
|
e.preventDefault();
|
|
// Note that we may have an href here, but we trust the stream id more,
|
|
// so we re-encode the hash.
|
|
const stream_id = Number.parseInt($(this).attr("data-stream-id")!, 10);
|
|
if (stream_id) {
|
|
browser_history.go_to_location(hash_util.by_stream_url(stream_id));
|
|
return;
|
|
}
|
|
window.location.href = this.href;
|
|
});
|
|
|
|
$("body").on("click", "#scroll-to-bottom-button-clickable-area", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Since it take a few milliseconds for this button complete disappear transition,
|
|
// it is possible for user to click it before it hides when switching narrows.
|
|
if (narrow_state.is_message_feed_visible()) {
|
|
navigate.to_end();
|
|
}
|
|
});
|
|
|
|
$("body").on("click", ".message_row", function () {
|
|
$(".selected_msg_for_touchscreen").removeClass("selected_msg_for_touchscreen");
|
|
$(this).addClass("selected_msg_for_touchscreen");
|
|
});
|
|
|
|
// MESSAGE EDITING
|
|
|
|
$("body").on("click", ".edit_content_button", function (e) {
|
|
assert(message_lists.current !== undefined);
|
|
const $row = message_lists.current.get_row(rows.id($(this).closest(".message_row")));
|
|
message_lists.current.select_id(rows.id($row));
|
|
message_edit.start($row);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", ".move_message_button", function (e) {
|
|
assert(message_lists.current !== undefined);
|
|
const $row = message_lists.current.get_row(rows.id($(this).closest(".message_row")));
|
|
const message_id = rows.id($row);
|
|
const message = message_lists.current.get(message_id);
|
|
assert(message?.type === "stream");
|
|
void stream_popover.build_move_topic_to_stream_popover(
|
|
message.stream_id,
|
|
message.topic,
|
|
false,
|
|
message,
|
|
);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", ".on_hover_topic_edit", function (e) {
|
|
const $recipient_row = $(this).closest(".recipient_row");
|
|
message_edit.start_inline_topic_edit($recipient_row);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", ".topic_edit_save", function (e) {
|
|
const $recipient_row = $(this).closest(".recipient_row");
|
|
message_edit.try_save_inline_topic_edit($recipient_row);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", ".topic_edit_cancel", function (e) {
|
|
const $recipient_row = $(this).closest(".recipient_row");
|
|
message_edit.end_inline_topic_edit($recipient_row);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", ".message_edit_save", function (e) {
|
|
const $row = $(this).closest(".message_row");
|
|
message_edit.save_message_row_edit($row);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", ".message_edit_cancel", function (e) {
|
|
const $row = $(this).closest(".message_row");
|
|
message_edit.end_message_row_edit($row);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", ".message_edit_close", function (e) {
|
|
const $row = $(this).closest(".message_row");
|
|
message_edit.end_message_row_edit($row);
|
|
e.stopPropagation();
|
|
});
|
|
$("body").on("click", "a", function () {
|
|
if (document.activeElement === this) {
|
|
ui_util.blur_active_element();
|
|
}
|
|
});
|
|
$("body").on("click", ".message_edit_form .compose_upload_file", function (e) {
|
|
e.preventDefault();
|
|
|
|
const row_id = rows.id($(this).closest(".message_row"));
|
|
$(`#edit_form_${CSS.escape(`${row_id}`)} .file_input`).trigger("click");
|
|
});
|
|
$("body").on(
|
|
"focus",
|
|
".message_edit_form textarea.message_edit_content",
|
|
function (this: HTMLTextAreaElement, _event) {
|
|
compose_state.set_last_focused_compose_type_input(this);
|
|
},
|
|
);
|
|
|
|
$("body").on("click", ".message_edit_form .markdown_preview", function (this: HTMLElement, e) {
|
|
e.preventDefault();
|
|
message_edit.show_preview_area($(this));
|
|
});
|
|
|
|
$("body").on(
|
|
"click",
|
|
".message_edit_form .undo_markdown_preview",
|
|
function (this: HTMLElement, e) {
|
|
e.preventDefault();
|
|
message_edit.clear_preview_area($(this));
|
|
},
|
|
);
|
|
|
|
// RESOLVED TOPICS
|
|
$("body").on("click", ".message_header .on_hover_topic_resolve", (e) => {
|
|
e.stopPropagation();
|
|
const $recipient_row = $(e.target).closest(".recipient_row");
|
|
const message_id = rows.id_for_recipient_row($recipient_row);
|
|
const topic_name = $(e.target).attr("data-topic-name")!;
|
|
message_edit.toggle_resolve_topic(message_id, topic_name, false, $recipient_row);
|
|
});
|
|
|
|
$("body").on("click", ".message_header .on_hover_topic_unresolve", (e) => {
|
|
e.stopPropagation();
|
|
const $recipient_row = $(e.target).closest(".recipient_row");
|
|
const message_id = rows.id_for_recipient_row($recipient_row);
|
|
const topic_name = $(e.target).attr("data-topic-name")!;
|
|
message_edit.toggle_resolve_topic(message_id, topic_name, false, $recipient_row);
|
|
});
|
|
|
|
// RECIPIENT BARS
|
|
|
|
function get_row_id_for_narrowing(narrow_link_elem: HTMLElement): number {
|
|
const $group = rows.get_closest_group(narrow_link_elem);
|
|
const msg_id = rows.id_for_recipient_row($group);
|
|
|
|
assert(message_lists.current !== undefined);
|
|
const nearest = message_lists.current.get(msg_id)!;
|
|
const selected = message_lists.current.selected_message();
|
|
if (selected !== undefined && util.same_recipient(nearest, selected)) {
|
|
return selected.id;
|
|
}
|
|
return nearest.id;
|
|
}
|
|
|
|
$("#message_feed_container").on(
|
|
"click",
|
|
".narrows_by_recipient",
|
|
function (this: HTMLElement, e) {
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
const row_id = get_row_id_for_narrowing(this);
|
|
message_view.narrow_by_recipient(row_id, {trigger: "message header"});
|
|
},
|
|
);
|
|
|
|
$("#message_feed_container").on("click", ".narrows_by_topic", function (this: HTMLElement, e) {
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
const row_id = get_row_id_for_narrowing(this);
|
|
message_view.narrow_by_topic(row_id, {trigger: "message header"});
|
|
});
|
|
|
|
// SIDEBARS
|
|
$("body").on("click", "#compose-new-direct-message", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
compose_actions.start({
|
|
message_type: "private",
|
|
trigger: "new direct message",
|
|
keep_composebox_empty: true,
|
|
});
|
|
});
|
|
|
|
$(".buddy-list-section").on("click", ".selectable_sidebar_block", (e) => {
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
return;
|
|
}
|
|
if ($(e.target).parents(".user-profile-picture").length === 1) {
|
|
return;
|
|
}
|
|
|
|
const $li = $(e.target).parents("li");
|
|
|
|
activity_ui.narrow_for_user({$li});
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
sidebar_ui.hide_userlist_sidebar();
|
|
$(".tooltip").remove();
|
|
});
|
|
|
|
// Doesn't show tooltip on touch devices.
|
|
function do_render_buddy_list_tooltip(
|
|
$elem: JQuery,
|
|
title_data: buddy_data.TitleData,
|
|
get_target_node?: (tippy_instance: tippy.Instance) => HTMLElement,
|
|
check_reference_removed?: (
|
|
mutation: MutationRecord,
|
|
tippy_instance: tippy.Instance,
|
|
) => boolean,
|
|
subtree = false,
|
|
parent_element_to_append: HTMLElement | null = null,
|
|
is_custom_observer_needed = true,
|
|
): void {
|
|
let placement: tippy.Placement = "left";
|
|
let observer: MutationObserver;
|
|
if (window.innerWidth < media_breakpoints_num.md) {
|
|
// On small devices display tooltips based on available space.
|
|
// This will default to "bottom" placement for this tooltip.
|
|
placement = "auto";
|
|
}
|
|
tippy.default(util.the($elem), {
|
|
// Quickly display and hide right sidebar tooltips
|
|
// so that they don't stick and overlap with
|
|
// each other.
|
|
delay: 0,
|
|
// Don't show tooltip on touch devices (99% mobile) since touch pressing on users in the left or right
|
|
// sidebar leads to narrow being changed and the sidebar is hidden. So, there is no user displayed
|
|
// to show tooltip for. It is safe to show the tooltip on long press but it not worth
|
|
// the inconvenience of having a tooltip hanging around on a small mobile screen if anything going wrong.
|
|
touch: false,
|
|
content: () => parse_html(render_buddy_list_tooltip_content(title_data)),
|
|
arrow: true,
|
|
placement,
|
|
showOnCreate: true,
|
|
onHidden(instance) {
|
|
instance.destroy();
|
|
if (is_custom_observer_needed) {
|
|
observer.disconnect();
|
|
}
|
|
},
|
|
onShow(instance) {
|
|
if (!is_custom_observer_needed) {
|
|
return;
|
|
}
|
|
assert(get_target_node !== undefined);
|
|
assert(check_reference_removed !== undefined);
|
|
// We cannot use MutationObserver directly on the reference element because
|
|
// it will be removed and we need to attach it on an element which will remain in the DOM.
|
|
const target_node = get_target_node(instance);
|
|
// We only need to know if any of the `li` elements were removed.
|
|
const config = {attributes: false, childList: true, subtree};
|
|
const callback: MutationCallback = function (mutationsList) {
|
|
for (const mutation of mutationsList) {
|
|
// Hide instance if reference is in the removed node list.
|
|
if (check_reference_removed(mutation, instance)) {
|
|
popover_menus.hide_current_popover_if_visible(instance);
|
|
}
|
|
}
|
|
};
|
|
observer = new MutationObserver(callback);
|
|
observer.observe(target_node, config);
|
|
},
|
|
appendTo: () => parent_element_to_append ?? document.body,
|
|
});
|
|
}
|
|
|
|
// BUDDY LIST TOOLTIPS (not displayed on touch devices)
|
|
$(".buddy-list-section").on("mouseenter", ".selectable_sidebar_block", (e) => {
|
|
e.stopPropagation();
|
|
const user_id_string = $(e.currentTarget)
|
|
.closest(".user_sidebar_entry")
|
|
.attr("data-user-id")!;
|
|
const title_data = buddy_data.get_title_data(user_id_string, false);
|
|
|
|
// `target_node` is the `ul` element since it stays in DOM even after updates.
|
|
function get_target_node(): HTMLElement {
|
|
return util.the($(e.target).parents(".buddy-list-section"));
|
|
}
|
|
|
|
function check_reference_removed(
|
|
mutation: MutationRecord,
|
|
instance: tippy.Instance,
|
|
): boolean {
|
|
return Array.prototype.includes.call(
|
|
mutation.removedNodes,
|
|
instance.reference.parentElement,
|
|
);
|
|
}
|
|
|
|
const $elem = $(e.currentTarget)
|
|
.closest(".user_sidebar_entry")
|
|
.find(".selectable_sidebar_block");
|
|
do_render_buddy_list_tooltip($elem, title_data, get_target_node, check_reference_removed);
|
|
|
|
/*
|
|
The following implements a little tooltip giving the name for status emoji
|
|
when hovering them in the right sidebar. This requires special logic, to avoid
|
|
conflicting with the main tooltip or showing duplicate tooltips.
|
|
*/
|
|
$(".user_sidebar_entry .status-emoji-name").off("mouseenter").off("mouseleave");
|
|
$(".user_sidebar_entry .status-emoji-name").on("mouseenter", () => {
|
|
const element: tippy.ReferenceElement = util.the($elem);
|
|
const instance = element._tippy;
|
|
if (instance?.state.isVisible) {
|
|
instance.destroy();
|
|
}
|
|
});
|
|
$(".user_sidebar_entry .status-emoji-name").on("mouseleave", () => {
|
|
do_render_buddy_list_tooltip(
|
|
$elem,
|
|
title_data,
|
|
get_target_node,
|
|
check_reference_removed,
|
|
);
|
|
});
|
|
});
|
|
|
|
// DIRECT MESSAGE LIST TOOLTIPS (not displayed on touch devices)
|
|
$("body").on("mouseenter", ".dm-user-status", function (this: HTMLElement, e) {
|
|
e.stopPropagation();
|
|
const $elem = $(this);
|
|
const user_ids_string = $elem.attr("data-user-ids-string")!;
|
|
// This converts from 'true' in the DOM to true.
|
|
const is_group = z.boolean().parse(JSON.parse($elem.attr("data-is-group")!));
|
|
|
|
const title_data = buddy_data.get_title_data(user_ids_string, is_group);
|
|
|
|
// Since anything inside `#left_sidebar_scroll_container` can be replaced, it is our target node here.
|
|
function get_target_node(): HTMLElement {
|
|
return document.querySelector("#left_sidebar_scroll_container")!;
|
|
}
|
|
|
|
// Whole list is just replaced, so we need to check for that.
|
|
function check_reference_removed(
|
|
mutation: MutationRecord,
|
|
instance: tippy.Instance,
|
|
): boolean {
|
|
return Array.prototype.includes.call(
|
|
mutation.removedNodes,
|
|
$(instance.reference).parents(".dm-list")[0],
|
|
);
|
|
}
|
|
|
|
const check_subtree = true;
|
|
do_render_buddy_list_tooltip(
|
|
$elem,
|
|
title_data,
|
|
get_target_node,
|
|
check_reference_removed,
|
|
check_subtree,
|
|
);
|
|
|
|
/*
|
|
The following implements a little tooltip giving the name for status emoji
|
|
when hovering them in the left sidebar. This requires special logic, to avoid
|
|
conflicting with the main tooltip or showing duplicate tooltips.
|
|
*/
|
|
$(".dm-user-status .status-emoji-name").off("mouseenter").off("mouseleave");
|
|
$(".dm-user-status .status-emoji-name").on("mouseenter", () => {
|
|
const element: tippy.ReferenceElement = util.the($elem);
|
|
const instance = element._tippy;
|
|
if (instance?.state.isVisible) {
|
|
instance.destroy();
|
|
}
|
|
});
|
|
$(".dm-user-status .status-emoji-name").on("mouseleave", () => {
|
|
do_render_buddy_list_tooltip(
|
|
$elem,
|
|
title_data,
|
|
get_target_node,
|
|
check_reference_removed,
|
|
);
|
|
});
|
|
});
|
|
|
|
// Left sidebar channel rows
|
|
$("body").on("click", ".channel-new-topic-button", function (this: HTMLElement, e) {
|
|
e.stopPropagation();
|
|
const stream_id = Number.parseInt(this.dataset.streamId!, 10);
|
|
compose_actions.start({
|
|
message_type: "stream",
|
|
stream_id,
|
|
topic: "",
|
|
trigger: "clear topic button",
|
|
keep_composebox_empty: true,
|
|
});
|
|
});
|
|
|
|
// Recent conversations direct messages (Not displayed on small widths)
|
|
$("body").on(
|
|
"mouseenter",
|
|
".recent_topic_stream .pm_status_icon",
|
|
function (this: HTMLElement, e) {
|
|
e.stopPropagation();
|
|
const $elem = $(this);
|
|
const user_ids_string = $elem.attr("data-user-ids-string");
|
|
// Don't show tooltip for group direct messages.
|
|
if (!user_ids_string || user_ids_string.split(",").length !== 1) {
|
|
return;
|
|
}
|
|
const title_data = recent_view_ui.get_pm_tooltip_data(user_ids_string);
|
|
do_render_buddy_list_tooltip(
|
|
$elem,
|
|
title_data,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
undefined,
|
|
false,
|
|
);
|
|
},
|
|
);
|
|
|
|
// MISC
|
|
|
|
{
|
|
const sel = [
|
|
"#stream_filters",
|
|
"#left-sidebar-navigation-list",
|
|
"#buddy-list-users-matching-view",
|
|
].join(", ");
|
|
|
|
$(sel).on("click", "a", function (this: HTMLElement) {
|
|
this.blur();
|
|
});
|
|
}
|
|
|
|
$("body").on("click", ".logout_button", () => {
|
|
$("#logout_form").trigger("submit");
|
|
});
|
|
|
|
$("#settings_page").on("click", ".collapse-settings-button", () => {
|
|
settings_toggle.toggle_org_setting_collapse();
|
|
});
|
|
|
|
$("body").on("click", ".reload_link", () => {
|
|
window.location.reload();
|
|
});
|
|
|
|
// COMPOSE
|
|
|
|
$("body").on("click", ".empty_feed_compose_stream", (e) => {
|
|
compose_actions.start({
|
|
message_type: "stream",
|
|
trigger: "empty feed message",
|
|
});
|
|
e.preventDefault();
|
|
});
|
|
$("body").on("click", ".empty_feed_compose_private", (e) => {
|
|
compose_actions.start({
|
|
message_type: "private",
|
|
trigger: "empty feed message",
|
|
});
|
|
e.preventDefault();
|
|
});
|
|
|
|
$("body").on("click", "[data-overlay-trigger]", function () {
|
|
const target = $(this).attr("data-overlay-trigger")!;
|
|
browser_history.go_to_location(target);
|
|
});
|
|
|
|
$("body").on("click", ".formatting-control-scroller-button", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const $target = $(e.currentTarget);
|
|
const $button_container = $target.closest(".compose-scrolling-buttons-container");
|
|
const $button_bar = $button_container.find(".compose-scrollable-buttons");
|
|
|
|
const button_container_width = Number(
|
|
$button_container.attr("data-button-container-width"),
|
|
);
|
|
const button_bar_max_left_scroll = Number(
|
|
$button_container.attr("data-button-bar-max-left-scroll"),
|
|
);
|
|
const button_bar_scroll_left = Number($button_bar.scrollLeft());
|
|
|
|
// Buttons do not scale, so as to provide a generous click
|
|
// area while not overwhelming the visibility of buttons on
|
|
// narrower viewports at larger font sizes
|
|
const scroller_button_width_px = 48;
|
|
// We scroll 80% of the viewable area on each click...
|
|
const button_bar_scroll_percentage = 80 / 100;
|
|
// ...less the width of the two scroller buttons.
|
|
const button_adjusted_scroll_shift =
|
|
button_bar_scroll_percentage * (button_container_width - 2 * scroller_button_width_px);
|
|
let new_scroll_position = 0;
|
|
|
|
assert(typeof button_bar_scroll_left === "number");
|
|
|
|
if ($target.hasClass("formatting-scroller-forward")) {
|
|
new_scroll_position = button_bar_scroll_left + button_adjusted_scroll_shift;
|
|
// If we're less than the width of the scroller button from
|
|
// the end, just scroll the rest of the way forward
|
|
if (button_bar_max_left_scroll <= new_scroll_position - scroller_button_width_px) {
|
|
new_scroll_position = button_bar_max_left_scroll;
|
|
}
|
|
} else {
|
|
new_scroll_position = button_bar_scroll_left - button_adjusted_scroll_shift;
|
|
// If we're less than the width of the scroller button from
|
|
// the start, just scroll the rest of the way back
|
|
if (new_scroll_position <= scroller_button_width_px) {
|
|
new_scroll_position = 0;
|
|
}
|
|
}
|
|
|
|
$button_bar.scrollLeft(new_scroll_position);
|
|
});
|
|
|
|
function handle_compose_click(e: JQuery.ClickEvent): void {
|
|
const $target = $(e.target);
|
|
// Emoji clicks should be handled by their own click handler in emoji_picker.js
|
|
if ($target.is(".emoji_map, img.emoji, .drag, .compose_gif_icon")) {
|
|
return;
|
|
}
|
|
|
|
if ($target.is("#send_later i")) {
|
|
// Since the click for this is handled by tippyjs, we cannot add stopPropagation
|
|
// there without adding a special click event handler to show the popover,
|
|
// so it is better just do it here.
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
// The dropdown menu needs to process clicks to open and close.
|
|
if ($target.parents("#compose_select_recipient_widget_wrapper").length > 0) {
|
|
return;
|
|
}
|
|
|
|
// The mobile compose button has its own popover when clicked, so it already.
|
|
// hides other popovers.
|
|
if ($target.is(".compose_mobile_button, .compose_mobile_button *")) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$("body").on("click", "#compose-content", handle_compose_click);
|
|
|
|
$("body").on("click", "#compose_close", () => {
|
|
compose_actions.cancel();
|
|
});
|
|
|
|
$("body").on(
|
|
"focus",
|
|
"textarea#compose-textarea",
|
|
function (this: HTMLTextAreaElement, _event: JQuery.Event) {
|
|
compose_state.set_last_focused_compose_type_input(this);
|
|
},
|
|
);
|
|
|
|
// LEFT SIDEBAR
|
|
|
|
$("body").on("click", "#clear_search_topic_button", topic_list.clear_topic_search);
|
|
|
|
$(".streams_filter_icon").on("click", (e) => {
|
|
e.stopPropagation();
|
|
stream_list.toggle_filter_displayed(e);
|
|
});
|
|
|
|
$("body").on("click", "#direct-messages-section-header.zoom-out", (e) => {
|
|
if ($(e.target).closest("#show-all-direct-messages").length === 1) {
|
|
// Let the browser handle the "direct message feed" widget.
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const $left_sidebar_scrollbar = $(
|
|
"#left_sidebar_scroll_container .simplebar-content-wrapper",
|
|
);
|
|
const scroll_position = $left_sidebar_scrollbar.scrollTop();
|
|
|
|
if (stream_list.is_zoomed_in()) {
|
|
stream_list.zoom_out();
|
|
}
|
|
|
|
// This next bit of logic is a bit subtle; this header
|
|
// button scrolls to the top of the direct messages
|
|
// section is uncollapsed but out of view; otherwise, we
|
|
// toggle its collapsed state.
|
|
if (scroll_position === 0 || pm_list.is_private_messages_collapsed()) {
|
|
pm_list.toggle_private_messages_section();
|
|
}
|
|
$left_sidebar_scrollbar.scrollTop(0);
|
|
});
|
|
|
|
/* The DIRECT MESSAGES label's click behavior is complicated;
|
|
* only when zoomed in does it have a navigation effect, so we need
|
|
* this click handler rather than just a link. */
|
|
$("body").on("click", "#direct-messages-section-header.zoom-in", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
window.location.hash = "narrow/is/dm";
|
|
});
|
|
|
|
$("body").on("click", ".direct-messages-list-filter", (e) => {
|
|
// We don't want clicking on the filter to trigger the DM
|
|
// narrow defined on click for
|
|
// `#direct-messages-section-header.zoom-in`.
|
|
e.stopPropagation();
|
|
});
|
|
|
|
// disable the draggability for left-sidebar components
|
|
$("#stream_filters, #left-sidebar-navigation-list").on("dragstart", (e) => {
|
|
e.target.blur();
|
|
return false;
|
|
});
|
|
|
|
// Chrome focuses an element when dragging it which can be confusing when
|
|
// users involuntarily drag something and we show them the focus outline.
|
|
$("body").on("dragstart", "a", function (this: HTMLElement) {
|
|
this.blur();
|
|
});
|
|
|
|
// Don't focus links on middle click.
|
|
$("body").on("mouseup", "a", function (this: HTMLElement, e) {
|
|
if (e.button === 1) {
|
|
// middle click
|
|
this.blur();
|
|
}
|
|
});
|
|
|
|
// Don't focus links on context menu.
|
|
$("body").on("contextmenu", "a", function (this: HTMLElement) {
|
|
this.blur();
|
|
});
|
|
|
|
$("body").on("click", ".language_selection_widget button", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
settings_preferences.launch_default_language_setting_modal();
|
|
});
|
|
|
|
$("body").on("click", "#header-container .brand", (e) => {
|
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
hashchange.set_hash_to_home_view();
|
|
});
|
|
|
|
// MAIN CLICK HANDLER
|
|
|
|
$(document).on("click", (e) => {
|
|
if (e.button !== 0 || $(e.target).is(".drag")) {
|
|
// Firefox emits right click events on the document, but not on
|
|
// the child nodes, so the #compose stopPropagation doesn't get a
|
|
// chance to capture right clicks.
|
|
return;
|
|
}
|
|
|
|
if (compose_state.composing() && $(e.target).parents("#compose").length === 0) {
|
|
if (
|
|
$(e.target).closest("a").length > 0 ||
|
|
$(e.target).closest(".copy_codeblock").length > 0
|
|
) {
|
|
// Refocus compose message text box if one clicks an external
|
|
// link/url to view something else while composing a message.
|
|
// See issue #4331 for more details.
|
|
//
|
|
// We do the same when copying a code block, since the
|
|
// most likely next action within Zulip is to paste it
|
|
// into compose and modify it.
|
|
$("textarea#compose-textarea").trigger("focus");
|
|
return;
|
|
} else if (
|
|
!window.getSelection()?.toString() &&
|
|
// Clicking any input or text area should not close
|
|
// the compose box; this means using the sidebar
|
|
// filters or search widgets won't unnecessarily close
|
|
// compose.
|
|
$(e.target).closest("input").length === 0 &&
|
|
$(e.target).closest(".todo-widget label.checkbox").length === 0 &&
|
|
$(e.target).closest("textarea").length === 0 &&
|
|
$(e.target).closest("select").length === 0 &&
|
|
// Clicks inside an overlay, popover, custom
|
|
// modal, or backdrop of one of the above
|
|
// should not have any effect on the compose
|
|
// state.
|
|
$(e.target).closest(".overlay").length === 0 &&
|
|
$(e.target).closest(".micromodal").length === 0 &&
|
|
$(e.target).closest("[data-tippy-root]").length === 0 &&
|
|
$(e.target).closest(".typeahead").length === 0 &&
|
|
$(e.target).closest(".flatpickr-calendar").length === 0 &&
|
|
$(e.target).closest("body").length > 0
|
|
) {
|
|
// Unfocus our compose area if we click out of it. Don't let exits out
|
|
// of overlays or selecting text (for copy+paste) trigger cancelling.
|
|
// Check if the click is within the body to prevent extensions from
|
|
// interfering with the compose box.
|
|
compose_actions.cancel();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Workaround for Bootstrap issue #5900, which basically makes dropdowns
|
|
// unclickable on mobile devices.
|
|
// https://github.com/twitter/bootstrap/issues/5900
|
|
$(".dropdown-menu a").on("touchstart", (e) => {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
$(".settings-header.mobile .fa-chevron-left").on("click", () => {
|
|
settings_panel_menu.mobile_deactivate_section();
|
|
});
|
|
}
|