From 7c90d2cee5587a0bfd75713d01d779f9d6c7af67 Mon Sep 17 00:00:00 2001 From: Evy Kassirer Date: Fri, 19 Sep 2025 11:15:55 -0700 Subject: [PATCH] compose_setup: Convert module to typescript. --- tools/test-js-with-node | 2 +- web/src/compose_actions.ts | 4 +- web/src/compose_recipient.ts | 2 +- .../{compose_setup.js => compose_setup.ts} | 112 ++++++++++-------- web/src/dialog_widget.ts | 4 +- web/src/ui_init.js | 2 +- web/tests/compose_video.test.cjs | 23 ++-- 7 files changed, 79 insertions(+), 70 deletions(-) rename web/src/{compose_setup.js => compose_setup.ts} (88%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index d524f9dfc4..b434f9abce 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -80,7 +80,7 @@ EXEMPT_FILES = make_set( "web/src/compose_recipient.ts", "web/src/compose_reply.ts", "web/src/compose_send_menu_popover.ts", - "web/src/compose_setup.js", + "web/src/compose_setup.ts", "web/src/compose_state.ts", "web/src/compose_textarea.ts", "web/src/compose_tooltips.ts", diff --git a/web/src/compose_actions.ts b/web/src/compose_actions.ts index 97bdfc7738..3d2ce2db80 100644 --- a/web/src/compose_actions.ts +++ b/web/src/compose_actions.ts @@ -230,10 +230,10 @@ export let complete_starting_tasks = (opts: ComposeActionsOpts): void => { if (is_new_topic_triggered) { compose_recipient.set_high_attention_recipient_row(); } - // We explicitly call this function here apart from compose_setup.js + // We explicitly call this function here apart from compose_setup.ts // 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. + // the function call in compose_setup.ts not displaying banner. if (!narrow_state.narrowed_by_reply()) { compose_notifications.maybe_show_one_time_interleaved_view_messages_fading_banner(); } diff --git a/web/src/compose_recipient.ts b/web/src/compose_recipient.ts index 7711364c53..2f73566a29 100644 --- a/web/src/compose_recipient.ts +++ b/web/src/compose_recipient.ts @@ -79,7 +79,7 @@ export let update_recipient_row_attention_level = (): void => { // row is focused, that puts users outside the low-attention // recipient-row state--including the `c` hotkey or the // Start new conversation button being clicked. But that - // logic is handled via the event handlers in compose_setup.js + // logic is handled via the event handlers in compose_setup.ts // that call set_high_attention_recipient_row(). if ( (composing_to_current_topic_narrow() || diff --git a/web/src/compose_setup.js b/web/src/compose_setup.ts similarity index 88% rename from web/src/compose_setup.js rename to web/src/compose_setup.ts index b35e73d86c..151f2b6685 100644 --- a/web/src/compose_setup.js +++ b/web/src/compose_setup.ts @@ -1,5 +1,7 @@ import $ from "jquery"; import _ from "lodash"; +import assert from "minimalistic-assert"; +import * as z from "zod/mini"; import {unresolve_name} from "../shared/src/resolved_topic.ts"; import render_add_poll_modal from "../templates/add_poll_modal.hbs"; @@ -41,11 +43,11 @@ import * as user_topics from "./user_topics.ts"; import * as util from "./util.ts"; import * as widget_modal from "./widget_modal.ts"; -export function abort_xhr() { +export function abort_xhr(): void { upload.compose_upload_cancel(); } -function setup_compose_actions_hooks() { +function setup_compose_actions_hooks(): void { compose_actions.register_compose_box_clear_hook(compose.clear_invites); compose_actions.register_compose_box_clear_hook(compose.clear_private_stream_alert); compose_actions.register_compose_box_clear_hook(compose.clear_preview_area); @@ -54,7 +56,7 @@ function setup_compose_actions_hooks() { compose_actions.register_compose_cancel_hook(compose_call.abort_video_callbacks); } -export function initialize() { +export function initialize(): void { // Register hooks for compose_actions. setup_compose_actions_hooks(); @@ -66,10 +68,16 @@ export function initialize() { ); $("textarea#compose-textarea").on("keydown", (event) => { - compose_ui.handle_keydown(event, $("textarea#compose-textarea").expectOne()); + compose_ui.handle_keydown( + event, + $("textarea#compose-textarea").expectOne(), + ); }); $("textarea#compose-textarea").on("keyup", (event) => { - compose_ui.handle_keyup(event, $("textarea#compose-textarea").expectOne()); + compose_ui.handle_keyup( + event, + $("textarea#compose-textarea").expectOne(), + ); }); $("textarea#compose-textarea").on("input", () => { @@ -116,9 +124,12 @@ export function initialize() { resize.reset_compose_message_max_height(); }); }); - update_compose_max_height.observe(document.querySelector("#compose")); + update_compose_max_height.observe(document.querySelector("#compose")!); - function get_input_info(event) { + function get_input_info(event: JQuery.ClickEvent): { + is_edit_input: boolean; + $banner_container: JQuery; + } { const $edit_banners_container = $(event.target).closest(".edit_form_banners"); const is_edit_input = $edit_banners_container.length > 0; const $banner_container = is_edit_input ? $edit_banners_container : $("#compose_banners"); @@ -133,11 +144,12 @@ export function initialize() { (event) => { event.preventDefault(); const {$banner_container, is_edit_input} = get_input_info(event); + assert(event.target instanceof HTMLElement); const $row = $(event.target).closest(".message_row"); compose_validate.clear_stream_wildcard_warnings($banner_container); compose_validate.set_user_acknowledged_stream_wildcard_flag(true); if (is_edit_input) { - message_edit.save_message_row_edit($row); + void message_edit.save_message_row_edit($row); } else if (event.target.dataset.validationTrigger === "schedule") { compose_send_menu_popover.open_schedule_message_menu( undefined, @@ -168,6 +180,7 @@ export function initialize() { return; } const sub = stream_data.get_sub_by_id(stream_id); + assert(sub !== undefined); stream_settings_components.sub_or_unsub(sub); $(user_not_subscribed_selector).remove(); }, @@ -180,8 +193,8 @@ export function initialize() { event.preventDefault(); const $target = $(event.target).parents(".main-view-banner"); - const stream_id = Number.parseInt($target.attr("data-stream-id"), 10); - const topic_name = $target.attr("data-topic-name"); + const stream_id = Number.parseInt($target.attr("data-stream-id")!, 10); + const topic_name = $target.attr("data-topic-name")!; message_edit.with_first_message_id(stream_id, topic_name, (message_id) => { if (message_id === undefined) { @@ -214,7 +227,7 @@ export function initialize() { } else { message_edit.toggle_resolve_topic(message_id, topic_name, true); } - compose_validate.clear_topic_resolved_warning(true); + compose_validate.clear_topic_resolved_warning(); }); }, ); @@ -228,8 +241,8 @@ export function initialize() { event.preventDefault(); const $target = $(event.target).parents(".main-view-banner"); - const stream_id = Number.parseInt($target.attr("data-stream-id"), 10); - const topic_name = $target.attr("data-topic-name"); + const stream_id = Number.parseInt($target.attr("data-stream-id")!, 10); + const topic_name = $target.attr("data-topic-name")!; user_topics.set_user_topic_visibility_policy( stream_id, @@ -248,9 +261,7 @@ export function initialize() { (event) => { event.preventDefault(); if ($(event.target).attr("data-action") === "mark-as-read") { - $(event.target) - .parents(`${automatic_new_visibility_policy_banner_selector}`) - .remove(); + $(event.target).parents(automatic_new_visibility_policy_banner_selector).remove(); onboarding_steps.post_onboarding_step_as_read("visibility_policy_banner"); return; } @@ -266,6 +277,10 @@ export function initialize() { (event) => { event.preventDefault(); const send_at_timestamp = scheduled_messages.get_selected_send_later_timestamp(); + // When clicking the button to reschedule, the send later timestamp from the + // recently unscheduled message is saved in `selected_send_later_timestamp` and + // won't be undefined. + assert(send_at_timestamp !== undefined); compose_send_menu_popover.do_schedule_message(send_at_timestamp); }, ); @@ -283,14 +298,15 @@ export function initialize() { const user_id = Number($invite_row.attr("data-user-id")); const stream_id = Number($invite_row.attr("data-stream-id")); - function success() { + function success(): void { $invite_row.remove(); } - function xhr_failure(xhr) { + function xhr_failure(xhr: JQuery.jqXHR): void { let error_message = "Failed to subscribe user!"; - if (xhr.responseJSON?.msg) { - error_message = xhr.responseJSON.msg; + const parsed = z.object({msg: z.string()}).safeParse(xhr.responseJSON); + if (parsed.success) { + error_message = parsed.data.msg; } compose.clear_invites(); compose_banner.show_error_message( @@ -303,6 +319,7 @@ export function initialize() { } const sub = sub_store.get(stream_id); + assert(sub !== undefined); subscriber_api.add_user_ids_to_stream([user_id], sub, true, success, xhr_failure); }, @@ -323,7 +340,7 @@ export function initialize() { `${jump_to_conversation_banner_selector} .main-view-banner-action-button`, (event) => { event.preventDefault(); - $(event.target).parents(`${jump_to_conversation_banner_selector}`).remove(); + $(event.target).parents(jump_to_conversation_banner_selector).remove(); onboarding_steps.post_onboarding_step_as_read("jump_to_conversation_banner"); }, ); @@ -334,9 +351,7 @@ export function initialize() { `${non_interleaved_view_messages_fading_banner_selector} .main-view-banner-action-button`, (event) => { event.preventDefault(); - $(event.target) - .parents(`${non_interleaved_view_messages_fading_banner_selector}`) - .remove(); + $(event.target).parents(non_interleaved_view_messages_fading_banner_selector).remove(); onboarding_steps.post_onboarding_step_as_read("non_interleaved_view_messages_fading"); }, ); @@ -347,7 +362,7 @@ export function initialize() { `${interleaved_view_messages_fading_banner_selector} .main-view-banner-action-button`, (event) => { event.preventDefault(); - $(event.target).parents(`${interleaved_view_messages_fading_banner_selector}`).remove(); + $(event.target).parents(interleaved_view_messages_fading_banner_selector).remove(); onboarding_steps.post_onboarding_step_as_read("interleaved_view_messages_fading"); }, ); @@ -370,7 +385,7 @@ export function initialize() { $("#compose .file_input").trigger("click"); }); - $("body").on("click", ".video_link", (e) => { + $("body").on("click", ".video_link", function (this: HTMLElement, e): void { e.preventDefault(); e.stopPropagation(); @@ -380,10 +395,10 @@ export function initialize() { return; } - compose_call_ui.generate_and_insert_audio_or_video_call_link($(e.target), false); + compose_call_ui.generate_and_insert_audio_or_video_call_link($(this), false); }); - $("body").on("click", ".audio_link", (e) => { + $("body").on("click", ".audio_link", function (this: HTMLElement, e): void { e.preventDefault(); e.stopPropagation(); @@ -393,31 +408,34 @@ export function initialize() { return; } - compose_call_ui.generate_and_insert_audio_or_video_call_link($(e.target), true); + compose_call_ui.generate_and_insert_audio_or_video_call_link($(this), true); }); - $("body").on("click", ".time_pick", function (e) { + $("body").on("click", ".time_pick", function (this: HTMLElement, e) { e.preventDefault(); e.stopPropagation(); let $target_textarea; - let edit_message_id; const $compose_click_target = $(this); if ($compose_click_target.parents(".message_edit_form").length === 1) { - edit_message_id = rows.id($compose_click_target.parents(".message_row")); - $target_textarea = $(`#edit_form_${CSS.escape(edit_message_id)} .message_edit_content`); + const edit_message_id = rows.id($compose_click_target.parents(".message_row")); + $target_textarea = $( + `#edit_form_${edit_message_id} textarea.message_edit_content`, + ); } else { - $target_textarea = $compose_click_target.closest("form").find("textarea"); + $target_textarea = $compose_click_target + .closest("form") + .find("textarea"); } if (!flatpickr.is_open()) { - const on_timestamp_selection = (val) => { - const timestr = ` `; + const on_timestamp_selection = (time: string): void => { + const timestr = ` `; compose_ui.insert_syntax_and_focus(timestr, $target_textarea); }; flatpickr.show_flatpickr( - $compose_click_target[0], + util.the($compose_click_target), on_timestamp_selection, get_timestamp_for_flatpickr(), { @@ -425,7 +443,7 @@ export function initialize() { position: "auto center", // Since we want to handle close of flatpickr manually, we don't want // flatpickr to hide automatically on clicking its trigger element. - ignoredFocusElements: [e.currentTarget], + ignoredFocusElements: [this], }, ); } else { @@ -437,8 +455,8 @@ export function initialize() { e.preventDefault(); e.stopPropagation(); - function validate_input() { - const question = $("#poll-question-input").val().trim(); + function validate_input(): boolean { + const question = $("#poll-question-input").val()!.trim(); if (question === "") { ui_report.error( @@ -494,7 +512,7 @@ export function initialize() { e.preventDefault(); e.stopPropagation(); - function validate_input(e) { + function validate_input(e: JQuery.ClickEvent): boolean { let is_valid = true; e.preventDefault(); e.stopPropagation(); @@ -584,7 +602,7 @@ export function initialize() { // input is blurred, we immediately update the topic's // displayed text and compose-area placeholder when the // compose textarea is focused. - const $input = $("input#stream_message_recipient_topic"); + const $input = $("input#stream_message_recipient_topic"); compose_recipient.update_topic_displayed_text($input.val()); compose_recipient.update_compose_area_placeholder_text(); compose_fade.do_update_all(); @@ -597,7 +615,7 @@ export function initialize() { $(".compose-scrollable-buttons").on( "scroll", - _.throttle((e) => { + _.throttle((e: JQuery.ScrollEvent) => { compose_ui.handle_scrolling_formatting_buttons(e); }, 150), ); @@ -618,14 +636,14 @@ export function initialize() { // To track delayed effects originating from the "blur" event // and its use of setTimeout, we need to set up a variable to // reference the timeout's ID across events. - let recipient_focused_timeout; + let recipient_focused_timeout: ReturnType; $("input#stream_message_recipient_topic").on("focus", () => { // We don't want the `recently-focused` class removed via // a setTimeout from the "blur" event, if we're suddenly // focused again. clearTimeout(recipient_focused_timeout); const $compose_recipient = $("#compose-recipient"); - const $input = $("input#stream_message_recipient_topic"); + const $input = $("input#stream_message_recipient_topic"); compose_recipient.update_topic_displayed_text($input.val(), true); compose_recipient.update_compose_area_placeholder_text(); // When the topic input is focused, we no longer treat @@ -657,7 +675,7 @@ export function initialize() { $("input#stream_message_recipient_topic, #private_message_recipient").on("blur", () => { const $compose_recipient = $("#compose-recipient"); - const $input = $("input#stream_message_recipient_topic"); + const $input = $("input#stream_message_recipient_topic"); // To correct for an edge case when clearing the topic box // via the left sidebar, we do the following actions after a // delay; these will not have an effect for DMs, and so can @@ -683,7 +701,7 @@ export function initialize() { $("body").on("click", ".formatting_button", function (e) { const $compose_click_target = $(this); const $textarea = $compose_click_target.closest("form").find("textarea"); - const format_type = $(this).attr("data-format-type"); + const format_type = $(this).attr("data-format-type")!; compose_ui.format_text($textarea, format_type); popovers.hide_all(); $textarea.trigger("focus"); diff --git a/web/src/dialog_widget.ts b/web/src/dialog_widget.ts index ee54332987..5d95a38fac 100644 --- a/web/src/dialog_widget.ts +++ b/web/src/dialog_widget.ts @@ -73,7 +73,7 @@ export type DialogWidgetConfig = { id?: string; single_footer_button?: boolean; form_id?: string; - validate_input?: (e: unknown) => boolean; + validate_input?: (e: JQuery.ClickEvent) => boolean; on_show?: () => void; on_shown?: () => void; on_hide?: () => void; @@ -247,7 +247,7 @@ export function launch(conf: DialogWidgetConfig): string { } // Set up handlers. - $submit_button.on("click", (e) => { + $submit_button.on("click", (e: JQuery.ClickEvent) => { e.preventDefault(); if (conf.validate_input && !conf.validate_input(e)) { diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 3a92488620..af7d233871 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -34,7 +34,7 @@ import * as compose_pm_pill from "./compose_pm_pill.ts"; import * as compose_recipient from "./compose_recipient.ts"; import * as compose_reply from "./compose_reply.ts"; import * as compose_send_menu_popover from "./compose_send_menu_popover.ts"; -import * as compose_setup from "./compose_setup.js"; +import * as compose_setup from "./compose_setup.ts"; import * as compose_textarea from "./compose_textarea.ts"; import * as compose_tooltips from "./compose_tooltips.ts"; import * as compose_validate from "./compose_validate.ts"; diff --git a/web/tests/compose_video.test.cjs b/web/tests/compose_video.test.cjs index b506b0e4f4..f366294268 100644 --- a/web/tests/compose_video.test.cjs +++ b/web/tests/compose_video.test.cjs @@ -113,9 +113,6 @@ test("videos", ({override}) => { const ev = { preventDefault() {}, stopPropagation() {}, - target: { - to_$: () => $textarea, - }, }; override(compose_ui, "insert_syntax_and_focus", (syntax) => { @@ -139,7 +136,7 @@ test("videos", ({override}) => { override(realm, "realm_jitsi_server_url", null); override(realm, "server_jitsi_server_url", "https://server.example.com"); - handler(ev); + handler.call($textarea, ev); // video link ids consist of 15 random digits let video_link_regex = /\[translated: Join video call\.]\(https:\/\/server.example.com\/\d{15}#config.startWithVideoMuted=false\)/; @@ -148,7 +145,7 @@ test("videos", ({override}) => { override(realm, "realm_jitsi_server_url", "https://realm.example.com"); override(realm, "server_jitsi_server_url", null); - handler(ev); + handler.call($textarea, ev); video_link_regex = /\[translated: Join video call\.]\(https:\/\/realm.example.com\/\d{15}#config.startWithVideoMuted=false\)/; assert.ok(called); @@ -156,7 +153,7 @@ test("videos", ({override}) => { override(realm, "realm_jitsi_server_url", "https://realm.example.com"); override(realm, "server_jitsi_server_url", "https://server.example.com"); - handler(ev); + handler.call($textarea, ev); video_link_regex = /\[translated: Join video call\.]\(https:\/\/realm.example.com\/\d{15}#config.startWithVideoMuted=false\)/; assert.ok(called); @@ -173,9 +170,6 @@ test("videos", ({override}) => { const ev = { preventDefault() {}, stopPropagation() {}, - target: { - to_$: () => $textarea, - }, }; override(compose_ui, "insert_syntax_and_focus", (syntax) => { @@ -206,14 +200,14 @@ test("videos", ({override}) => { $("textarea#compose-textarea").val(""); const video_handler = $("body").get_on_handler("click", ".video_link"); - video_handler(ev); + video_handler.call($textarea, ev); const video_link_regex = /\[translated: Join video call\.]\(example\.zoom\.com\)/; assert.ok(called); assert.match(syntax_to_insert, video_link_regex); $("textarea#compose-textarea").val(""); const audio_handler = $("body").get_on_handler("click", ".audio_link"); - audio_handler(ev); + audio_handler.call($textarea, ev); const audio_link_regex = /\[translated: Join voice call\.]\(example\.zoom\.com\)/; assert.ok(called); assert.match(syntax_to_insert, audio_link_regex); @@ -229,9 +223,6 @@ test("videos", ({override}) => { const ev = { preventDefault() {}, stopPropagation() {}, - target: { - to_$: () => $textarea, - }, }; override(compose_ui, "insert_syntax_and_focus", (syntax) => { @@ -265,14 +256,14 @@ test("videos", ({override}) => { $("textarea#compose-textarea").val(""); const video_handler = $("body").get_on_handler("click", ".video_link"); - video_handler(ev); + video_handler.call($textarea, ev); const video_link_regex = /\[translated: Join video call\.]\(\/calls\/bigbluebutton\/join\?meeting_id=%22zulip-1%22&moderator=%22AAAAAAAAAA%22&lock_settings_disable_cam=false&checksum=%2232702220bff2a22a44aee72e96cfdb4c4091752e%22\)/; assert.ok(called); assert.match(syntax_to_insert, video_link_regex); const audio_handler = $("body").get_on_handler("click", ".audio_link"); - audio_handler(ev); + audio_handler.call($textarea, ev); const audio_link_regex = /\[translated: Join voice call\.]\(\/calls\/bigbluebutton\/join\?meeting_id=%22zulip-1%22&moderator=%22AAAAAAAAAA%22&lock_settings_disable_cam=true&checksum=%2232702220bff2a22a44aee72e96cfdb4c4091752e%22\)/; assert.ok(called);