mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			533 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			533 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import autosize from "autosize";
 | 
						|
import $ from "jquery";
 | 
						|
 | 
						|
import * as fenced_code from "../shared/js/fenced_code";
 | 
						|
 | 
						|
import * as channel from "./channel";
 | 
						|
import * as common from "./common";
 | 
						|
import * as compose from "./compose";
 | 
						|
import * as compose_fade from "./compose_fade";
 | 
						|
import * as compose_pm_pill from "./compose_pm_pill";
 | 
						|
import * as compose_state from "./compose_state";
 | 
						|
import * as compose_ui from "./compose_ui";
 | 
						|
import * as drafts from "./drafts";
 | 
						|
import * as hash_util from "./hash_util";
 | 
						|
import {$t} from "./i18n";
 | 
						|
import * as message_lists from "./message_lists";
 | 
						|
import * as message_viewport from "./message_viewport";
 | 
						|
import * as narrow_state from "./narrow_state";
 | 
						|
import * as notifications from "./notifications";
 | 
						|
import {page_params} from "./page_params";
 | 
						|
import * as people from "./people";
 | 
						|
import * as reload_state from "./reload_state";
 | 
						|
import * as stream_bar from "./stream_bar";
 | 
						|
import * as stream_data from "./stream_data";
 | 
						|
import * as ui_util from "./ui_util";
 | 
						|
import * as unread_ops from "./unread_ops";
 | 
						|
 | 
						|
export function blur_compose_inputs() {
 | 
						|
    $(".message_comp").find("input, textarea, button, #private_message_recipient").trigger("blur");
 | 
						|
}
 | 
						|
 | 
						|
function hide_box() {
 | 
						|
    blur_compose_inputs();
 | 
						|
    $("#stream-message").hide();
 | 
						|
    $("#private-message").hide();
 | 
						|
    $(".new_message_textarea").css("min-height", "");
 | 
						|
    compose_fade.clear_compose();
 | 
						|
    $(".message_comp").hide();
 | 
						|
    $("#compose_controls").show();
 | 
						|
    compose.clear_preview_area();
 | 
						|
}
 | 
						|
 | 
						|
function get_focus_area(msg_type, opts) {
 | 
						|
    // Set focus to "Topic" when narrowed to a stream+topic and "New topic" button clicked.
 | 
						|
    if (msg_type === "stream" && opts.stream && !opts.topic) {
 | 
						|
        return "#stream_message_recipient_topic";
 | 
						|
    } else if (
 | 
						|
        (msg_type === "stream" && opts.stream) ||
 | 
						|
        (msg_type === "private" && opts.private_message_recipient)
 | 
						|
    ) {
 | 
						|
        if (opts.trigger === "new topic button") {
 | 
						|
            return "#stream_message_recipient_topic";
 | 
						|
        }
 | 
						|
        return "#compose-textarea";
 | 
						|
    }
 | 
						|
 | 
						|
    if (msg_type === "stream") {
 | 
						|
        return "#stream_message_recipient_stream";
 | 
						|
    }
 | 
						|
    return "#private_message_recipient";
 | 
						|
}
 | 
						|
 | 
						|
// Export for testing
 | 
						|
export const _get_focus_area = get_focus_area;
 | 
						|
 | 
						|
export function set_focus(msg_type, opts) {
 | 
						|
    const focus_area = get_focus_area(msg_type, opts);
 | 
						|
    if (focus_area === undefined) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (window.getSelection().toString() === "" || opts.trigger !== "message click") {
 | 
						|
        const elt = $(focus_area);
 | 
						|
        elt.trigger("focus").trigger("select");
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Show the compose box.
 | 
						|
function show_box(msg_type, opts) {
 | 
						|
    if (msg_type === "stream") {
 | 
						|
        $("#private-message").hide();
 | 
						|
        $("#stream-message").show();
 | 
						|
        $("#stream_toggle").addClass("active");
 | 
						|
        $("#private_message_toggle").removeClass("active");
 | 
						|
    } else {
 | 
						|
        $("#private-message").show();
 | 
						|
        $("#stream-message").hide();
 | 
						|
        $("#stream_toggle").removeClass("active");
 | 
						|
        $("#private_message_toggle").addClass("active");
 | 
						|
    }
 | 
						|
    $("#compose-send-status").removeClass(common.status_classes).hide();
 | 
						|
    $("#compose").css({visibility: "visible"});
 | 
						|
    // When changing this, edit the 42px in _maybe_autoscroll
 | 
						|
    $(".new_message_textarea").css("min-height", "3em");
 | 
						|
 | 
						|
    set_focus(msg_type, opts);
 | 
						|
}
 | 
						|
 | 
						|
export function clear_textarea() {
 | 
						|
    $("#compose").find("input[type=text], textarea").val("");
 | 
						|
}
 | 
						|
 | 
						|
function clear_box() {
 | 
						|
    compose.clear_invites();
 | 
						|
 | 
						|
    // TODO: Better encapsulate at-mention warnings.
 | 
						|
    compose.clear_all_everyone_warnings();
 | 
						|
    compose.clear_announce_warnings();
 | 
						|
    compose.clear_private_stream_alert();
 | 
						|
    compose.reset_user_acknowledged_all_everyone_flag();
 | 
						|
    compose.reset_user_acknowledged_announce_flag();
 | 
						|
 | 
						|
    clear_textarea();
 | 
						|
    $("#compose-textarea").removeData("draft-id");
 | 
						|
    compose_ui.autosize_textarea($("#compose-textarea"));
 | 
						|
    $("#compose-send-status").hide(0);
 | 
						|
}
 | 
						|
 | 
						|
export function autosize_message_content() {
 | 
						|
    autosize($("#compose-textarea"), {
 | 
						|
        callback() {
 | 
						|
            maybe_scroll_up_selected_message();
 | 
						|
        },
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function expand_compose_box() {
 | 
						|
    $("#compose_close").show();
 | 
						|
    $("#compose_controls").hide();
 | 
						|
    $(".message_comp").show();
 | 
						|
}
 | 
						|
 | 
						|
export function complete_starting_tasks(msg_type, opts) {
 | 
						|
    // 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();
 | 
						|
    ui_util.change_tab_to("#message_feed_container");
 | 
						|
    compose_fade.start_compose(msg_type);
 | 
						|
    stream_bar.decorate(opts.stream, $("#stream-message .message_header_stream"), true);
 | 
						|
    $(document).trigger(new $.Event("compose_started.zulip", opts));
 | 
						|
    update_placeholder_text();
 | 
						|
}
 | 
						|
 | 
						|
export function maybe_scroll_up_selected_message() {
 | 
						|
    // 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.offset().top + selected_row.height() - $("#compose").offset().top;
 | 
						|
    if (cover > 0) {
 | 
						|
        message_viewport.user_initiated_animate_scroll(cover + 20);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function fill_in_opts_from_current_narrowed_view(msg_type, opts) {
 | 
						|
    return {
 | 
						|
        message_type: msg_type,
 | 
						|
        stream: "",
 | 
						|
        topic: "",
 | 
						|
        private_message_recipient: "",
 | 
						|
        trigger: "unknown",
 | 
						|
 | 
						|
        // Set default parameters based on the current narrowed view.
 | 
						|
        ...narrow_state.set_compose_defaults(),
 | 
						|
 | 
						|
        ...opts,
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
function same_recipient_as_before(msg_type, opts) {
 | 
						|
    return (
 | 
						|
        compose_state.get_message_type() === msg_type &&
 | 
						|
        ((msg_type === "stream" &&
 | 
						|
            opts.stream === compose_state.stream_name() &&
 | 
						|
            opts.topic === compose_state.topic()) ||
 | 
						|
            (msg_type === "private" &&
 | 
						|
                opts.private_message_recipient === compose_state.private_message_recipient()))
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
export function update_placeholder_text() {
 | 
						|
    // Change compose placeholder text only if compose box is open.
 | 
						|
    if (!$("#compose-textarea").is(":visible")) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const opts = {
 | 
						|
        message_type: compose_state.get_message_type(),
 | 
						|
        stream: $("#stream_message_recipient_stream").val(),
 | 
						|
        topic: $("#stream_message_recipient_topic").val(),
 | 
						|
        private_message_recipient: compose_pm_pill.get_emails(),
 | 
						|
    };
 | 
						|
 | 
						|
    $("#compose-textarea").attr("placeholder", compose_ui.compute_placeholder_text(opts));
 | 
						|
}
 | 
						|
 | 
						|
export function start(msg_type, opts) {
 | 
						|
    autosize_message_content();
 | 
						|
 | 
						|
    if (reload_state.is_in_progress()) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    notifications.clear_compose_notifications();
 | 
						|
    expand_compose_box();
 | 
						|
 | 
						|
    opts = fill_in_opts_from_current_narrowed_view(msg_type, opts);
 | 
						|
    // 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
 | 
						|
    // PM recipient should be.
 | 
						|
    if (opts.trigger === "compose_hotkey" || opts.trigger === "new topic button") {
 | 
						|
        opts.topic = "";
 | 
						|
        opts.private_message_recipient = "";
 | 
						|
    }
 | 
						|
 | 
						|
    const subbed_streams = stream_data.subscribed_subs();
 | 
						|
    if (
 | 
						|
        subbed_streams.length === 1 &&
 | 
						|
        (opts.trigger === "new topic button" ||
 | 
						|
            (opts.trigger === "compose_hotkey" && msg_type === "stream"))
 | 
						|
    ) {
 | 
						|
        opts.stream = subbed_streams[0].name;
 | 
						|
    }
 | 
						|
 | 
						|
    if (compose_state.composing() && !same_recipient_as_before(msg_type, opts)) {
 | 
						|
        // Clear the compose box if the existing message is to a different recipient
 | 
						|
        clear_box();
 | 
						|
    }
 | 
						|
 | 
						|
    // We set the stream/topic/private_message_recipient
 | 
						|
    // unconditionally here, which assumes the caller will have passed
 | 
						|
    // '' or undefined for these values if they are not appropriate
 | 
						|
    // for this message.
 | 
						|
    //
 | 
						|
    // TODO: Move these into a conditional on message_type, using an
 | 
						|
    // explicit "clear" function for compose_state.
 | 
						|
    compose_state.stream_name(opts.stream);
 | 
						|
    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.replace(/,\s*/g, ", "));
 | 
						|
 | 
						|
    // If the user opens the compose box, types some text, and then clicks on a
 | 
						|
    // different stream/topic, we want to keep the text in the compose box
 | 
						|
    if (opts.content !== undefined) {
 | 
						|
        compose_state.message_content(opts.content);
 | 
						|
    }
 | 
						|
 | 
						|
    compose_state.set_message_type(msg_type);
 | 
						|
 | 
						|
    // Show either stream/topic fields or "You and" field.
 | 
						|
    show_box(msg_type, opts);
 | 
						|
 | 
						|
    complete_starting_tasks(msg_type, opts);
 | 
						|
}
 | 
						|
 | 
						|
export function cancel() {
 | 
						|
    $("#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();
 | 
						|
    $("#compose_close").hide();
 | 
						|
    clear_box();
 | 
						|
    notifications.clear_compose_notifications();
 | 
						|
    compose.abort_xhr();
 | 
						|
    compose.abort_video_callbacks(undefined);
 | 
						|
    compose_state.set_message_type(false);
 | 
						|
    compose_pm_pill.clear();
 | 
						|
    $(document).trigger("compose_canceled.zulip");
 | 
						|
}
 | 
						|
 | 
						|
export function respond_to_message(opts) {
 | 
						|
    let msg_type;
 | 
						|
    // Before initiating a reply to a message, if there's an
 | 
						|
    // in-progress composition, snapshot it.
 | 
						|
    drafts.update_draft();
 | 
						|
 | 
						|
    const message = message_lists.current.selected_message();
 | 
						|
 | 
						|
    if (message === undefined) {
 | 
						|
        // empty narrow implementation
 | 
						|
        if (
 | 
						|
            !narrow_state.narrowed_by_pm_reply() &&
 | 
						|
            !narrow_state.narrowed_by_stream_reply() &&
 | 
						|
            !narrow_state.narrowed_by_topic_reply()
 | 
						|
        ) {
 | 
						|
            compose.nonexistent_stream_reply_error();
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        const current_filter = narrow_state.filter();
 | 
						|
        const first_term = current_filter.operators()[0];
 | 
						|
        const first_operator = first_term.operator;
 | 
						|
        const first_operand = first_term.operand;
 | 
						|
 | 
						|
        if (first_operator === "stream" && !stream_data.is_subscribed(first_operand)) {
 | 
						|
            compose.nonexistent_stream_reply_error();
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // Set msg_type to stream by default in the case of an empty
 | 
						|
        // home view.
 | 
						|
        msg_type = "stream";
 | 
						|
        if (narrow_state.narrowed_by_pm_reply()) {
 | 
						|
            msg_type = "private";
 | 
						|
        }
 | 
						|
 | 
						|
        const new_opts = fill_in_opts_from_current_narrowed_view(msg_type, opts);
 | 
						|
        start(new_opts.message_type, new_opts);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (message_lists.current.can_mark_messages_read()) {
 | 
						|
        unread_ops.notify_server_message_read(message);
 | 
						|
    }
 | 
						|
 | 
						|
    // Important note: A reply_type of 'personal' is for the R hotkey
 | 
						|
    // (replying to a message's sender with a private message).  All
 | 
						|
    // other replies can just copy message.type.
 | 
						|
    if (opts.reply_type === "personal" || message.type === "private") {
 | 
						|
        msg_type = "private";
 | 
						|
    } else {
 | 
						|
        msg_type = message.type;
 | 
						|
    }
 | 
						|
 | 
						|
    let stream = "";
 | 
						|
    let topic = "";
 | 
						|
    let pm_recipient = "";
 | 
						|
    if (msg_type === "stream") {
 | 
						|
        stream = message.stream;
 | 
						|
        topic = message.topic;
 | 
						|
    } else {
 | 
						|
        pm_recipient = message.reply_to;
 | 
						|
        if (opts.reply_type === "personal") {
 | 
						|
            // reply_to for private messages is everyone involved, so for
 | 
						|
            // personals replies we need to set the private message
 | 
						|
            // recipient to just the sender
 | 
						|
            pm_recipient = people.get_by_user_id(message.sender_id).email;
 | 
						|
        } else {
 | 
						|
            pm_recipient = people.pm_reply_to(message);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    start(msg_type, {
 | 
						|
        stream,
 | 
						|
        topic,
 | 
						|
        private_message_recipient: pm_recipient,
 | 
						|
        trigger: opts.trigger,
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function reply_with_mention(opts) {
 | 
						|
    respond_to_message(opts);
 | 
						|
    const message = message_lists.current.selected_message();
 | 
						|
    const mention = people.get_mention_syntax(message.sender_full_name, message.sender_id);
 | 
						|
    compose_ui.insert_syntax_and_focus(mention);
 | 
						|
}
 | 
						|
 | 
						|
export function on_topic_narrow() {
 | 
						|
    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()) {
 | 
						|
        // If we changed streams, then we only leave the
 | 
						|
        // compose box open if there is content.
 | 
						|
        if (compose_state.has_message_content()) {
 | 
						|
            compose_fade.update_message_list();
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // Otherwise, avoid a mix.
 | 
						|
        cancel();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (compose_state.topic() && compose_state.has_message_content()) {
 | 
						|
        // If the user has written something to a different topic,
 | 
						|
        // they probably want that content, so leave compose open.
 | 
						|
        //
 | 
						|
        // This effectively uses the heuristic of whether there is
 | 
						|
        // content in compose 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_fade.set_focused_recipient("stream");
 | 
						|
    compose_fade.update_message_list();
 | 
						|
    $("#compose-textarea").trigger("focus").trigger("select");
 | 
						|
}
 | 
						|
 | 
						|
export function quote_and_reply(opts) {
 | 
						|
    const textarea = $("#compose-textarea");
 | 
						|
    const message_id = message_lists.current.selected_id();
 | 
						|
    const message = message_lists.current.selected_message();
 | 
						|
 | 
						|
    if (compose_state.has_message_content()) {
 | 
						|
        // The user already started typing a message,
 | 
						|
        // so we won't re-open the compose box.
 | 
						|
        // (If you did re-open the compose box, you
 | 
						|
        // are prone to glitches where you select the
 | 
						|
        // text, plus it's a complicated codepath that
 | 
						|
        // can have other unintended consequences.)
 | 
						|
        //
 | 
						|
        // Note also that we always put the quoted text
 | 
						|
        // above the current text, which explains us
 | 
						|
        // moving the caret below.  I think this is what
 | 
						|
        // most users will want, and it's consistent with
 | 
						|
        // the behavior we had on FF before this change
 | 
						|
        // (which may have been an accident of
 | 
						|
        // implementation).  If we change this decision,
 | 
						|
        // we'll need to make `insert_syntax_and_focus`
 | 
						|
        // smarter about newlines.
 | 
						|
        textarea.caret(0);
 | 
						|
    } else {
 | 
						|
        respond_to_message(opts);
 | 
						|
    }
 | 
						|
 | 
						|
    compose_ui.insert_syntax_and_focus("[Quoting…]\n", textarea);
 | 
						|
 | 
						|
    function replace_content(message) {
 | 
						|
        // Final message looks like:
 | 
						|
        //     @_**Iago|5** [said](link to message):
 | 
						|
        //     ```quote
 | 
						|
        //     message content
 | 
						|
        //     ```
 | 
						|
        let content = $t(
 | 
						|
            {defaultMessage: "{username} [said]({link_to_message}):"},
 | 
						|
            {
 | 
						|
                username: `@_**${message.sender_full_name}|${message.sender_id}**`,
 | 
						|
                link_to_message: `${hash_util.by_conversation_and_time_uri(message)}`,
 | 
						|
            },
 | 
						|
        );
 | 
						|
        content += "\n";
 | 
						|
        const fence = fenced_code.get_unused_fence(message.raw_content);
 | 
						|
        content += `${fence}quote\n${message.raw_content}\n${fence}`;
 | 
						|
        compose_ui.replace_syntax("[Quoting…]", content, textarea);
 | 
						|
        compose_ui.autosize_textarea($("#compose-textarea"));
 | 
						|
    }
 | 
						|
 | 
						|
    if (message && message.raw_content) {
 | 
						|
        replace_content(message);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    channel.get({
 | 
						|
        url: "/json/messages/" + message_id,
 | 
						|
        idempotent: true,
 | 
						|
        success(data) {
 | 
						|
            message.raw_content = data.raw_content;
 | 
						|
            replace_content(message);
 | 
						|
        },
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function on_narrow(opts) {
 | 
						|
    // We use force_close when jumping between PM 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_message_content()) {
 | 
						|
        compose_fade.update_message_list();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (narrow_state.narrowed_by_pm_reply()) {
 | 
						|
        opts = fill_in_opts_from_current_narrowed_view("private", opts);
 | 
						|
        // Do not open compose box if triggered by search and invalid recipient
 | 
						|
        // is present.
 | 
						|
        if (opts.trigger === "search" && !opts.private_message_recipient) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        start("private");
 | 
						|
        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();
 | 
						|
}
 |