From 911b3b5365d9bfd935b04b5406bb302616c02783 Mon Sep 17 00:00:00 2001 From: Yuri Sulyma Date: Fri, 26 Aug 2022 15:12:21 -0400 Subject: [PATCH] ts: Convert util.js to TypeScript; add (Raw)Message type in types.ts. Co-authored-by: Zixuan James Li --- frontend_tests/node_tests/util.js | 4 +- static/js/types.ts | 57 ++++++++++++++ static/js/{util.js => util.ts} | 120 ++++++++++++++++++------------ tools/linter_lib/custom_check.py | 2 +- 4 files changed, 131 insertions(+), 52 deletions(-) rename static/js/{util.js => util.ts} (76%) diff --git a/frontend_tests/node_tests/util.js b/frontend_tests/node_tests/util.js index f6348024a5..30c6153d61 100644 --- a/frontend_tests/node_tests/util.js +++ b/frontend_tests/node_tests/util.js @@ -81,8 +81,6 @@ run_test("same_recipient", () => { assert.ok(!util.same_recipient({type: "private", to_user_ids: undefined}, {type: "private"})); - assert.ok(!util.same_recipient({type: "unknown type"}, {type: "unknown type"})); - assert.ok(!util.same_recipient(undefined, {type: "private"})); assert.ok(!util.same_recipient(undefined, undefined)); @@ -264,6 +262,7 @@ run_test("clean_user_content_links", () => { 'fragment' + '
' + 'upload ' + + 'button ' + "
", ), 'good ' + @@ -273,6 +272,7 @@ run_test("clean_user_content_links", () => { 'fragment' + '
' + 'upload ' + + 'button ' + "
", ); }); diff --git a/static/js/types.ts b/static/js/types.ts index a9229eaad5..dc14653c3a 100644 --- a/static/js/types.ts +++ b/static/js/types.ts @@ -1,3 +1,26 @@ +// TODO/typescript: Move this to message_store +export type MatchedMessage = { + match_content?: string; + match_subject?: string; +}; + +// TODO/typescript: Move this to message_store +export type MessageType = "private" | "stream"; + +// TODO/typescript: Move this to message_store +export type RawMessage = { + sender_email: string; + stream_id: number; + subject: string; + type: MessageType; +} & MatchedMessage; + +// TODO/typescript: Move this to message_store +export type Message = RawMessage & { + to_user_ids: string; + topic: string; +}; + // TODO/typescript: Move this to server_events_dispatch export type UserGroupUpdateEvent = { id: number; @@ -9,6 +32,40 @@ export type UserGroupUpdateEvent = { }; }; +// TODO/typescript: Move this to server_events +export type TopicLink = { + text: string; + url: string; +}; + +// TODO/typescript: Move this to server_events +export type UpdateMessageEvent = { + id: number; + type: string; + user_id: number | null; + rendering_only: boolean; + message_id: number; + message_ids: number[]; + flags: string[]; + edit_timestamp: number; + stream_name?: string; + stream_id?: number; + new_stream_id?: number; + propagate_mode?: string; + orig_subject?: string; + subject?: string; + topic_links?: TopicLink[]; + orig_content?: string; + orig_rendered_content?: string; + prev_rendered_content_version?: number; + content?: string; + rendered_content?: string; + is_me_message?: boolean; + // The server is still using subject. + // This will not be set until it gets fixed. + topic?: string; +}; + // TODO/typescript: Move the User and Stream placeholder // types to their appropriate modules. export type User = Record; diff --git a/static/js/util.js b/static/js/util.ts similarity index 76% rename from static/js/util.js rename to static/js/util.ts index aa65f1998a..d7ad5a51fe 100644 --- a/static/js/util.js +++ b/static/js/util.ts @@ -2,9 +2,10 @@ import _ from "lodash"; import * as blueslip from "./blueslip"; import {$t} from "./i18n"; +import type {MatchedMessage, Message, RawMessage, UpdateMessageEvent} from "./types"; // From MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random -export function random_int(min, max) { +export function random_int(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } @@ -18,7 +19,11 @@ export function random_int(min, max) { // for some i and false otherwise. // // Usage: lower_bound(array, value, less) -export function lower_bound(array, value, less) { +export function lower_bound( + array: T[], + value: T, + less: (item: T, value: T, middle: number) => boolean, +): number { let first = 0; const last = array.length; @@ -39,7 +44,7 @@ export function lower_bound(array, value, less) { return first; } -export const lower_same = function lower_same(a, b) { +export const lower_same = function lower_same(a?: string, b?: string): boolean { if (a === undefined || b === undefined) { blueslip.error( `Cannot compare strings; at least one value is undefined: \ @@ -50,17 +55,28 @@ ${a ?? "(undefined)"}, ${b ?? "(undefined)"}`, return a.toLowerCase() === b.toLowerCase(); }; -export const same_stream_and_topic = function util_same_stream_and_topic(a, b) { +export type StreamTopic = { + stream_id: number; + topic: string; +}; + +export const same_stream_and_topic = function util_same_stream_and_topic( + a: StreamTopic, + b: StreamTopic, +): boolean { // Streams and topics are case-insensitive. return a.stream_id === b.stream_id && lower_same(a.topic, b.topic); }; - -export function extract_pm_recipients(recipients) { +export function extract_pm_recipients(recipients: string): string[] { return recipients.split(/\s*[,;]\s*/).filter((recipient) => recipient.trim() !== ""); } -export const same_recipient = function util_same_recipient(a, b) { +// When the type is "private", properties from PMRecipient, namely to_user_ids might be +// undefined. See https://github.com/zulip/zulip/pull/23032#discussion_r1038480596. +type Recipient = {to_user_ids?: string; type: "private"} | (StreamTopic & {type: "stream"}); + +export const same_recipient = function util_same_recipient(a?: Recipient, b?: Recipient): boolean { if (a === undefined || b === undefined) { return false; } @@ -77,7 +93,7 @@ export const same_recipient = function util_same_recipient(a, b) { return false; }; -export const same_sender = function util_same_sender(a, b) { +export const same_sender = function util_same_sender(a: RawMessage, b: RawMessage): boolean { return ( a !== undefined && b !== undefined && @@ -85,7 +101,7 @@ export const same_sender = function util_same_sender(a, b) { ); }; -export function normalize_recipients(recipients) { +export function normalize_recipients(recipients: string): string { // Converts a string listing emails of message recipients // into a canonical formatting: emails sorted ASCIIbetically // with exactly one comma and no spaces between each. @@ -101,7 +117,7 @@ export function normalize_recipients(recipients) { // one by one until the decode succeeds. This makes sense if // we are decoding input that the user is in the middle of // typing. -export function robust_uri_decode(str) { +export function robust_uri_decode(str: string): string { let end = str.length; while (end > 0) { try { @@ -120,22 +136,22 @@ export function robust_uri_decode(str) { // doesn't support the ECMAScript Internationalization API // Specification, do a dumb string comparison because // String.localeCompare is really slow. -export function make_strcmp() { +export function make_strcmp(): (x: string, y: string) => number { try { const collator = new Intl.Collator(); - return collator.compare; + return collator.compare.bind(collator); } catch { // continue regardless of error } - return function util_strcmp(a, b) { + return function util_strcmp(a: string, b: string): number { return a < b ? -1 : a > b ? 1 : 0; }; } export const strcmp = make_strcmp(); -export const array_compare = function util_array_compare(a, b) { +export const array_compare = function util_array_compare(a: T[], b: T[]): boolean { if (a.length !== b.length) { return false; } @@ -155,27 +171,29 @@ export const array_compare = function util_array_compare(a, b) { * You must supply a option to the constructor called compute_value * which should be a function that computes the uncached value. */ -const unassigned_value_sentinel = Symbol("unassigned_value_sentinel"); -export class CachedValue { - _value = unassigned_value_sentinel; +const unassigned_value_sentinel: unique symbol = Symbol("unassigned_value_sentinel"); +export class CachedValue { + _value: T | typeof unassigned_value_sentinel = unassigned_value_sentinel; - constructor(opts) { + private compute_value: () => T; + + constructor(opts: {compute_value: () => T}) { this.compute_value = opts.compute_value; } - get() { + get(): T { if (this._value === unassigned_value_sentinel) { this._value = this.compute_value(); } return this._value; } - reset() { + reset(): void { this._value = unassigned_value_sentinel; } } -export function find_wildcard_mentions(message_content) { +export function find_wildcard_mentions(message_content: string): string | null { const mention = message_content.match(/(^|\s)(@\*{2}(all|everyone|stream)\*{2})($|\s)/); if (mention === null) { return null; @@ -183,13 +201,13 @@ export function find_wildcard_mentions(message_content) { return mention[3]; } -export const move_array_elements_to_front = function util_move_array_elements_to_front( - array, - selected, -) { +export const move_array_elements_to_front = function util_move_array_elements_to_front( + array: T[], + selected: T[], +): T[] { const selected_hash = new Set(selected); - const selected_elements = []; - const unselected_elements = []; + const selected_elements: T[] = []; + const unselected_elements: T[] = []; for (const element of array) { (selected_hash.has(element) ? selected_elements : unselected_elements).push(element); } @@ -197,12 +215,12 @@ export const move_array_elements_to_front = function util_move_array_elements_to }; // check by the userAgent string if a user's client is likely mobile. -export function is_mobile() { +export function is_mobile(): boolean { const regex = "Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini"; return new RegExp(regex, "i").test(window.navigator.userAgent); } -export function sorted_ids(ids) { +export function sorted_ids(ids: string[]): number[] { // This mapping makes sure we are using ints, and // it also makes sure we don't mutate the list. let id_list = ids.map((s) => Number.parseInt(s, 10)); @@ -212,16 +230,16 @@ export function sorted_ids(ids) { return id_list; } -export function set_match_data(target, source) { +export function set_match_data(target: Message, source: MatchedMessage): void { target.match_subject = source.match_subject; target.match_content = source.match_content; } -export function get_match_topic(obj) { +export function get_match_topic(obj: Message): string | undefined { return obj.match_subject; } -export function get_edit_event_topic(obj) { +export function get_edit_event_topic(obj: UpdateMessageEvent): string | undefined { if (obj.topic === undefined) { return obj.subject; } @@ -231,21 +249,21 @@ export function get_edit_event_topic(obj) { return obj.topic; } -export function get_edit_event_orig_topic(obj) { +export function get_edit_event_orig_topic(obj: UpdateMessageEvent): string | undefined { return obj.orig_subject; } -export function is_topic_synonym(operator) { +export function is_topic_synonym(operator: string): boolean { return operator === "subject"; } -export function convert_message_topic(message) { +export function convert_message_topic(message: Message): void { if (message.topic === undefined) { message.topic = message.subject; } } -export function clean_user_content_links(html) { +export function clean_user_content_links(html: string): string { const content = new DOMParser().parseFromString(html, "text/html").body; for (const elt of content.querySelectorAll("a")) { // Ensure that all external links have target="_blank" @@ -258,6 +276,9 @@ export function clean_user_content_links(html) { // Zulip web app using our hashchange system, do not require // these attributes. const href = elt.getAttribute("href"); + if (href === null) { + continue; + } let url; try { url = new URL(href, window.location.href); @@ -289,15 +310,16 @@ export function clean_user_content_links(html) { if (is_inline_image) { // For inline images we want to handle the tooltips explicitly, and disable // the browser's built in handling of the title attribute. - if (elt.getAttribute("title")) { - elt.setAttribute("aria-label", elt.getAttribute("title")); + const title = elt.getAttribute("title"); + if (title !== null) { + elt.setAttribute("aria-label", title); elt.removeAttribute("title"); } } else { // For non-image user uploads, the following block ensures that the title // attribute always displays the filename as a security measure. - let title; - let legacy_title; + let title: string; + let legacy_title: string; if ( url.origin === window.location.origin && url.pathname.startsWith("/user_uploads/") @@ -311,7 +333,7 @@ export function clean_user_content_links(html) { {filename: url.pathname.slice(url.pathname.lastIndexOf("/") + 1)}, ); } else { - title = url; + title = url.toString(); legacy_title = href; } elt.setAttribute( @@ -323,12 +345,12 @@ export function clean_user_content_links(html) { return content.innerHTML; } -export function filter_by_word_prefix_match( - items, - search_term, - item_to_text, - word_separator_regex = /\s/, -) { +export function filter_by_word_prefix_match( + items: T[], + search_term: string, + item_to_text: (item: T) => string, + word_separator_regex: RegExp = /\s/, +): T[] { if (search_term === "") { return items; } @@ -353,14 +375,14 @@ export function filter_by_word_prefix_match( return filtered_items; } -export function get_time_from_date_muted(date_muted) { +export function get_time_from_date_muted(date_muted: number | undefined): number { if (date_muted === undefined) { return Date.now(); } return date_muted * 1000; } -export function call_function_periodically(callback, delay) { +export function call_function_periodically(callback: () => void, delay: number): void { // We previously used setInterval for this purpose, but // empirically observed that after unsuspend, Chrome can end // up trying to "catch up" by doing dozens of these requests diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index 725113b1d8..440d32588f 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -116,7 +116,7 @@ js_rules = RuleList( rules=[ { "pattern": "subject|SUBJECT", - "exclude": {"static/js/util.js", "frontend_tests/"}, + "exclude": {"static/js/types.ts", "static/js/util.ts", "frontend_tests/"}, "exclude_pattern": "emails", "description": "avoid subject in JS code", "good_lines": ["topic_name"],