diff --git a/tools/test-js-with-node b/tools/test-js-with-node index c6a007c921..a21cb4bb48 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -144,7 +144,7 @@ EXEMPT_FILES = make_set( "web/src/message_scroll_state.ts", "web/src/message_util.ts", "web/src/message_view_header.js", - "web/src/message_viewport.js", + "web/src/message_viewport.ts", "web/src/messages_overlay_ui.ts", "web/src/modals.ts", "web/src/muted_users_ui.js", diff --git a/web/src/message_viewport.js b/web/src/message_viewport.ts similarity index 77% rename from web/src/message_viewport.js rename to web/src/message_viewport.ts index 787ec88e57..beec3ba069 100644 --- a/web/src/message_viewport.js +++ b/web/src/message_viewport.ts @@ -1,30 +1,54 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; import * as blueslip from "./blueslip"; import * as message_lists from "./message_lists"; import * as message_scroll_state from "./message_scroll_state"; +import type {Message} from "./message_store"; import * as rows from "./rows"; import * as util from "./util"; +import type {CachedValue} from "./util"; + +type MessageViewportInfo = { + visible_top: number; + visible_bottom: number; + visible_height: number; +}; + +type DimenFunc = JQuery["height"] | JQuery["width"]; +type DimenWrapper = { + (): number; + (val: string | number): JQuery; +}; export const $scroll_container = $("html"); -let $jwindow; -const dimensions = {}; +let $jwindow: JQuery; +const dimensions: Record> = {}; let in_stoppable_autoscroll = false; -function make_dimen_wrapper(dimen_name, dimen_func) { +function make_dimen_wrapper(dimen_name: string, dimen_func: DimenFunc): DimenWrapper { dimensions[dimen_name] = new util.CachedValue({ compute_value() { - return dimen_func(); + return dimen_func() ?? 0; }, }); - return function viewport_dimension_wrapper(val) { + + // Inner function overload for the case when dimen_func is () => number | undefined + function viewport_dimension_wrapper(): number; + + // Inner function overload for the case when dimen_func is (val: string | number) => JQuery + function viewport_dimension_wrapper(val: string | number): JQuery; + + function viewport_dimension_wrapper(val?: string | number): number | JQuery { if (val !== undefined) { dimensions[dimen_name].reset(); return dimen_func(val); } return dimensions[dimen_name].get(); - }; + } + + return viewport_dimension_wrapper; } export const height = make_dimen_wrapper("height", $.fn.height.bind($scroll_container)); @@ -33,23 +57,23 @@ export const width = make_dimen_wrapper("width", $.fn.width.bind($scroll_contain // TODO: This function lets us use the DOM API instead of jquery // (<10x faster) for condense.js, but we want to eventually do a // bigger of refactor `height` and `width` above to do the same. -export function max_message_height() { - return document.querySelector("html").offsetHeight * 0.65; +export function max_message_height(): number { + return document.querySelector("html")!.offsetHeight * 0.65; } // Includes both scroll and arrow events. Negative means scroll up, // positive means scroll down. export let last_movement_direction = 1; -export function set_last_movement_direction(value) { +export function set_last_movement_direction(value: number): void { last_movement_direction = value; } -export function at_top() { +export function at_top(): boolean { return scrollTop() <= 0; } -export function message_viewport_info() { +export function message_viewport_info(): MessageViewportInfo { // Return a structure that tells us details of the viewport // accounting for fixed elements like the top navbar. // @@ -78,7 +102,7 @@ export function message_viewport_info() { }; } -export function at_bottom() { +export function at_bottom(): boolean { const bottom = scrollTop() + height(); const full_height = $scroll_container.prop("scrollHeight"); @@ -91,7 +115,7 @@ export function at_bottom() { // This differs from at_bottom in that it only requires the bottom message to // be visible, but you may be able to scroll down further. -export function bottom_message_visible() { +export function bottom_message_visible(): boolean { const $last_row = rows.last_visible(); if ($last_row.length) { const message_bottom = $last_row[0].getBoundingClientRect().bottom; @@ -101,11 +125,11 @@ export function bottom_message_visible() { return false; } -export function is_below_visible_bottom(offset) { - return offset > scrollTop() + height() - $("#compose").height(); +export function is_below_visible_bottom(offset: number): boolean { + return offset > scrollTop() + height() - ($("#compose").height() ?? 0); } -export function is_scrolled_up() { +export function is_scrolled_up(): boolean { // Let's determine whether the user was already dealing // with messages off the screen, which can guide auto // scrolling decisions. @@ -119,7 +143,7 @@ export function is_scrolled_up() { return offset > 0; } -export function offset_from_bottom($last_row) { +export function offset_from_bottom($last_row: JQuery): number { // A positive return value here means the last row is // below the bottom of the feed (i.e. obscured by the compose // box or even further below the bottom). @@ -129,7 +153,12 @@ export function offset_from_bottom($last_row) { return message_bottom - info.visible_bottom; } -export function set_message_position(message_top, message_height, viewport_info, ratio) { +export function set_message_position( + message_top: number, + message_height: number, + viewport_info: MessageViewportInfo, + ratio: number, +): void { // message_top = offset of the top of a message that you are positioning // message_height = height of the message that you are positioning // viewport_info = result of calling message_viewport.message_viewport_info @@ -160,7 +189,12 @@ export function set_message_position(message_top, message_height, viewport_info, scrollTop(new_scroll_top); } -function in_viewport_or_tall(rect, top_of_feed, bottom_of_feed, require_fully_visible) { +function in_viewport_or_tall( + rect: DOMRect, + top_of_feed: number, + bottom_of_feed: number, + require_fully_visible: boolean, +): boolean { if (require_fully_visible) { return ( rect.top > top_of_feed && // Message top is in view and @@ -171,14 +205,14 @@ function in_viewport_or_tall(rect, top_of_feed, bottom_of_feed, require_fully_vi return rect.bottom > top_of_feed && rect.top < bottom_of_feed; } -function add_to_visible( - $candidates, - visible, - top_of_feed, - bottom_of_feed, - require_fully_visible, - row_to_id, -) { +function add_to_visible( + $candidates: JQuery, + visible: T[], + top_of_feed: number, + bottom_of_feed: number, + require_fully_visible: boolean, + row_to_id: ($row: HTMLElement) => T, +): void { for (const row of $candidates) { const row_rect = row.getBoundingClientRect(); // Mark very tall messages as read once we've gotten past them @@ -209,13 +243,13 @@ const bottom_of_feed = new util.CachedValue({ }, }); -function _visible_divs( - $selected_row, - row_min_height, - row_to_output, - div_class, - require_fully_visible, -) { +function _visible_divs( + $selected_row: JQuery, + row_min_height: number, + row_to_output: ($row: HTMLElement) => T, + div_class: string, + require_fully_visible: boolean, +): T[] { // Note that when using getBoundingClientRect() we are getting offsets // relative to the visible window, but when using jQuery's offset() we are // getting offsets relative to the full scrollable window. You can't try to @@ -225,7 +259,7 @@ function _visible_divs( // We do this explicitly without merges and without recalculating // the feed bounds to keep this computation as cheap as possible. - const visible = []; + const visible: T[] = []; const $above_pointer = $selected_row .prevAll(`div.${CSS.escape(div_class)}`) .slice(0, num_neighbors); @@ -260,7 +294,8 @@ function _visible_divs( return visible; } -export function visible_groups(require_fully_visible) { +export function visible_groups(require_fully_visible: boolean): HTMLElement[] { + assert(message_lists.current !== undefined); const $selected_row = message_lists.current.selected_row(); if ($selected_row === undefined || $selected_row.length === 0) { return []; @@ -268,29 +303,45 @@ export function visible_groups(require_fully_visible) { const $selected_group = rows.get_message_recipient_row($selected_row); - function get_row(row) { + function get_row(row: HTMLElement): HTMLElement { return row; } // Being simplistic about this, the smallest group is about 75 px high. - return _visible_divs($selected_group, 75, get_row, "recipient_row", require_fully_visible); + return _visible_divs( + $selected_group, + 75, + get_row, + "recipient_row", + require_fully_visible, + ); } -export function visible_messages(require_fully_visible) { +export function visible_messages(require_fully_visible: boolean): Message[] { + assert(message_lists.current !== undefined); const $selected_row = message_lists.current.selected_row(); - function row_to_id(row) { - return message_lists.current.get(rows.id($(row))); + function row_to_id(row: HTMLElement): Message { + assert(message_lists.current !== undefined); + return message_lists.current.get(rows.id($(row))!)!; } // Being simplistic about this, the smallest message is 25 px high. - return _visible_divs($selected_row, 25, row_to_id, "message_row", require_fully_visible); + return _visible_divs( + $selected_row, + 25, + row_to_id, + "message_row", + require_fully_visible, + ); } -export function scrollTop(target_scrollTop) { +export function scrollTop(): number; +export function scrollTop(target_scrollTop: number): JQuery; +export function scrollTop(target_scrollTop?: number): JQuery | number { const orig_scrollTop = $scroll_container.scrollTop(); if (target_scrollTop === undefined) { - return orig_scrollTop; + return orig_scrollTop ?? 0; } let $ret = $scroll_container.scrollTop(target_scrollTop); const new_scrollTop = $scroll_container.scrollTop(); @@ -327,13 +378,13 @@ export function scrollTop(target_scrollTop) { return $ret; } -export function stop_auto_scrolling() { +export function stop_auto_scrolling(): void { if (in_stoppable_autoscroll) { $scroll_container.stop(); } } -export function system_initiated_animate_scroll(scroll_amount) { +export function system_initiated_animate_scroll(scroll_amount: number): void { message_scroll_state.set_update_selection_on_next_scroll(false); const viewport_offset = scrollTop(); in_stoppable_autoscroll = true; @@ -345,7 +396,7 @@ export function system_initiated_animate_scroll(scroll_amount) { }); } -export function user_initiated_animate_scroll(scroll_amount) { +export function user_initiated_animate_scroll(scroll_amount: number): void { message_scroll_state.set_update_selection_on_next_scroll(false); in_stoppable_autoscroll = false; // defensive @@ -356,7 +407,10 @@ export function user_initiated_animate_scroll(scroll_amount) { }); } -export function recenter_view($message, {from_scroll = false, force_center = false} = {}) { +export function recenter_view( + $message: JQuery, + {from_scroll = false, force_center = false} = {}, +): void { // BarnOwl-style recentering: if the pointer is too high, move it to // the 1/2 marks. If the pointer is too low, move it to the 1/7 mark. // See keep_pointer_in_view() for related logic to keep the pointer onscreen. @@ -394,7 +448,8 @@ export function recenter_view($message, {from_scroll = false, force_center = fal } } -export function maybe_scroll_to_show_message_top() { +export function maybe_scroll_to_show_message_top(): void { + assert(message_lists.current !== undefined); // Sets the top of the message to the top of the viewport. // Only applies if the top of the message is out of view above the visible area. const $selected_message = message_lists.current.selected_row(); @@ -406,13 +461,14 @@ export function maybe_scroll_to_show_message_top() { } } -export function is_message_below_viewport($message_row) { +export function is_message_below_viewport($message_row: JQuery): boolean { const info = message_viewport_info(); const offset = $message_row.get_offset_to_window(); return offset.top >= info.visible_bottom; } -export function keep_pointer_in_view() { +export function keep_pointer_in_view(): void { + assert(message_lists.current !== undefined); // See message_viewport.recenter_view() for related logic to keep the pointer onscreen. // This function mostly comes into place for mouse scrollers, and it // keeps the pointer in view. For people who purely scroll with the @@ -430,7 +486,7 @@ export function keep_pointer_in_view() { const top_threshold = info.visible_top + (1 / 10) * info.visible_height; const bottom_threshold = info.visible_top + (9 / 10) * info.visible_height; - function message_is_far_enough_down() { + function message_is_far_enough_down(): boolean { if (at_top()) { return true; } @@ -455,11 +511,14 @@ export function keep_pointer_in_view() { return false; } - function message_is_far_enough_up() { + function message_is_far_enough_up(): boolean { return at_bottom() || $next_row.get_offset_to_window().top <= bottom_threshold; } - function adjust(in_view, get_next_row) { + function adjust( + in_view: () => boolean, + get_next_row: ($message_row: JQuery) => JQuery, + ): boolean { // return true only if we make an actual adjustment, so // that we know to short circuit the other direction if (in_view()) { @@ -479,10 +538,11 @@ export function keep_pointer_in_view() { adjust(message_is_far_enough_up, rows.prev_visible); } - message_lists.current.select_id(rows.id($next_row), {from_scroll: true}); + message_lists.current.select_id(rows.id($next_row)!, {from_scroll: true}); } -export function scroll_to_selected() { +export function scroll_to_selected(): void { + assert(message_lists.current !== undefined); const $selected_row = message_lists.current.selected_row(); if ($selected_row && $selected_row.length !== 0) { recenter_view($selected_row); @@ -491,11 +551,11 @@ export function scroll_to_selected() { export let scroll_to_selected_planned = false; -export function plan_scroll_to_selected() { +export function plan_scroll_to_selected(): void { scroll_to_selected_planned = true; } -export function maybe_scroll_to_selected() { +export function maybe_scroll_to_selected(): void { // If we have made a plan to scroll to the selected message but // deferred doing so, do so here. if (scroll_to_selected_planned) { @@ -504,9 +564,8 @@ export function maybe_scroll_to_selected() { } } -export function initialize() { +export function initialize(): void { $jwindow = $(window); - // This handler must be placed before all resize handlers in our application $jwindow.on("resize", () => { dimensions.height.reset();