mirror of
https://github.com/zulip/zulip.git
synced 2025-10-28 10:33:54 +00:00
Reproducer: * Upload a file. * Close compose box before upload is finished. * Open compose box. Send message button is disabled without any error in this state. Fixed by resetting the send button state when compose box is closed.
634 lines
23 KiB
TypeScript
634 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 {realm} from "./state_data.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(.needs-empty-compose)").removeClass(
|
|
"disabled-on-hover",
|
|
);
|
|
// Reset send button status.
|
|
$(".message-send-controls").removeClass("disabled-message-send-controls");
|
|
}
|
|
|
|
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_compose_area_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_recipient.update_topic_displayed_text(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(.needs-empty-compose)").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() || !realm.realm_mandatory_topics) &&
|
|
compose_state.has_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_recipient.update_topic_displayed_text(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();
|
|
}
|