ts: Migrate `message_viewport` to typescript.

Used function overloading for methods - `make_dimen_wrapper`
and `scrollTop`.
This commit is contained in:
Lalit 2024-01-20 14:57:51 +05:30 committed by Tim Abbott
parent f2c700b6e1
commit 6f6795f607
2 changed files with 120 additions and 61 deletions

View File

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

View File

@ -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<Window & typeof globalThis>;
const dimensions: Record<string, CachedValue<number>> = {};
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<T>(
$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<T>(
$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<HTMLElement>(
$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<Message>(
$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();