From 4b7f07ef6d4edea88366d4b900778cda5291f0fd Mon Sep 17 00:00:00 2001 From: wuyudi Date: Tue, 7 Feb 2023 23:16:06 +0900 Subject: [PATCH] first step --- index.html | 2 +- package-lock.json | 28 ++- package.json | 1 + src/custom.d.ts | 4 + src/{font.js => font.ts} | 0 src/{index.jsx => index.tsx} | 0 src/util/{AsyncSearch.js => AsyncSearch.ts} | 38 +++- src/util/{Postie.js => Postie.ts} | 38 ++-- src/util/{colorMXID.js => colorMXID.ts} | 12 +- src/util/common.ts | 233 ++++++++++++++++++++ src/util/{matrixUtil.js => matrixUtil.ts} | 59 ++--- src/util/{mimetypes.js => mimetypes.ts} | 2 +- src/util/{sanitize.js => sanitize.ts} | 62 +++++- src/util/{sort.js => sort.ts} | 4 +- 14 files changed, 404 insertions(+), 79 deletions(-) create mode 100644 src/custom.d.ts rename src/{font.js => font.ts} (100%) rename src/{index.jsx => index.tsx} (100%) rename src/util/{AsyncSearch.js => AsyncSearch.ts} (81%) rename src/util/{Postie.js => Postie.ts} (66%) rename src/util/{colorMXID.js => colorMXID.ts} (68%) create mode 100644 src/util/common.ts rename src/util/{matrixUtil.js => matrixUtil.ts} (77%) rename src/util/{mimetypes.js => mimetypes.ts} (93%) rename src/util/{sanitize.js => sanitize.ts} (72%) rename src/util/{sort.js => sort.ts} (89%) diff --git a/index.html b/index.html index af1a6268..55ea6520 100644 --- a/index.html +++ b/index.html @@ -96,6 +96,6 @@ - + diff --git a/package-lock.json b/package-lock.json index bef7acdd..6639aac8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", + "@types/sanitize-html": "2.8.0", "@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/parser": "5.46.1", "@vitejs/plugin-react": "3.0.0", @@ -1165,6 +1166,15 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "node_modules/@types/sanitize-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.8.0.tgz", + "integrity": "sha512-Uih6caOm3DsBYnVGOYn0A9NoTNe1c4aPStmHC/YA2JrpP9kx//jzaRcIklFvSpvVQEcpl/ZCr4DgISSf/YxTvg==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -3668,9 +3678,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -4974,9 +4984,9 @@ } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -5059,9 +5069,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz", - "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==", + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", "funding": [ { "type": "opencollective", diff --git a/package.json b/package.json index 7e026b7b..72ca6805 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", + "@types/sanitize-html": "2.8.0", "@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/parser": "5.46.1", "@vitejs/plugin-react": "3.0.0", diff --git a/src/custom.d.ts b/src/custom.d.ts new file mode 100644 index 00000000..006534e2 --- /dev/null +++ b/src/custom.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: React.FunctionComponent>; + export default content; +} diff --git a/src/font.js b/src/font.ts similarity index 100% rename from src/font.js rename to src/font.ts diff --git a/src/index.jsx b/src/index.tsx similarity index 100% rename from src/index.jsx rename to src/index.tsx diff --git a/src/util/AsyncSearch.js b/src/util/AsyncSearch.ts similarity index 81% rename from src/util/AsyncSearch.js rename to src/util/AsyncSearch.ts index d0a2130e..5b0c51da 100644 --- a/src/util/AsyncSearch.js +++ b/src/util/AsyncSearch.ts @@ -1,6 +1,18 @@ import EventEmitter from 'events'; class AsyncSearch extends EventEmitter { + RESULT_SENT: string; + dataList: (string | object)[]; + term: any; + searchKeys: any; + isContain: boolean; + isCaseSensitive: boolean; + normalizeUnicode: boolean; + ignoreWhitespace: boolean; + limit: number; + findingList: any[]; + searchUptoIndex: number; + sessionStartTimestamp: number; constructor() { super(); @@ -44,7 +56,17 @@ class AsyncSearch extends EventEmitter { * @param {boolean} [opts.ignoreWhitespace=true] * @param {number} [opts.limit=null] - Stop search after limit */ - setup(dataList, opts) { + setup( + dataList: (string | object)[], + opts: { + keys?: string | string[]; + isContain?: boolean; + isCaseSensitive?: boolean; + normalizeUnicode?: boolean; + ignoreWhitespace?: boolean; + limit: number; + } + ) { this._reset(); this.dataList = dataList; this.searchKeys = opts?.keys || null; @@ -55,7 +77,7 @@ class AsyncSearch extends EventEmitter { this.limit = opts?.limit || null; } - search(term) { + search(term: any) { this._softReset(); this.term = this._normalize(term); @@ -67,7 +89,7 @@ class AsyncSearch extends EventEmitter { this._find(this.sessionStartTimestamp, 0); } - _find(sessionTimestamp, lastFindingCount) { + _find(sessionTimestamp: number, lastFindingCount: number) { if (sessionTimestamp !== this.sessionStartTimestamp) return; this.sessionStartTimestamp = window.performance.now(); @@ -93,12 +115,12 @@ class AsyncSearch extends EventEmitter { } } - if (lastFindingCount !== this.findingList.length - || lastFindingCount === 0) this._sendFindings(); + if (lastFindingCount !== this.findingList.length || lastFindingCount === 0) + this._sendFindings(); this._softReset(); } - _match(item) { + _match(item: string | object) { if (typeof item === 'string') { return this._compare(item); } @@ -113,14 +135,14 @@ class AsyncSearch extends EventEmitter { return false; } - _compare(item) { + _compare(item: string) { if (typeof item !== 'string') return false; const myItem = this._normalize(item); if (this.isContain) return myItem.indexOf(this.term) !== -1; return myItem.startsWith(this.term); } - _normalize(item) { + _normalize(item: string) { let myItem = item.normalize(this.normalizeUnicode ? 'NFKC' : 'NFC'); if (!this.isCaseSensitive) myItem = myItem.toLocaleLowerCase(); if (this.ignoreWhitespace) myItem = myItem.replace(/\s/g, ''); diff --git a/src/util/Postie.js b/src/util/Postie.ts similarity index 66% rename from src/util/Postie.js rename to src/util/Postie.ts index 73c8f9e8..b646a266 100644 --- a/src/util/Postie.js +++ b/src/util/Postie.ts @@ -1,9 +1,10 @@ class Postie { + _topics: Map>>; constructor() { this._topics = new Map(); } - _getSubscribers(topic) { + _getSubscribers(topic: string) { const subscribers = this._topics.get(topic); if (subscribers === undefined) { throw new Error(`Topic:"${topic}" doesn't exist.`); @@ -11,7 +12,7 @@ class Postie { return subscribers; } - _getInboxes(topic, address) { + _getInboxes(topic: string, address: string) { const subscribers = this._getSubscribers(topic); const inboxes = subscribers.get(address); if (inboxes === undefined) { @@ -20,19 +21,17 @@ class Postie { return inboxes; } - hasTopic(topic) { + hasTopic(topic: string) { return this._topics.get(topic) !== undefined; } - hasSubscriber(topic, address) { + hasSubscriber(topic: string, address: string) { const subscribers = this._getSubscribers(topic); return subscribers.get(address) !== undefined; } - hasTopicAndSubscriber(topic, address) { - return (this.hasTopic(topic)) - ? this.hasSubscriber(topic, address) - : false; + hasTopicAndSubscriber(topic: string, address: string) { + return this.hasTopic(topic) ? this.hasSubscriber(topic, address) : false; } /** @@ -40,12 +39,12 @@ class Postie { * @param {string} address - Address of subscriber * @param {function} inbox - The inbox function to receive post data */ - subscribe(topic, address, inbox) { + subscribe(topic: string, address: string, inbox: Set) { if (typeof inbox !== 'function') { throw new TypeError('Inbox must be a function.'); } - if (this._topics.has(topic) === false) { + if (!this._topics.has(topic)) { this._topics.set(topic, new Map()); } const subscribers = this._topics.get(topic); @@ -57,14 +56,17 @@ class Postie { return () => this.unsubscribe(topic, address, inbox); } - unsubscribe(topic, address, inbox) { + unsubscribe(topic: string, address: string, inbox: Function) { const subscribers = this._getSubscribers(topic); if (!subscribers) throw new Error(`Unable to unsubscribe. Topic: "${topic}" doesn't exist.`); const inboxes = subscribers.get(address); - if (!inboxes) throw new Error(`Unable to unsubscribe. Subscriber on topic:"${topic}" at address:"${address}" doesn't exist`); + if (!inboxes) + throw new Error( + `Unable to unsubscribe. Subscriber on topic:"${topic}" at address:"${address}" doesn't exist` + ); - if (!inboxes.delete(inbox)) throw new Error('Unable to unsubscribe. Inbox doesn\'t exist'); + if (!inboxes.delete(inbox)) throw new Error("Unable to unsubscribe. Inbox doesn't exist"); if (inboxes.size === 0) subscribers.delete(address); if (subscribers.size === 0) this._topics.delete(topic); @@ -75,10 +77,12 @@ class Postie { * @param {string|string[]} address - Address of subscriber * @param {*} data - Data to deliver to subscriber */ - post(topic, address, data) { - const sendPost = (inboxes, addr) => { + post(topic: string, address: string | string[], data: any) { + const sendPost = (inboxes: Set, addr: string) => { if (inboxes === undefined) { - throw new Error(`Unable to post on topic:"${topic}" at address:"${addr}". Subscriber doesn't exist.`); + throw new Error( + `Unable to post on topic:"${topic}" at address:"${addr}". Subscriber doesn't exist.` + ); } inboxes.forEach((inbox) => inbox(data)); }; @@ -88,7 +92,7 @@ class Postie { return; } const subscribers = this._getSubscribers(topic); - address.forEach((addr) => { + address.forEach((addr: string) => { sendPost(subscribers.get(addr), addr); }); } diff --git a/src/util/colorMXID.js b/src/util/colorMXID.ts similarity index 68% rename from src/util/colorMXID.js rename to src/util/colorMXID.ts index 4d303aae..5b5f9899 100644 --- a/src/util/colorMXID.js +++ b/src/util/colorMXID.ts @@ -1,27 +1,27 @@ // https://github.com/cloudrac3r/cadencegq/blob/master/pug/mxid.pug -export function hashCode(str) { +export function hashCode(str: string) { let hash = 0; - let i; - let chr; + let i: number; + let chr: number; if (str.length === 0) { return hash; } for (i = 0; i < str.length; i += 1) { chr = str.charCodeAt(i); // eslint-disable-next-line no-bitwise - hash = ((hash << 5) - hash) + chr; + hash = (hash << 5) - hash + chr; // eslint-disable-next-line no-bitwise hash |= 0; } return Math.abs(hash); } -export function cssColorMXID(userId) { +export function cssColorMXID(userId: string) { const colorNumber = hashCode(userId) % 8; return `--mx-uc-${colorNumber + 1}`; } -export default function colorMXID(userId) { +export default function colorMXID(userId: string) { return `var(${cssColorMXID(userId)})`; } diff --git a/src/util/common.ts b/src/util/common.ts new file mode 100644 index 00000000..e11ec344 --- /dev/null +++ b/src/util/common.ts @@ -0,0 +1,233 @@ +/* eslint-disable max-classes-per-file */ +export function bytesToSize(bytes: number) { + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) return 'n/a'; + const i = Math.floor(Math.floor(Math.log(bytes) / Math.log(1024))); + if (i === 0) return `${bytes} ${sizes[i]}`; + return `${(bytes / 1024 ** i).toFixed(1)} ${sizes[i]}`; +} + +export function diffMinutes(dt2: { getTime: () => number }, dt1: { getTime: () => number }) { + let diff = (dt2.getTime() - dt1.getTime()) / 1000; + diff /= 60; + return Math.abs(Math.round(diff)); +} + +export function isInSameDay(dt2: Date, dt1: Date) { + return ( + dt2.getFullYear() === dt1.getFullYear() && + dt2.getMonth() === dt1.getMonth() && + dt2.getDate() === dt1.getDate() + ); +} + +/** + * @param {Event} ev + * @param {string} [targetSelector] element selector for Element.matches([selector]) + */ +export function getEventCords(ev: Event, targetSelector: string) { + let boxInfo: DOMRect; + + const path = ev.composedPath(); + const target = targetSelector + ? path.find((element) => (element as HTMLElement).matches?.(targetSelector)) + : null; + if (target) { + boxInfo = (target as HTMLElement).getBoundingClientRect(); + } else { + boxInfo = (ev.target as HTMLElement).getBoundingClientRect(); + } + + return { + x: boxInfo.x, + y: boxInfo.y, + width: boxInfo.width, + height: boxInfo.height, + detail: (ev as MouseEvent).detail, + }; +} + +export function abbreviateNumber(number: number) { + if (number > 99) return '99+'; + return number; +} + +export class Debounce { + timeoutId: any; + constructor() { + this.timeoutId = null; + } + + /** + * @param {function} func - callback function + * @param {number} wait - wait in milliseconds to call func + * @returns {func} debounceCallback - to pass arguments to func callback + */ + _(func: Function, wait: number) { + const debounceCallback = (...args) => { + clearTimeout(this.timeoutId); + this.timeoutId = setTimeout(() => { + func(args); + this.timeoutId = null; + }, wait); + }; + return debounceCallback; + } +} + +export class Throttle { + timeoutId: any; + constructor() { + this.timeoutId = null; + } + + /** + * @param {function} func - callback function + * @param {number} wait - wait in milliseconds to call func + * @returns {function} throttleCallback - to pass arguments to func callback + */ + _(func: Function, wait: number) { + const throttleCallback = (...args) => { + if (this.timeoutId !== null) return; + this.timeoutId = setTimeout(() => { + func(args); + this.timeoutId = null; + }, wait); + }; + return throttleCallback; + } +} + +export function getUrlPrams(paramName: string) { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + return urlParams.get(paramName); +} + +export function getScrollInfo(target: HTMLElement) { + const scroll = { + top: Math.round(target.scrollTop), + height: Math.round(target.scrollHeight), + viewHeight: Math.round(target.offsetHeight), + isScrollable: this.height > this.viewHeight, + }; + return scroll; +} + +export function avatarInitials(text) { + return [...text][0]; +} + +export function cssVar(name: string) { + return getComputedStyle(document.body).getPropertyValue(name); +} + +export function setFavicon(url: string) { + const favicon = document.querySelector('#favicon'); + if (!favicon) return; + favicon.setAttribute('href', url); +} + +export function copyToClipboard(text: string) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + } else { + const host = document.body; + const copyInput = document.createElement('input'); + copyInput.style.position = 'fixed'; + copyInput.style.opacity = '0'; + copyInput.value = text; + host.append(copyInput); + + copyInput.select(); + copyInput.setSelectionRange(0, 99999); + document.execCommand('Copy'); + copyInput.remove(); + } +} + +export function suffixRename(name: string | number, validator: Function) { + let suffix = 2; + let newName = name; + do { + newName = `name${suffix}`; + suffix += 1; + } while (validator(newName)); + + return newName; +} + +export function getImageDimension(file: Blob | MediaSource) { + return new Promise((resolve) => { + const img = new Image(); + img.onload = async () => { + resolve({ + w: img.width, + h: img.height, + }); + URL.revokeObjectURL(img.src); + }; + img.src = URL.createObjectURL(file); + }); +} + +export function scaleDownImage(imageFile: Blob, width: number, height: number) { + return new Promise((resolve) => { + const imgURL = URL.createObjectURL(imageFile); + const img = new Image(); + + img.onload = () => { + let newWidth = img.width; + let newHeight = img.height; + if (newHeight <= height && newWidth <= width) { + resolve(imageFile); + } + + if (newHeight > height) { + newWidth = Math.floor(newWidth * (height / newHeight)); + newHeight = height; + } + if (newWidth > width) { + newHeight = Math.floor(newHeight * (width / newWidth)); + newWidth = width; + } + + const canvas = document.createElement('canvas'); + canvas.width = newWidth; + canvas.height = newHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, newWidth, newHeight); + + canvas.toBlob((thumbnail) => { + URL.revokeObjectURL(imgURL); + resolve(thumbnail); + }, imageFile.type); + }; + + img.src = imgURL; + }); +} + +/** + * @param {sigil} string sigil to search for (for example '@', '#' or '$') + * @param {flags} string regex flags + * @param {prefix} string prefix appended at the beginning of the regex + * @returns {RegExp} + */ +export function idRegex(sigil: string, flags: string, prefix: string): RegExp { + const servername = '(?:[a-zA-Z0-9-.]*[a-zA-Z0-9]+|\\[\\S+?\\])(?::\\d+)?'; + return new RegExp(`${prefix}(${sigil}\\S+:${servername})`, flags); +} + +const matrixToRegex = /^https?:\/\/matrix.to\/#\/(\S+:\S+)/; +/** + * Parses a matrix.to URL into an matrix id. + * This function can later be extended to support matrix: URIs + * @param {string} uri The URI to parse + * @returns {string|null} The id or null if the URI does not match + */ +export function parseIdUri(uri: string): string | null { + const res = decodeURIComponent(uri).match(matrixToRegex); + if (!res) return null; + return res[1]; +} diff --git a/src/util/matrixUtil.js b/src/util/matrixUtil.ts similarity index 77% rename from src/util/matrixUtil.js rename to src/util/matrixUtil.ts index 54ee31bb..30476f34 100644 --- a/src/util/matrixUtil.js +++ b/src/util/matrixUtil.ts @@ -6,10 +6,12 @@ import HashLockIC from '../../public/res/ic/outlined/hash-lock.svg'; import SpaceIC from '../../public/res/ic/outlined/space.svg'; import SpaceGlobeIC from '../../public/res/ic/outlined/space-globe.svg'; import SpaceLockIC from '../../public/res/ic/outlined/space-lock.svg'; +import { RoomMember } from 'matrix-js-sdk'; +import { Room } from 'matrix-js-sdk'; const WELL_KNOWN_URI = '/.well-known/matrix/client'; -export async function getBaseUrl(servername) { +export async function getBaseUrl(servername: string) { let protocol = 'https://'; if (servername.match(/^https?:\/\//) !== null) protocol = ''; const serverDiscoveryUrl = `${protocol}${servername}${WELL_KNOWN_URI}`; @@ -24,7 +26,7 @@ export async function getBaseUrl(servername) { } } -export function getUsername(userId) { +export function getUsername(userId: string) { const mx = initMatrix.matrixClient; const user = mx.getUser(userId); if (user === null) return userId; @@ -35,11 +37,11 @@ export function getUsername(userId) { return username; } -export function getUsernameOfRoomMember(roomMember) { +export function getUsernameOfRoomMember(roomMember: RoomMember) { return roomMember.name || roomMember.userId; } -export async function isRoomAliasAvailable(alias) { +export async function isRoomAliasAvailable(alias: string) { try { const result = await initMatrix.matrixClient.resolveRoomAlias(alias); if (result.room_id) return false; @@ -50,7 +52,7 @@ export async function isRoomAliasAvailable(alias) { } } -export function getPowerLabel(powerLevel) { +export function getPowerLabel(powerLevel: number) { if (powerLevel > 9000) return 'Goku'; if (powerLevel > 100) return 'Founder'; if (powerLevel === 100) return 'Admin'; @@ -58,7 +60,7 @@ export function getPowerLabel(powerLevel) { return null; } -export function parseReply(rawBody) { +export function parseReply(rawBody: string) { if (rawBody?.indexOf('>') !== 0) return null; let body = rawBody.slice(rawBody.indexOf('<') + 1); const user = body.slice(0, body.indexOf('>')); @@ -79,7 +81,7 @@ export function parseReply(rawBody) { }; } -export function trimHTMLReply(html) { +export function trimHTMLReply(html: string | string[]) { if (!html) return html; const suffix = ''; const i = html.indexOf(suffix); @@ -89,7 +91,7 @@ export function trimHTMLReply(html) { return html.slice(i + suffix.length); } -export function hasDMWith(userId) { +export function hasDMWith(userId: string) { const mx = initMatrix.matrixClient; const directIds = [...initMatrix.roomList.directs]; @@ -103,18 +105,22 @@ export function hasDMWith(userId) { }); } -export function joinRuleToIconSrc(joinRule, isSpace) { - return ({ - restricted: () => (isSpace ? SpaceIC : HashIC), - knock: () => (isSpace ? SpaceLockIC : HashLockIC), - invite: () => (isSpace ? SpaceLockIC : HashLockIC), - public: () => (isSpace ? SpaceGlobeIC : HashGlobeIC), - }[joinRule]?.() || null); +export function joinRuleToIconSrc(joinRule: string | number, isSpace: boolean) { + return ( + { + restricted: () => (isSpace ? SpaceIC : HashIC), + knock: () => (isSpace ? SpaceLockIC : HashLockIC), + invite: () => (isSpace ? SpaceLockIC : HashLockIC), + public: () => (isSpace ? SpaceGlobeIC : HashGlobeIC), + }[joinRule]?.() || null + ); } // NOTE: it gives userId with minimum power level 50; -function getHighestPowerUserId(room) { - const userIdToPower = room.currentState.getStateEvents('m.room.power_levels', '')?.getContent().users; +function getHighestPowerUserId(room: Room) { + const userIdToPower = room.currentState + .getStateEvents('m.room.power_levels', '') + ?.getContent().users; let powerUserId = null; if (!userIdToPower) return powerUserId; @@ -131,12 +137,12 @@ function getHighestPowerUserId(room) { return powerUserId; } -export function getIdServer(userId) { +export function getIdServer(userId: string) { const idParts = userId.split(':'); return idParts[1]; } -export function getServerToPopulation(room) { +export function getServerToPopulation(room: Room) { const members = room.getMembers(); const serverToPop = {}; @@ -154,7 +160,7 @@ export function getServerToPopulation(room) { return serverToPop; } -export function genRoomVia(room) { +export function genRoomVia(room: Room) { const via = []; const userId = getHighestPowerUserId(room); if (userId) { @@ -163,7 +169,7 @@ export function genRoomVia(room) { } const serverToPop = getServerToPopulation(room); const sortedServers = Object.keys(serverToPop).sort( - (svrA, svrB) => serverToPop[svrB] - serverToPop[svrA], + (svrA, svrB) => serverToPop[svrB] - serverToPop[svrA] ); const mostPop3 = sortedServers.slice(0, 3); if (via.length === 0) return mostPop3; @@ -173,7 +179,7 @@ export function genRoomVia(room) { return via.concat(mostPop3.slice(0, 2)); } -export function isCrossVerified(deviceId) { +export function isCrossVerified(deviceId: string) { try { const mx = initMatrix.matrixClient; const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId()); @@ -201,7 +207,7 @@ export function getDefaultSSKey() { } } -export function getSSKeyInfo(key) { +export function getSSKeyInfo(key: string) { const mx = initMatrix.matrixClient; try { return mx.getAccountData(`m.secret_storage.key.${key}`).getContent(); @@ -210,12 +216,13 @@ export function getSSKeyInfo(key) { } } -export async function hasDevices(userId) { +export async function hasDevices(userId: string) { const mx = initMatrix.matrixClient; try { const usersDeviceMap = await mx.downloadKeys([userId, mx.getUserId()]); - return Object.values(usersDeviceMap) - .every((userDevices) => (Object.keys(userDevices).length > 0)); + return Object.values(usersDeviceMap).every( + (userDevices) => Object.keys(userDevices).length > 0 + ); } catch (e) { console.error("Error determining if it's possible to encrypt to all users: ", e); return false; diff --git a/src/util/mimetypes.js b/src/util/mimetypes.ts similarity index 93% rename from src/util/mimetypes.js rename to src/util/mimetypes.ts index bf7efbce..095496f8 100644 --- a/src/util/mimetypes.js +++ b/src/util/mimetypes.ts @@ -25,7 +25,7 @@ export const ALLOWED_BLOB_MIMETYPES = [ 'audio/x-flac', ]; -export function getBlobSafeMimeType(mimetype) { +export function getBlobSafeMimeType(mimetype: string) { if (typeof mimetype !== 'string') return 'application/octet-stream'; const [type] = mimetype.split(';'); if (!ALLOWED_BLOB_MIMETYPES.includes(type)) { diff --git a/src/util/sanitize.js b/src/util/sanitize.ts similarity index 72% rename from src/util/sanitize.js rename to src/util/sanitize.ts index 79cc0418..80c33b97 100644 --- a/src/util/sanitize.js +++ b/src/util/sanitize.ts @@ -1,21 +1,64 @@ +import { MatrixClient } from 'matrix-js-sdk'; +import { HTMLAttributes } from 'react'; import sanitizeHtml from 'sanitize-html'; const MAX_TAG_NESTING = 100; let mx = null; const permittedHtmlTags = [ - 'font', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', - 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', - 'hr', 'br', 'div', 'table', 'thead', 'tbody', 'tr', 'th', - 'td', 'caption', 'pre', 'span', 'img', 'details', 'summary', + 'font', + 'del', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'p', + 'a', + 'ul', + 'ol', + 'sup', + 'sub', + 'li', + 'b', + 'i', + 'u', + 'strong', + 'em', + 'strike', + 'code', + 'hr', + 'br', + 'div', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + 'caption', + 'pre', + 'span', + 'img', + 'details', + 'summary', ]; const urlSchemes = ['https', 'http', 'ftp', 'mailto', 'magnet']; const permittedTagToAttributes = { font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'], - span: ['style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-maths', 'data-mx-pill', 'data-mx-ping'], + span: [ + 'style', + 'data-mx-bg-color', + 'data-mx-color', + 'data-mx-spoiler', + 'data-mx-maths', + 'data-mx-pill', + 'data-mx-ping', + ], div: ['data-mx-maths'], a: ['name', 'target', 'href', 'rel'], img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'], @@ -60,7 +103,8 @@ function transformATag(tagName, attribs) { return pill; } - const rex = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/ug; + const rex = + /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/gu; const newHref = attribs.href.replace(rex, (match) => `[e-${match.codePointAt(0).toString(16)}]`); return { @@ -96,7 +140,7 @@ function transformImgTag(tagName, attribs) { }; } -export function sanitizeCustomHtml(matrixClient, body) { +export function sanitizeCustomHtml(matrixClient: MatrixClient, body: string) { mx = matrixClient; return sanitizeHtml(body, { allowedTags: permittedHtmlTags, @@ -128,7 +172,7 @@ export function sanitizeCustomHtml(matrixClient, body) { }); } -export function sanitizeText(body) { +export function sanitizeText(body: string) { const tagsToReplace = { '&': '&', '<': '<', diff --git a/src/util/sort.js b/src/util/sort.ts similarity index 89% rename from src/util/sort.js rename to src/util/sort.ts index f9a0b790..93022cea 100644 --- a/src/util/sort.js +++ b/src/util/sort.ts @@ -1,13 +1,13 @@ import initMatrix from '../client/initMatrix'; -export function roomIdByActivity(id1, id2) { +export function roomIdByActivity(id1: string, id2: string) { const room1 = initMatrix.matrixClient.getRoom(id1); const room2 = initMatrix.matrixClient.getRoom(id2); return room2.getLastActiveTimestamp() - room1.getLastActiveTimestamp(); } -export function roomIdByAtoZ(aId, bId) { +export function roomIdByAtoZ(aId: string, bId: string) { let aName = initMatrix.matrixClient.getRoom(aId).name; let bName = initMatrix.matrixClient.getRoom(bId).name;