compose: Add UI to schedule messages.

Fixes #20971
This commit is contained in:
Aman Agrawal 2023-04-14 19:34:41 +00:00 committed by Tim Abbott
parent a4a8fd7bf3
commit ff52187289
15 changed files with 367 additions and 16 deletions

View File

@ -659,6 +659,14 @@ export function initialize() {
return;
}
if ($target.is("#send_later i")) {
// Since the click for this is handled by tippyjs, we cannot add stopPropagation
// there without adding a special click event handler to show the popover,
// so it is better just do it here.
e.stopPropagation();
return;
}
// The mobile compose button has its own popover when clicked, so it already.
// hides other popovers.
if ($target.is(".compose_mobile_button, .compose_mobile_button *")) {

View File

@ -21,10 +21,12 @@ import * as message_edit from "./message_edit";
import * as narrow from "./narrow";
import {page_params} from "./page_params";
import * as people from "./people";
import * as popover_menus from "./popover_menus";
import * as reminder from "./reminder";
import * as rendered_markdown from "./rendered_markdown";
import * as resize from "./resize";
import * as rows from "./rows";
import * as scheduled_messages from "./scheduled_messages";
import * as sent_messages from "./sent_messages";
import * as server_events from "./server_events";
import * as stream_data from "./stream_data";
@ -205,6 +207,7 @@ export function clear_compose_box() {
compose_banner.clear_errors();
compose_banner.clear_warnings();
compose_ui.hide_compose_spinner();
reset_compose_scheduling_state();
}
export function send_message_success(local_id, message_id, locally_echoed) {
@ -285,6 +288,7 @@ export function send_message(request = create_message_object()) {
}
transmit.send_message(request, success, error);
scheduled_messages.delete_scheduled_message_if_sent_directly();
server_events.assert_get_events_running(
"Restarting get_events because it was not running during send",
);
@ -345,7 +349,9 @@ export function finish() {
return false;
}
if (reminder.is_deferred_delivery(message_content)) {
if (popover_menus.is_time_selected_for_schedule()) {
schedule_message_to_custom_date();
} else if (reminder.is_deferred_delivery(message_content)) {
reminder.schedule_message();
} else {
send_message();
@ -789,3 +795,26 @@ export function initialize() {
}
}
}
export function reset_compose_scheduling_state(reset_edit_state = true) {
$("#compose-textarea").prop("disabled", false);
$("#compose-schedule-confirm-button").hide();
popover_menus.reset_selected_schedule_time();
$("#compose-send-button").show();
if (reset_edit_state) {
$("#compose-textarea").removeAttr("data-scheduled-message-id");
}
}
function schedule_message_to_custom_date() {
const request = create_message_object();
const selected_send_later_time = popover_menus.get_selected_send_later_time();
request.content = `/schedule ${selected_send_later_time}\n` + request.content;
// If this is an edit request `scheduled_message_id` will be defined.
let scheduled_message_id;
if ($("#compose-textarea").attr("data-scheduled-message-id")) {
scheduled_message_id = $("#compose-textarea").attr("data-scheduled-message-id");
$("#compose-textarea").removeAttr("data-scheduled-message-id");
}
reminder.schedule_message(request, clear_compose_box, scheduled_message_id);
}

View File

@ -120,6 +120,7 @@ function clear_box() {
compose_ui.autosize_textarea($("#compose-textarea"));
compose_banner.clear_errors();
compose_banner.clear_warnings();
compose.reset_compose_scheduling_state();
}
export function autosize_message_content() {

View File

@ -493,3 +493,10 @@ export function get_compose_click_target(e) {
}
return e.target;
}
export function get_submit_button() {
if (popover_menus.is_time_selected_for_schedule()) {
return $("#compose-schedule-confirm-button");
}
return $("#compose-send-button");
}

View File

@ -222,7 +222,8 @@ function handle_keydown(e) {
// could result in focus being moved to the "Send
// button" after sending the message, preventing
// typing a next message!
$("#compose-send-button").trigger("focus");
compose_ui.get_submit_button().trigger("focus");
e.preventDefault();
e.stopPropagation();
}
@ -232,7 +233,7 @@ function handle_keydown(e) {
e.preventDefault();
if (
compose_validate.validate_message_length() &&
!$("#compose-send-button").prop("disabled")
!compose_ui.get_submit_button().prop("disabled")
) {
compose.finish();
}

View File

@ -447,6 +447,14 @@ export function process_enter_key(e) {
return true;
}
// Transfer the enter keypress from button to the `<i>` tag inside
// it since it is the trigger for the popover. <button> is already used
// to trigger the tooltip so it cannot be used to trigger the popover.
if (e.target.id === "send_later") {
$("#send_later i").trigger("click");
return true;
}
if ($(e.target).attr("role") === "button") {
e.target.click();
return true;

View File

@ -3,6 +3,7 @@
popovers system in popovers.js. */
import ClipboardJS from "clipboard";
import {format} from "date-fns";
import $ from "jquery";
import tippy, {delegate} from "tippy.js";
@ -14,17 +15,20 @@ import render_delete_topic_modal from "../templates/confirm_dialog/confirm_delet
import render_drafts_sidebar_actions from "../templates/drafts_sidebar_action.hbs";
import render_left_sidebar_stream_setting_popover from "../templates/left_sidebar_stream_setting_popover.hbs";
import render_mobile_message_buttons_popover_content from "../templates/mobile_message_buttons_popover_content.hbs";
import render_send_later_popover from "../templates/send_later_popover.hbs";
import render_starred_messages_sidebar_actions from "../templates/starred_messages_sidebar_actions.hbs";
import render_topic_sidebar_actions from "../templates/topic_sidebar_actions.hbs";
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as common from "./common";
import * as compose from "./compose";
import * as compose_actions from "./compose_actions";
import * as condense from "./condense";
import * as confirm_dialog from "./confirm_dialog";
import * as drafts from "./drafts";
import * as emoji_picker from "./emoji_picker";
import * as flatpickr from "./flatpickr";
import * as giphy from "./giphy";
import {$t, $t_html} from "./i18n";
import * as message_edit from "./message_edit";
@ -45,6 +49,7 @@ import {user_settings} from "./user_settings";
import * as user_topics from "./user_topics";
let message_actions_popover_keyboard_toggle = false;
let selected_send_later_time;
const popover_instances = {
compose_control_buttons: null,
@ -56,6 +61,7 @@ const popover_instances = {
compose_mobile_button: null,
compose_enter_sends: null,
topics_menu: null,
send_later: null,
};
export function sidebar_menu_instance_handle_keyboard(instance, key) {
@ -70,6 +76,25 @@ export function get_topic_menu_popover() {
return popover_instances.topics_menu;
}
export function is_time_selected_for_schedule() {
return selected_send_later_time !== undefined;
}
export function get_selected_send_later_time() {
if (!selected_send_later_time) {
return undefined;
}
return format(new Date(selected_send_later_time), "MMM d yyyy 'at' h:mm a");
}
export function reset_selected_schedule_time() {
selected_send_later_time = undefined;
}
export function get_scheduled_messages_popover() {
return popover_instances.send_later;
}
export function get_compose_control_buttons_popover() {
return popover_instances.compose_control_buttons;
}
@ -192,6 +217,25 @@ export function toggle_message_actions_menu(message) {
return true;
}
export function show_schedule_confirm_button(send_at_time, not_from_flatpickr = false) {
// If no time was selected by the user, we don't show the schedule button.
if (!send_at_time) {
return;
}
// flatpickr.show_flatpickr doesn't pass any value for not_from_flatpickr,
// making it false by default and we pass it as true in other cases.
// This is used to determine if flatpickr was used to select time for the
// message to be sent.
if (!not_from_flatpickr) {
send_at_time = format(new Date(send_at_time), "MMM d yyyy h:mm a");
}
selected_send_later_time = send_at_time;
$("#compose-schedule-confirm-button").show();
$("#compose-send-button").hide();
}
export function initialize() {
tippy_no_propagation("#streams_inline_icon", {
onShow(instance) {
@ -731,4 +775,130 @@ export function initialize() {
popover_instances.all_messages = undefined;
},
});
$("body").on("click", "#compose-schedule-confirm-button", (e) => {
compose.finish();
e.preventDefault();
e.stopPropagation();
});
const send_later_today = {
today_nine_am: {
text: $t({defaultMessage: "Today at 9:00 AM"}),
time: "9:00 am",
},
today_four_pm: {
text: $t({defaultMessage: "Today at 4:00 PM "}),
time: "4:00 pm",
},
};
const send_later_tomorrow = {
tomorrow_nine_am: {
text: $t({defaultMessage: "Tomorrow at 9:00 AM"}),
time: "9:00 am",
},
tomorrow_four_pm: {
text: $t({defaultMessage: "Tomorrow at 4:00 PM "}),
time: "4:00 pm",
},
};
const send_later_custom = {
text: $t({defaultMessage: "Custom"}),
};
function set_compose_box_schedule(element) {
const send_later_in = element.id;
const send_later_class = element.classList[0];
switch (send_later_class) {
case "send_later_tomorrow": {
const send_time = send_later_tomorrow[send_later_in].time;
const date = new Date();
const scheduled_date = date.setDate(date.getDate() + 1);
const send_at_time = format(scheduled_date, "MMM d yyyy ") + send_time;
return send_at_time;
}
case "send_later_today": {
const send_time = send_later_today[send_later_in].time;
const date = new Date();
const send_at_time =
format(date.setDate(date.getDate()), "MMM d yyyy ") + send_time;
return send_at_time;
}
// No default
}
blueslip.error("Not a valid time.");
return false;
}
delegate("body", {
...default_popover_props,
target: "#send_later i",
onUntrigger() {
// This is only called when the popover is closed by clicking on `target`.
$("#compose-textarea").trigger("focus");
},
onShow(instance) {
popovers.hide_all_except_sidebars(instance);
// Only show send later options that are possible today.
const date = new Date();
const hours = date.getHours();
let possible_send_later_today = {};
if (hours <= 8) {
possible_send_later_today = send_later_today;
} else if (hours <= 15) {
possible_send_later_today.today_four_pm = send_later_today.today_four_pm;
} else {
possible_send_later_today = false;
}
const formatted_send_later_time = get_selected_send_later_time();
instance.setContent(
parse_html(
render_send_later_popover({
possible_send_later_today,
send_later_tomorrow,
send_later_custom,
formatted_send_later_time,
}),
),
);
popover_instances.send_later = instance;
$(instance.popper).one("click", instance.hide);
},
onMount(instance) {
const $popper = $(instance.popper);
$popper.one("click", "#send-later-custom-input", () => {
flatpickr.show_flatpickr(
$("#send_later")[0],
show_schedule_confirm_button,
new Date(),
);
});
$popper.one("click", ".send_later_today, .send_later_tomorrow", (e) => {
const send_at_time = set_compose_box_schedule(e.currentTarget);
const not_from_flatpickr = true;
show_schedule_confirm_button(send_at_time, not_from_flatpickr);
instance.hide();
e.stopPropagation();
e.preventDefault();
});
$popper.one("click", "#clear_compose_schedule_state", () => {
// We don't to want to reset the edit state of the scheduled message when
// clicks "Now", since the user can still change the date to a future date.
compose.reset_compose_scheduling_state(false);
});
},
onHidden(instance) {
instance.destroy();
popover_instances.send_later = undefined;
},
});
}

View File

@ -3,8 +3,9 @@ import $ from "jquery";
import * as channel from "./channel";
import * as compose from "./compose";
import * as compose_banner from "./compose_banner";
import * as compose_ui from "./compose_ui";
import * as hash_util from "./hash_util";
import {$t, $t_html} from "./i18n";
import {$t} from "./i18n";
import * as message_lists from "./message_lists";
import * as notifications from "./notifications";
import {page_params} from "./page_params";
@ -46,7 +47,11 @@ export function patch_request_for_scheduling(request, message_content, deliver_a
return new_request;
}
export function schedule_message(request = compose.create_message_object()) {
export function schedule_message(
request = compose.create_message_object(),
success_callback = () => {},
scheduled_message_id = undefined,
) {
const raw_message = request.content.split("\n");
const command_line = raw_message[0];
const message = raw_message.slice(1).join("\n");
@ -67,7 +72,9 @@ export function schedule_message(request = compose.create_message_object()) {
} else if (deliver_at.trim() === "") {
error_message = $t({defaultMessage: "Please specify a date or time."});
} else if (message.trim() === "") {
error_message = $t({defaultMessage: "You have nothing to send!"});
$("#compose-textarea").toggleClass("invalid", false);
$("#compose-textarea").prop("disabled", false);
return;
}
if (error_message) {
@ -87,18 +94,25 @@ export function schedule_message(request = compose.create_message_object()) {
deferred_message_type.delivery_type,
);
const success = function (data) {
const success = function () {
if (request.delivery_type === deferred_message_types.scheduled.delivery_type) {
const deliver_at = data.deliver_at;
notifications.notify_above_composebox(
$t_html({defaultMessage: `Message scheduled for {deliver_at}`}, {deliver_at}),
$t(
{defaultMessage: `Your message has been scheduled for {deliver_at}.`},
{deliver_at},
),
"scheduled_message_banner",
"/#scheduled",
"",
$t({defaultMessage: "View scheduled messages"}),
);
}
$("#compose-textarea").prop("disabled", false);
compose.clear_compose_box();
success_callback();
};
const error = function (response) {
$("#compose-textarea").prop("disabled", false);
compose_ui.hide_compose_spinner();
compose_banner.show_error_message(
response,
compose_banner.CLASSNAMES.generic_compose_error,
@ -110,6 +124,9 @@ export function schedule_message(request = compose.create_message_object()) {
$("#compose-textarea").prop("disabled", true);
const future_message = true;
if (scheduled_message_id) {
request.scheduled_message_id = scheduled_message_id;
}
transmit.send_message(request, success, error, future_message);
}

View File

@ -503,4 +503,25 @@ export function initialize() {
delay: LONG_HOVER_DELAY,
appendTo: () => document.body,
});
delegate("body", {
target: "#compose-schedule-confirm-button",
onShow(instance) {
if (popover_menus.get_scheduled_messages_popover()) {
return false;
}
const send_at_time = popover_menus.get_selected_send_later_time();
instance.setContent(
parse_html(
$t(
{defaultMessage: "Schedule message for <br/> {send_at_time}"},
{send_at_time},
),
),
);
return true;
},
appendTo: () => document.body,
});
}

View File

@ -543,13 +543,14 @@ input.recipient_box {
width: 100%;
}
#compose-schedule-confirm-button,
#compose-send-button {
height: 24px;
padding-top: 3px;
padding-bottom: 3px;
margin-bottom: 0;
font-weight: 600;
font-size: 0.9em;
border-radius: 4px 0 0 4px;
.loader {
display: none;
@ -716,11 +717,10 @@ input.recipient_box {
.compose_right_float_container {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-direction: row;
white-space: nowrap;
gap: 4px;
margin-top: 2px;
height: 24px;
}
a.compose_control_button {
@ -805,6 +805,31 @@ a.compose_control_button.hide {
margin-right: 4px;
}
/* `^` icon located next to `Send` / `Scheduled` button which shows
options to schedule the message. */
#send_later {
float: right;
color: hsl(0deg 0% 100%);
border-radius: 0 4px 4px 0;
border-left: 1px solid hsl(213deg 14% 12% / 15%);
padding: 0;
margin: 0;
.fa {
padding: 4.5px 4px;
&::before {
position: relative;
top: -1px;
}
}
&:hover,
&:focus {
box-shadow: none;
}
}
@media (width < $xl_min) {
#compose-content {
margin-right: 7px;

View File

@ -863,6 +863,11 @@
color: inherit;
}
.send_later_popover_header,
.selected_send_later_time {
color: hsl(236deg 33% 90%);
}
.nav-list > li > a,
.nav-list .nav-header {
text-shadow: none;

View File

@ -856,3 +856,19 @@ ul {
column-count: 1;
}
}
#send_later_popover {
& hr {
margin: 5px 0;
}
.send_later_popover_header {
text-align: center;
font-weight: bold;
}
.selected_send_later_time {
text-align: center;
margin-top: 3px;
}
}

View File

@ -108,10 +108,16 @@
<div id="below-compose-content">
<div class="compose_bottom_top_container">
<div class="compose_right_float_container order-3">
<button type="submit" id="compose-send-button" class="button small send_message animated-purple-button" title="{{t 'Send' }} (Ctrl + Enter)">
<button type="submit" id="compose-send-button" class="button small send_message animated-purple-button" title="{{t 'Send' }} (Ctrl + Enter)" tabindex=0>
<img class="loader" alt="" src="" />
<span>{{t 'Send' }}</span>
</button>
<button id="compose-schedule-confirm-button" class="button small hide animated-purple-button" tabindex=0>
<span>{{t 'Schedule' }}</span>
</button>
<button class="animated-purple-button message-control-button tippy-zulip-tooltip" data-tippy-content="{{t 'Send later' }}" id="send_later" tabindex=0 type="button">
<i class="fa fa-chevron-up"></i>
</button>
</div>
{{> compose_control_buttons }}
</div>

View File

@ -0,0 +1,36 @@
<ul id="send_later_popover" class="nav nav-list">
<li class="send_later_popover_header">
{{t "Schedule message" }}
</li>
{{#if formatted_send_later_time }}
<li class="selected_send_later_time">
{{ formatted_send_later_time }}
</li>
<hr />
<li>
<a id="clear_compose_schedule_state">{{t "Now" }}</a>
</li>
{{/if}}
<hr />
{{#if possible_send_later_today}}
{{#each possible_send_later_today}}
<li>
<a id="{{@key}}" class="send_later_today">{{this.text}}</a>
</li>
{{/each}}
<hr />
{{/if}}
{{#each send_later_tomorrow}}
<li>
<a id="{{@key}}" class="send_later_tomorrow">{{this.text}}</a>
</li>
{{/each}}
<hr />
<li>
<a id="send-later-custom-input">{{send_later_custom.text}}</a>
</li>
<hr />
<li>
<a href="#scheduled">{{t "View scheduled messages" }}</a>
</li>
</ul>

View File

@ -1208,6 +1208,7 @@ test("initialize", ({override, mock_template}) => {
$("form#send_message_form").off("keydown");
$("form#send_message_form").off("keyup");
$("#private_message_recipient").off("blur");
$("#send_later").css = noop;
ct.initialize();
// Now let's make sure that all the stub functions have been called