mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
In this case, for TypeScript narrowing to work correctly, we need a if statement that check that both of the type are same. The one condition we had that checked that both have the same type doesn't work with TypeScript narrowing yet.
340 lines
10 KiB
JavaScript
340 lines
10 KiB
JavaScript
import _ from "lodash";
|
|
|
|
// From MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random
|
|
export function random_int(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
// Like C++'s std::lower_bound. Returns the first index at which
|
|
// `value` could be inserted without changing the ordering. Assumes
|
|
// the array is sorted.
|
|
//
|
|
// `first` and `last` are indices and `less` is an optionally-specified
|
|
// function that returns true if
|
|
// array[i] < value
|
|
// for some i and false otherwise.
|
|
//
|
|
// Usage: lower_bound(array, value, less)
|
|
export function lower_bound(array, value, less) {
|
|
let first = 0;
|
|
const last = array.length;
|
|
|
|
let len = last - first;
|
|
let middle;
|
|
let step;
|
|
while (len > 0) {
|
|
step = Math.floor(len / 2);
|
|
middle = first + step;
|
|
if (less(array[middle], value, middle)) {
|
|
first = middle;
|
|
first += 1;
|
|
len = len - step - 1;
|
|
} else {
|
|
len = step;
|
|
}
|
|
}
|
|
return first;
|
|
}
|
|
|
|
function lower_same(a, b) {
|
|
return a.toLowerCase() === b.toLowerCase();
|
|
}
|
|
|
|
export const same_stream_and_topic = function util_same_stream_and_topic(a, b) {
|
|
// Streams and topics are case-insensitive.
|
|
return a.stream_id === b.stream_id && lower_same(a.topic, b.topic);
|
|
};
|
|
|
|
export function is_pm_recipient(user_id, message) {
|
|
const recipients = message.to_user_ids.split(",");
|
|
return recipients.includes(user_id.toString());
|
|
}
|
|
|
|
export function extract_pm_recipients(recipients) {
|
|
return recipients.split(/\s*[,;]\s*/).filter((recipient) => recipient.trim() !== "");
|
|
}
|
|
|
|
export const same_recipient = function util_same_recipient(a, b) {
|
|
if (a === undefined || b === undefined) {
|
|
return false;
|
|
}
|
|
|
|
if (a.type === "private" && b.type === "private") {
|
|
if (a.to_user_ids === undefined) {
|
|
return false;
|
|
}
|
|
return a.to_user_ids === b.to_user_ids;
|
|
} else if (a.type === "stream" && b.type === "stream") {
|
|
return same_stream_and_topic(a, b);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
export const same_sender = function util_same_sender(a, b) {
|
|
return (
|
|
a !== undefined &&
|
|
b !== undefined &&
|
|
a.sender_email.toLowerCase() === b.sender_email.toLowerCase()
|
|
);
|
|
};
|
|
|
|
export function normalize_recipients(recipients) {
|
|
// Converts a string listing emails of message recipients
|
|
// into a canonical formatting: emails sorted ASCIIbetically
|
|
// with exactly one comma and no spaces between each.
|
|
return recipients
|
|
.split(",")
|
|
.map((s) => s.trim().toLowerCase())
|
|
.filter((s) => s.length > 0)
|
|
.sort()
|
|
.join(",");
|
|
}
|
|
|
|
// Avoid URI decode errors by removing characters from the end
|
|
// 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) {
|
|
let end = str.length;
|
|
while (end > 0) {
|
|
try {
|
|
return decodeURIComponent(str.slice(0, end));
|
|
} catch (error) {
|
|
if (!(error instanceof URIError)) {
|
|
throw error;
|
|
}
|
|
end -= 1;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// If we can, use a locale-aware sorter. However, if the browser
|
|
// doesn't support the ECMAScript Internationalization API
|
|
// Specification, do a dumb string comparison because
|
|
// String.localeCompare is really slow.
|
|
export function make_strcmp() {
|
|
try {
|
|
const collator = new Intl.Collator();
|
|
return collator.compare;
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
|
|
return function util_strcmp(a, b) {
|
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
};
|
|
}
|
|
|
|
export const strcmp = make_strcmp();
|
|
|
|
export const array_compare = function util_array_compare(a, b) {
|
|
if (a.length !== b.length) {
|
|
return false;
|
|
}
|
|
let i;
|
|
for (i = 0; i < a.length; i += 1) {
|
|
if (a[i] !== b[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/* Represents a value that is expensive to compute and should be
|
|
* computed on demand and then cached. The value can be forcefully
|
|
* recalculated on the next call to get() by calling reset().
|
|
*
|
|
* 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;
|
|
|
|
constructor(opts) {
|
|
this.compute_value = opts.compute_value;
|
|
}
|
|
|
|
get() {
|
|
if (this._value === unassigned_value_sentinel) {
|
|
this._value = this.compute_value();
|
|
}
|
|
return this._value;
|
|
}
|
|
|
|
reset() {
|
|
this._value = unassigned_value_sentinel;
|
|
}
|
|
}
|
|
|
|
export function find_wildcard_mentions(message_content) {
|
|
const mention = message_content.match(/(^|\s)(@\*{2}(all|everyone|stream)\*{2})($|\s)/);
|
|
if (mention === null) {
|
|
return null;
|
|
}
|
|
return mention[3];
|
|
}
|
|
|
|
export const move_array_elements_to_front = function util_move_array_elements_to_front(
|
|
array,
|
|
selected,
|
|
) {
|
|
const selected_hash = new Set(selected);
|
|
const selected_elements = [];
|
|
const unselected_elements = [];
|
|
for (const element of array) {
|
|
(selected_hash.has(element) ? selected_elements : unselected_elements).push(element);
|
|
}
|
|
return [...selected_elements, ...unselected_elements];
|
|
};
|
|
|
|
// check by the userAgent string if a user's client is likely mobile.
|
|
export function is_mobile() {
|
|
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) {
|
|
// 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));
|
|
id_list.sort((a, b) => a - b);
|
|
id_list = _.sortedUniq(id_list);
|
|
|
|
return id_list;
|
|
}
|
|
|
|
export function set_match_data(target, source) {
|
|
target.match_subject = source.match_subject;
|
|
target.match_content = source.match_content;
|
|
}
|
|
|
|
export function get_match_topic(obj) {
|
|
return obj.match_subject;
|
|
}
|
|
|
|
export function get_draft_topic(obj) {
|
|
// We will need to support subject for old drafts.
|
|
return obj.topic || obj.subject || "";
|
|
}
|
|
|
|
export function get_reload_topic(obj) {
|
|
// When we first upgrade to releases that have
|
|
// topic=foo in the code, the user's reload URL
|
|
// may still have subject=foo from the prior version.
|
|
return obj.topic || obj.subject || "";
|
|
}
|
|
|
|
export function get_edit_event_topic(obj) {
|
|
if (obj.topic === undefined) {
|
|
return obj.subject;
|
|
}
|
|
|
|
// This code won't be reachable till we fix the
|
|
// server, but we use it now in tests.
|
|
return obj.topic;
|
|
}
|
|
|
|
export function get_edit_event_orig_topic(obj) {
|
|
return obj.orig_subject;
|
|
}
|
|
|
|
export function get_edit_event_prev_topic(obj) {
|
|
return obj.prev_subject;
|
|
}
|
|
|
|
export function is_topic_synonym(operator) {
|
|
return operator === "subject";
|
|
}
|
|
|
|
export function convert_message_topic(message) {
|
|
if (message.topic === undefined) {
|
|
message.topic = message.subject;
|
|
}
|
|
}
|
|
|
|
export function clean_user_content_links(html) {
|
|
const content = new DOMParser().parseFromString(html, "text/html").body;
|
|
for (const elt of content.querySelectorAll("a")) {
|
|
// Ensure that all external links have target="_blank"
|
|
// rel="opener noreferrer". This ensures that external links
|
|
// never replace the Zulip web app while also protecting
|
|
// against reverse tabnapping attacks, without relying on the
|
|
// correctness of how Zulip's Markdown processor generates links.
|
|
//
|
|
// Fragment links, which we intend to only open within the
|
|
// Zulip web app using our hashchange system, do not require
|
|
// these attributes.
|
|
const href = elt.getAttribute("href");
|
|
let url;
|
|
try {
|
|
url = new URL(href, window.location.href);
|
|
} catch {
|
|
elt.removeAttribute("href");
|
|
elt.removeAttribute("title");
|
|
continue;
|
|
}
|
|
|
|
// eslint-disable-next-line no-script-url
|
|
if (["data:", "javascript:", "vbscript:"].includes(url.protocol)) {
|
|
// Remove unsafe links completely.
|
|
elt.removeAttribute("href");
|
|
elt.removeAttribute("title");
|
|
continue;
|
|
}
|
|
|
|
// We detect URLs that are just fragments by comparing the URL
|
|
// against a new URL generated using only the hash.
|
|
if (url.hash === "" || url.href !== new URL(url.hash, window.location.href).href) {
|
|
elt.setAttribute("target", "_blank");
|
|
elt.setAttribute("rel", "noopener noreferrer");
|
|
} else {
|
|
elt.removeAttribute("target");
|
|
}
|
|
|
|
// Ensure that the title displays the real URL.
|
|
let title;
|
|
let legacy_title;
|
|
if (url.origin === window.location.origin && url.pathname.startsWith("/user_uploads/")) {
|
|
title = legacy_title = url.pathname.slice(url.pathname.lastIndexOf("/") + 1);
|
|
} else {
|
|
title = url;
|
|
legacy_title = href;
|
|
}
|
|
elt.setAttribute(
|
|
"title",
|
|
["", legacy_title].includes(elt.title) ? title : `${title}\n${elt.title}`,
|
|
);
|
|
}
|
|
return content.innerHTML;
|
|
}
|
|
|
|
export function filter_by_word_prefix_match(items, search_term, item_to_text) {
|
|
if (search_term === "") {
|
|
return items;
|
|
}
|
|
|
|
let search_terms = search_term.toLowerCase().split(",");
|
|
search_terms = search_terms.map((s) => s.trim());
|
|
|
|
const filtered_items = items.filter((item) =>
|
|
search_terms.some((search_term) => {
|
|
const lower_name = item_to_text(item).toLowerCase();
|
|
const cands = lower_name.split(" ");
|
|
cands.push(lower_name);
|
|
return cands.some((name) => name.startsWith(search_term));
|
|
}),
|
|
);
|
|
|
|
return filtered_items;
|
|
}
|
|
|
|
export function get_time_from_date_muted(date_muted) {
|
|
if (date_muted === undefined) {
|
|
return Date.now();
|
|
}
|
|
return date_muted * 1000;
|
|
}
|