diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 2be722b86e..be881d3b05 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -73,7 +73,7 @@ EXEMPT_FILES = make_set( "web/src/compose_fade.ts", "web/src/compose_notifications.ts", "web/src/compose_popovers.js", - "web/src/compose_recipient.js", + "web/src/compose_recipient.ts", "web/src/compose_reply.js", "web/src/compose_setup.js", "web/src/compose_state.ts", diff --git a/web/src/compose_recipient.js b/web/src/compose_recipient.ts similarity index 78% rename from web/src/compose_recipient.js rename to web/src/compose_recipient.ts index ce37e1c452..9d176b0b80 100644 --- a/web/src/compose_recipient.js +++ b/web/src/compose_recipient.ts @@ -1,7 +1,9 @@ /* Compose box module responsible for the message's recipient */ import $ from "jquery"; -import _ from "lodash"; +import _, {isNumber} from "lodash"; +import assert from "minimalistic-assert"; +import type {Instance, Placement} from "tippy.js"; import render_inline_decorated_stream_name from "../templates/inline_decorated_stream_name.hbs"; @@ -10,9 +12,11 @@ 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"; +import type {ComposePlaceholderOptions, ComposeTriggeredOptions} from "./compose_ui"; import * as compose_validate from "./compose_validate"; import * as drafts from "./drafts"; import * as dropdown_widget from "./dropdown_widget"; +import type {Option} from "./dropdown_widget"; import {$t} from "./i18n"; import * as narrow_state from "./narrow_state"; import * as people from "./people"; @@ -23,19 +27,29 @@ import * as sub_store from "./sub_store"; import * as ui_util from "./ui_util"; import * as util from "./util"; -function composing_to_current_topic_narrow() { +type MessageType = "stream" | "private"; +type DirectMessagesOption = { + is_direct_message: boolean; + unique_id: string | number; + name: string; +}; + +function composing_to_current_topic_narrow(): boolean { return ( - util.lower_same(compose_state.stream_name(), narrow_state.stream_name() || "") && - util.lower_same(compose_state.topic(), narrow_state.topic() || "") + util.lower_same(compose_state.stream_name(), narrow_state.stream_name() ?? "") && + util.lower_same(compose_state.topic(), narrow_state.topic() ?? "") ); } -function composing_to_current_private_message_narrow() { +function composing_to_current_private_message_narrow(): boolean { const compose_state_recipient = compose_state.private_message_recipient(); const narrow_state_recipient = narrow_state.pm_emails_string(); + if (narrow_state_recipient === undefined) { + return false; + } return ( - compose_state_recipient && - narrow_state_recipient && + Boolean(compose_state_recipient) && + Boolean(narrow_state_recipient) && _.isEqual( compose_state_recipient .split(",") @@ -49,7 +63,7 @@ function composing_to_current_private_message_narrow() { ); } -export function update_narrow_to_recipient_visibility() { +export function update_narrow_to_recipient_visibility(): void { const message_type = compose_state.get_message_type(); if (message_type === "stream") { const stream_exists = Boolean(compose_state.stream_id()); @@ -76,7 +90,7 @@ export function update_narrow_to_recipient_visibility() { $(".narrow_to_compose_recipients").toggleClass("invisible", true); } -function update_fade() { +function update_fade(): void { if (!compose_state.composing()) { return; } @@ -92,14 +106,14 @@ function update_fade() { compose_fade.update_all(); } -export function update_on_recipient_change() { +export function update_on_recipient_change(): void { update_fade(); update_narrow_to_recipient_visibility(); drafts.update_compose_draft_count(); check_posting_policy_for_compose_box(); } -export function get_posting_policy_error_message() { +export function get_posting_policy_error_message(): string { if (compose_state.selected_recipient_id === "direct") { const recipients = compose_pm_pill.get_user_ids_string(); if (!people.user_can_direct_message(recipients)) { @@ -110,6 +124,10 @@ export function get_posting_policy_error_message() { return ""; } + if (!isNumber(compose_state.selected_recipient_id)) { + return ""; + } + const stream = sub_store.get(compose_state.selected_recipient_id); if (stream && !stream_data.can_post_messages_in_stream(stream)) { return $t({ @@ -119,7 +137,7 @@ export function get_posting_policy_error_message() { return ""; } -export function check_posting_policy_for_compose_box() { +export function check_posting_policy_for_compose_box(): void { const banner_text = get_posting_policy_error_message(); if (banner_text === "") { compose_validate.set_recipient_disallowed(false); @@ -135,14 +153,15 @@ export function check_posting_policy_for_compose_box() { compose_banner.show_error_message(banner_text, banner_classname, $("#compose_banners")); } -function switch_message_type(message_type) { +function switch_message_type(message_type: MessageType): void { $("#compose-content .alert").hide(); compose_state.set_message_type(message_type); const opts = { message_type, - stream_id: compose_state.stream_id(), + trigger: "switch_message_type", + stream_id: compose_state.stream_id()!, topic: compose_state.topic(), private_message_recipient: compose_state.private_message_recipient(), }; @@ -151,7 +170,7 @@ function switch_message_type(message_type) { compose_ui.set_focus(opts); } -function update_recipient_label(stream_id) { +function update_recipient_label(stream_id: number): void { const stream = stream_data.get_sub_by_id(stream_id); if (stream === undefined) { $("#compose_select_recipient_widget .dropdown_widget_value").text( @@ -164,7 +183,7 @@ function update_recipient_label(stream_id) { } } -export function update_compose_for_message_type(opts) { +export function update_compose_for_message_type(opts: ComposeTriggeredOptions): void { if (opts.message_type === "stream") { $("#compose-direct-recipient").hide(); $("#compose_recipient_box").show(); @@ -192,10 +211,10 @@ export function update_compose_for_message_type(opts) { compose_banner.clear_uploads(); } -export function on_compose_select_recipient_update() { +export function on_compose_select_recipient_update(): void { const prev_message_type = compose_state.get_message_type(); - let curr_message_type = "stream"; + let curr_message_type: MessageType = "stream"; if (compose_state.selected_recipient_id === compose_state.DIRECT_MESSAGE_ID) { curr_message_type = "private"; } @@ -207,20 +226,23 @@ export function on_compose_select_recipient_update() { if (curr_message_type === "stream") { // Update stream name in the recipient box. const stream_id = compose_state.stream_id(); + assert(stream_id !== undefined); update_recipient_label(stream_id); } update_on_recipient_change(); } -export function possibly_update_stream_name_in_compose(stream_id) { +export function possibly_update_stream_name_in_compose(stream_id: number): void { if (compose_state.selected_recipient_id === stream_id) { on_compose_select_recipient_update(); } } -function item_click_callback(event, dropdown) { - let recipient_id = $(event.currentTarget).attr("data-unique-id"); +function item_click_callback(event: JQuery.ClickEvent, dropdown: Instance): void { + const recipient_id_str = $(event.currentTarget).attr("data-unique-id"); + assert(recipient_id_str !== undefined); + let recipient_id: string | number = recipient_id_str; if (recipient_id !== compose_state.DIRECT_MESSAGE_ID) { recipient_id = Number.parseInt(recipient_id, 10); } @@ -231,8 +253,9 @@ function item_click_callback(event, dropdown) { event.stopPropagation(); } -function get_options_for_recipient_widget() { - const options = stream_data.get_options_for_dropdown_widget(); +function get_options_for_recipient_widget(): Option[] { + const options: (Option | DirectMessagesOption)[] = + stream_data.get_options_for_dropdown_widget(); const direct_messages_option = { is_direct_message: true, @@ -251,7 +274,7 @@ function get_options_for_recipient_widget() { return options; } -function compose_recipient_dropdown_on_show(dropdown) { +function compose_recipient_dropdown_on_show(dropdown: Instance): void { // Offset to display dropdown above compose. let top_offset = 5; const window_height = window.innerHeight; @@ -263,12 +286,12 @@ function compose_recipient_dropdown_on_show(dropdown) { // pixels below compose starting from top of compose box. const bottom_space = window_height - recipient_input_top - search_box_and_padding_height; // Show dropdown on top / bottom based on available space. - let placement = "top-start"; + let placement: Placement = "top-start"; if (bottom_space > top_space) { placement = "bottom-start"; top_offset = -30; } - const offset = [-10, top_offset]; + const offset: [number, number] = [-10, top_offset]; dropdown.setProps({placement, offset}); const height = Math.min( dropdown_widget.DEFAULT_DROPDOWN_HEIGHT, @@ -278,16 +301,16 @@ function compose_recipient_dropdown_on_show(dropdown) { $popper.find(".dropdown-list-wrapper").css("max-height", height + "px"); } -export function open_compose_recipient_dropdown() { +export function open_compose_recipient_dropdown(): void { $("#compose_select_recipient_widget").trigger("click"); } -function focus_compose_recipient() { +function focus_compose_recipient(): void { $("#compose_select_recipient_widget_wrapper").trigger("focus"); } // NOTE: Since tippy triggers this on `mousedown` it is always triggered before say a `click` on `textarea`. -function on_hidden_callback() { +function on_hidden_callback(): void { if (compose_state.get_message_type() === "stream") { // Always move focus to the topic input even if it's not empty, // since it's likely the user will want to update the topic @@ -302,13 +325,13 @@ function on_hidden_callback() { } } -export function handle_middle_pane_transition() { - if (compose_state.composing) { +export function handle_middle_pane_transition(): void { + if (compose_state.composing()) { update_narrow_to_recipient_visibility(); } } -export function initialize() { +export function initialize(): void { new dropdown_widget.DropdownWidget({ widget_name: "compose_select_recipient", get_options: get_options_for_recipient_widget, @@ -334,18 +357,28 @@ export function initialize() { }); } -export function update_placeholder_text() { +export function update_placeholder_text(): void { // Change compose placeholder text only if compose box is open. if (!$("textarea#compose-textarea").is(":visible")) { return; } + const message_type = compose_state.get_message_type(); + assert(message_type !== undefined); - const opts = { - message_type: compose_state.get_message_type(), - stream_id: compose_state.stream_id(), - topic: compose_state.topic(), - direct_message_user_ids: compose_pm_pill.get_user_ids(), - }; + let opts: ComposePlaceholderOptions; + if (message_type === "stream") { + const stream_id = compose_state.stream_id(); + opts = { + message_type, + stream_id, + topic: compose_state.topic(), + }; + } else { + opts = { + message_type, + direct_message_user_ids: compose_pm_pill.get_user_ids(), + }; + } $("textarea#compose-textarea").attr("placeholder", compose_ui.compute_placeholder_text(opts)); } diff --git a/web/src/compose_ui.ts b/web/src/compose_ui.ts index c6f80ef0a7..124ab8b7df 100644 --- a/web/src/compose_ui.ts +++ b/web/src/compose_ui.ts @@ -22,7 +22,7 @@ import * as stream_data from "./stream_data"; import * as user_status from "./user_status"; import * as util from "./util"; -type ComposeTriggeredOptions = { +export type ComposeTriggeredOptions = { trigger: string; } & ( | { @@ -35,7 +35,7 @@ type ComposeTriggeredOptions = { private_message_recipient: string; } ); -type ComposePlaceholderOptions = +export type ComposePlaceholderOptions = | { message_type: "stream"; stream_id: number | undefined; diff --git a/web/src/dropdown_widget.ts b/web/src/dropdown_widget.ts index 83b8bb1f4b..b6b74b7d97 100644 --- a/web/src/dropdown_widget.ts +++ b/web/src/dropdown_widget.ts @@ -60,7 +60,7 @@ type DropdownWidgetOptions = { tippy_props?: Partial; // NOTE: Any value other than `undefined` will be rendered when class is initialized. default_id?: string | number; - unique_id_type: DataTypes; + unique_id_type?: DataTypes; // Text to show if the current value is not in `get_options()`. text_if_current_value_not_in_options?: string; hide_search_box?: boolean; @@ -90,7 +90,7 @@ export class DropdownWidget { instance: tippy.Instance | undefined; default_id: string | number | undefined; current_value: string | number | undefined; - unique_id_type: DataTypes; + unique_id_type: DataTypes | undefined; $events_container: JQuery; text_if_current_value_not_in_options: string; hide_search_box: boolean;