2021-02-28 21:32:22 +01:00
|
|
|
import autosize from "autosize";
|
2021-03-11 05:43:45 +01:00
|
|
|
import $ from "jquery";
|
2022-09-26 21:01:43 +02:00
|
|
|
import _ from "lodash";
|
2021-02-28 21:32:22 +01:00
|
|
|
|
|
|
|
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";
|
2021-06-25 15:16:29 +02:00
|
|
|
import * as compose_validate from "./compose_validate";
|
2021-02-28 21:32:22 +01:00
|
|
|
import * as drafts from "./drafts";
|
|
|
|
import * as hash_util from "./hash_util";
|
2021-04-13 06:51:54 +02:00
|
|
|
import {$t} from "./i18n";
|
2021-03-30 02:21:21 +02:00
|
|
|
import * as message_lists from "./message_lists";
|
2021-02-28 21:32:22 +01:00
|
|
|
import * as message_viewport from "./message_viewport";
|
|
|
|
import * as narrow_state from "./narrow_state";
|
|
|
|
import * as notifications from "./notifications";
|
2021-03-25 22:35:45 +01:00
|
|
|
import {page_params} from "./page_params";
|
2021-02-28 21:32:22 +01:00
|
|
|
import * as people from "./people";
|
2021-06-10 14:18:46 +02:00
|
|
|
import * as recent_topics_ui from "./recent_topics_ui";
|
|
|
|
import * as recent_topics_util from "./recent_topics_util";
|
2021-02-28 21:32:22 +01:00
|
|
|
import * as reload_state from "./reload_state";
|
compose-box: Fix compose-box from covering last messages of stream.
While writing a long message in compose-box, the last few messages of
the current stream gets covered by the compose-box and it gets pretty
annoying sometimes trying to figure out a way to read the last message
of the stream while writing. Right now, the only way to get past this
is to resize `compose-textarea` by using the resize tool at the
bottom-right corner of the `compose-textarea`. But, that small resize
tool is not always readily visible to the user.
The proposed solution in this commit is to reset the `max-height`
property of `#compose-textarea` everytime `bottom_whitespace_height`
is resized such that the total height of `#compose` is always less
than or equal to the height of `bottom_whitespace_height`. Doing
this, the compose-box never covers the last message of the current
stream.
The only problem with this is that if the compose-box is closed at the
time of bottom-whitespace resize, we cannot find the
`compose_non_textarea_height` and so, we cannot reset the max-height
of `#compose-textarea`. To solve this, max-height of
`compose-textarea` is also reset everytime a new compose-box is opened
according to the value of `bottom_whitespace_height` at that time.
Thus, if the compose-box is already open at the time of
bottom-whitespace resize, the max-height of `#compose-textarea` will
also get reset at the same time, whereas, if the compose-box is closed
at the time of bottom-whitespace resize, the max-height of
`#compose-textarea` won't get reset at that time, but it will surely
get reset whenever the user will open the compose-box.
Tested on my Ubuntu Development Environment on Chrome and Firefox browsers.
Fixes: #16038.
2021-01-18 14:23:29 +01:00
|
|
|
import * as resize from "./resize";
|
2021-09-07 04:09:12 +02:00
|
|
|
import * as spectators from "./spectators";
|
2021-03-22 18:29:15 +01:00
|
|
|
import * as stream_bar from "./stream_bar";
|
2021-02-28 21:32:22 +01:00
|
|
|
import * as stream_data from "./stream_data";
|
|
|
|
import * as unread_ops from "./unread_ops";
|
2021-08-16 17:10:43 +02:00
|
|
|
import * as util from "./util";
|
2021-02-28 21:32:22 +01:00
|
|
|
|
|
|
|
export function blur_compose_inputs() {
|
2020-07-18 20:57:27 +02:00
|
|
|
$(".message_comp").find("input, textarea, button, #private_message_recipient").trigger("blur");
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
function hide_box() {
|
2021-05-12 21:49:05 +02:00
|
|
|
// This is the main hook for saving drafts when closing the compose box.
|
|
|
|
drafts.update_draft();
|
2021-02-28 21:32:22 +01:00
|
|
|
blur_compose_inputs();
|
2020-07-15 01:29:15 +02:00
|
|
|
$("#stream-message").hide();
|
|
|
|
$("#private-message").hide();
|
2017-04-14 16:26:00 +02:00
|
|
|
$(".new_message_textarea").css("min-height", "");
|
|
|
|
compose_fade.clear_compose();
|
2020-07-15 01:29:15 +02:00
|
|
|
$(".message_comp").hide();
|
2017-04-14 16:26:00 +02:00
|
|
|
$("#compose_controls").show();
|
|
|
|
}
|
|
|
|
|
|
|
|
function get_focus_area(msg_type, opts) {
|
|
|
|
// Set focus to "Topic" when narrowed to a stream+topic and "New topic" button clicked.
|
2020-07-15 01:29:15 +02:00
|
|
|
if (msg_type === "stream" && opts.stream && !opts.topic) {
|
|
|
|
return "#stream_message_recipient_topic";
|
2020-07-15 00:34:28 +02:00
|
|
|
} else if (
|
|
|
|
(msg_type === "stream" && opts.stream) ||
|
|
|
|
(msg_type === "private" && opts.private_message_recipient)
|
|
|
|
) {
|
2017-04-14 16:26:00 +02:00
|
|
|
if (opts.trigger === "new topic button") {
|
2020-07-15 01:29:15 +02:00
|
|
|
return "#stream_message_recipient_topic";
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
2020-07-15 01:29:15 +02:00
|
|
|
return "#compose-textarea";
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
if (msg_type === "stream") {
|
|
|
|
return "#stream_message_recipient_stream";
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
2020-07-15 01:29:15 +02:00
|
|
|
return "#private_message_recipient";
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
2021-02-28 21:32:22 +01:00
|
|
|
|
2017-04-14 16:26:00 +02:00
|
|
|
// Export for testing
|
2021-02-28 21:32:22 +01:00
|
|
|
export const _get_focus_area = get_focus_area;
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function set_focus(msg_type, opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const focus_area = get_focus_area(msg_type, opts);
|
2020-07-15 00:34:28 +02:00
|
|
|
if (window.getSelection().toString() === "" || opts.trigger !== "message click") {
|
2022-01-25 11:36:19 +01:00
|
|
|
const $elt = $(focus_area);
|
|
|
|
$elt.trigger("focus").trigger("select");
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2022-04-27 01:23:44 +02:00
|
|
|
function show_compose_box(msg_type, opts) {
|
2017-04-14 16:26:00 +02:00
|
|
|
if (msg_type === "stream") {
|
2020-07-15 01:29:15 +02:00
|
|
|
$("#private-message").hide();
|
|
|
|
$("#stream-message").show();
|
2017-04-14 16:26:00 +02:00
|
|
|
$("#stream_toggle").addClass("active");
|
|
|
|
$("#private_message_toggle").removeClass("active");
|
|
|
|
} else {
|
2020-07-15 01:29:15 +02:00
|
|
|
$("#private-message").show();
|
|
|
|
$("#stream-message").hide();
|
2017-04-14 16:26:00 +02:00
|
|
|
$("#stream_toggle").removeClass("active");
|
|
|
|
$("#private_message_toggle").addClass("active");
|
|
|
|
}
|
2017-11-26 19:58:36 +01:00
|
|
|
$("#compose-send-status").removeClass(common.status_classes).hide();
|
2020-07-15 01:29:15 +02:00
|
|
|
$("#compose").css({visibility: "visible"});
|
2019-02-05 22:12:28 +01:00
|
|
|
// When changing this, edit the 42px in _maybe_autoscroll
|
2017-04-14 16:26:00 +02:00
|
|
|
$(".new_message_textarea").css("min-height", "3em");
|
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
set_focus(msg_type, opts);
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function clear_textarea() {
|
2020-07-15 01:29:15 +02:00
|
|
|
$("#compose").find("input[type=text], textarea").val("");
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
function clear_box() {
|
|
|
|
compose.clear_invites();
|
|
|
|
|
|
|
|
// TODO: Better encapsulate at-mention warnings.
|
2021-07-27 21:48:11 +02:00
|
|
|
compose_validate.clear_topic_resolved_warning();
|
2021-06-25 15:16:29 +02:00
|
|
|
compose_validate.clear_all_everyone_warnings();
|
2017-10-14 15:18:21 +02:00
|
|
|
compose.clear_private_stream_alert();
|
2021-06-25 15:16:29 +02:00
|
|
|
compose_validate.set_user_acknowledged_all_everyone_flag(undefined);
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2022-08-23 11:41:09 +02:00
|
|
|
compose.clear_preview_area();
|
2021-02-28 21:32:22 +01:00
|
|
|
clear_textarea();
|
2021-07-10 19:33:45 +02:00
|
|
|
compose_validate.check_overflow_text();
|
2017-11-26 20:37:44 +01:00
|
|
|
$("#compose-textarea").removeData("draft-id");
|
2020-09-04 23:49:49 +02:00
|
|
|
compose_ui.autosize_textarea($("#compose-textarea"));
|
2017-11-26 19:58:36 +01:00
|
|
|
$("#compose-send-status").hide(0);
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function autosize_message_content() {
|
2021-05-17 13:15:11 +02:00
|
|
|
if (!compose_ui.is_full_size()) {
|
|
|
|
autosize($("#compose-textarea"), {
|
|
|
|
callback() {
|
|
|
|
maybe_scroll_up_selected_message();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function expand_compose_box() {
|
2017-04-14 16:26:00 +02:00
|
|
|
$("#compose_close").show();
|
|
|
|
$("#compose_controls").hide();
|
2020-07-15 01:29:15 +02:00
|
|
|
$(".message_comp").show();
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2021-08-16 17:10:43 +02:00
|
|
|
function composing_to_current_topic_narrow() {
|
|
|
|
return (
|
|
|
|
util.lower_same(compose_state.stream_name(), narrow_state.stream() || "") &&
|
|
|
|
util.lower_same(compose_state.topic(), narrow_state.topic() || "")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-09-26 21:01:43 +02:00
|
|
|
function composing_to_current_private_message_narrow() {
|
|
|
|
const compose_state_recipient = compose_state.private_message_recipient();
|
|
|
|
const narrow_state_recipient = narrow_state.pm_emails_string();
|
|
|
|
return (
|
|
|
|
compose_state_recipient &&
|
|
|
|
narrow_state_recipient &&
|
|
|
|
_.isEqual(
|
|
|
|
compose_state_recipient
|
|
|
|
.split(",")
|
|
|
|
.map((s) => s.trim())
|
|
|
|
.sort(),
|
|
|
|
narrow_state_recipient
|
|
|
|
.split(",")
|
|
|
|
.map((s) => s.trim())
|
|
|
|
.sort(),
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-08-16 17:10:43 +02:00
|
|
|
export function update_narrow_to_recipient_visibility() {
|
|
|
|
const message_type = compose_state.get_message_type();
|
|
|
|
if (message_type === "stream") {
|
|
|
|
const stream_name = compose_state.stream_name();
|
|
|
|
const stream_exists = Boolean(stream_data.get_stream_id(stream_name));
|
|
|
|
|
|
|
|
if (
|
|
|
|
stream_exists &&
|
|
|
|
!composing_to_current_topic_narrow() &&
|
2022-09-26 21:01:43 +02:00
|
|
|
compose_state.has_full_recipient()
|
|
|
|
) {
|
|
|
|
$(".narrow_to_compose_recipients").toggleClass("invisible", false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else if (message_type === "private") {
|
|
|
|
const recipients = compose_state.private_message_recipient();
|
|
|
|
if (
|
|
|
|
recipients &&
|
|
|
|
!composing_to_current_private_message_narrow() &&
|
|
|
|
compose_state.has_full_recipient()
|
2021-08-16 17:10:43 +02:00
|
|
|
) {
|
2022-09-26 21:01:43 +02:00
|
|
|
$(".narrow_to_compose_recipients").toggleClass("invisible", false);
|
2021-08-16 17:10:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2022-09-26 21:01:43 +02:00
|
|
|
$(".narrow_to_compose_recipients").toggleClass("invisible", true);
|
2021-08-16 17:10:43 +02:00
|
|
|
}
|
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function complete_starting_tasks(msg_type, opts) {
|
2017-04-14 16:26:00 +02:00
|
|
|
// 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.
|
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
maybe_scroll_up_selected_message();
|
2017-04-14 16:26:00 +02:00
|
|
|
compose_fade.start_compose(msg_type);
|
2021-03-22 18:29:15 +01:00
|
|
|
stream_bar.decorate(opts.stream, $("#stream-message .message_header_stream"), true);
|
2020-12-11 04:26:23 +01:00
|
|
|
$(document).trigger(new $.Event("compose_started.zulip", opts));
|
2021-02-28 21:32:22 +01:00
|
|
|
update_placeholder_text();
|
2021-08-16 17:10:43 +02:00
|
|
|
update_narrow_to_recipient_visibility();
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function maybe_scroll_up_selected_message() {
|
2017-04-14 16:26:00 +02:00
|
|
|
// If the compose box is obscuring the currently selected message,
|
|
|
|
// scroll up until the message is no longer occluded.
|
2021-03-30 02:21:21 +02:00
|
|
|
if (message_lists.current.selected_id() === -1) {
|
2017-04-14 16:26:00 +02:00
|
|
|
// If there's no selected message, there's no need to
|
|
|
|
// scroll the compose box to avoid it.
|
|
|
|
return;
|
|
|
|
}
|
2022-01-25 11:36:19 +01:00
|
|
|
const $selected_row = message_lists.current.selected_row();
|
2018-04-27 23:19:58 +02:00
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
if ($selected_row.height() > message_viewport.height() - 100) {
|
2018-04-27 23:19:58 +02:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
const cover = $selected_row.offset().top + $selected_row.height() - $("#compose").offset().top;
|
2017-04-14 16:26:00 +02:00
|
|
|
if (cover > 0) {
|
2018-08-29 20:49:47 +02:00
|
|
|
message_viewport.user_initiated_animate_scroll(cover + 20);
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
function fill_in_opts_from_current_narrowed_view(msg_type, opts) {
|
2020-02-09 04:15:38 +01:00
|
|
|
return {
|
2018-12-18 19:34:45 +01:00
|
|
|
message_type: msg_type,
|
2020-07-15 01:29:15 +02:00
|
|
|
stream: "",
|
|
|
|
topic: "",
|
|
|
|
private_message_recipient: "",
|
|
|
|
trigger: "unknown",
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2020-02-09 04:15:38 +01:00
|
|
|
// Set default parameters based on the current narrowed view.
|
|
|
|
...narrow_state.set_compose_defaults(),
|
|
|
|
|
2021-12-01 07:22:32 +01:00
|
|
|
// Set parameters based on provided opts, overwriting
|
|
|
|
// those set based on current narrowed view, if necessary.
|
2020-02-09 04:15:38 +01:00
|
|
|
...opts,
|
|
|
|
};
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function same_recipient_as_before(msg_type, opts) {
|
2020-07-15 00:34:28 +02:00
|
|
|
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()))
|
|
|
|
);
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function update_placeholder_text() {
|
2020-07-29 17:32:39 +02:00
|
|
|
// Change compose placeholder text only if compose box is open.
|
|
|
|
if (!$("#compose-textarea").is(":visible")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-27 16:07:38 +02:00
|
|
|
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));
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2019-07-23 20:13:43 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function start(msg_type, opts) {
|
2021-05-19 13:18:43 +02:00
|
|
|
if (page_params.is_spectator) {
|
2021-09-07 04:09:12 +02:00
|
|
|
spectators.login_to_access();
|
2021-05-19 13:18:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-11 01:17:51 +01:00
|
|
|
// We may be able to clear it to change the recipient, so save any
|
|
|
|
// existing content as a draft.
|
|
|
|
drafts.update_draft();
|
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
autosize_message_content();
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2018-08-04 15:40:25 +02:00
|
|
|
if (reload_state.is_in_progress()) {
|
2017-04-14 16:26:00 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
notifications.clear_compose_notifications();
|
2021-02-28 21:32:22 +01:00
|
|
|
expand_compose_box();
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
opts = fill_in_opts_from_current_narrowed_view(msg_type, opts);
|
2021-03-18 13:57:28 +01:00
|
|
|
|
2019-03-06 22:01:37 +01:00
|
|
|
// 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.
|
2021-07-05 11:48:15 +02:00
|
|
|
if (
|
|
|
|
opts.trigger === "compose_hotkey" ||
|
|
|
|
opts.trigger === "new topic button" ||
|
|
|
|
opts.trigger === "new private message"
|
|
|
|
) {
|
2020-07-15 01:29:15 +02:00
|
|
|
opts.topic = "";
|
|
|
|
opts.private_message_recipient = "";
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const subbed_streams = stream_data.subscribed_subs();
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
|
|
|
subbed_streams.length === 1 &&
|
|
|
|
(opts.trigger === "new topic button" ||
|
|
|
|
(opts.trigger === "compose_hotkey" && msg_type === "stream"))
|
|
|
|
) {
|
2019-07-27 08:12:14 +02:00
|
|
|
opts.stream = subbed_streams[0].name;
|
|
|
|
}
|
|
|
|
|
2017-04-14 16:26:00 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2019-12-02 18:01:31 +01:00
|
|
|
// 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.
|
2017-04-15 01:15:59 +02:00
|
|
|
compose_state.stream_name(opts.stream);
|
2018-11-15 19:14:16 +01:00
|
|
|
compose_state.topic(opts.topic);
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
// Set the recipients with a space after each comma, so it looks nice.
|
2019-12-02 17:53:55 +01:00
|
|
|
compose_state.private_message_recipient(opts.private_message_recipient.replace(/,\s*/g, ", "));
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
// If the user opens the compose box, types some text, and then clicks on a
|
2018-11-13 16:27:16 +01:00
|
|
|
// different stream/topic, we want to keep the text in the compose box
|
2017-04-14 16:26:00 +02:00
|
|
|
if (opts.content !== undefined) {
|
2017-04-15 01:15:59 +02:00
|
|
|
compose_state.message_content(opts.content);
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
|
reload: Manually save draft and preserve id when triggering reload.
We received a complaint about the generation of multiple duplicate
drafts for a single message. It was discovered that the likely cause
of this was how we were handling clients that were frequently
suspending/unsuspending, we would initiate a reload when we discovered
this, and expect the `beforeunload` handler to save the draft. This
behaved correctly, however, we would also save the compose state and
fill it in via `preserve_state` in reload.js. The important detail
here is that `preserve_state` would not encode and preserve the
`draft_id` for the current message, partly because it had no way of
knowing the `draft_id` of the draft... since we have not saved it yet,
the `beforeunload` event happens after `preserve_state`. As such,
performing any action that would trigger a draft to be saved, eg
pressing Esc to close the compose box, would save a duplicate draft of
the same message.
To resolve the above bug, we (1) ensure that we call
`drafts.update_draft()` in `preserve_state`, this returns a draft_id
to us, which we (2) ensure that we encode as part of the url and (3)
set on the `#composebox-textarea` as a `draft-id` data attribute,
which we check the next time we try to save the draft, post reload.
Note that this causes us to save the draft twice, once from
preserve_state and then again from the `beforeunload` handler, but we
do not add two drafts since the second update_draft call just edits
the timestamp because it finds the `draft-id` data attribute on the
`#composebox-textarea` set by the first call.
2021-11-30 17:57:42 +01:00
|
|
|
if (opts.draft_id) {
|
|
|
|
$("#compose-textarea").data("draft-id", opts.draft_id);
|
|
|
|
}
|
|
|
|
|
2017-04-15 01:15:59 +02:00
|
|
|
compose_state.set_message_type(msg_type);
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
// Show either stream/topic fields or "You and" field.
|
2022-04-27 01:23:44 +02:00
|
|
|
show_compose_box(msg_type, opts);
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2021-07-27 21:48:11 +02:00
|
|
|
// Show a warning if topic is resolved
|
2022-03-16 01:31:01 +01:00
|
|
|
compose_validate.warn_if_topic_resolved(true);
|
2021-07-27 21:48:11 +02:00
|
|
|
|
compose-box: Fix compose-box from covering last messages of stream.
While writing a long message in compose-box, the last few messages of
the current stream gets covered by the compose-box and it gets pretty
annoying sometimes trying to figure out a way to read the last message
of the stream while writing. Right now, the only way to get past this
is to resize `compose-textarea` by using the resize tool at the
bottom-right corner of the `compose-textarea`. But, that small resize
tool is not always readily visible to the user.
The proposed solution in this commit is to reset the `max-height`
property of `#compose-textarea` everytime `bottom_whitespace_height`
is resized such that the total height of `#compose` is always less
than or equal to the height of `bottom_whitespace_height`. Doing
this, the compose-box never covers the last message of the current
stream.
The only problem with this is that if the compose-box is closed at the
time of bottom-whitespace resize, we cannot find the
`compose_non_textarea_height` and so, we cannot reset the max-height
of `#compose-textarea`. To solve this, max-height of
`compose-textarea` is also reset everytime a new compose-box is opened
according to the value of `bottom_whitespace_height` at that time.
Thus, if the compose-box is already open at the time of
bottom-whitespace resize, the max-height of `#compose-textarea` will
also get reset at the same time, whereas, if the compose-box is closed
at the time of bottom-whitespace resize, the max-height of
`#compose-textarea` won't get reset at that time, but it will surely
get reset whenever the user will open the compose-box.
Tested on my Ubuntu Development Environment on Chrome and Firefox browsers.
Fixes: #16038.
2021-01-18 14:23:29 +01:00
|
|
|
// 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.
|
2022-02-02 06:06:56 +01:00
|
|
|
resize.reset_compose_message_max_height();
|
compose-box: Fix compose-box from covering last messages of stream.
While writing a long message in compose-box, the last few messages of
the current stream gets covered by the compose-box and it gets pretty
annoying sometimes trying to figure out a way to read the last message
of the stream while writing. Right now, the only way to get past this
is to resize `compose-textarea` by using the resize tool at the
bottom-right corner of the `compose-textarea`. But, that small resize
tool is not always readily visible to the user.
The proposed solution in this commit is to reset the `max-height`
property of `#compose-textarea` everytime `bottom_whitespace_height`
is resized such that the total height of `#compose` is always less
than or equal to the height of `bottom_whitespace_height`. Doing
this, the compose-box never covers the last message of the current
stream.
The only problem with this is that if the compose-box is closed at the
time of bottom-whitespace resize, we cannot find the
`compose_non_textarea_height` and so, we cannot reset the max-height
of `#compose-textarea`. To solve this, max-height of
`compose-textarea` is also reset everytime a new compose-box is opened
according to the value of `bottom_whitespace_height` at that time.
Thus, if the compose-box is already open at the time of
bottom-whitespace resize, the max-height of `#compose-textarea` will
also get reset at the same time, whereas, if the compose-box is closed
at the time of bottom-whitespace resize, the max-height of
`#compose-textarea` won't get reset at that time, but it will surely
get reset whenever the user will open the compose-box.
Tested on my Ubuntu Development Environment on Chrome and Firefox browsers.
Fixes: #16038.
2021-01-18 14:23:29 +01:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
complete_starting_tasks(msg_type, opts);
|
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function cancel() {
|
2021-05-17 13:15:11 +02:00
|
|
|
// As user closes the compose box, restore the compose box max height
|
2021-10-25 23:32:59 +02:00
|
|
|
if (compose_ui.is_full_size()) {
|
|
|
|
compose_ui.make_compose_box_original_size();
|
|
|
|
}
|
2021-05-17 13:15:11 +02:00
|
|
|
|
2017-11-26 20:37:44 +01:00
|
|
|
$("#compose-textarea").height(40 + "px");
|
2017-04-14 16:26:00 +02:00
|
|
|
|
|
|
|
if (page_params.narrow !== undefined) {
|
|
|
|
// Never close the compose box in narrow embedded windows, but
|
2018-11-15 19:14:16 +01:00
|
|
|
// at least clear the topic and unfade.
|
2017-04-14 16:26:00 +02:00
|
|
|
compose_fade.clear_compose();
|
|
|
|
if (page_params.narrow_topic !== undefined) {
|
2018-11-04 17:04:17 +01:00
|
|
|
compose_state.topic(page_params.narrow_topic);
|
2017-04-14 16:26:00 +02:00
|
|
|
} else {
|
2018-11-04 17:04:17 +01:00
|
|
|
compose_state.topic("");
|
2017-04-14 16:26:00 +02:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
hide_box();
|
|
|
|
$("#compose_close").hide();
|
|
|
|
clear_box();
|
|
|
|
notifications.clear_compose_notifications();
|
|
|
|
compose.abort_xhr();
|
2020-10-28 11:35:48 +01:00
|
|
|
compose.abort_video_callbacks(undefined);
|
2017-04-15 01:15:59 +02:00
|
|
|
compose_state.set_message_type(false);
|
2018-03-06 15:07:55 +01:00
|
|
|
compose_pm_pill.clear();
|
2020-12-11 04:26:23 +01:00
|
|
|
$(document).trigger("compose_canceled.zulip");
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 16:26:00 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function respond_to_message(opts) {
|
2021-03-18 13:57:28 +01:00
|
|
|
let message;
|
|
|
|
let msg_type;
|
2021-06-10 14:18:46 +02:00
|
|
|
if (recent_topics_util.is_visible()) {
|
|
|
|
message = recent_topics_ui.get_focused_row_message();
|
2021-03-18 13:57:28 +01:00
|
|
|
if (message === undefined) {
|
2021-05-10 14:05:30 +02:00
|
|
|
// Open empty compose with nothing pre-filled since
|
|
|
|
// user is not focused on any table row.
|
2021-11-23 23:29:18 +01:00
|
|
|
start("stream", {trigger: "recent_topics_nofocus"});
|
2017-12-11 11:39:49 +01:00
|
|
|
return;
|
|
|
|
}
|
2021-03-18 13:57:28 +01:00
|
|
|
} else {
|
|
|
|
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()
|
|
|
|
) {
|
2021-05-10 17:21:20 +02:00
|
|
|
start("stream", {trigger: "empty_narrow_compose"});
|
2021-03-18 13:57:28 +01:00
|
|
|
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;
|
|
|
|
|
2021-11-23 00:37:45 +01:00
|
|
|
if (first_operator === "stream" && !stream_data.is_subscribed_by_name(first_operand)) {
|
2021-05-10 17:21:20 +02:00
|
|
|
start("stream", {trigger: "empty_narrow_compose"});
|
2021-03-18 13:57:28 +01:00
|
|
|
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);
|
2017-04-21 09:33:05 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-18 13:57:28 +01:00
|
|
|
if (message_lists.current.can_mark_messages_read()) {
|
|
|
|
unread_ops.notify_server_message_read(message);
|
2017-04-21 09:33:05 +02:00
|
|
|
}
|
2019-07-10 02:03:41 +02:00
|
|
|
}
|
2017-04-14 19:27:12 +02:00
|
|
|
|
2019-12-02 18:01:31 +01:00
|
|
|
// 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.
|
2020-07-15 01:29:15 +02:00
|
|
|
if (opts.reply_type === "personal" || message.type === "private") {
|
|
|
|
msg_type = "private";
|
2019-12-02 18:01:31 +01:00
|
|
|
} else {
|
|
|
|
msg_type = message.type;
|
|
|
|
}
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
let stream = "";
|
|
|
|
let topic = "";
|
|
|
|
let pm_recipient = "";
|
2019-12-02 18:01:31 +01:00
|
|
|
if (msg_type === "stream") {
|
2017-04-14 19:27:12 +02:00
|
|
|
stream = message.stream;
|
2020-02-19 00:04:12 +01:00
|
|
|
topic = message.topic;
|
2019-12-02 18:01:31 +01:00
|
|
|
} else {
|
|
|
|
pm_recipient = message.reply_to;
|
2017-04-14 19:27:12 +02:00
|
|
|
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
|
2020-02-05 14:30:59 +01:00
|
|
|
pm_recipient = people.get_by_user_id(message.sender_id).email;
|
2017-04-14 19:27:12 +02:00
|
|
|
} else {
|
|
|
|
pm_recipient = people.pm_reply_to(message);
|
|
|
|
}
|
|
|
|
}
|
2019-12-02 18:01:31 +01:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
start(msg_type, {
|
2020-07-20 22:18:43 +02:00
|
|
|
stream,
|
|
|
|
topic,
|
2020-07-15 00:34:28 +02:00
|
|
|
private_message_recipient: pm_recipient,
|
|
|
|
trigger: opts.trigger,
|
|
|
|
});
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 19:27:12 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function reply_with_mention(opts) {
|
|
|
|
respond_to_message(opts);
|
2021-03-30 02:21:21 +02:00
|
|
|
const message = message_lists.current.selected_message();
|
2019-11-02 00:06:25 +01:00
|
|
|
const mention = people.get_mention_syntax(message.sender_full_name, message.sender_id);
|
2018-02-26 16:04:40 +01:00
|
|
|
compose_ui.insert_syntax_and_focus(mention);
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-14 19:27:12 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function on_topic_narrow() {
|
2017-04-19 17:21:49 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-04-25 15:25:31 +02:00
|
|
|
if (compose_state.stream_name() !== narrow_state.stream()) {
|
2017-04-19 17:21:49 +02:00
|
|
|
// 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.
|
2021-02-28 21:32:22 +01:00
|
|
|
cancel();
|
2017-04-19 17:21:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-11-04 17:04:17 +01:00
|
|
|
if (compose_state.topic() && compose_state.has_message_content()) {
|
2018-04-03 02:43:22 +02:00
|
|
|
// 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();
|
2017-04-19 17:21:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-03 02:43:22 +02:00
|
|
|
// 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.
|
2018-11-04 17:04:17 +01:00
|
|
|
compose_state.topic(narrow_state.topic());
|
2022-03-16 01:31:01 +01:00
|
|
|
compose_validate.warn_if_topic_resolved(true);
|
2018-04-06 00:21:02 +02:00
|
|
|
compose_fade.set_focused_recipient("stream");
|
|
|
|
compose_fade.update_message_list();
|
2020-07-20 21:24:26 +02:00
|
|
|
$("#compose-textarea").trigger("focus").trigger("select");
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2017-04-19 17:21:49 +02:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function quote_and_reply(opts) {
|
2022-01-25 11:36:19 +01:00
|
|
|
const $textarea = $("#compose-textarea");
|
2021-03-30 02:21:21 +02:00
|
|
|
const message_id = message_lists.current.selected_id();
|
|
|
|
const message = message_lists.current.selected_message();
|
2021-11-28 05:57:08 +01:00
|
|
|
const quoting_placeholder = $t({defaultMessage: "[Quoting…]"});
|
2018-01-31 04:23:02 +01:00
|
|
|
|
2019-01-16 16:29:18 +01:00
|
|
|
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.)
|
2020-12-31 12:53:54 +01:00
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
if ($textarea.caret() !== 0) {
|
2020-12-31 12:53:54 +01:00
|
|
|
// Insert a newline before quoted message if there is
|
|
|
|
// already some content in the compose box and quoted
|
|
|
|
// message is not being inserted at the beginning.
|
2022-01-25 11:36:19 +01:00
|
|
|
$textarea.caret("\n");
|
2020-12-31 12:53:54 +01:00
|
|
|
}
|
2019-01-16 16:29:18 +01:00
|
|
|
} else {
|
2021-02-28 21:32:22 +01:00
|
|
|
respond_to_message(opts);
|
2019-01-16 16:29:18 +01:00
|
|
|
}
|
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
compose_ui.insert_syntax_and_focus(quoting_placeholder + "\n", $textarea);
|
2018-11-01 23:43:29 +01:00
|
|
|
|
2019-08-26 08:10:46 +02:00
|
|
|
function replace_content(message) {
|
|
|
|
// Final message looks like:
|
|
|
|
// @_**Iago|5** [said](link to message):
|
|
|
|
// ```quote
|
|
|
|
// message content
|
|
|
|
// ```
|
2022-01-25 11:36:19 +01:00
|
|
|
const prev_caret = $textarea.caret();
|
2021-04-13 06:51:54 +02:00
|
|
|
let content = $t(
|
|
|
|
{defaultMessage: "{username} [said]({link_to_message}):"},
|
|
|
|
{
|
|
|
|
username: `@_**${message.sender_full_name}|${message.sender_id}**`,
|
2022-03-01 19:14:26 +01:00
|
|
|
link_to_message: `${hash_util.by_conversation_and_time_url(message)}`,
|
2021-04-13 06:51:54 +02:00
|
|
|
},
|
|
|
|
);
|
2021-03-04 20:43:27 +01:00
|
|
|
content += "\n";
|
2019-08-26 09:04:34 +02:00
|
|
|
const fence = fenced_code.get_unused_fence(message.raw_content);
|
|
|
|
content += `${fence}quote\n${message.raw_content}\n${fence}`;
|
2021-11-28 05:51:21 +01:00
|
|
|
|
2022-08-18 20:49:42 +02:00
|
|
|
const placeholder_offset = $textarea.val().indexOf(quoting_placeholder);
|
2022-01-25 11:36:19 +01:00
|
|
|
compose_ui.replace_syntax(quoting_placeholder, content, $textarea);
|
2021-03-31 13:40:26 +02:00
|
|
|
compose_ui.autosize_textarea($("#compose-textarea"));
|
2021-11-28 05:51:21 +01:00
|
|
|
|
|
|
|
// When replacing content in a textarea, we need to move the
|
|
|
|
// cursor to preserve its logical position if and only if the
|
|
|
|
// content we just added was before the current cursor
|
|
|
|
// position. If we do, we need to move it by the increase in
|
|
|
|
// the length of the content before the placeholder.
|
|
|
|
if (prev_caret >= placeholder_offset + quoting_placeholder.length) {
|
2022-01-25 11:36:19 +01:00
|
|
|
$textarea.caret(prev_caret + content.length - quoting_placeholder.length);
|
2021-11-28 05:51:21 +01:00
|
|
|
} else if (prev_caret > placeholder_offset) {
|
|
|
|
/* In the rare case that our cursor was inside the
|
|
|
|
* placeholder, we treat that as though the cursor was
|
|
|
|
* just after the placeholder. */
|
2022-01-25 11:36:19 +01:00
|
|
|
$textarea.caret(placeholder_offset + content.length + 1);
|
2021-11-28 05:51:21 +01:00
|
|
|
} else {
|
2022-01-25 11:36:19 +01:00
|
|
|
$textarea.caret(prev_caret);
|
2021-11-28 05:51:21 +01:00
|
|
|
}
|
2019-01-16 15:56:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (message && message.raw_content) {
|
2019-08-26 08:10:46 +02:00
|
|
|
replace_content(message);
|
2018-11-11 19:36:26 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-01-31 04:23:02 +01:00
|
|
|
channel.get({
|
2020-07-15 01:29:15 +02:00
|
|
|
url: "/json/messages/" + message_id,
|
2020-07-20 22:18:43 +02:00
|
|
|
success(data) {
|
2018-11-15 08:54:20 +01:00
|
|
|
message.raw_content = data.raw_content;
|
2019-08-26 08:10:46 +02:00
|
|
|
replace_content(message);
|
2018-01-31 04:23:02 +01:00
|
|
|
},
|
|
|
|
});
|
2021-02-28 21:32:22 +01:00
|
|
|
}
|
2018-01-31 04:23:02 +01:00
|
|
|
|
2021-02-28 21:32:22 +01:00
|
|
|
export function on_narrow(opts) {
|
2018-02-16 15:56:25 +01:00
|
|
|
// 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.
|
2021-02-28 21:32:22 +01:00
|
|
|
cancel();
|
2018-02-16 15:56:25 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-11-30 00:20:10 +01:00
|
|
|
if (opts.trigger === "narrow_to_compose_target") {
|
|
|
|
compose_fade.update_message_list();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-25 15:25:31 +02:00
|
|
|
if (narrow_state.narrowed_by_topic_reply()) {
|
2021-02-28 21:32:22 +01:00
|
|
|
on_topic_narrow();
|
2017-04-19 17:21:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-16 16:48:31 +02:00
|
|
|
if (compose_state.has_message_content()) {
|
|
|
|
compose_fade.update_message_list();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-25 15:25:31 +02:00
|
|
|
if (narrow_state.narrowed_by_pm_reply()) {
|
2020-07-15 01:29:15 +02:00
|
|
|
opts = fill_in_opts_from_current_narrowed_view("private", opts);
|
2018-05-28 11:44:28 +02:00
|
|
|
// Do not open compose box if triggered by search and invalid recipient
|
|
|
|
// is present.
|
|
|
|
if (opts.trigger === "search" && !opts.private_message_recipient) {
|
|
|
|
return;
|
|
|
|
}
|
2021-02-28 21:32:22 +01:00
|
|
|
start("private");
|
2017-04-16 16:48:31 +02:00
|
|
|
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.
|
2021-02-28 21:32:22 +01:00
|
|
|
cancel();
|
|
|
|
}
|