mirror of https://github.com/zulip/zulip.git
ts: Convert util.js to TypeScript; add (Raw)Message type in types.ts.
Co-authored-by: Zixuan James Li <p359101898@gmail.com>
This commit is contained in:
parent
b757c1a9b6
commit
911b3b5365
|
@ -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", () => {
|
|||
'<a href="/#fragment" target="_blank">fragment</a>' +
|
||||
'<div class="message_inline_image">' +
|
||||
'<a href="http://zulip.zulipdev.com/user_uploads/w/ha/tever/inline.png" title="inline image">upload</a> ' +
|
||||
'<a role="button">button</a> ' +
|
||||
"</div>",
|
||||
),
|
||||
'<a href="http://example.com" target="_blank" rel="noopener noreferrer" title="http://example.com/">good</a> ' +
|
||||
|
@ -273,6 +272,7 @@ run_test("clean_user_content_links", () => {
|
|||
'<a href="/#fragment" title="http://zulip.zulipdev.com/#fragment">fragment</a>' +
|
||||
'<div class="message_inline_image">' +
|
||||
'<a href="http://zulip.zulipdev.com/user_uploads/w/ha/tever/inline.png" target="_blank" rel="noopener noreferrer" aria-label="inline image">upload</a> ' +
|
||||
'<a role="button">button</a> ' +
|
||||
"</div>",
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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<string, never>;
|
||||
|
|
|
@ -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<T>(
|
||||
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<T>(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<T> {
|
||||
_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<T>(
|
||||
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<T>(
|
||||
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
|
|
@ -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"],
|
||||
|
|
Loading…
Reference in New Issue