message_list: Convert module to typescript.

This commit is contained in:
evykassirer 2024-09-12 10:52:47 -07:00 committed by Tim Abbott
parent 620db3057b
commit deb5d90941
11 changed files with 129 additions and 161 deletions

View File

@ -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",

View File

@ -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);
}

View File

@ -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";

View File

@ -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";

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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<number, MessageList>();

View File

@ -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";

View File

@ -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";

View File

@ -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.