first step

This commit is contained in:
wuyudi 2023-02-07 23:16:06 +09:00
parent 44318d1ecd
commit 4b7f07ef6d
14 changed files with 404 additions and 79 deletions

View file

@ -96,6 +96,6 @@
<audio id="inviteSound">
<source src="./public/sound/invite.ogg" type="audio/ogg" />
</audio>
<script type="module" src="./src/index.jsx"></script>
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

28
package-lock.json generated
View file

@ -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",

View file

@ -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",

4
src/custom.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export default content;
}

View file

@ -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, '');

View file

@ -1,9 +1,10 @@
class Postie {
_topics: Map<string, Map<string, Set<Function>>>;
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<Function>) {
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<Function>, 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);
});
}

View file

@ -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)})`;
}

233
src/util/common.ts Normal file
View file

@ -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];
}

View file

@ -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 = '</mx-reply>';
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;

View file

@ -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)) {

View file

@ -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 = {
'&': '&amp;',
'<': '&lt;',

View file

@ -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;