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_scroll_state.ts",
"web/src/message_util.ts", "web/src/message_util.ts",
"web/src/message_view_header.js", "web/src/message_view_header.js",
"web/src/message_viewport.js", "web/src/message_viewport.ts",
"web/src/messages_overlay_ui.ts", "web/src/messages_overlay_ui.ts",
"web/src/modals.ts", "web/src/modals.ts",
"web/src/muted_users_ui.js", "web/src/muted_users_ui.js",

View File

@ -1,30 +1,54 @@
import $ from "jquery"; import $ from "jquery";
import assert from "minimalistic-assert";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
import * as message_scroll_state from "./message_scroll_state"; import * as message_scroll_state from "./message_scroll_state";
import type {Message} from "./message_store";
import * as rows from "./rows"; import * as rows from "./rows";
import * as util from "./util"; 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"); export const $scroll_container = $("html");
let $jwindow; let $jwindow: JQuery<Window & typeof globalThis>;
const dimensions = {}; const dimensions: Record<string, CachedValue<number>> = {};
let in_stoppable_autoscroll = false; 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({ dimensions[dimen_name] = new util.CachedValue({
compute_value() { 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) { if (val !== undefined) {
dimensions[dimen_name].reset(); dimensions[dimen_name].reset();
return dimen_func(val); return dimen_func(val);
} }
return dimensions[dimen_name].get(); return dimensions[dimen_name].get();
}; }
return viewport_dimension_wrapper;
} }
export const height = make_dimen_wrapper("height", $.fn.height.bind($scroll_container)); 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 // TODO: This function lets us use the DOM API instead of jquery
// (<10x faster) for condense.js, but we want to eventually do a // (<10x faster) for condense.js, but we want to eventually do a
// bigger of refactor `height` and `width` above to do the same. // bigger of refactor `height` and `width` above to do the same.
export function max_message_height() { export function max_message_height(): number {
return document.querySelector("html").offsetHeight * 0.65; return document.querySelector("html")!.offsetHeight * 0.65;
} }
// Includes both scroll and arrow events. Negative means scroll up, // Includes both scroll and arrow events. Negative means scroll up,
// positive means scroll down. // positive means scroll down.
export let last_movement_direction = 1; 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; last_movement_direction = value;
} }
export function at_top() { export function at_top(): boolean {
return scrollTop() <= 0; return scrollTop() <= 0;
} }
export function message_viewport_info() { export function message_viewport_info(): MessageViewportInfo {
// Return a structure that tells us details of the viewport // Return a structure that tells us details of the viewport
// accounting for fixed elements like the top navbar. // 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 bottom = scrollTop() + height();
const full_height = $scroll_container.prop("scrollHeight"); 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 // 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. // 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(); const $last_row = rows.last_visible();
if ($last_row.length) { if ($last_row.length) {
const message_bottom = $last_row[0].getBoundingClientRect().bottom; const message_bottom = $last_row[0].getBoundingClientRect().bottom;
@ -101,11 +125,11 @@ export function bottom_message_visible() {
return false; return false;
} }
export function is_below_visible_bottom(offset) { export function is_below_visible_bottom(offset: number): boolean {
return offset > scrollTop() + height() - $("#compose").height(); 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 // Let's determine whether the user was already dealing
// with messages off the screen, which can guide auto // with messages off the screen, which can guide auto
// scrolling decisions. // scrolling decisions.
@ -119,7 +143,7 @@ export function is_scrolled_up() {
return offset > 0; 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 // A positive return value here means the last row is
// below the bottom of the feed (i.e. obscured by the compose // below the bottom of the feed (i.e. obscured by the compose
// box or even further below the bottom). // box or even further below the bottom).
@ -129,7 +153,12 @@ export function offset_from_bottom($last_row) {
return message_bottom - info.visible_bottom; 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_top = offset of the top of a message that you are positioning
// message_height = height of the 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 // 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); 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) { if (require_fully_visible) {
return ( return (
rect.top > top_of_feed && // Message top is in view and 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; return rect.bottom > top_of_feed && rect.top < bottom_of_feed;
} }
function add_to_visible( function add_to_visible<T>(
$candidates, $candidates: JQuery,
visible, visible: T[],
top_of_feed, top_of_feed: number,
bottom_of_feed, bottom_of_feed: number,
require_fully_visible, require_fully_visible: boolean,
row_to_id, row_to_id: ($row: HTMLElement) => T,
) { ): void {
for (const row of $candidates) { for (const row of $candidates) {
const row_rect = row.getBoundingClientRect(); const row_rect = row.getBoundingClientRect();
// Mark very tall messages as read once we've gotten past them // 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( function _visible_divs<T>(
$selected_row, $selected_row: JQuery,
row_min_height, row_min_height: number,
row_to_output, row_to_output: ($row: HTMLElement) => T,
div_class, div_class: string,
require_fully_visible, require_fully_visible: boolean,
) { ): T[] {
// Note that when using getBoundingClientRect() we are getting offsets // Note that when using getBoundingClientRect() we are getting offsets
// relative to the visible window, but when using jQuery's offset() we are // 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 // 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 // We do this explicitly without merges and without recalculating
// the feed bounds to keep this computation as cheap as possible. // the feed bounds to keep this computation as cheap as possible.
const visible = []; const visible: T[] = [];
const $above_pointer = $selected_row const $above_pointer = $selected_row
.prevAll(`div.${CSS.escape(div_class)}`) .prevAll(`div.${CSS.escape(div_class)}`)
.slice(0, num_neighbors); .slice(0, num_neighbors);
@ -260,7 +294,8 @@ function _visible_divs(
return visible; 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(); const $selected_row = message_lists.current.selected_row();
if ($selected_row === undefined || $selected_row.length === 0) { if ($selected_row === undefined || $selected_row.length === 0) {
return []; return [];
@ -268,29 +303,45 @@ export function visible_groups(require_fully_visible) {
const $selected_group = rows.get_message_recipient_row($selected_row); const $selected_group = rows.get_message_recipient_row($selected_row);
function get_row(row) { function get_row(row: HTMLElement): HTMLElement {
return row; return row;
} }
// Being simplistic about this, the smallest group is about 75 px high. // 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(); const $selected_row = message_lists.current.selected_row();
function row_to_id(row) { function row_to_id(row: HTMLElement): Message {
return message_lists.current.get(rows.id($(row))); assert(message_lists.current !== undefined);
return message_lists.current.get(rows.id($(row))!)!;
} }
// Being simplistic about this, the smallest message is 25 px high. // 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(); const orig_scrollTop = $scroll_container.scrollTop();
if (target_scrollTop === undefined) { if (target_scrollTop === undefined) {
return orig_scrollTop; return orig_scrollTop ?? 0;
} }
let $ret = $scroll_container.scrollTop(target_scrollTop); let $ret = $scroll_container.scrollTop(target_scrollTop);
const new_scrollTop = $scroll_container.scrollTop(); const new_scrollTop = $scroll_container.scrollTop();
@ -327,13 +378,13 @@ export function scrollTop(target_scrollTop) {
return $ret; return $ret;
} }
export function stop_auto_scrolling() { export function stop_auto_scrolling(): void {
if (in_stoppable_autoscroll) { if (in_stoppable_autoscroll) {
$scroll_container.stop(); $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); message_scroll_state.set_update_selection_on_next_scroll(false);
const viewport_offset = scrollTop(); const viewport_offset = scrollTop();
in_stoppable_autoscroll = true; 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); message_scroll_state.set_update_selection_on_next_scroll(false);
in_stoppable_autoscroll = false; // defensive 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 // 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. // 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. // 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. // 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. // Only applies if the top of the message is out of view above the visible area.
const $selected_message = message_lists.current.selected_row(); 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 info = message_viewport_info();
const offset = $message_row.get_offset_to_window(); const offset = $message_row.get_offset_to_window();
return offset.top >= info.visible_bottom; 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. // See message_viewport.recenter_view() for related logic to keep the pointer onscreen.
// This function mostly comes into place for mouse scrollers, and it // This function mostly comes into place for mouse scrollers, and it
// keeps the pointer in view. For people who purely scroll with the // 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 top_threshold = info.visible_top + (1 / 10) * info.visible_height;
const bottom_threshold = info.visible_top + (9 / 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()) { if (at_top()) {
return true; return true;
} }
@ -455,11 +511,14 @@ export function keep_pointer_in_view() {
return false; 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; 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 // return true only if we make an actual adjustment, so
// that we know to short circuit the other direction // that we know to short circuit the other direction
if (in_view()) { if (in_view()) {
@ -479,10 +538,11 @@ export function keep_pointer_in_view() {
adjust(message_is_far_enough_up, rows.prev_visible); 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(); const $selected_row = message_lists.current.selected_row();
if ($selected_row && $selected_row.length !== 0) { if ($selected_row && $selected_row.length !== 0) {
recenter_view($selected_row); recenter_view($selected_row);
@ -491,11 +551,11 @@ export function scroll_to_selected() {
export let scroll_to_selected_planned = false; 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; 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 // If we have made a plan to scroll to the selected message but
// deferred doing so, do so here. // deferred doing so, do so here.
if (scroll_to_selected_planned) { if (scroll_to_selected_planned) {
@ -504,9 +564,8 @@ export function maybe_scroll_to_selected() {
} }
} }
export function initialize() { export function initialize(): void {
$jwindow = $(window); $jwindow = $(window);
// This handler must be placed before all resize handlers in our application // This handler must be placed before all resize handlers in our application
$jwindow.on("resize", () => { $jwindow.on("resize", () => {
dimensions.height.reset(); dimensions.height.reset();