From deb5d909412304811227cdbcb36ad7b48d5a906f Mon Sep 17 00:00:00 2001 From: evykassirer Date: Thu, 12 Sep 2024 10:52:47 -0700 Subject: [PATCH] message_list: Convert module to typescript. --- tools/test-js-with-node | 2 +- web/src/compose_reply.ts | 1 + web/src/message_events_util.ts | 2 +- web/src/message_feed_top_notices.ts | 2 +- web/src/message_fetch.ts | 13 +- web/src/{message_list.js => message_list.ts} | 200 +++++++++++-------- web/src/message_list_view.ts | 4 +- web/src/message_lists.ts | 60 +----- web/src/message_util.ts | 2 +- web/src/submessage.ts | 2 +- web/tests/message_list.test.js | 2 +- 11 files changed, 129 insertions(+), 161 deletions(-) rename web/src/{message_list.js => message_list.ts} (80%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 4f36f3e7fb..6d26f16a70 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -143,7 +143,7 @@ EXEMPT_FILES = make_set( "web/src/message_feed_loading.ts", "web/src/message_feed_top_notices.ts", "web/src/message_fetch.ts", - "web/src/message_list.js", + "web/src/message_list.ts", "web/src/message_list_data.ts", "web/src/message_list_data_cache.ts", "web/src/message_list_hover.js", diff --git a/web/src/compose_reply.ts b/web/src/compose_reply.ts index aed8eb8507..3b3b2cad6c 100644 --- a/web/src/compose_reply.ts +++ b/web/src/compose_reply.ts @@ -161,6 +161,7 @@ export function reply_with_mention(opts: { keep_composebox_empty: true, }); const message = message_lists.current.selected_message(); + assert(message !== undefined); const mention = people.get_mention_syntax(message.sender_full_name, message.sender_id); compose_ui.insert_syntax_and_focus(mention); } diff --git a/web/src/message_events_util.ts b/web/src/message_events_util.ts index 0ec149534d..766d30529b 100644 --- a/web/src/message_events_util.ts +++ b/web/src/message_events_util.ts @@ -3,8 +3,8 @@ import {z} from "zod"; import * as blueslip from "./blueslip"; import * as channel from "./channel"; import * as compose_notifications from "./compose_notifications"; +import type {MessageList, RenderInfo} from "./message_list"; import * as message_lists from "./message_lists"; -import type {MessageList, RenderInfo} from "./message_lists"; import * as message_store from "./message_store"; import type {Message} from "./message_store"; import * as narrow_state from "./narrow_state"; diff --git a/web/src/message_feed_top_notices.ts b/web/src/message_feed_top_notices.ts index 9c56671993..cefb413461 100644 --- a/web/src/message_feed_top_notices.ts +++ b/web/src/message_feed_top_notices.ts @@ -3,8 +3,8 @@ import _ from "lodash"; import assert from "minimalistic-assert"; import * as hash_util from "./hash_util"; +import type {MessageList} from "./message_list"; import * as message_lists from "./message_lists"; -import type {MessageList} from "./message_lists"; import * as narrow_banner from "./narrow_banner"; import * as narrow_state from "./narrow_state"; import * as people from "./people"; diff --git a/web/src/message_fetch.ts b/web/src/message_fetch.ts index 02d1086193..f91281d383 100644 --- a/web/src/message_fetch.ts +++ b/web/src/message_fetch.ts @@ -11,6 +11,7 @@ import * as direct_message_group_data from "./direct_message_group_data"; import * as message_feed_loading from "./message_feed_loading"; import * as message_feed_top_notices from "./message_feed_top_notices"; import * as message_helper from "./message_helper"; +import type {MessageList} from "./message_list"; import type {MessageListData} from "./message_list_data"; import * as message_lists from "./message_lists"; import {raw_message_schema} from "./message_store"; @@ -44,7 +45,7 @@ type MessageFetchOptions = { cont: (data: MessageFetchResponse, args: MessageFetchOptions) => void; fetch_again?: boolean; msg_list_data: MessageListData; - msg_list?: message_lists.MessageList | undefined; + msg_list?: MessageList | undefined; validate_filter_topic_post_fetch?: boolean | undefined; }; @@ -496,7 +497,7 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void { export function load_messages_for_narrow(opts: { anchor: string | number; - msg_list: message_lists.MessageList; + msg_list: MessageList; cont: () => void; validate_filter_topic_post_fetch?: boolean | undefined; }): void { @@ -520,7 +521,7 @@ export function get_backfill_anchor(msg_list_data: MessageListData): string | nu return "first_unread"; } -export function get_frontfill_anchor(msg_list: message_lists.MessageList): number | string { +export function get_frontfill_anchor(msg_list: MessageList): number | string { const last_msg = msg_list.data.last_including_muted(); if (last_msg) { @@ -557,7 +558,7 @@ export function maybe_load_older_messages(opts: { recent_view?: boolean; first_unread_message_id?: number; cont?: () => void; - msg_list?: message_lists.MessageList | undefined; + msg_list?: MessageList | undefined; msg_list_data: MessageListData; }): void { // This function gets called when you scroll to the top @@ -626,7 +627,7 @@ export function do_backfill(opts: { num_before: number; cont?: () => void; msg_list_data: MessageListData; - msg_list?: message_lists.MessageList | undefined; + msg_list?: MessageList | undefined; }): void { const msg_list_data = opts.msg_list_data; const anchor = get_backfill_anchor(msg_list_data); @@ -648,7 +649,7 @@ export function do_backfill(opts: { }); } -export function maybe_load_newer_messages(opts: {msg_list: message_lists.MessageList}): void { +export function maybe_load_newer_messages(opts: {msg_list: MessageList}): void { // This function gets called when you scroll to the bottom // of your window, and you want to get messages newer // than what the browsers originally fetched. diff --git a/web/src/message_list.js b/web/src/message_list.ts similarity index 80% rename from web/src/message_list.js rename to web/src/message_list.ts index d97775aa44..9e1782d17e 100644 --- a/web/src/message_list.js +++ b/web/src/message_list.ts @@ -4,9 +4,11 @@ import assert from "minimalistic-assert"; import * as blueslip from "./blueslip"; import * as compose_tooltips from "./compose_tooltips"; +import type {Filter} from "./filter"; import {MessageListData} from "./message_list_data"; import * as message_list_tooltips from "./message_list_tooltips"; import {MessageListView} from "./message_list_view"; +import type {Message} from "./message_store"; import * as narrow_banner from "./narrow_banner"; import * as narrow_state from "./narrow_state"; import {page_params} from "./page_params"; @@ -15,21 +17,57 @@ import * as stream_data from "./stream_data"; import * as unread from "./unread"; import {user_settings} from "./user_settings"; +export type RenderInfo = {need_user_to_scroll: boolean}; + +type SelectIdOpts = { + then_scroll?: boolean; + target_scroll_offset?: number; + use_closest?: boolean; + empty_ok?: boolean; + mark_read?: boolean; + force_rerender?: boolean; + from_scroll?: boolean; + from_rendering?: boolean; +}; + +// A MessageList is the main interface for a message feed that is +// rendered in the DOM. Code outside the message feed rendering +// internals will directly call this module in order to manipulate +// a message feed. +// +// Each MessageList has an associated MessageListData, which +// manages the messages, and a MessageListView, which manages the +// the templates/HTML rendering as well as invisible pagination. +// +// TODO: The abstraction boundary between this and MessageListView +// is not particularly well-defined; it could be nice to figure +// out a good rule. export class MessageList { static id_counter = 0; - // A MessageList is the main interface for a message feed that is - // rendered in the DOM. Code outside the message feed rendering - // internals will directly call this module in order to manipulate - // a message feed. + + id: number; + data: MessageListData; + // The MessageListView object that is responsible for + // maintaining this message feed's HTML representation in the + // DOM. + view: MessageListView; + // If this message list is for the combined feed view. + is_combined_feed_view: boolean; + // Keeps track of whether the user has done a UI interaction, + // such as "Mark as unread", that should disable marking + // messages as read until prevent_reading is called again. // - // Each MessageList has an associated MessageListData, which - // manages the messages, and a MessageListView, which manages the - // the templates/HTML rendering as well as invisible pagination. - // - // TODO: The abstraction boundary between this and MessageListView - // is not particularly well-defined; it could be nice to figure - // out a good rule. - constructor(opts) { + // Distinct from filter.can_mark_messages_read(), which is a + // property of the type of narrow, regardless of actions by + // the user. Possibly this can be unified in some nice way. + reading_prevented: boolean; + last_message_historical?: boolean; + constructor(opts: { + data: MessageListData; + filter: Filter; + excludes_muted_topics: boolean; + is_node_test: boolean; + }) { MessageList.id_counter += 1; this.id = MessageList.id_counter; // The MessageListData keeps track of the actual sequence of @@ -55,27 +93,14 @@ export class MessageList { // query .data.filter directly. const collapse_messages = this.data.filter.supports_collapsing_recipients(); - // The MessageListView object that is responsible for - // maintaining this message feed's HTML representation in the - // DOM. this.view = new MessageListView(this, collapse_messages, opts.is_node_test); - - // If this message list is for the combined feed view. this.is_combined_feed_view = this.data.filter.is_in_home(); - - // Keeps track of whether the user has done a UI interaction, - // such as "Mark as unread", that should disable marking - // messages as read until prevent_reading is called again. - // - // Distinct from filter.can_mark_messages_read(), which is a - // property of the type of narrow, regardless of actions by - // the user. Possibly this can be unified in some nice way. this.reading_prevented = false; return this; } - should_preserve_current_rendered_state() { + should_preserve_current_rendered_state(): boolean { // Whether this message list is preserved in the DOM even // when viewing other views -- a valuable optimization for // fast toggling between the combined feed and other views, @@ -125,19 +150,22 @@ export class MessageList { return true; } - is_current_message_list() { + is_current_message_list(): boolean { return this.view.is_current_message_list(); } - prevent_reading() { + prevent_reading(): void { this.reading_prevented = true; } - resume_reading() { + resume_reading(): void { this.reading_prevented = false; } - add_messages(messages, opts) { + add_messages( + messages: Message[], + append_to_view_opts: {messages_are_new?: boolean}, + ): RenderInfo | undefined { // This adds all messages to our data, but only returns // the currently viewable ones. const info = this.data.add_messages(messages); @@ -161,7 +189,7 @@ export class MessageList { } if (bottom_messages.length > 0) { - render_info = this.append_to_view(bottom_messages, opts); + render_info = this.append_to_view(bottom_messages, append_to_view_opts); } if (!this.visibly_empty() && this.is_current_message_list()) { @@ -185,51 +213,51 @@ export class MessageList { return render_info; } - get(id) { + get(id: number): Message | undefined { return this.data.get(id); } - msg_id_in_fetched_range(msg_id) { + msg_id_in_fetched_range(msg_id: number): boolean { return this.data.msg_id_in_fetched_range(msg_id); } - num_items() { + num_items(): number { return this.data.num_items(); } - empty() { + empty(): boolean { return this.data.empty(); } - visibly_empty() { + visibly_empty(): boolean { return this.data.visibly_empty(); } - first() { + first(): Message | undefined { return this.data.first(); } - last() { + last(): Message | undefined { return this.data.last(); } - prev() { + prev(): number | undefined { return this.data.prev(); } - next() { + next(): number | undefined { return this.data.next(); } - is_at_end() { + is_at_end(): boolean { return this.data.is_at_end(); } - is_keyword_search() { + is_keyword_search(): boolean { return this.data.is_keyword_search(); } - can_mark_messages_read() { + can_mark_messages_read(): boolean { /* Automatically marking messages as read can be disabled for three different reasons: * The view is structurally a search view, encoded in the @@ -256,7 +284,7 @@ export class MessageList { ); } - can_mark_messages_read_without_setting() { + can_mark_messages_read_without_setting(): boolean { /* Similar to can_mark_messages_read() above, this is a helper function to check if messages can be automatically read without @@ -265,7 +293,7 @@ export class MessageList { return this.data.can_mark_messages_read() && !this.reading_prevented; } - clear({clear_selected_id = true} = {}) { + clear({clear_selected_id = true} = {}): void { this.data.clear(); this.view.clear_rendering_state(true); @@ -274,34 +302,31 @@ export class MessageList { } } - selected_id() { + selected_id(): number { return this.data.selected_id(); } - select_id(id, opts) { - opts = { + select_id(id: number | string, select_id_opts?: SelectIdOpts): void { + if (typeof id === "string") { + blueslip.warn("Call to select_id with string id"); + id = Number.parseFloat(id); + if (Number.isNaN(id)) { + throw new TypeError("Bad message id " + id); + } + } + const opts = { then_scroll: false, target_scroll_offset: undefined, use_closest: false, empty_ok: false, mark_read: true, force_rerender: false, - ...opts, + ...select_id_opts, id, msg_list: this, previously_selected_id: this.data.selected_id(), }; - const convert_id = (str_id) => { - const id = Number.parseFloat(str_id); - if (Number.isNaN(id)) { - throw new TypeError("Bad message id " + str_id); - } - return id; - }; - - id = convert_id(id); - const closest_id = this.closest_id(id); let error_data; @@ -353,30 +378,30 @@ export class MessageList { } } - selected_message() { + selected_message(): Message | undefined { return this.get(this.data.selected_id()); } - selected_row() { + selected_row(): JQuery { return this.get_row(this.data.selected_id()); } - closest_id(id) { + closest_id(id: number): number { return this.data.closest_id(id); } - advance_past_messages(msg_ids) { - return this.data.advance_past_messages(msg_ids); + advance_past_messages(msg_ids: number[]): void { + this.data.advance_past_messages(msg_ids); } - selected_idx() { + selected_idx(): number { return this.data.selected_idx(); } // Maintains a trailing bookend element explaining any changes in // your subscribed/unsubscribed status at the bottom of the // message list. - update_trailing_bookend(force_render = false) { + update_trailing_bookend(force_render = false): void { this.view.clear_trailing_bookend(); if (this.is_combined_feed_view) { return; @@ -415,7 +440,7 @@ export class MessageList { } this.view.render_trailing_bookend( - sub.name, + sub?.name, subscribed, deactivated, just_unsubscribed, @@ -426,26 +451,25 @@ export class MessageList { ); } - unmuted_messages(messages) { + unmuted_messages(messages: Message[]): Message[] { return this.data.unmuted_messages(messages); } - append(messages, opts) { + append(messages: Message[], opts: {messages_are_new: boolean}): void { const viewable_messages = this.data.append(messages); this.append_to_view(viewable_messages, opts); } - append_to_view(messages, {messages_are_new = false} = {}) { - const render_info = this.view.append(messages, messages_are_new); - return render_info; + append_to_view(messages: Message[], {messages_are_new = false} = {}): RenderInfo | undefined { + return this.view.append(messages, messages_are_new); } - remove_and_rerender(message_ids) { + remove_and_rerender(message_ids: number[]): void { this.data.remove(message_ids); this.rerender(); } - show_edit_message($row, $form) { + show_edit_message($row: JQuery, $form: JQuery): void { if ($row.find(".message_edit_form form").length !== 0) { return; } @@ -457,7 +481,7 @@ export class MessageList { autosize($row.find(".message_edit_content")); } - hide_edit_message($row) { + hide_edit_message($row: JQuery): void { if ($row.find(".message_edit_form form").length === 0) { return; } @@ -468,7 +492,7 @@ export class MessageList { $row.trigger("mouseleave"); } - show_edit_topic_on_recipient_row($recipient_row, $form) { + show_edit_topic_on_recipient_row($recipient_row: JQuery, $form: JQuery): void { $recipient_row.find(".topic_edit_form").append($form); $recipient_row.find(".on_hover_topic_edit").hide(); $recipient_row.find(".edit_message_button").hide(); @@ -479,7 +503,7 @@ export class MessageList { $recipient_row.find(".on_hover_topic_unresolve").hide(); } - hide_edit_topic_on_recipient_row($recipient_row) { + hide_edit_topic_on_recipient_row($recipient_row: JQuery): void { $recipient_row.find(".stream_topic").show(); $recipient_row.find(".on_hover_topic_edit").show(); $recipient_row.find(".edit_message_button").show(); @@ -490,7 +514,7 @@ export class MessageList { $recipient_row.find(".on_hover_topic_unresolve").show(); } - reselect_selected_id() { + reselect_selected_id(): void { const selected_id = this.data.selected_id(); if (selected_id !== -1) { @@ -498,12 +522,12 @@ export class MessageList { } } - rerender_view() { + rerender_view(): void { this.view.rerender_preserving_scrolltop(); this.reselect_selected_id(); } - rerender() { + rerender(): void { // We need to destroy all the tippy instances from the DOM before re-rendering to // prevent the appearance of tooltips whose reference has been removed. message_list_tooltips.destroy_all_message_list_tooltips(); @@ -531,7 +555,7 @@ export class MessageList { this.rerender_view(); } - update_muting_and_rerender() { + update_muting_and_rerender(): void { this.data.update_items_for_muting(); // We need to rerender whether or not the narrow hides muted // topics, because we need to update recipient bars for topics @@ -549,34 +573,34 @@ export class MessageList { this.rerender(); } - all_messages() { + all_messages(): Message[] { return this.data.all_messages(); } - first_unread_message_id() { + first_unread_message_id(): number | undefined { return this.data.first_unread_message_id(); } - has_unread_messages() { + has_unread_messages(): boolean { return this.data.has_unread_messages(); } - message_range(start, end) { + message_range(start: number, end: number): Message[] { return this.data.message_range(start, end); } - get_row(id) { + get_row(id: number): JQuery { return this.view.get_row(id); } - change_message_id(old_id, new_id) { + change_message_id(old_id: number, new_id: number): void { const require_rerender = this.data.change_message_id(old_id, new_id); if (require_rerender) { this.rerender_view(); } } - get_last_message_sent_by_me() { + get_last_message_sent_by_me(): Message | undefined { return this.data.get_last_message_sent_by_me(); } } diff --git a/web/src/message_list_view.ts b/web/src/message_list_view.ts index 29d539a039..931aac5a13 100644 --- a/web/src/message_list_view.ts +++ b/web/src/message_list_view.ts @@ -19,9 +19,9 @@ import * as condense from "./condense"; import * as hash_util from "./hash_util"; import {$t} from "./i18n"; import * as message_edit from "./message_edit"; +import type {MessageList} from "./message_list"; import * as message_list_tooltips from "./message_list_tooltips"; import * as message_lists from "./message_lists"; -import type {MessageList} from "./message_lists"; import * as message_store from "./message_store"; import type {Message} from "./message_store"; import * as message_viewport from "./message_viewport"; @@ -1936,7 +1936,7 @@ export class MessageListView { } render_trailing_bookend( - stream_name: string, + stream_name: string | undefined, subscribed: boolean, deactivated: boolean, just_unsubscribed: boolean, diff --git a/web/src/message_lists.ts b/web/src/message_lists.ts index f80dbd4ed2..6e06ea5bd5 100644 --- a/web/src/message_lists.ts +++ b/web/src/message_lists.ts @@ -1,69 +1,11 @@ import $ from "jquery"; import * as inbox_util from "./inbox_util"; +import type {MessageList} from "./message_list"; import type {MessageListData} from "./message_list_data"; import * as message_list_data_cache from "./message_list_data_cache"; -import type {MessageListView} from "./message_list_view"; -import type {Message} from "./message_store"; import * as ui_util from "./ui_util"; -export type RenderInfo = {need_user_to_scroll: boolean}; - -export type SelectIdOpts = { - then_scroll?: boolean; - target_scroll_offset?: number; - use_closest?: boolean; - empty_ok?: boolean; - mark_read?: boolean; - force_rerender?: boolean; - from_scroll?: boolean; -}; - -export type MessageList = { - id: number; - view: MessageListView; - is_combined_feed_view: boolean; - selected_id: () => number; - selected_row: () => JQuery; - selected_idx: () => number; - all_messages: () => Message[]; - get: (id: number) => Message | undefined; - has_unread_messages: () => boolean; - can_mark_messages_read: () => boolean; - can_mark_messages_read_without_setting: () => boolean; - change_message_id: (old_id: number, new_id: number) => boolean; - remove_and_rerender: (id: number[]) => void; - rerender_view: () => void; - update_muting_and_rerender: () => void; - prev: () => number | undefined; - next: () => number | undefined; - is_at_end: () => boolean; - prevent_reading: () => void; - resume_reading: () => void; - data: MessageListData; - select_id: (message_id: number, opts?: SelectIdOpts) => void; - get_row: (message_id: number) => JQuery; - add_messages: ( - messages: Message[], - append_opts: {messages_are_new: boolean}, - ) => RenderInfo | undefined; - first: () => Message | undefined; - last: () => Message | undefined; - visibly_empty: () => boolean; - selected_message: () => Message; - should_preserve_current_rendered_state: () => boolean; - show_edit_message: ($row: JQuery, $form: JQuery) => void; - show_edit_topic_on_recipient_row: ($recipient_row: JQuery, $form: JQuery) => void; - hide_edit_topic_on_recipient_row: ($recipient_row: JQuery) => void; - hide_edit_message: ($row: JQuery) => void; - get_last_message_sent_by_me: () => Message | undefined; - num_items: () => number; - last_message_historical: boolean; - reselect_selected_id: () => void; - is_keyword_search: () => boolean; - update_trailing_bookend: (force_render?: boolean) => void; -}; - export let current: MessageList | undefined; export const rendered_message_lists = new Map(); diff --git a/web/src/message_util.ts b/web/src/message_util.ts index 719aa9c0ea..dff7b81c52 100644 --- a/web/src/message_util.ts +++ b/web/src/message_util.ts @@ -1,8 +1,8 @@ import assert from "minimalistic-assert"; import {all_messages_data} from "./all_messages_data"; +import type {MessageList, RenderInfo} from "./message_list"; import type {MessageListData} from "./message_list_data"; -import {type MessageList, type RenderInfo} from "./message_lists"; import * as message_lists from "./message_lists"; import * as message_store from "./message_store"; import type {Message} from "./message_store"; diff --git a/web/src/submessage.ts b/web/src/submessage.ts index d62a15b0f5..4972d0e2b4 100644 --- a/web/src/submessage.ts +++ b/web/src/submessage.ts @@ -2,7 +2,7 @@ import {z} from "zod"; import * as blueslip from "./blueslip"; import * as channel from "./channel"; -import type {MessageList} from "./message_lists"; +import type {MessageList} from "./message_list"; import * as message_store from "./message_store"; import type {Message} from "./message_store"; import type {PollWidgetOutboundData} from "./poll_widget"; diff --git a/web/tests/message_list.test.js b/web/tests/message_list.test.js index 4c9ff4e268..2c22aa20ee 100644 --- a/web/tests/message_list.test.js +++ b/web/tests/message_list.test.js @@ -9,7 +9,7 @@ const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {current_user} = require("./lib/zpage_params"); -// These unit tests for web/src/message_list.js emphasize the model-ish +// These unit tests for web/src/message_list.ts emphasize the model-ish // aspects of the MessageList class. We have to stub out a few functions // related to views and events to get the tests working.