diff --git a/.eslintrc.js b/.eslintrc.js index 12997d23..9b526a36 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -57,5 +57,6 @@ module.exports = { "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/ban-types": "off", }, }; diff --git a/.vscode/settings.json b/.vscode/settings.json index 8272ea1e..edbde217 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,11 @@ { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + } } diff --git a/src/client/state/Notifications.ts b/src/client/state/Notifications.ts index e406996b..f1155c2a 100644 --- a/src/client/state/Notifications.ts +++ b/src/client/state/Notifications.ts @@ -1,4 +1,5 @@ import EventEmitter from 'events'; +import { MatrixClient, MatrixEvent, NotificationCountType, Room } from 'matrix-js-sdk'; import renderAvatar from '../../app/atoms/avatar/render'; import { cssColorMXID } from '../../util/colorMXID'; import { selectRoom } from '../action/navigation'; @@ -11,7 +12,6 @@ import LogoSVG from '../../../public/res/svg/cinny.svg'; import LogoUnreadSVG from '../../../public/res/svg/cinny-unread.svg'; import LogoHighlightSVG from '../../../public/res/svg/cinny-highlight.svg'; import { html, plain } from '../../util/markdown'; -import { MatrixClient, MatrixEvent, NotificationCountType, Room } from 'matrix-js-sdk'; import RoomList from './RoomList'; function isNotifEvent(mEvent: MatrixEvent) { @@ -35,14 +35,23 @@ function findMutedRule(overrideRules, roomId) { class Notifications extends EventEmitter { initialized: boolean; + favicon: string; + matrixClient: MatrixClient; + roomList: RoomList; + roomIdToNoti: Map; + roomIdToPopupNotis: Map; + eventIdToPopupNoti: Map; + _notiAudio: any; + _inviteAudio: any; + constructor(roomList) { super(); @@ -333,7 +342,7 @@ class Notifications extends EventEmitter { } _listenEvents() { - //@ts-ignore + // @ts-ignore this.matrixClient.on('Room.timeline', (mEvent: MatrixEvent, room: Room) => { if (mEvent.isRedaction()) this._deletePopupNoti(mEvent.event.redacts); @@ -360,7 +369,7 @@ class Notifications extends EventEmitter { this._displayPopupNoti(mEvent, room); } }); - //@ts-ignore + // @ts-ignore this.matrixClient.on('accountData', (mEvent: MatrixEvent, oldMEvent: MatrixEvent) => { if (mEvent.getType() === 'm.push_rules') { const override = mEvent?.getContent()?.global?.override; @@ -397,7 +406,7 @@ class Notifications extends EventEmitter { }); } }); - //@ts-ignore + // @ts-ignore this.matrixClient.on('Room.receipt', (mEvent: MatrixEvent, room: Room) => { if (mEvent.getType() === 'm.receipt') { if (room.isSpaceRoom()) return; @@ -411,7 +420,7 @@ class Notifications extends EventEmitter { this._deletePopupRoomNotis(room.roomId); } }); - //@ts-ignore + // @ts-ignore this.matrixClient.on('Room.myMembership', (room: Room, membership) => { if (membership === 'leave' && this.hasNoti(room.roomId)) { this.deleteNoti(room.roomId); diff --git a/src/client/state/RoomTimeline.ts b/src/client/state/RoomTimeline.ts index bec7f3c0..e827c7be 100644 --- a/src/client/state/RoomTimeline.ts +++ b/src/client/state/RoomTimeline.ts @@ -5,7 +5,6 @@ import { MatrixClient, MatrixEvent, Room, - TimelineIndex, } from 'matrix-js-sdk'; import initMatrix from '../initMatrix'; import cons from './cons'; @@ -53,18 +52,18 @@ function addToMap(myMap: Map, mEvent: MatrixEvent) { function getFirstLinkedTimeline(timeline: EventTimeline) { let tm = timeline; - //@ts-ignore + // @ts-ignore while (tm.prevTimeline) { - //@ts-ignore + // @ts-ignore tm = tm.prevTimeline; } return tm; } function getLastLinkedTimeline(timeline: EventTimeline) { let tm = timeline; - //@ts-ignore + // @ts-ignore while (tm.nextTimeline) { - //@ts-ignore + // @ts-ignore tm = tm.nextTimeline; } return tm; @@ -78,9 +77,9 @@ function iterateLinkedTimelines( let tm = timeline; while (tm) { callback(tm); - //@ts-ignore + // @ts-ignore if (backwards) tm = tm.prevTimeline; - //@ts-ignore + // @ts-ignore else tm = tm.nextTimeline; } } @@ -89,7 +88,7 @@ function isTimelineLinked(tm1: EventTimeline, tm2: EventTimeline) { let tm = getFirstLinkedTimeline(tm1); while (tm) { if (tm === tm2) return true; - //@ts-ignore + // @ts-ignore tm = tm.nextTimeline; } return false; @@ -97,17 +96,29 @@ function isTimelineLinked(tm1: EventTimeline, tm2: EventTimeline) { class RoomTimeline extends EventEmitter { timeline: any[]; + editedTimeline: Map; + reactionTimeline: Map; + typingMembers: Set; + matrixClient: MatrixClient; + roomId: string; + room: any; + liveTimeline; + activeTimeline: any; + isOngoingPagination: boolean; + ongoingDecryptionCount: number; + initialized: boolean; + _listenRoomTimeline: ( event: any, room: any, @@ -115,10 +126,15 @@ class RoomTimeline extends EventEmitter { removed: any, data: any ) => void; + _listenDecryptEvent: (event: any) => void; + _listenRedaction: (mEvent: MatrixEvent, room: any) => void; + _listenTypingEvent: (event: any, member: any) => void; + _listenReciptEvent: (event: any, room: any) => void; + constructor(roomId: string) { super(); // These are local timelines @@ -141,7 +157,7 @@ class RoomTimeline extends EventEmitter { setTimeout(() => this.room.loadMembersIfNeeded()); // TODO: remove below line - //@ts-ignore + // @ts-ignore window.selectedRoom = this; } @@ -440,29 +456,29 @@ class RoomTimeline extends EventEmitter { this.emit(cons.events.roomTimeline.LIVE_RECEIPT); } }; - //@ts-ignore + // @ts-ignore this.matrixClient.on('Room.timeline', this._listenRoomTimeline); - //@ts-ignore + // @ts-ignore this.matrixClient.on('Room.redaction', this._listenRedaction); - //@ts-ignore + // @ts-ignore this.matrixClient.on('Event.decrypted', this._listenDecryptEvent); - //@ts-ignore + // @ts-ignore this.matrixClient.on('RoomMember.typing', this._listenTypingEvent); - //@ts-ignore + // @ts-ignore this.matrixClient.on('Room.receipt', this._listenReciptEvent); } removeInternalListeners() { if (!this.initialized) return; - //@ts-ignore + // @ts-ignore this.matrixClient.removeListener('Room.timeline', this._listenRoomTimeline); - //@ts-ignore + // @ts-ignore this.matrixClient.removeListener('Room.redaction', this._listenRedaction); - //@ts-ignore + // @ts-ignore this.matrixClient.removeListener('Event.decrypted', this._listenDecryptEvent); - //@ts-ignore + // @ts-ignore this.matrixClient.removeListener('RoomMember.typing', this._listenTypingEvent); - //@ts-ignore + // @ts-ignore this.matrixClient.removeListener('Room.receipt', this._listenReciptEvent); } } diff --git a/src/client/state/RoomsHierarchy.ts b/src/client/state/RoomsHierarchy.ts index d10cd652..ef119562 100644 --- a/src/client/state/RoomsHierarchy.ts +++ b/src/client/state/RoomsHierarchy.ts @@ -3,10 +3,15 @@ import { RoomHierarchy } from 'matrix-js-sdk/lib/room-hierarchy'; class RoomsHierarchy { matrixClient: MatrixClient; + _maxDepth: number; + _suggestedOnly: boolean; + _limit: number; + roomIdToHierarchy: Map; + constructor(matrixClient: MatrixClient, limit = 20, maxDepth = 1, suggestedOnly = false) { this.matrixClient = matrixClient; this._maxDepth = maxDepth; diff --git a/src/client/state/RoomsInput.ts b/src/client/state/RoomsInput.ts index 939eaeab..565f5bde 100644 --- a/src/client/state/RoomsInput.ts +++ b/src/client/state/RoomsInput.ts @@ -1,17 +1,18 @@ import EventEmitter from 'events'; import encrypt from 'browser-encrypt-attachment'; import { encode } from 'blurhash'; +import { IContent, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji'; import { getBlobSafeMimeType } from '../../util/mimetypes'; import { sanitizeText } from '../../util/sanitize'; import cons from './cons'; import settings from './settings'; import { markdown, plain } from '../../util/markdown'; -import { IContent, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import RoomList from './RoomList'; const blurhashField = 'xyz.amorgan.blurhash'; +// eslint-disable-next-line no-undef function encodeBlurhash(img: CanvasImageSource) { const canvas = document.createElement('canvas'); canvas.width = 100; @@ -66,6 +67,7 @@ function loadVideo(videoFile: Blob) { }); } function getVideoThumbnail( + // eslint-disable-next-line no-undef video: CanvasImageSource, width: number, height: number, @@ -107,8 +109,11 @@ function getVideoThumbnail( class RoomsInput extends EventEmitter { matrixClient: MatrixClient; + roomList: RoomList; + roomIdToInput: Map; + constructor(mx: MatrixClient, roomList: RoomList) { super(); @@ -221,7 +226,7 @@ class RoomsInput extends EventEmitter { const autoMarkdown = options?.autoMarkdown ?? true; const room = this.matrixClient.getRoom(roomId); - //@ts-ignore + // @ts-ignore const userNames = room.currentState.userIdsToDisplayNames; const parentIds = this.roomList.getAllParentSpaces(room.roomId); const parentRooms = [...parentIds].map((id) => this.matrixClient.getRoom(id)); diff --git a/src/client/state/navigation.ts b/src/client/state/navigation.ts index 378254f9..5eb0c3a3 100644 --- a/src/client/state/navigation.ts +++ b/src/client/state/navigation.ts @@ -1,5 +1,5 @@ import EventEmitter from 'events'; -import { MatrixClient, Room } from 'matrix-js-sdk'; +import { MatrixClient } from 'matrix-js-sdk'; import appDispatcher from '../dispatcher'; import AccountData from './AccountData'; import cons from './cons'; @@ -7,18 +7,27 @@ import RoomList from './RoomList'; class Navigation extends EventEmitter { initMatrix: { roomList: RoomList; accountData: AccountData; matrixClient: MatrixClient }; + selectedTab: string; + selectedSpaceId: string; + selectedSpacePath: string[]; + selectedRoomId: string; + isRoomSettings: boolean; + recentRooms: string[]; + spaceToRoom: Map; + rawModelStack: any[]; + constructor() { super(); // this will attached by initMatrix - //@ts-ignore + // @ts-ignore this.initMatrix = {}; this.selectedTab = cons.tabs.HOME; diff --git a/src/client/state/secretStorageKeys.ts b/src/client/state/secretStorageKeys.ts index 21fe4157..c55b8105 100644 --- a/src/client/state/secretStorageKeys.ts +++ b/src/client/state/secretStorageKeys.ts @@ -16,7 +16,7 @@ export function getPrivateKey(keyId) { } export function deletePrivateKey(keyId) { - //@ts-ignore + // @ts-ignore delete secretStorageKeys.delete(keyId); } diff --git a/src/client/state/settings.ts b/src/client/state/settings.ts index 7444cb3f..7cac02d8 100644 --- a/src/client/state/settings.ts +++ b/src/client/state/settings.ts @@ -3,6 +3,7 @@ import appDispatcher from '../dispatcher'; import cons from './cons'; +// eslint-disable-next-line no-use-before-define function getSettings(): Settings { const settings = localStorage.getItem('settings'); if (settings === null) return null; @@ -11,6 +12,7 @@ function getSettings(): Settings { function setSettings(key: string, value) { let settings = getSettings(); + // eslint-disable-next-line no-use-before-define if (settings === null) settings = new Settings(); settings[key] = value; localStorage.setItem('settings', JSON.stringify(settings)); @@ -18,15 +20,25 @@ function setSettings(key: string, value) { class Settings extends EventEmitter { themes: string[]; + themeIndex: number; + useSystemTheme: boolean; + isMarkdown: boolean; + isPeopleDrawer: boolean; + hideMembershipEvents: boolean; + hideNickAvatarEvents: boolean; + _showNotifications: boolean; + isNotificationSounds: boolean; + isTouchScreenDevice: boolean; + constructor() { super(); diff --git a/src/util/AsyncSearch.ts b/src/util/AsyncSearch.ts index 5b0c51da..d0f41ac1 100644 --- a/src/util/AsyncSearch.ts +++ b/src/util/AsyncSearch.ts @@ -2,17 +2,29 @@ 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(); diff --git a/src/util/Postie.ts b/src/util/Postie.ts index b646a266..de9008db 100644 --- a/src/util/Postie.ts +++ b/src/util/Postie.ts @@ -1,5 +1,6 @@ class Postie { _topics: Map>>; + constructor() { this._topics = new Map(); } @@ -78,7 +79,7 @@ class Postie { * @param {*} data - Data to deliver to subscriber */ post(topic: string, address: string | string[], data: any) { - const sendPost = (inboxes: Set, addr: string) => { + 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.` diff --git a/src/util/common.js b/src/util/common.js deleted file mode 100644 index 2affe27d..00000000 --- a/src/util/common.js +++ /dev/null @@ -1,230 +0,0 @@ -/* eslint-disable max-classes-per-file */ -export function bytesToSize(bytes) { - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) return 'n/a'; - const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); - if (i === 0) return `${bytes} ${sizes[i]}`; - return `${(bytes / (1024 ** i)).toFixed(1)} ${sizes[i]}`; -} - -export function diffMinutes(dt2, dt1) { - let diff = (dt2.getTime() - dt1.getTime()) / 1000; - diff /= 60; - return Math.abs(Math.round(diff)); -} - -export function isInSameDay(dt2, dt1) { - 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, targetSelector) { - let boxInfo; - - const path = ev.nativeEvent.composedPath(); - const target = targetSelector - ? path.find((element) => element.matches?.(targetSelector)) - : null; - if (target) { - boxInfo = target.getBoundingClientRect(); - } else { - boxInfo = ev.target.getBoundingClientRect(); - } - - return { - x: boxInfo.x, - y: boxInfo.y, - width: boxInfo.width, - height: boxInfo.height, - detail: ev.detail, - }; -} - -export function abbreviateNumber(number) { - if (number > 99) return '99+'; - return number; -} - -export class Debounce { - 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, wait) { - const that = this; - return function debounceCallback(...args) { - clearTimeout(that.timeoutId); - that.timeoutId = setTimeout(() => { - func.apply(this, args); - that.timeoutId = null; - }, wait); - }; - } -} - -export class Throttle { - 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, wait) { - const that = this; - return function throttleCallback(...args) { - if (that.timeoutId !== null) return; - that.timeoutId = setTimeout(() => { - func.apply(this, args); - that.timeoutId = null; - }, wait); - }; - } -} - -export function getUrlPrams(paramName) { - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - return urlParams.get(paramName); -} - -export function getScrollInfo(target) { - const scroll = {}; - scroll.top = Math.round(target.scrollTop); - scroll.height = Math.round(target.scrollHeight); - scroll.viewHeight = Math.round(target.offsetHeight); - scroll.isScrollable = scroll.height > scroll.viewHeight; - return scroll; -} - -export function avatarInitials(text) { - return [...text][0]; -} - -export function cssVar(name) { - return getComputedStyle(document.body).getPropertyValue(name); -} - -export function setFavicon(url) { - const favicon = document.querySelector('#favicon'); - if (!favicon) return; - favicon.setAttribute('href', url); -} - -export function copyToClipboard(text) { - 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, validator) { - let suffix = 2; - let newName = name; - do { - newName = name + suffix; - suffix += 1; - } while (validator(newName)); - - return newName; -} - -export function getImageDimension(file) { - 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, width, height) { - 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, flags, prefix) { - 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) { - const res = decodeURIComponent(uri).match(matrixToRegex); - if (!res) return null; - return res[1]; -} diff --git a/src/util/common.ts b/src/util/common.ts index e11ec344..b38c343c 100644 --- a/src/util/common.ts +++ b/src/util/common.ts @@ -54,6 +54,7 @@ export function abbreviateNumber(number: number) { export class Debounce { timeoutId: any; + constructor() { this.timeoutId = null; } @@ -63,7 +64,7 @@ export class Debounce { * @param {number} wait - wait in milliseconds to call func * @returns {func} debounceCallback - to pass arguments to func callback */ - _(func: Function, wait: number) { + _(func: (...args) => void, wait: number) { const debounceCallback = (...args) => { clearTimeout(this.timeoutId); this.timeoutId = setTimeout(() => { @@ -77,6 +78,7 @@ export class Debounce { export class Throttle { timeoutId: any; + constructor() { this.timeoutId = null; } @@ -86,7 +88,7 @@ export class Throttle { * @param {number} wait - wait in milliseconds to call func * @returns {function} throttleCallback - to pass arguments to func callback */ - _(func: Function, wait: number) { + _(func: (...args) => void, wait: number) { const throttleCallback = (...args) => { if (this.timeoutId !== null) return; this.timeoutId = setTimeout(() => { @@ -146,7 +148,7 @@ export function copyToClipboard(text: string) { } } -export function suffixRename(name: string | number, validator: Function) { +export function suffixRename(name: string, validator: (newName: string) => boolean) { let suffix = 2; let newName = name; do { diff --git a/src/util/sanitize.ts b/src/util/sanitize.ts index 3e15d463..fa987e81 100644 --- a/src/util/sanitize.ts +++ b/src/util/sanitize.ts @@ -5,45 +5,11 @@ 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'];