diff --git a/tools/test-js-with-node b/tools/test-js-with-node index a21cb4bb48..78bc31d540 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -244,7 +244,7 @@ EXEMPT_FILES = make_set( "web/src/timerender.ts", "web/src/tippyjs.ts", "web/src/todo_widget.js", - "web/src/topic_list.js", + "web/src/topic_list.ts", "web/src/topic_popover.js", "web/src/tutorial.js", "web/src/types.ts", diff --git a/web/src/topic_list.js b/web/src/topic_list.ts similarity index 66% rename from web/src/topic_list.js rename to web/src/topic_list.ts index 8f08326b3a..684e4caf01 100644 --- a/web/src/topic_list.js +++ b/web/src/topic_list.ts @@ -1,5 +1,6 @@ import $ from "jquery"; import _ from "lodash"; +import assert from "minimalistic-assert"; import render_more_topics from "../templates/more_topics.hbs"; import render_more_topics_spinner from "../templates/more_topics_spinner.hbs"; @@ -11,28 +12,29 @@ import * as scroll_util from "./scroll_util"; import * as stream_topic_history from "./stream_topic_history"; import * as stream_topic_history_util from "./stream_topic_history_util"; import * as topic_list_data from "./topic_list_data"; +import type {TopicInfo} from "./topic_list_data"; import * as vdom from "./vdom"; /* - Track all active widgets with a Map. + Track all active widgets with a Map by stream_id. (We have at max one for now, but we may eventually allow multiple streams to be expanded.) */ -const active_widgets = new Map(); +const active_widgets = new Map(); // We know whether we're zoomed or not. let zoomed = false; -export function update() { +export function update(): void { for (const widget of active_widgets.values()) { widget.build(); } } -export function clear() { +export function clear(): void { popover_menus.get_topic_menu_popover()?.hide(); for (const widget of active_widgets.values()) { @@ -42,12 +44,12 @@ export function clear() { active_widgets.clear(); } -export function close() { +export function close(): void { zoomed = false; clear(); } -export function zoom_out() { +export function zoom_out(): void { zoomed = false; const stream_ids = [...active_widgets.keys()]; @@ -59,75 +61,96 @@ export function zoom_out() { const stream_id = stream_ids[0]; const widget = active_widgets.get(stream_id); + assert(widget !== undefined); const parent_widget = widget.get_parent(); rebuild(parent_widget, stream_id); } -export function keyed_topic_li(conversation) { - const render = () => render_topic_list_item(conversation); +type ListInfoNodeOptions = + | { + type: "topic"; + conversation: TopicInfo; + } + | { + type: "more_items"; + more_topics_unreads: number; + } + | { + type: "spinner"; + }; - const eq = (other) => _.isEqual(conversation, other.conversation); +type ListInfoNode = vdom.Node; + +export function keyed_topic_li(conversation: TopicInfo): ListInfoNode { + const render = (): string => render_topic_list_item(conversation); + + const eq = (other: ListInfoNode): boolean => + other.type === "topic" && _.isEqual(conversation, other.conversation); const key = "t:" + conversation.topic_name; return { key, render, + type: "topic", conversation, eq, }; } export function more_li( - more_topics_unreads, - more_topics_have_unread_mention_messages, - more_topics_unread_count_muted, -) { - const render = () => + more_topics_unreads: number, + more_topics_have_unread_mention_messages: boolean, + more_topics_unread_count_muted: boolean, +): ListInfoNode { + const render = (): string => render_more_topics({ more_topics_unreads, more_topics_have_unread_mention_messages, more_topics_unread_count_muted, }); - const eq = (other) => other.more_items && more_topics_unreads === other.more_topics_unreads; + const eq = (other: ListInfoNode): boolean => + other.type === "more_items" && more_topics_unreads === other.more_topics_unreads; const key = "more"; return { key, - more_items: true, + type: "more_items", more_topics_unreads, render, eq, }; } -export function spinner_li() { - const render = () => render_more_topics_spinner(); +export function spinner_li(): ListInfoNode { + const render = (): string => render_more_topics_spinner(); - const eq = (other) => other.spinner; + const eq = (other: ListInfoNode): boolean => other.type === "spinner"; const key = "more"; return { key, - spinner: true, + type: "spinner", render, eq, }; } export class TopicListWidget { - prior_dom = undefined; + prior_dom?: vdom.Tag = undefined; + $parent_elem: JQuery; + my_stream_id: number; - constructor($parent_elem, my_stream_id) { + constructor($parent_elem: JQuery, my_stream_id: number) { this.$parent_elem = $parent_elem; this.my_stream_id = my_stream_id; } - build_list(spinner) { + build_list(spinner: boolean): vdom.Tag { const list_info = topic_list_data.get_list_info( this.my_stream_id, zoomed, @@ -143,7 +166,7 @@ export class TopicListWidget { list_info.items.length === num_possible_topics && stream_topic_history.is_complete_for_stream_id(this.my_stream_id); - const attrs = [["class", "topic-list"]]; + const attrs: [string, string][] = [["class", "topic-list"]]; const nodes = list_info.items.map((conversation) => keyed_topic_li(conversation)); @@ -167,28 +190,29 @@ export class TopicListWidget { return dom; } - get_parent() { + get_parent(): JQuery { return this.$parent_elem; } - get_stream_id() { + get_stream_id(): number { return this.my_stream_id; } - remove() { + remove(): void { this.$parent_elem.find(".topic-list").remove(); this.prior_dom = undefined; } - build(spinner) { + build(spinner = false): void { const new_dom = this.build_list(spinner); - const replace_content = (html) => { + const replace_content = (html?: string): void => { + assert(html !== undefined); this.remove(); this.$parent_elem.append(html); }; - const find = () => this.$parent_elem.find(".topic-list"); + const find = (): JQuery => this.$parent_elem.find(".topic-list"); vdom.update(replace_content, find, new_dom, this.prior_dom); @@ -202,7 +226,7 @@ export class TopicListWidget { } } -export function clear_topic_search(e) { +export function clear_topic_search(e: JQuery.Event): void { e.stopPropagation(); const $input = $("#filter-topic-input"); if ($input.length) { @@ -215,13 +239,14 @@ export function clear_topic_search(e) { const stream_id = stream_ids[0]; const widget = active_widgets.get(stream_id); + assert(widget !== undefined); const parent_widget = widget.get_parent(); rebuild(parent_widget, stream_id); } } -export function active_stream_id() { +export function active_stream_id(): number | undefined { const stream_ids = [...active_widgets.keys()]; if (stream_ids.length !== 1) { @@ -231,7 +256,7 @@ export function active_stream_id() { return stream_ids[0]; } -export function get_stream_li() { +export function get_stream_li(): JQuery | undefined { const widgets = [...active_widgets.values()]; if (widgets.length !== 1) { @@ -242,7 +267,7 @@ export function get_stream_li() { return $stream_li; } -export function rebuild($stream_li, stream_id) { +export function rebuild($stream_li: JQuery, stream_id: number): void { const active_widget = active_widgets.get(stream_id); if (active_widget) { @@ -259,19 +284,20 @@ export function rebuild($stream_li, stream_id) { // For zooming, we only do topic-list stuff here...let stream_list // handle hiding/showing the non-narrowed streams -export function zoom_in() { +export function zoom_in(): void { zoomed = true; const stream_id = active_stream_id(); - if (!stream_id) { + if (stream_id === undefined) { blueslip.error("Cannot find widget for topic history zooming."); return; } const active_widget = active_widgets.get(stream_id); + assert(active_widget !== undefined); - function on_success() { - if (!active_widgets.has(stream_id)) { + function on_success(): void { + if (!active_widgets.has(stream_id!)) { blueslip.warn("User re-narrowed before topic history was returned."); return; } @@ -285,7 +311,7 @@ export function zoom_in() { return; } - active_widget.build(); + active_widget!.build(); } scroll_util.get_scroll_element($("#left_sidebar_scroll_container")).scrollTop(0); @@ -296,15 +322,20 @@ export function zoom_in() { stream_topic_history_util.get_server_history(stream_id, on_success); } -export function get_topic_search_term() { - const $filter = $("#filter-topic-input"); - if ($filter.val() === undefined) { +export function get_topic_search_term(): string { + const $filter = $("input#filter-topic-input"); + const filter_val = $filter.val(); + if (filter_val === undefined) { return ""; } - return $filter.val().trim(); + return filter_val.trim(); } -export function initialize({on_topic_click}) { +export function initialize({ + on_topic_click, +}: { + on_topic_click: (stream_id: number, topic?: string) => void; +}): void { $("#stream_filters").on( "click", ".sidebar-topic-check, .topic-name, .topic-markers-and-controls", @@ -317,7 +348,9 @@ export function initialize({on_topic_click}) { } const $stream_row = $(e.target).parents(".narrow-filter"); - const stream_id = Number.parseInt($stream_row.attr("data-stream-id"), 10); + const stream_id_string = $stream_row.attr("data-stream-id"); + assert(stream_id_string !== undefined); + const stream_id = Number.parseInt(stream_id_string, 10); const topic = $(e.target).parents("li").attr("data-topic-name"); on_topic_click(stream_id, topic); @@ -325,7 +358,9 @@ export function initialize({on_topic_click}) { }, ); - $("body").on("input", "#filter-topic-input", () => { - active_widgets.get(active_stream_id()).build(); + $("body").on("input", "#filter-topic-input", (): void => { + const stream_id = active_stream_id(); + assert(stream_id !== undefined); + active_widgets.get(stream_id)?.build(); }); } diff --git a/web/src/topic_list_data.ts b/web/src/topic_list_data.ts index 2378d77439..1ccd24aecf 100644 --- a/web/src/topic_list_data.ts +++ b/web/src/topic_list_data.ts @@ -13,7 +13,7 @@ import * as util from "./util"; const max_topics = 8; const max_topics_with_unread = 12; -type TopicInfo = { +export type TopicInfo = { topic_name: string; topic_resolved_prefix: string; topic_display_name: string;