diff --git a/frontend_tests/node_tests/compose.js b/frontend_tests/node_tests/compose.js index 1b44c3f435..9efe078088 100644 --- a/frontend_tests/node_tests/compose.js +++ b/frontend_tests/node_tests/compose.js @@ -43,6 +43,7 @@ const server_events = mock_esm("../../static/js/server_events"); const transmit = mock_esm("../../static/js/transmit"); const upload = mock_esm("../../static/js/upload"); +const compose_ui = zrequire("compose_ui"); const compose_closed_ui = zrequire("compose_closed_ui"); const compose_state = zrequire("compose_state"); const compose = zrequire("compose"); @@ -361,6 +362,7 @@ test_ui("finish", ({override, override_rewire, mock_template}) => { $("#compose-send-button .loader").hide(); $("#compose-textarea").off("select"); $("#compose-textarea").val(""); + compose_ui.compose_spinner_visible = false; const res = compose.finish(); assert.equal(res, false); assert.ok(!$("#compose_banners .recipient_not_subscribed").visible()); @@ -374,6 +376,7 @@ test_ui("finish", ({override, override_rewire, mock_template}) => { $("#compose .preview_message_area").show(); $("#compose .markdown_preview").hide(); $("#compose-textarea").val("foobarfoobar"); + compose_ui.compose_spinner_visible = false; compose_state.set_message_type("private"); override(compose_pm_pill, "get_emails", () => "bob@example.com"); override(compose_pm_pill, "get_user_ids", () => []); @@ -399,6 +402,7 @@ test_ui("finish", ({override, override_rewire, mock_template}) => { $("#compose .preview_message_area").show(); $("#compose .markdown_preview").hide(); $("#compose-textarea").val("foobarfoobar"); + compose_ui.compose_spinner_visible = false; compose_state.set_message_type("stream"); compose_state.set_stream_name("social"); override_rewire(people, "get_by_user_id", () => []); diff --git a/static/js/compose.js b/static/js/compose.js index 13fd1f4def..e61e597ea3 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -290,7 +290,17 @@ export function enter_with_preview_open(ctrl_pressed = false) { } } +// Common entrypoint for asking the server to send the message +// currently drafted in the compose box, including for scheduled +// messages. export function finish() { + if (compose_ui.compose_spinner_visible) { + // Avoid sending a message twice in parallel in races where + // the user clicks the `Send` button very quickly twice or + // presses enter and the send button simultaneously. + return undefined; + } + clear_preview_area(); clear_invites(); clear_private_stream_alert(); diff --git a/static/js/compose_ui.js b/static/js/compose_ui.js index 22661ab314..8e7ca68cb5 100644 --- a/static/js/compose_ui.js +++ b/static/js/compose_ui.js @@ -10,6 +10,7 @@ import * as popover_menus from "./popover_menus"; import * as rtl from "./rtl"; import * as user_status from "./user_status"; +export let compose_spinner_visible = false; let full_size_status = false; // true or false // Some functions to handle the full size status explicitly @@ -404,12 +405,14 @@ export function format_text($textarea, type) { } export function hide_compose_spinner() { + compose_spinner_visible = false; $("#compose-send-button .loader").hide(); $("#compose-send-button span").show(); $("#compose-send-button").removeClass("disable-btn"); } export function show_compose_spinner() { + compose_spinner_visible = true; // Always use white spinner. loading.show_button_spinner($("#compose-send-button .loader"), true); $("#compose-send-button span").hide();