mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	Defer saved snippets dropdown creation until the compose box is opened for better performance. Fixes #31831.
		
			
				
	
	
		
			626 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			626 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/* Module primarily for opening/closing the compose box. */
 | 
						|
 | 
						|
import autosize from "autosize";
 | 
						|
import $ from "jquery";
 | 
						|
 | 
						|
import * as blueslip from "./blueslip.ts";
 | 
						|
import * as compose_banner from "./compose_banner.ts";
 | 
						|
import * as compose_fade from "./compose_fade.ts";
 | 
						|
import * as compose_notifications from "./compose_notifications.ts";
 | 
						|
import * as compose_pm_pill from "./compose_pm_pill.ts";
 | 
						|
import * as compose_recipient from "./compose_recipient.ts";
 | 
						|
import * as compose_state from "./compose_state.ts";
 | 
						|
import * as compose_ui from "./compose_ui.ts";
 | 
						|
import type {ComposeTriggeredOptions} from "./compose_ui.ts";
 | 
						|
import * as compose_validate from "./compose_validate.ts";
 | 
						|
import * as drafts from "./drafts.ts";
 | 
						|
import * as message_lists from "./message_lists.ts";
 | 
						|
import type {Message} from "./message_store.ts";
 | 
						|
import * as message_util from "./message_util.ts";
 | 
						|
import * as message_viewport from "./message_viewport.ts";
 | 
						|
import * as narrow_state from "./narrow_state.ts";
 | 
						|
import {page_params} from "./page_params.ts";
 | 
						|
import * as people from "./people.ts";
 | 
						|
import * as popovers from "./popovers.ts";
 | 
						|
import * as reload_state from "./reload_state.ts";
 | 
						|
import * as resize from "./resize.ts";
 | 
						|
import * as saved_snippets_ui from "./saved_snippets_ui.ts";
 | 
						|
import * as spectators from "./spectators.ts";
 | 
						|
import * as stream_data from "./stream_data.ts";
 | 
						|
 | 
						|
// Opts sent to `compose_actions.start`.
 | 
						|
type ComposeActionsStartOpts = {
 | 
						|
    message_type: "private" | "stream";
 | 
						|
    force_close?: boolean;
 | 
						|
    trigger?: string;
 | 
						|
    private_message_recipient?: string;
 | 
						|
    message?: Message | undefined;
 | 
						|
    stream_id?: number | undefined;
 | 
						|
    topic?: string;
 | 
						|
    content?: string;
 | 
						|
    draft_id?: string;
 | 
						|
    skip_scrolling_selected_message?: boolean;
 | 
						|
    is_reply?: boolean;
 | 
						|
    keep_composebox_empty?: boolean | undefined;
 | 
						|
};
 | 
						|
 | 
						|
// An iteration on `ComposeActionsStartOpts` that enforces that
 | 
						|
// some values are present.
 | 
						|
type ComposeActionsOpts = ComposeActionsStartOpts & {
 | 
						|
    topic: string;
 | 
						|
    private_message_recipient: string;
 | 
						|
    trigger: string;
 | 
						|
};
 | 
						|
 | 
						|
type ComposeHook = () => void;
 | 
						|
 | 
						|
const compose_clear_box_hooks: ComposeHook[] = [];
 | 
						|
const compose_cancel_hooks: ComposeHook[] = [];
 | 
						|
 | 
						|
export function register_compose_box_clear_hook(hook: ComposeHook): void {
 | 
						|
    compose_clear_box_hooks.push(hook);
 | 
						|
}
 | 
						|
 | 
						|
export function register_compose_cancel_hook(hook: ComposeHook): void {
 | 
						|
    compose_cancel_hooks.push(hook);
 | 
						|
}
 | 
						|
 | 
						|
function call_hooks(hooks: ComposeHook[]): void {
 | 
						|
    for (const f of hooks) {
 | 
						|
        f();
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export let blur_compose_inputs = (): void => {
 | 
						|
    $(".message_comp").find("input, textarea, button, #private_message_recipient").trigger("blur");
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_blur_compose_inputs(value: typeof blur_compose_inputs): void {
 | 
						|
    blur_compose_inputs = value;
 | 
						|
}
 | 
						|
 | 
						|
function hide_box(): void {
 | 
						|
    // This is the main hook for saving drafts when closing the compose box.
 | 
						|
    drafts.update_draft();
 | 
						|
    blur_compose_inputs();
 | 
						|
    $("#compose_recipient_box").hide();
 | 
						|
    $("#compose-direct-recipient").hide();
 | 
						|
    $(".new_message_textarea").css("min-height", "");
 | 
						|
    compose_fade.clear_compose();
 | 
						|
    $(".message_comp").hide();
 | 
						|
    $("#compose_controls").show();
 | 
						|
}
 | 
						|
 | 
						|
function show_compose_box(opts: ComposeActionsOpts): void {
 | 
						|
    let opts_by_message_type: ComposeTriggeredOptions;
 | 
						|
    if (opts.message_type === "private") {
 | 
						|
        opts_by_message_type = {
 | 
						|
            trigger: opts.trigger,
 | 
						|
            message_type: "private",
 | 
						|
            private_message_recipient: opts.private_message_recipient,
 | 
						|
        };
 | 
						|
    } else {
 | 
						|
        opts_by_message_type = {
 | 
						|
            trigger: opts.trigger,
 | 
						|
            message_type: "stream",
 | 
						|
            stream_id: opts.stream_id,
 | 
						|
            topic: opts.topic,
 | 
						|
        };
 | 
						|
    }
 | 
						|
    compose_recipient.update_compose_for_message_type(opts_by_message_type);
 | 
						|
    $("#compose").css({visibility: "visible"});
 | 
						|
    // When changing this, edit the 42px in _maybe_autoscroll
 | 
						|
    $(".new_message_textarea").css("min-height", "3em");
 | 
						|
    compose_ui.set_focus(opts_by_message_type);
 | 
						|
}
 | 
						|
 | 
						|
export let clear_textarea = (): void => {
 | 
						|
    $("#compose").find("input[type=text], textarea").val("");
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_clear_textarea(value: typeof clear_textarea): void {
 | 
						|
    clear_textarea = value;
 | 
						|
}
 | 
						|
 | 
						|
function clear_box(): void {
 | 
						|
    call_hooks(compose_clear_box_hooks);
 | 
						|
 | 
						|
    // TODO: Better encapsulate at-mention warnings.
 | 
						|
    compose_validate.clear_topic_resolved_warning();
 | 
						|
    compose_validate.clear_stream_wildcard_warnings($("#compose_banners"));
 | 
						|
    compose_validate.clear_guest_in_dm_recipient_warning();
 | 
						|
    compose_validate.set_user_acknowledged_stream_wildcard_flag(false);
 | 
						|
 | 
						|
    compose_state.set_recipient_edited_manually(false);
 | 
						|
    compose_state.set_is_content_unedited_restored_draft(false);
 | 
						|
    clear_textarea();
 | 
						|
    compose_validate.check_overflow_text($("#send_message_form"));
 | 
						|
    drafts.set_compose_draft_id(undefined);
 | 
						|
    $("textarea#compose-textarea").toggleClass("invalid", false);
 | 
						|
    compose_ui.autosize_textarea($("textarea#compose-textarea"));
 | 
						|
    compose_banner.clear_errors();
 | 
						|
    compose_banner.clear_warnings();
 | 
						|
    compose_banner.clear_uploads();
 | 
						|
    $(".compose_control_button_container:has(.add-poll)").removeClass("disabled-on-hover");
 | 
						|
}
 | 
						|
 | 
						|
let autosize_callback_opts: ComposeActionsStartOpts;
 | 
						|
export let autosize_message_content = (opts: ComposeActionsStartOpts): void => {
 | 
						|
    if (!compose_ui.is_expanded()) {
 | 
						|
        autosize_callback_opts = opts;
 | 
						|
        let has_resized_once = false;
 | 
						|
        $("textarea#compose-textarea")
 | 
						|
            .off("autosize:resized")
 | 
						|
            .on("autosize:resized", (e) => {
 | 
						|
                if (!has_resized_once) {
 | 
						|
                    has_resized_once = true;
 | 
						|
                    maybe_scroll_up_selected_message(autosize_callback_opts);
 | 
						|
                }
 | 
						|
                const height = $(e.currentTarget).height()!;
 | 
						|
                const max_height = Number.parseFloat($(e.currentTarget).css("max-height"));
 | 
						|
                // We add 5px to account for minor differences in height detected in Chrome.
 | 
						|
                if (height + 5 >= max_height) {
 | 
						|
                    $("#compose").addClass("automatically-expanded");
 | 
						|
                } else {
 | 
						|
                    $("#compose").removeClass("automatically-expanded");
 | 
						|
                }
 | 
						|
            });
 | 
						|
        autosize($("textarea#compose-textarea"));
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_autosize_message_content(value: typeof autosize_message_content): void {
 | 
						|
    autosize_message_content = value;
 | 
						|
}
 | 
						|
 | 
						|
export let expand_compose_box = (): void => {
 | 
						|
    $("#compose_close").attr("data-tooltip-template-id", "compose_close_tooltip_template");
 | 
						|
    $("#compose_controls").hide();
 | 
						|
    $(".message_comp").show();
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_expand_compose_box(value: typeof expand_compose_box): void {
 | 
						|
    expand_compose_box = value;
 | 
						|
}
 | 
						|
 | 
						|
export let complete_starting_tasks = (opts: ComposeActionsOpts): void => {
 | 
						|
    // This is sort of a kitchen sink function, and it's called only
 | 
						|
    // by compose.start() for now.  Having this as a separate function
 | 
						|
    // makes testing a bit easier.
 | 
						|
 | 
						|
    maybe_scroll_up_selected_message(opts);
 | 
						|
    compose_fade.start_compose(opts.message_type);
 | 
						|
    $(document).trigger(new $.Event("compose_started.zulip", opts));
 | 
						|
    compose_recipient.update_placeholder_text();
 | 
						|
    compose_recipient.update_narrow_to_recipient_visibility();
 | 
						|
    // We explicitly call this function here apart from compose_setup.js
 | 
						|
    // as this helps to show banner when responding in an interleaved view.
 | 
						|
    // While responding, the compose box opens before fading resulting in
 | 
						|
    // the function call in compose_setup.js not displaying banner.
 | 
						|
    if (!narrow_state.narrowed_by_reply()) {
 | 
						|
        compose_notifications.maybe_show_one_time_interleaved_view_messages_fading_banner();
 | 
						|
    }
 | 
						|
    compose_ui.maybe_show_scrolling_formatting_buttons("#message-formatting-controls-container");
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_complete_starting_tasks(value: typeof complete_starting_tasks): void {
 | 
						|
    complete_starting_tasks = value;
 | 
						|
}
 | 
						|
 | 
						|
export function maybe_scroll_up_selected_message(opts: ComposeActionsStartOpts): void {
 | 
						|
    if (opts.skip_scrolling_selected_message) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (message_lists.current === undefined) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If the compose box is obscuring the currently selected message,
 | 
						|
    // scroll up until the message is no longer occluded.
 | 
						|
    if (message_lists.current.selected_id() === -1) {
 | 
						|
        // If there's no selected message, there's no need to
 | 
						|
        // scroll the compose box to avoid it.
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    const $selected_row = message_lists.current.selected_row();
 | 
						|
 | 
						|
    if ($selected_row.height()! > message_viewport.height() - 100) {
 | 
						|
        // For very tall messages whose height is close to the entire
 | 
						|
        // height of the viewport, don't auto-scroll the viewport to
 | 
						|
        // the end of the message (since that makes it feel annoying
 | 
						|
        // to work with very tall messages).  See #8941 for details.
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const cover =
 | 
						|
        $selected_row.get_offset_to_window().bottom - $("#compose").get_offset_to_window().top;
 | 
						|
    if (cover > 0) {
 | 
						|
        message_viewport.user_initiated_animate_scroll(cover + 20);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function fill_in_opts_from_current_narrowed_view(
 | 
						|
    opts: ComposeActionsStartOpts,
 | 
						|
): ComposeActionsOpts {
 | 
						|
    return {
 | 
						|
        stream_id: undefined,
 | 
						|
        topic: "",
 | 
						|
        private_message_recipient: "",
 | 
						|
        trigger: "unknown",
 | 
						|
 | 
						|
        // Set default parameters based on the current narrowed view.
 | 
						|
        ...narrow_state.set_compose_defaults(),
 | 
						|
 | 
						|
        // Set parameters based on provided opts, overwriting
 | 
						|
        // those set based on current narrowed view, if necessary.
 | 
						|
        ...opts,
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
function same_recipient_as_before(opts: ComposeActionsOpts): boolean {
 | 
						|
    return (
 | 
						|
        compose_state.get_message_type() === opts.message_type &&
 | 
						|
        ((opts.message_type === "stream" &&
 | 
						|
            opts.stream_id === compose_state.stream_id() &&
 | 
						|
            opts.topic === compose_state.topic()) ||
 | 
						|
            (opts.message_type === "private" &&
 | 
						|
                opts.private_message_recipient === compose_state.private_message_recipient()))
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
export let start = (raw_opts: ComposeActionsStartOpts): void => {
 | 
						|
    if (page_params.is_spectator) {
 | 
						|
        spectators.login_to_access();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!raw_opts.message_type) {
 | 
						|
        // We prefer callers to be explicit about the message type, but
 | 
						|
        // we if we don't know, we open a stream compose box by default,
 | 
						|
        // which opens stream selection dropdown.
 | 
						|
        // Also, message_type is used to check if compose box is open in compose_state.composing().
 | 
						|
        raw_opts.message_type = "stream";
 | 
						|
        blueslip.warn("Empty message type in compose.start");
 | 
						|
    }
 | 
						|
 | 
						|
    popovers.hide_all();
 | 
						|
    autosize_message_content(raw_opts);
 | 
						|
 | 
						|
    if (reload_state.is_in_progress()) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    compose_banner.clear_message_sent_banners();
 | 
						|
    expand_compose_box();
 | 
						|
 | 
						|
    const opts = fill_in_opts_from_current_narrowed_view(raw_opts);
 | 
						|
    const is_clear_topic_button_triggered = opts.trigger === "clear topic button";
 | 
						|
 | 
						|
    // If we are invoked by a compose hotkey (c or x) or new topic
 | 
						|
    // button, do not assume that we know what the message's topic or
 | 
						|
    // direct message recipient should be.
 | 
						|
    if (
 | 
						|
        opts.trigger === "compose_hotkey" ||
 | 
						|
        is_clear_topic_button_triggered ||
 | 
						|
        opts.trigger === "new direct message"
 | 
						|
    ) {
 | 
						|
        opts.topic = "";
 | 
						|
        opts.private_message_recipient = "";
 | 
						|
    }
 | 
						|
 | 
						|
    const subbed_streams = stream_data.subscribed_subs();
 | 
						|
    if (
 | 
						|
        subbed_streams.length === 1 &&
 | 
						|
        subbed_streams[0] !== undefined &&
 | 
						|
        (is_clear_topic_button_triggered ||
 | 
						|
            (opts.trigger === "compose_hotkey" && opts.message_type === "stream"))
 | 
						|
    ) {
 | 
						|
        opts.stream_id = subbed_streams[0].stream_id;
 | 
						|
    }
 | 
						|
 | 
						|
    // If we go to a different narrow or there is new message content to populate the compose box
 | 
						|
    // with (like from a draft), save any existing content as a draft, and clear the compose box.
 | 
						|
    if (
 | 
						|
        compose_state.composing() &&
 | 
						|
        (!same_recipient_as_before(opts) || opts.content !== undefined)
 | 
						|
    ) {
 | 
						|
        drafts.update_draft();
 | 
						|
        clear_box();
 | 
						|
    }
 | 
						|
 | 
						|
    if (opts.message_type === "private") {
 | 
						|
        compose_state.set_compose_recipient_id(compose_state.DIRECT_MESSAGE_ID);
 | 
						|
        compose_recipient.on_compose_select_recipient_update();
 | 
						|
    } else if (opts.stream_id && opts.topic) {
 | 
						|
        compose_state.set_stream_id(opts.stream_id);
 | 
						|
        compose_recipient.on_compose_select_recipient_update();
 | 
						|
    } else if (opts.stream_id) {
 | 
						|
        const stream = stream_data.get_sub_by_id(opts.stream_id);
 | 
						|
        if (stream && stream_data.can_post_messages_in_stream(stream)) {
 | 
						|
            compose_state.set_stream_id(opts.stream_id);
 | 
						|
            compose_recipient.on_compose_select_recipient_update();
 | 
						|
        } else {
 | 
						|
            opts.stream_id = undefined;
 | 
						|
            compose_state.set_stream_id("");
 | 
						|
            opts.topic = "";
 | 
						|
            compose_recipient.toggle_compose_recipient_dropdown();
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        // Open stream selection dropdown if no stream is selected.
 | 
						|
        compose_state.set_stream_id("");
 | 
						|
        compose_recipient.toggle_compose_recipient_dropdown();
 | 
						|
    }
 | 
						|
    compose_state.topic(opts.topic);
 | 
						|
 | 
						|
    // Set the recipients with a space after each comma, so it looks nice.
 | 
						|
    compose_state.private_message_recipient(
 | 
						|
        opts.private_message_recipient.replaceAll(/,\s*/g, ", "),
 | 
						|
    );
 | 
						|
 | 
						|
    // If we're not explicitly opening a different draft, restore the last
 | 
						|
    // saved draft (if it exists).
 | 
						|
    let restoring_last_draft = false;
 | 
						|
    if (
 | 
						|
        compose_state.can_restore_drafts() &&
 | 
						|
        !opts.content &&
 | 
						|
        opts.draft_id === undefined &&
 | 
						|
        compose_state.message_content().length === 0 &&
 | 
						|
        !opts.keep_composebox_empty
 | 
						|
    ) {
 | 
						|
        const possible_last_draft = drafts.get_last_restorable_draft_based_on_compose_state();
 | 
						|
        if (possible_last_draft !== undefined) {
 | 
						|
            restoring_last_draft = true;
 | 
						|
            opts.draft_id = possible_last_draft.id;
 | 
						|
            // Add a space at the end so that if the user starts typing
 | 
						|
            // as soon as the composebox opens, they have a bit of separation
 | 
						|
            // from the restored draft. This won't result in a long trail of
 | 
						|
            // spaces if a draft is restored several times, because we trim
 | 
						|
            // whitespace whenever we save drafts.
 | 
						|
            opts.content = possible_last_draft.content + " ";
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (opts.content !== undefined) {
 | 
						|
        const replace_all_without_undo_support = true;
 | 
						|
        compose_ui.insert_and_scroll_into_view(
 | 
						|
            opts.content,
 | 
						|
            $("textarea#compose-textarea"),
 | 
						|
            false,
 | 
						|
            replace_all_without_undo_support,
 | 
						|
        );
 | 
						|
        $(".compose_control_button_container:has(.add-poll)").addClass("disabled-on-hover");
 | 
						|
        // If we were provided with message content, we might need to
 | 
						|
        // display that it's too long.
 | 
						|
        compose_validate.check_overflow_text($("#send_message_form"));
 | 
						|
    }
 | 
						|
    // This has to happen after we insert the content, so that the next "input" event
 | 
						|
    // is from user input.
 | 
						|
    if (restoring_last_draft) {
 | 
						|
        compose_state.set_is_content_unedited_restored_draft(true);
 | 
						|
    }
 | 
						|
 | 
						|
    compose_state.set_message_type(opts.message_type);
 | 
						|
 | 
						|
    // Show either stream/topic fields or "You and" field.
 | 
						|
    show_compose_box(opts);
 | 
						|
 | 
						|
    if (opts.draft_id) {
 | 
						|
        drafts.set_compose_draft_id(opts.draft_id);
 | 
						|
    }
 | 
						|
 | 
						|
    // Show a warning if topic is resolved
 | 
						|
    compose_validate.warn_if_topic_resolved(true);
 | 
						|
    // Show a warning if dm recipient contains guest
 | 
						|
    compose_validate.warn_if_guest_in_dm_recipient();
 | 
						|
    // Show a warning if the user is in a search narrow when replying to a message
 | 
						|
    if (opts.is_reply) {
 | 
						|
        compose_validate.warn_if_in_search_view();
 | 
						|
    }
 | 
						|
 | 
						|
    compose_recipient.check_posting_policy_for_compose_box();
 | 
						|
    drafts.update_compose_draft_count();
 | 
						|
 | 
						|
    // Reset the `max-height` property of `compose-textarea` so that the
 | 
						|
    // compose-box do not cover the last messages of the current stream
 | 
						|
    // while writing a long message.
 | 
						|
    resize.reset_compose_message_max_height();
 | 
						|
 | 
						|
    complete_starting_tasks(opts);
 | 
						|
 | 
						|
    saved_snippets_ui.setup_saved_snippets_dropdown_widget_if_needed();
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_start(value: typeof start): void {
 | 
						|
    start = value;
 | 
						|
}
 | 
						|
 | 
						|
export let cancel = (): void => {
 | 
						|
    // As user closes the compose box, restore the compose box max height
 | 
						|
    if (compose_ui.is_expanded()) {
 | 
						|
        compose_ui.make_compose_box_original_size();
 | 
						|
    }
 | 
						|
 | 
						|
    $("textarea#compose-textarea").height(40 + "px");
 | 
						|
 | 
						|
    if (page_params.narrow !== undefined) {
 | 
						|
        // Never close the compose box in narrow embedded windows, but
 | 
						|
        // at least clear the topic and unfade.
 | 
						|
        compose_fade.clear_compose();
 | 
						|
        if (page_params.narrow_topic !== undefined) {
 | 
						|
            compose_state.topic(page_params.narrow_topic);
 | 
						|
        } else {
 | 
						|
            compose_state.topic("");
 | 
						|
        }
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    hide_box();
 | 
						|
    clear_box();
 | 
						|
    compose_banner.clear_message_sent_banners();
 | 
						|
    compose_banner.clear_non_interleaved_view_messages_fading_banner();
 | 
						|
    compose_banner.clear_interleaved_view_messages_fading_banner();
 | 
						|
    call_hooks(compose_cancel_hooks);
 | 
						|
    compose_state.set_message_type(undefined);
 | 
						|
    compose_pm_pill.clear();
 | 
						|
    $(document).trigger("compose_canceled.zulip");
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_cancel(value: typeof cancel): void {
 | 
						|
    cancel = value;
 | 
						|
}
 | 
						|
 | 
						|
export function on_show_navigation_view(): void {
 | 
						|
    /* This function dictates the behavior of the compose box
 | 
						|
     * when navigating to a view, as opposed to a narrow. */
 | 
						|
 | 
						|
    // Leave the compose box closed if it was already closed.
 | 
						|
    if (!compose_state.composing()) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Leave the compose box open if there is content or if the recipient was edited.
 | 
						|
    if (compose_state.has_novel_message_content() || compose_state.is_recipient_edited_manually()) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Otherwise, close the compose box.
 | 
						|
    cancel();
 | 
						|
}
 | 
						|
 | 
						|
export let on_topic_narrow = (): void => {
 | 
						|
    if (!compose_state.composing()) {
 | 
						|
        // If our compose box is closed, then just
 | 
						|
        // leave it closed, assuming that the user is
 | 
						|
        // catching up on their feed and not actively
 | 
						|
        // composing.
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (compose_state.stream_name() !== narrow_state.stream_name()) {
 | 
						|
        // If we changed streams, then we only leave the
 | 
						|
        // compose box open if there is content or if the recipient was edited.
 | 
						|
        if (
 | 
						|
            compose_state.has_novel_message_content() ||
 | 
						|
            compose_state.is_recipient_edited_manually()
 | 
						|
        ) {
 | 
						|
            compose_fade.update_message_list();
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // Otherwise, avoid a mix.
 | 
						|
        cancel();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
        (compose_state.topic() && compose_state.has_novel_message_content()) ||
 | 
						|
        compose_state.is_recipient_edited_manually()
 | 
						|
    ) {
 | 
						|
        // If the user has written something to a different topic or edited it,
 | 
						|
        // they probably want that content, so leave compose open.
 | 
						|
        //
 | 
						|
        // This effectively uses the heuristic of whether there is
 | 
						|
        // content in compose or the topic was edited to determine whether
 | 
						|
        // the user had firmly decided to compose to the old topic or is
 | 
						|
        // just looking to reply to what they see.
 | 
						|
        compose_fade.update_message_list();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If we got this far, then the compose box has the correct stream
 | 
						|
    // filled in, and either compose is empty or no topic was set, so
 | 
						|
    // we should update the compose topic to match the new narrow.
 | 
						|
    // See #3300 for context--a couple users specifically asked for
 | 
						|
    // this convenience.
 | 
						|
    compose_state.topic(narrow_state.topic());
 | 
						|
    compose_validate.warn_if_topic_resolved(true);
 | 
						|
    compose_fade.set_focused_recipient("stream");
 | 
						|
    compose_fade.update_message_list();
 | 
						|
    drafts.update_compose_draft_count();
 | 
						|
    $("textarea#compose-textarea").trigger("focus");
 | 
						|
};
 | 
						|
 | 
						|
export function rewire_on_topic_narrow(value: typeof on_topic_narrow): void {
 | 
						|
    on_topic_narrow = value;
 | 
						|
}
 | 
						|
 | 
						|
// TODO/typescript: Fill this in when converting narrow.js to typescripot.
 | 
						|
type NarrowActivateOpts = {
 | 
						|
    trigger?: string;
 | 
						|
    force_close?: boolean;
 | 
						|
    private_message_recipient?: string;
 | 
						|
};
 | 
						|
 | 
						|
export function on_narrow(opts: NarrowActivateOpts): void {
 | 
						|
    // We use force_close when jumping between direct message narrows with
 | 
						|
    // the "p" key, so that we don't have an open compose box that makes
 | 
						|
    // it difficult to cycle quickly through unread messages.
 | 
						|
    if (opts.force_close) {
 | 
						|
        // This closes the compose box if it was already open, and it is
 | 
						|
        // basically a noop otherwise.
 | 
						|
        cancel();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (opts.trigger === "narrow_to_compose_target") {
 | 
						|
        compose_fade.update_message_list();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (narrow_state.narrowed_by_topic_reply()) {
 | 
						|
        on_topic_narrow();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (compose_state.has_novel_message_content() || compose_state.is_recipient_edited_manually()) {
 | 
						|
        compose_fade.update_message_list();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (narrow_state.narrowed_by_pm_reply()) {
 | 
						|
        opts = fill_in_opts_from_current_narrowed_view({
 | 
						|
            ...opts,
 | 
						|
            message_type: "private",
 | 
						|
        });
 | 
						|
        // Do not open compose box if an invalid recipient is present.
 | 
						|
        if (!opts.private_message_recipient) {
 | 
						|
            if (compose_state.composing()) {
 | 
						|
                cancel();
 | 
						|
            }
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        // Do not open compose box if sender is not allowed to send direct message.
 | 
						|
        const recipient_ids_string = people.emails_strings_to_user_ids_string(
 | 
						|
            opts.private_message_recipient,
 | 
						|
        );
 | 
						|
 | 
						|
        if (
 | 
						|
            recipient_ids_string &&
 | 
						|
            !message_util.user_can_send_direct_message(recipient_ids_string)
 | 
						|
        ) {
 | 
						|
            // If we are navigating between direct message conversation,
 | 
						|
            // we want the compose box to close for non-bot users.
 | 
						|
            if (compose_state.composing()) {
 | 
						|
                cancel();
 | 
						|
            }
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // Open the compose box, passing the option to skip attempting
 | 
						|
        // an animated adjustment to scroll position, which is useless
 | 
						|
        // because we are called before the narrowing process has set
 | 
						|
        // the view's scroll position. recenter_view is responsible
 | 
						|
        // for taking the open compose box into account when placing
 | 
						|
        // the selecting message.
 | 
						|
        start({
 | 
						|
            message_type: "private",
 | 
						|
            skip_scrolling_selected_message: true,
 | 
						|
        });
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If we got this far, then we assume the user is now in "reading"
 | 
						|
    // mode, so we close the compose box to make it easier to use navigation
 | 
						|
    // hotkeys and to provide more screen real estate for messages.
 | 
						|
    cancel();
 | 
						|
}
 |