diff --git a/templates/zerver/app/index.html b/templates/zerver/app/index.html index 332cdfc638..493b05581f 100644 --- a/templates/zerver/app/index.html +++ b/templates/zerver/app/index.html @@ -109,6 +109,7 @@
+
diff --git a/tools/test-js-with-node b/tools/test-js-with-node index b6386a702d..b6023917c7 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -144,6 +144,8 @@ EXEMPT_FILES = make_set( "web/src/reminder.js", "web/src/resize.js", "web/src/rows.js", + "web/src/scheduled_messages.js", + "web/src/scheduled_messages_overlay_ui.js", "web/src/scroll_bar.ts", "web/src/search_pill_widget.js", "web/src/sent_messages.js", diff --git a/web/src/bundles/app.ts b/web/src/bundles/app.ts index c4dd1262ed..ac67d32de2 100644 --- a/web/src/bundles/app.ts +++ b/web/src/bundles/app.ts @@ -38,6 +38,7 @@ import "../../styles/modal.css"; import "../../styles/settings.css"; import "../../styles/image_upload_widget.css"; import "../../styles/subscriptions.css"; +import "../../styles/scheduled_messages.css"; import "../../styles/drafts.css"; import "../../styles/input_pill.css"; import "../../styles/informational_overlays.css"; diff --git a/web/src/compose_banner.ts b/web/src/compose_banner.ts index 79112d92fa..52d4ea2236 100644 --- a/web/src/compose_banner.ts +++ b/web/src/compose_banner.ts @@ -15,6 +15,7 @@ export const ERROR = "error"; const MESSAGE_SENT_CLASSNAMES = { sent_scroll_to_view: "sent_scroll_to_view", narrow_to_recipient: "narrow_to_recipient", + scheduled_message_banner: "scheduled_message_banner", }; export const CLASSNAMES = { diff --git a/web/src/hash_util.js b/web/src/hash_util.js index 2c3464fa45..64be597d8a 100644 --- a/web/src/hash_util.js +++ b/web/src/hash_util.js @@ -208,6 +208,7 @@ export function is_overlay_hash(hash) { "message-formatting", "search-operators", "about-zulip", + "scheduled", ]; const main_hash = get_hash_category(hash); diff --git a/web/src/hashchange.js b/web/src/hashchange.js index 15a5946834..27b23157f8 100644 --- a/web/src/hashchange.js +++ b/web/src/hashchange.js @@ -16,6 +16,7 @@ import * as overlays from "./overlays"; import {page_params} from "./page_params"; import * as recent_topics_ui from "./recent_topics_ui"; import * as recent_topics_util from "./recent_topics_util"; +import * as scheduled_messages_overlay_ui from "./scheduled_messages_overlay_ui"; import * as search from "./search"; import * as settings from "./settings"; import * as settings_panel_menu from "./settings_panel_menu"; @@ -212,6 +213,7 @@ function do_hashchange_normal(from_reload) { case "#organization": case "#settings": case "#about-zulip": + case "#scheduled": blueslip.error("overlay logic skipped for: " + hash); break; default: @@ -368,6 +370,10 @@ function do_hashchange_overlay(old_hash) { if (base === "about-zulip") { about_zulip.launch(); } + + if (base === "scheduled") { + scheduled_messages_overlay_ui.launch(); + } } function hashchanged(from_reload, e) { diff --git a/web/src/overlays.ts b/web/src/overlays.ts index 2b30023eff..55ae129432 100644 --- a/web/src/overlays.ts +++ b/web/src/overlays.ts @@ -85,6 +85,10 @@ export function drafts_open(): boolean { return open_overlay_name === "drafts"; } +export function scheduled_messages_open(): boolean { + return open_overlay_name === "scheduled"; +} + export function active_modal(): string | undefined { if (!is_modal_open()) { blueslip.error("Programming error — Called active_modal when there is no modal open"); diff --git a/web/src/scheduled_messages.js b/web/src/scheduled_messages.js new file mode 100644 index 0000000000..c37974194a --- /dev/null +++ b/web/src/scheduled_messages.js @@ -0,0 +1,102 @@ +import $ from "jquery"; + +import * as channel from "./channel"; +import * as compose from "./compose"; +import * as compose_actions from "./compose_actions"; +import * as compose_fade from "./compose_fade"; +import * as compose_ui from "./compose_ui"; +import * as narrow from "./narrow"; +import * as overlays from "./overlays"; +import * as people from "./people"; +import * as popover_menus from "./popover_menus"; + +// This is only updated when user opens the scheduled messages overlay. +export let scheduled_messages_data = []; + +export function override_scheduled_messages_data(data) { + scheduled_messages_data = data; +} + +export function edit_scheduled_message(scheduled_msg_id) { + const scheduled_msg = scheduled_messages_data.find( + (msg) => msg.message_id === scheduled_msg_id, + ); + + let compose_args; + + if (scheduled_msg.type === "stream") { + compose_args = { + type: "stream", + stream: scheduled_msg.stream_name, + topic: scheduled_msg.topic, + content: scheduled_msg.content, + }; + } else { + const recipient_emails = []; + if (scheduled_msg.to) { + for (const recipient_id of scheduled_msg.to) { + recipient_emails.push(people.get_by_user_id(recipient_id).email); + } + } + compose_args = { + type: scheduled_msg.type, + private_message_recipient: recipient_emails.join(","), + content: scheduled_msg.content, + }; + } + + if (compose_args.type === "stream") { + narrow.activate( + [ + {operator: "stream", operand: compose_args.stream}, + {operator: "topic", operand: compose_args.topic}, + ], + {trigger: "edit scheduled message"}, + ); + } else { + narrow.activate([{operator: "pm-with", operand: compose_args.private_message_recipient}], { + trigger: "edit scheduled message", + }); + } + + overlays.close_overlay("scheduled"); + compose_fade.clear_compose(); + compose.clear_preview_area(); + compose_actions.start(compose_args.type, compose_args); + compose_ui.autosize_textarea($("#compose-textarea")); + $("#compose-textarea").attr("data-scheduled-message-id", scheduled_msg_id); + popover_menus.show_schedule_confirm_button(scheduled_msg.formatted_send_at_time, true); +} + +export function delete_scheduled_message(scheduled_msg_id) { + channel.del({ + url: "/json/scheduled_messages/" + scheduled_msg_id, + success() { + // TODO: Do this via events received from the server in server_events_dispatch. + if (overlays.scheduled_messages_open()) { + $( + `#scheduled_messages_overlay .scheduled-message-row[data-message-id=${scheduled_msg_id}]`, + ).remove(); + } + if ($("#compose-textarea").attr("data-scheduled-message-id")) { + const compose_scheduled_msg_id = $("#compose-textarea").attr( + "data-scheduled-message-id", + ); + // If user deleted the scheduled message which is being edited in compose, we clear + // the scheduled message id from there which converts this editing state into a normal + // schedule message state. So, clicking "Schedule" will now create a new scheduled message. + if (compose_scheduled_msg_id === scheduled_msg_id) { + $("#compose-textarea").removeAttr("data-scheduled-message-id"); + } + } + }, + }); +} + +export function delete_scheduled_message_if_sent_directly() { + // Delete old scheduled message if it was sent. + if ($("#compose-textarea").attr("data-scheduled-message-id")) { + delete_scheduled_message($("#compose-textarea").attr("data-scheduled-message-id")); + $("#compose-textarea").removeAttr("data-scheduled-message-id"); + } +} diff --git a/web/src/scheduled_messages_overlay_ui.js b/web/src/scheduled_messages_overlay_ui.js new file mode 100644 index 0000000000..965d2e2c3e --- /dev/null +++ b/web/src/scheduled_messages_overlay_ui.js @@ -0,0 +1,109 @@ +import * as date_fns from "date-fns"; +import $ from "jquery"; + +import render_scheduled_message from "../templates/scheduled_message.hbs"; +import render_scheduled_messages_overlay from "../templates/scheduled_messages_overlay.hbs"; + +import * as blueslip from "./blueslip"; +import * as browser_history from "./browser_history"; +import * as channel from "./channel"; +import * as loading from "./loading"; +import * as overlays from "./overlays"; +import * as people from "./people"; +import * as scheduled_messages from "./scheduled_messages"; +import * as stream_color from "./stream_color"; +import * as stream_data from "./stream_data"; +import * as timerender from "./timerender"; + +function hide_loading_indicator() { + loading.destroy_indicator($("#scheduled_messages_overlay .loading-indicator")); + $(".scheduled-messages-loading").hide(); +} + +function format(scheduled_messages) { + const formatted_msgs = []; + for (const msg of scheduled_messages) { + const msg_render_context = {...msg}; + if (msg.type === "stream") { + msg_render_context.is_stream = true; + msg_render_context.stream_id = msg.to[0]; + msg_render_context.stream_name = stream_data.maybe_get_stream_name( + msg_render_context.stream_id, + ); + const color = stream_data.get_color(msg_render_context.stream_name); + msg_render_context.recipient_bar_color = stream_color.get_recipient_bar_color(color); + msg_render_context.stream_privacy_icon_color = + stream_color.get_stream_privacy_icon_color(color); + } else { + msg_render_context.is_stream = false; + msg_render_context.recipients = people.get_recipients(msg.to.join(",")); + } + const time = new Date(msg.deliver_at); + msg_render_context.full_date_time = timerender.get_full_datetime(time); + msg_render_context.formatted_send_at_time = date_fns.format(time, "MMM d yyyy h:mm a"); + formatted_msgs.push(msg_render_context); + } + return formatted_msgs; +} + +export function launch() { + $("#scheduled_messages_overlay_container").empty(); + $("#scheduled_messages_overlay_container").append(render_scheduled_messages_overlay()); + overlays.open_overlay({ + name: "scheduled", + $overlay: $("#scheduled_messages_overlay"), + on_close() { + browser_history.exit_overlay(); + }, + }); + loading.make_indicator($("#scheduled_messages_overlay .loading-indicator"), { + abs_positioned: true, + }); + + channel.get({ + url: "/json/scheduled_messages", + success(data) { + hide_loading_indicator(); + if (data.scheduled_messages.length === 0) { + $(".no-overlay-messages").show(); + } else { + // Saving formatted data is helpful when user is trying to edit a scheduled message. + scheduled_messages.override_scheduled_messages_data( + format(data.scheduled_messages), + ); + const rendered_list = render_scheduled_message({ + scheduled_messages_data: scheduled_messages.scheduled_messages_data, + }); + const $messages_list = $("#scheduled_messages_overlay .overlay-messages-list"); + $messages_list.append(rendered_list); + } + }, + error(xhr) { + hide_loading_indicator(); + blueslip.error(xhr); + }, + }); +} + +export function initialize() { + $("body").on("click", ".scheduled-message-row .restore-overlay-message", (e) => { + let scheduled_msg_id = $(e.currentTarget) + .closest(".scheduled-message-row") + .attr("data-message-id"); + scheduled_msg_id = Number.parseInt(scheduled_msg_id, 10); + scheduled_messages.edit_scheduled_message(scheduled_msg_id); + + e.stopPropagation(); + e.preventDefault(); + }); + + $("body").on("click", ".scheduled-message-row .delete-overlay-message", (e) => { + const scheduled_msg_id = $(e.currentTarget) + .closest(".scheduled-message-row") + .attr("data-message-id"); + scheduled_messages.delete_scheduled_message(scheduled_msg_id); + + e.stopPropagation(); + e.preventDefault(); + }); +} diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 550d9b8c2b..b600e2c0b5 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -72,6 +72,7 @@ import * as reload from "./reload"; import * as rendered_markdown from "./rendered_markdown"; import * as resize from "./resize"; import * as rows from "./rows"; +import * as scheduled_messages_overlay_ui from "./scheduled_messages_overlay_ui"; import * as scroll_bar from "./scroll_bar"; import * as search from "./search"; import * as search_pill_widget from "./search_pill_widget"; @@ -647,6 +648,7 @@ export function initialize_everything() { spoilers.initialize(); lightbox.initialize(); click_handlers.initialize(); + scheduled_messages_overlay_ui.initialize(); copy_and_paste.initialize(); overlays.initialize(); invite.initialize(); diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index d4500775f8..127ae56104 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -1264,6 +1264,7 @@ background-color: hsl(212deg 28% 18%); } + .scheduled-messages-loading-logo, .alert-zulip-logo, .top-messages-logo, .bottom-messages-logo { diff --git a/web/styles/scheduled_messages.css b/web/styles/scheduled_messages.css new file mode 100644 index 0000000000..34f452375f --- /dev/null +++ b/web/styles/scheduled_messages.css @@ -0,0 +1,16 @@ +#scheduled_messages_overlay_container { + .scheduled-messages-loading { + margin-top: 10px; + display: grid; + + > * { + grid-row-start: 1; + grid-column-start: 1; + margin: auto; + } + } + + .no-overlay-messages { + display: none; + } +} diff --git a/web/styles/zulip.css b/web/styles/zulip.css index 62d15238fb..46b4da181c 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -201,6 +201,7 @@ p.n-margin { padding-top: var(--header-padding-bottom); } +.scheduled-messages-loading-logo, .alert-zulip-logo, .top-messages-logo, .bottom-messages-logo { diff --git a/web/templates/scheduled_message.hbs b/web/templates/scheduled_message.hbs new file mode 100644 index 0000000000..cbff66b79e --- /dev/null +++ b/web/templates/scheduled_message.hbs @@ -0,0 +1,57 @@ +{{#each scheduled_messages_data}} +
+
+ {{#if is_stream}} +
+
+
+ + {{> stream_privacy}} + + {{stream_name}} +
+ + +
+ {{topic}} +
+
+ +
{{ formatted_send_at_time }}
+
+
+ {{else}} +
+
+
+ + {{t "You and {recipients}" }} +
+
{{ formatted_send_at_time }}
+
+
+ {{/if}} +
+
+
+
+
+ + + + +
+
+
{{rendered_markdown rendered_content}}
+
+
+
+
+
+{{/each}} diff --git a/web/templates/scheduled_messages_overlay.hbs b/web/templates/scheduled_messages_overlay.hbs new file mode 100644 index 0000000000..5a89635993 --- /dev/null +++ b/web/templates/scheduled_messages_overlay.hbs @@ -0,0 +1,32 @@ +
+
+ +
+