diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles deleted file mode 100644 index d9177bebb5..0000000000 --- a/.eslintignore.errorfiles +++ /dev/null @@ -1,16 +0,0 @@ -# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. - -src/Markdown.js -src/NodeAnimator.js -src/components/structures/RoomDirectory.js -src/components/views/rooms/MemberList.js -src/ratelimitedfunc.js -src/utils/DMRoomMap.js -src/utils/MultiInviter.js -test/components/structures/MessagePanel-test.js -test/components/views/dialogs/InteractiveAuthDialog-test.js -test/mock-clock.js -src/component-index.js -test/end-to-end-tests/node_modules/ -test/end-to-end-tests/element/ -test/end-to-end-tests/synapse/ diff --git a/package.json b/package.json index 4ad585ba7d..f4af7b8313 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:js && yarn lint:style", - "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", + "lint:js": "eslint --max-warnings 0 src test", "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", @@ -54,7 +54,9 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", + "@types/commonmark": "^0.27.4", "await-lock": "^2.1.0", + "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "cheerio": "^1.0.0-rc.9", diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 515d867da5..878a4154cd 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +$timelineImageBorderRadius: 4px; + .mx_MImageBody { display: block; margin-right: 34px; @@ -25,7 +27,11 @@ limitations under the License. height: 100%; left: 0; top: 0; - border-radius: 4px; + border-radius: $timelineImageBorderRadius; + + > canvas { + border-radius: $timelineImageBorderRadius; + } } .mx_MImageBody_thumbnail_container { diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index bcc40f1181..afaed50fa4 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -48,6 +48,7 @@ limitations under the License. .mx_cryptoEvent_buttons { align-items: center; display: flex; + gap: 5px; } .mx_cryptoEvent_state { diff --git a/scripts/generate-eslint-error-ignore-file b/scripts/generate-eslint-error-ignore-file deleted file mode 100755 index 54aacfc9fa..0000000000 --- a/scripts/generate-eslint-error-ignore-file +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# -# generates .eslintignore.errorfiles to list the files which have errors in, -# so that they can be ignored in future automated linting. - -out=.eslintignore.errorfiles - -cd `dirname $0`/.. - -echo "generating $out" - -{ - cat < 0) | .filePath' | - sed -e 's/.*matrix-react-sdk\///'; -} > "$out" -# also append rules from eslintignore file -cat .eslintignore >> $out diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index cb54db3f8a..6e1e6ce83a 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields { } interface ThirdpartyLookupResponse { - userid: string, - protocol: string, - fields: ThirdpartyLookupResponseFields, + userid: string; + protocol: string; + fields: ThirdpartyLookupResponseFields; } // Unlike 'CallType' in js-sdk, this one includes screen sharing diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index ef0a89a690..1b8a3be37e 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -17,9 +17,10 @@ limitations under the License. */ import React from "react"; -import dis from './dispatcher/dispatcher'; -import { MatrixClientPeg } from './MatrixClientPeg'; +import { encode } from "blurhash"; import { MatrixClient } from "matrix-js-sdk/src/client"; + +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; @@ -47,6 +48,10 @@ const MAX_HEIGHT = 600; // 5669 px (x-axis) , 5669 px (y-axis) , per metre const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; +export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 +const BLURHASH_X_COMPONENTS = 6; +const BLURHASH_Y_COMPONENTS = 6; + export class UploadCanceledError extends Error {} type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; @@ -77,6 +82,7 @@ interface IThumbnail { }; w: number; h: number; + [BLURHASH_FIELD]: string; }; thumbnail: Blob; } @@ -124,7 +130,16 @@ function createThumbnail( const canvas = document.createElement("canvas"); canvas.width = targetWidth; canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + const context = canvas.getContext("2d"); + context.drawImage(element, 0, 0, targetWidth, targetHeight); + const imageData = context.getImageData(0, 0, targetWidth, targetHeight); + const blurhash = encode( + imageData.data, + imageData.width, + imageData.height, + BLURHASH_X_COMPONENTS, + BLURHASH_Y_COMPONENTS, + ); canvas.toBlob(function(thumbnail) { resolve({ info: { @@ -136,8 +151,9 @@ function createThumbnail( }, w: inputWidth, h: inputHeight, + [BLURHASH_FIELD]: blurhash, }, - thumbnail: thumbnail, + thumbnail, }); }, mimeType); }); @@ -220,7 +236,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) { } /** - * Load a file into a newly created video element. + * Load a file into a newly created video element and pull some strings + * in an attempt to guarantee the first frame will be showing. * * @param {File} videoFile The file to load in an video element. * @return {Promise} A promise that resolves with the video image element. @@ -229,20 +246,25 @@ function loadVideoElement(videoFile): Promise { return new Promise((resolve, reject) => { // Load the file into an html element const video = document.createElement("video"); + video.preload = "metadata"; + video.playsInline = true; + video.muted = true; const reader = new FileReader(); reader.onload = function(ev) { - video.src = ev.target.result as string; - - // Once ready, returns its size // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { + video.onloadeddata = async function() { resolve(video); + video.pause(); }; video.onerror = function(e) { reject(e); }; + + video.src = ev.target.result as string; + video.load(); + video.play(); }; reader.onerror = function(e) { reject(e); @@ -347,7 +369,7 @@ export function uploadFile( }); (prom as IAbortablePromise).abort = () => { canceled = true; - if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); + if (uploadPromise) matrixClient.cancelUpload(uploadPromise); }; return prom; } else { @@ -357,11 +379,11 @@ export function uploadFile( const promise1 = basePromise.then(function(url) { if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. - return { "url": url }; + return { url }; }); (promise1 as any).abort = () => { canceled = true; - MatrixClientPeg.get().cancelUpload(basePromise); + matrixClient.cancelUpload(basePromise); }; return promise1; } @@ -373,7 +395,7 @@ export default class ContentMessages { sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) { const startTime = CountlyAnalytics.getTimestamp(); - const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { + const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => { console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); throw e; }); @@ -415,7 +437,7 @@ export default class ContentMessages { if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); - await this.ensureMediaConfigFetched(); + await this.ensureMediaConfigFetched(matrixClient); modal.close(); } @@ -470,7 +492,7 @@ export default class ContentMessages { return this.inprogress.filter(u => !u.canceled); } - cancelUpload(promise: Promise) { + cancelUpload(promise: Promise, matrixClient: MatrixClient) { let upload: IUpload; for (let i = 0; i < this.inprogress.length; ++i) { if (this.inprogress[i].promise === promise) { @@ -480,7 +502,7 @@ export default class ContentMessages { } if (upload) { upload.canceled = true; - MatrixClientPeg.get().cancelUpload(upload.promise); + matrixClient.cancelUpload(upload.promise); dis.dispatch({ action: Action.UploadCanceled, upload }); } } @@ -621,11 +643,11 @@ export default class ContentMessages { return true; } - private ensureMediaConfigFetched() { + private ensureMediaConfigFetched(matrixClient: MatrixClient) { if (this.mediaConfig !== null) return; console.log("[Media Config] Fetching"); - return MatrixClientPeg.get().getMediaConfig().then((config) => { + return matrixClient.getMediaConfig().then((config) => { console.log("[Media Config] Fetched config:", config); return config; }).catch(() => { diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 39dcac4048..d4a340ddaf 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { randomString } from "matrix-js-sdk/src/randomstring"; +import { IContent } from "matrix-js-sdk/src/models/event"; import { getCurrentLanguage } from './languageHandler'; import PlatformPeg from './PlatformPeg'; @@ -255,7 +256,7 @@ interface ICreateRoomEvent extends IEvent { num_users: number; is_encrypted: boolean; is_public: boolean; - } + }; } interface IJoinRoomEvent extends IEvent { @@ -868,7 +869,7 @@ export default class CountlyAnalytics { roomId: string, isEdit: boolean, isReply: boolean, - content: {format?: string, msgtype: string}, + content: IContent, ) { if (this.disabled) return; const cli = MatrixClientPeg.get(); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index c80b50c566..016b557477 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -358,11 +358,11 @@ interface IOpts { stripReplyFallback?: boolean; returnString?: boolean; forComposerQuote?: boolean; - ref?: React.Ref; + ref?: React.Ref; } export interface IOptsReturnNode extends IOpts { - returnString: false; + returnString: false | undefined; } export interface IOptsReturnString extends IOpts { @@ -403,9 +403,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts try { if (highlights && highlights.length > 0) { const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); - const safeHighlights = highlights.map(function(highlight) { - return sanitizeHtml(highlight, sanitizeParams); - }); + const safeHighlights = highlights + // sanitizeHtml can hang if an unclosed HTML tag is thrown at it + // A search for ` !highlight.includes("<")) + .map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams)); // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join(''); diff --git a/src/Markdown.js b/src/Markdown.ts similarity index 74% rename from src/Markdown.js rename to src/Markdown.ts index f670bded12..96169d4011 100644 --- a/src/Markdown.js +++ b/src/Markdown.ts @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,16 +16,24 @@ limitations under the License. */ import * as commonmark from 'commonmark'; -import {escape} from "lodash"; +import { escape } from "lodash"; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; -function is_allowed_html_tag(node) { +// As far as @types/commonmark is concerned, these are not public, so add them +interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer { + paragraph: (node: commonmark.Node, entering: boolean) => void; + link: (node: commonmark.Node, entering: boolean) => void; + html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase + html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase +} + +function isAllowedHtmlTag(node: commonmark.Node): boolean { if (node.literal != null && - node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) { + node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) { return true; } @@ -39,21 +48,12 @@ function is_allowed_html_tag(node) { return false; } -function html_if_tag_allowed(node) { - if (is_allowed_html_tag(node)) { - this.lit(node.literal); - return; - } else { - this.lit(escape(node.literal)); - } -} - /* * Returns true if the parse output containing the node * comprises multiple block level elements (ie. lines), * or false if it is only a single line. */ -function is_multi_line(node) { +function isMultiLine(node: commonmark.Node): boolean { let par = node; while (par.parent) { par = par.parent; @@ -67,6 +67,9 @@ function is_multi_line(node) { * it's plain text. */ export default class Markdown { + private input: string; + private parsed: commonmark.Node; + constructor(input) { this.input = input; @@ -74,7 +77,7 @@ export default class Markdown { this.parsed = parser.parse(this.input); } - isPlainText() { + isPlainText(): boolean { const walker = this.parsed.walker(); let ev; @@ -87,7 +90,7 @@ export default class Markdown { // if it's an allowed html tag, we need to render it and therefore // we will need to use HTML. If it's not allowed, it's not HTML since // we'll just be treating it as text. - if (is_allowed_html_tag(node)) { + if (isAllowedHtmlTag(node)) { return false; } } else { @@ -97,7 +100,7 @@ export default class Markdown { return true; } - toHTML({ externalLinks = false } = {}) { + toHTML({ externalLinks = false } = {}): string { const renderer = new commonmark.HtmlRenderer({ safe: false, @@ -107,7 +110,7 @@ export default class Markdown { // block quote ends up all on one line // (https://github.com/vector-im/element-web/issues/3154) softbreak: '
', - }); + }) as CommonmarkHtmlRendererInternal; // Trying to strip out the wrapping

causes a lot more complication // than it's worth, i think. For instance, this code will go and strip @@ -118,16 +121,16 @@ export default class Markdown { // // Let's try sending with

s anyway for now, though. - const real_paragraph = renderer.paragraph; + const realParagraph = renderer.paragraph; - renderer.paragraph = function(node, entering) { + renderer.paragraph = function(node: commonmark.Node, entering: boolean) { // If there is only one top level node, just return the // bare text: it's a single line of text and so should be // 'inline', rather than unnecessarily wrapped in its own // p tag. If, however, we have multiple nodes, each gets // its own p tag to keep them as separate paragraphs. - if (is_multi_line(node)) { - real_paragraph.call(this, node, entering); + if (isMultiLine(node)) { + realParagraph.call(this, node, entering); } }; @@ -150,19 +153,26 @@ export default class Markdown { } }; - renderer.html_inline = html_if_tag_allowed; + renderer.html_inline = function(node: commonmark.Node) { + if (isAllowedHtmlTag(node)) { + this.lit(node.literal); + return; + } else { + this.lit(escape(node.literal)); + } + }; - renderer.html_block = function(node) { -/* + renderer.html_block = function(node: commonmark.Node) { + /* // as with `paragraph`, we only insert line breaks // if there are multiple lines in the markdown. const isMultiLine = is_multi_line(node); if (isMultiLine) this.cr(); -*/ - html_if_tag_allowed.call(this, node); -/* + */ + renderer.html_inline(node); + /* if (isMultiLine) this.cr(); -*/ + */ }; return renderer.render(this.parsed); @@ -177,23 +187,22 @@ export default class Markdown { * N.B. this does **NOT** render arbitrary MD to plain text - only MD * which has no formatting. Otherwise it emits HTML(!). */ - toPlaintext() { - const renderer = new commonmark.HtmlRenderer({safe: false}); - const real_paragraph = renderer.paragraph; + toPlaintext(): string { + const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal; - renderer.paragraph = function(node, entering) { + renderer.paragraph = function(node: commonmark.Node, entering: boolean) { // as with toHTML, only append lines to paragraphs if there are // multiple paragraphs - if (is_multi_line(node)) { + if (isMultiLine(node)) { if (!entering && node.next) { this.lit('\n\n'); } } }; - renderer.html_block = function(node) { + renderer.html_block = function(node: commonmark.Node) { this.lit(node.literal); - if (is_multi_line(node) && node.next) this.lit('\n\n'); + if (isMultiLine(node) && node.next) this.lit('\n\n'); }; return renderer.render(this.parsed); diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 6b372bba28..214047c4fa 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -42,8 +42,8 @@ let secretStorageBeingAccessed = false; let nonInteractive = false; let dehydrationCache: { - key?: Uint8Array, - keyInfo?: ISecretStorageKeyInfo, + key?: Uint8Array; + keyInfo?: ISecretStorageKeyInfo; } = {}; function isCachingAllowed(): boolean { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 0f38c5fffc..128ca9e5e2 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1181,7 +1181,7 @@ export const Commands = [ ]; // build a map from names and aliases to the Command objects. -export const CommandMap = new Map(); +export const CommandMap = new Map(); Commands.forEach(cmd => { CommandMap.set(cmd.command, cmd); cmd.aliases.forEach(alias => { @@ -1189,15 +1189,15 @@ Commands.forEach(cmd => { }); }); -export function parseCommandString(input: string) { +export function parseCommandString(input: string): { cmd?: string, args?: string } { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); if (input[0] !== '/') return {}; // not a command const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/); - let cmd; - let args; + let cmd: string; + let args: string; if (bits) { cmd = bits[1].substring(1).toLowerCase(); args = bits[2]; @@ -1208,6 +1208,11 @@ export function parseCommandString(input: string) { return { cmd, args }; } +interface ICmd { + cmd?: Command; + args?: string; +} + /** * Process the given text for /commands and return a bound method to perform them. * @param {string} roomId The room in which the command was performed. @@ -1216,7 +1221,7 @@ export function parseCommandString(input: string) { * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ -export function getCommand(input: string) { +export function getCommand(input: string): ICmd { const { cmd, args } = parseCommandString(input); if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) { diff --git a/src/Terms.ts b/src/Terms.ts index 3859cc1c73..0189810e7c 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -48,13 +48,13 @@ export interface Policy { } export type Policies = { - [policy: string]: Policy, + [policy: string]: Policy; }; export type TermsInteractionCallback = ( policiesAndServicePairs: { - service: Service, - policies: Policies, + service: Service; + policies: Policies; }[], agreedUrls: string[], extraClassNames?: string, @@ -180,8 +180,8 @@ export async function startTermsFlow( export function dialogTermsInteractionCallback( policiesAndServicePairs: { - service: Service, - policies: { [policy: string]: Policy }, + service: Service; + policies: { [policy: string]: Policy }; }[], agreedUrls: string[], extraClassNames?: string, diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index e8a9872b48..184d883dda 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -21,8 +21,8 @@ interface IProps extends Omit, "onScroll"> { className?: string; onScroll?: (event: Event) => void; onWheel?: (event: WheelEvent) => void; - style?: React.CSSProperties - tabIndex?: number, + style?: React.CSSProperties; + tabIndex?: number; wrappedRef?: (ref: HTMLDivElement) => void; } diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 21ef0c4f31..22a0e1283c 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -37,7 +37,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier'; interface IProps { roomId: string; onClose: () => void; - resizeNotifier: ResizeNotifier + resizeNotifier: ResizeNotifier; } interface IState { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 26bb0fe24a..1f870da900 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -48,7 +48,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay import RoomListStore from "../../stores/room-list/RoomListStore"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; -import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; +import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import Modal from "../../Modal"; import { ICollapseConfig } from "../../resizer/distributors/collapse"; import HostSignupContainer from '../views/host_signup/HostSignupContainer'; @@ -81,14 +81,14 @@ interface IProps { page_type: string; autoJoin: boolean; threepidInvite?: IThreepidInvite; - roomOobData?: object; + roomOobData?: IOOBData; currentRoomId: string; collapseLhs: boolean; config: { piwik: { policyUrl: string; - }, - [key: string]: any, + }; + [key: string]: any; }; currentUserId?: string; currentGroupId?: string; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c1e0b8d7cb..a63767108f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -203,7 +203,7 @@ interface IState { resizeNotifier: ResizeNotifier; serverConfig?: ValidatedServerConfig; ready: boolean; - threepidInvite?: IThreepidInvite, + threepidInvite?: IThreepidInvite; roomOobData?: object; pendingInitialSync?: boolean; justRegistered?: boolean; diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index b1424a2974..a2d419b4ba 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -24,7 +24,7 @@ interface IProps { } interface IState { - toasts: ComponentClass[], + toasts: ComponentClass[]; } @replaceableComponent("structures.NonUrgentToastContainer") diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index c608f0eee9..63027ab627 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -23,7 +23,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import dis from '../../dispatcher/dispatcher'; -import RateLimitedFunc from '../../ratelimitedfunc'; import GroupStore from '../../stores/GroupStore'; import { RIGHT_PANEL_PHASES_NO_ARGS, @@ -48,6 +47,7 @@ import FilePanel from "./FilePanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; +import { throttle } from 'lodash'; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -73,7 +73,6 @@ interface IState { export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - private readonly delayedUpdate: RateLimitedFunc; private dispatcherRef: string; constructor(props, context) { @@ -84,12 +83,12 @@ export default class RightPanel extends React.Component { isUserPrivilegedInGroup: null, member: this.getUserForPanel(), }; - - this.delayedUpdate = new RateLimitedFunc(() => { - this.forceUpdate(); - }, 500); } + private readonly delayedUpdate = throttle((): void => { + this.forceUpdate(); + }, 500, { leading: true, trailing: true }); + // Helper function to split out the logic for getPhaseFromProps() and the constructor // as both are called at the same time in the constructor. private getUserForPanel() { diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 2ac990436f..3acd9f1a2e 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -370,7 +370,7 @@ export default class RoomDirectory extends React.Component { private onFilterChange = (alias: string) => { this.setState({ - filterString: alias || null, + filterString: alias || "", }); // don't send the request for a little bit, @@ -389,7 +389,7 @@ export default class RoomDirectory extends React.Component { private onFilterClear = () => { // update immediately this.setState({ - filterString: null, + filterString: "", }, this.refreshRoomList); if (this.filterTimeout) { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 81000a87a6..2c8fc08dac 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -37,7 +37,6 @@ import Modal from '../../Modal'; import * as sdk from '../../index'; import CallHandler, { PlaceCallType } from '../../CallHandler'; import dis from '../../dispatcher/dispatcher'; -import rateLimitedFunc from '../../ratelimitedfunc'; import * as Rooms from '../../Rooms'; import eventSearch, { searchPagination } from '../../Searching'; import MainSplit from './MainSplit'; @@ -64,7 +63,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; import { XOR } from "../../@types/common"; -import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; +import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import EffectsOverlay from "../views/elements/EffectsOverlay"; import { containsEmoji } from '../../effects/utils'; import { CHAT_EFFECTS } from '../../effects'; @@ -82,6 +81,7 @@ import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; +import { throttle } from "lodash"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -94,22 +94,8 @@ if (DEBUG) { } interface IProps { - threepidInvite: IThreepidInvite, - - // Any data about the room that would normally come from the homeserver - // but has been passed out-of-band, eg. the room name and avatar URL - // from an email invite (a workaround for the fact that we can't - // get this information from the HS using an email invite). - // Fields: - // * name (string) The room's name - // * avatarUrl (string) The mxc:// avatar URL for the room - // * inviterName (string) The display name of the person who - // * invited us to the room - oobData?: { - name?: string; - avatarUrl?: string; - inviterName?: string; - }; + threepidInvite: IThreepidInvite; + oobData?: IOOBData; resizeNotifier: ResizeNotifier; justCreatedOpts?: IOpts; @@ -675,8 +661,8 @@ export default class RoomView extends React.Component { ); } - // cancel any pending calls to the rate_limited_funcs - this.updateRoomMembers.cancelPendingCall(); + // cancel any pending calls to the throttled updated + this.updateRoomMembers.cancel(); for (const watcher of this.settingWatchers) { SettingsStore.unwatchSetting(watcher); @@ -1092,7 +1078,7 @@ export default class RoomView extends React.Component { return; } - this.updateRoomMembers(member); + this.updateRoomMembers(); }; private onMyMembership = (room: Room, membership: string, oldMembership: string) => { @@ -1114,10 +1100,10 @@ export default class RoomView extends React.Component { } // rate limited because a power level change will emit an event for every member in the room. - private updateRoomMembers = rateLimitedFunc(() => { + private updateRoomMembers = throttle(() => { this.updateDMState(); this.updateE2EStatus(this.state.room); - }, 500); + }, 500, { leading: true, trailing: true }); private checkDesktopNotifications() { const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); @@ -1261,7 +1247,7 @@ export default class RoomView extends React.Component { }); }; - private injectSticker(url, info, text) { + private injectSticker(url: string, info: object, text: string) { if (this.context.isGuest()) { dis.dispatch({ action: 'require_registration' }); return; @@ -1460,13 +1446,6 @@ export default class RoomView extends React.Component { }); }; - private onLeaveClick = () => { - dis.dispatch({ - action: 'leave_room', - room_id: this.state.room.roomId, - }); - }; - private onForgetClick = () => { dis.dispatch({ action: 'forget_room', @@ -2106,7 +2085,6 @@ export default class RoomView extends React.Component { onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} - onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} e2eStatus={this.state.e2eStatus} onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null} appsShown={this.state.showApps} diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index e5c4372ab6..2ee0327420 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -58,7 +58,7 @@ export interface ISpaceSummaryRoom { avatar_url?: string; guest_can_join: boolean; name?: string; - num_joined_members: number + num_joined_members: number; room_id: string; topic?: string; world_readable: boolean; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index e4c7d15596..8d5d733082 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -125,7 +125,7 @@ interface IProps { onReadMarkerUpdated?(): void; // callback which is called when we wish to paginate the timeline window. - onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise, + onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise; } interface IState { diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index b8dce48235..c8e90a1c0a 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar"; import AccessibleButton from "../views/elements/AccessibleButton"; import { IUpload } from "../../models/IUpload"; import { replaceableComponent } from "../../utils/replaceableComponent"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; interface IProps { room: Room; @@ -38,6 +39,8 @@ interface IState { @replaceableComponent("structures.UploadBar") export default class UploadBar extends React.Component { + static contextType = MatrixClientContext; + private dispatcherRef: string; private mounted: boolean; @@ -82,7 +85,7 @@ export default class UploadBar extends React.Component { private onCancelClick = (ev) => { ev.preventDefault(); - ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise); + ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context); }; render() { diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index f27bed2cc3..57b758091a 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -49,7 +49,7 @@ interface IProps { // for operations like uploading cross-signing keys). onLoggedIn(params: { userId: string; - deviceId: string + deviceId: string; homeserverUrl: string; identityServerUrl?: string; accessToken: string; diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.tsx similarity index 81% rename from src/components/structures/auth/SetupEncryptionBody.js rename to src/components/structures/auth/SetupEncryptionBody.tsx index f0798b6d1a..13790c2e47 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,33 +15,43 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; -import * as sdk from '../../../index'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; +import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; +import AccessibleButton from '../../views/elements/AccessibleButton'; +import Spinner from '../../views/elements/Spinner'; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -function keyHasPassphrase(keyInfo) { - return ( +function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { + return Boolean( keyInfo.passphrase && keyInfo.passphrase.salt && - keyInfo.passphrase.iterations + keyInfo.passphrase.iterations, ); } -@replaceableComponent("structures.auth.SetupEncryptionBody") -export default class SetupEncryptionBody extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (boolean) => void; +} - constructor() { - super(); +interface IState { + phase: Phase; + verificationRequest: VerificationRequest; + backupInfo: IKeyBackupInfo; +} + +@replaceableComponent("structures.auth.SetupEncryptionBody") +export default class SetupEncryptionBody extends React.Component { + constructor(props) { + super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this._onStoreUpdate); + store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase, @@ -53,10 +63,10 @@ export default class SetupEncryptionBody extends React.Component { }; } - _onStoreUpdate = () => { + private onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { - this.props.onFinished(); + this.props.onFinished(true); return; } this.setState({ @@ -66,18 +76,18 @@ export default class SetupEncryptionBody extends React.Component { }); }; - componentWillUnmount() { + public componentWillUnmount() { const store = SetupEncryptionStore.sharedInstance(); - store.off("update", this._onStoreUpdate); + store.off("update", this.onStoreUpdate); store.stop(); } - _onUsePassphraseClick = async () => { + private onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); store.usePassPhrase(); - } + }; - _onVerifyClick = () => { + private onVerifyClick = () => { const cli = MatrixClientPeg.get(); const userId = cli.getUserId(); const requestPromise = cli.requestVerification(userId); @@ -91,42 +101,44 @@ export default class SetupEncryptionBody extends React.Component { request.cancel(); }, }); - } + }; - onSkipClick = () => { + private onSkipClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skip(); - } + }; - onSkipConfirmClick = () => { + private onSkipConfirmClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skipConfirm(); - } + }; - onSkipBackClick = () => { + private onSkipBackClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.returnAfterSkip(); - } + }; - onDoneClick = () => { + private onDoneClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.done(); - } + }; - render() { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + private onEncryptionPanelClose = () => { + this.props.onFinished(false); + }; + public render() { const { phase, } = this.state; if (this.state.verificationRequest) { - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); return ; } else if (phase === Phase.Intro) { const store = SetupEncryptionStore.sharedInstance(); @@ -139,14 +151,14 @@ export default class SetupEncryptionBody extends React.Component { let useRecoveryKeyButton; if (recoveryKeyPrompt) { - useRecoveryKeyButton = + useRecoveryKeyButton = {recoveryKeyPrompt} ; } let verifyButton; if (store.hasDevicesToVerifyAgainst) { - verifyButton = + verifyButton = { _t("Use another login") } ; } @@ -217,7 +229,6 @@ export default class SetupEncryptionBody extends React.Component { ); } else if (phase === Phase.Busy || phase === Phase.Loading) { - const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { console.log(`SetupEncryptionBody: Unknown phase ${phase}`); diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 7fb60a7b5d..3790028fea 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -49,7 +49,7 @@ interface IProps { fragmentAfterLogin?: string; // Called when the SSO login completes - onTokenLoginCompleted: () => void, + onTokenLoginCompleted: () => void; } interface IState { diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index 12f55a112c..a77dd0b683 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -52,8 +52,8 @@ interface IProps { interface IState { fieldValid: Partial>; - loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone, - password: "", + loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone; + password: ""; } enum LoginField { diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 950caefa02..5e6bf45f07 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler"; import TextWithTooltip from "../elements/TextWithTooltip"; import DMRoomMap from "../../../utils/DMRoomMap"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IOOBData } from "../../../stores/ThreepidInviteStore"; interface IProps { room: Room; avatarSize: number; displayBadge?: boolean; forceCount?: boolean; - oobData?: object; + oobData?: IOOBData; viewAvatarOnClick?: boolean; } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index c3f49d4a12..8ac8de8233 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -24,14 +24,14 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import { IOOBData } from '../../../stores/ThreepidInviteStore'; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - // TODO: type when js-sdk has types - oobData?: any; + oobData?: IOOBData; width?: number; height?: number; resizeMethod?: ResizeMethod; diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 5cdb4c664b..78fae390b5 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -29,7 +29,7 @@ interface IProps { // group member object. Supply either this or 'member' groupMember: GroupMemberType; // needed if a group member is specified - matrixClient?: MatrixClient, + matrixClient?: MatrixClient; action: string; // eg. 'Ban' title: string; // eg. 'Ban this user?' diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index d9dcb8fe00..bbb5f24162 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -70,9 +70,9 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; /* eslint-disable camelcase */ interface IRecentUser { - userId: string, - user: RoomMember, - lastActive: number, + userId: string; + user: RoomMember; + lastActive: number; } export const KIND_DM = "dm"; @@ -330,16 +330,16 @@ interface IInviteDialogProps { // The kind of invite being performed. Assumed to be KIND_DM if // not provided. - kind: string, + kind: string; // The room ID this dialog is for. Only required for KIND_INVITE. - roomId: string, + roomId: string; // The call to transfer. Only required for KIND_CALL_TRANSFER. - call: MatrixCall, + call: MatrixCall; // Initial value to populate the filter with - initialText: string, + initialText: string; } interface IInviteDialogState { @@ -356,8 +356,8 @@ interface IInviteDialogState { consultFirst: boolean; // These two flags are used for the 'Go' button to communicate what is going on. - busy: boolean, - errorText: string, + busy: boolean; + errorText: string; } @replaceableComponent("views.dialogs.InviteDialog") diff --git a/src/components/views/dialogs/NewSessionReviewDialog.js b/src/components/views/dialogs/NewSessionReviewDialog.js deleted file mode 100644 index 749f48ef48..0000000000 --- a/src/components/views/dialogs/NewSessionReviewDialog.js +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import Modal from '../../../Modal'; -import { replaceableComponent } from '../../../utils/replaceableComponent'; -import VerificationRequestDialog from './VerificationRequestDialog'; -import BaseDialog from './BaseDialog'; -import DialogButtons from '../elements/DialogButtons'; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import * as sdk from '../../../index'; - -@replaceableComponent("views.dialogs.NewSessionReviewDialog") -export default class NewSessionReviewDialog extends React.PureComponent { - static propTypes = { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - } - - onCancelClick = () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, { - headerImage: require("../../../../res/img/e2e/warning.svg"), - title: _t("Your account is not secure"), - description:

- {_t("One of the following may be compromised:")} -
    -
  • {_t("Your password")}
  • -
  • {_t("Your homeserver")}
  • -
  • {_t("This session, or the other session")}
  • -
  • {_t("The internet connection either session is using")}
  • -
-
- {_t("We recommend you change your password and Security Key in Settings immediately")} -
-
, - onFinished: () => this.props.onFinished(false), - }); - } - - onContinueClick = () => { - const { userId, device } = this.props; - const cli = MatrixClientPeg.get(); - const requestPromise = cli.requestVerification( - userId, - [device.deviceId], - ); - - this.props.onFinished(true); - Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, { - verificationRequestPromise: requestPromise, - member: cli.getUser(userId), - onFinished: async () => { - const request = await requestPromise; - request.cancel(); - }, - }); - } - - render() { - const { device } = this.props; - - const icon = ; - const titleText = _t("New session"); - - const title =

- {icon} - {titleText} -

; - - return ( - -
-

{_t( - "Use this session to verify your new one, " + - "granting it access to encrypted messages:", - )}

-
-
- - {device.getDisplayName()} - - ({device.deviceId}) - -
-
-

{_t( - "If you didn’t sign in to this session, " + - "your account may be compromised.", - )}

- -
-
- ); - } -} diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx index 818ac4b9e4..02a779743b 100644 --- a/src/components/views/dialogs/TermsDialog.tsx +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -46,19 +46,19 @@ interface ITermsDialogProps { * Array of [Service, policies] pairs, where policies is the response from the * /terms endpoint for that service */ - policiesAndServicePairs: any[], + policiesAndServicePairs: any[]; /** * urls that the user has already agreed to */ - agreedUrls?: string[], + agreedUrls?: string[]; /** * Called with: * * success {bool} True if the user accepted any douments, false if cancelled * * agreedUrls {string[]} List of agreed URLs */ - onFinished: (success: boolean, agreedUrls?: string[]) => void, + onFinished: (success: boolean, agreedUrls?: string[]) => void; } interface IState { diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.tsx similarity index 70% rename from src/components/views/dialogs/VerificationRequestDialog.js rename to src/components/views/dialogs/VerificationRequestDialog.tsx index bf5d63b895..4d3123c274 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,27 +15,33 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import BaseDialog from "./BaseDialog"; +import EncryptionPanel from "../right_panel/EncryptionPanel"; +import { User } from 'matrix-js-sdk'; + +interface IProps { + verificationRequest: VerificationRequest; + verificationRequestPromise: Promise; + onFinished: () => void; + member: User; +} + +interface IState { + verificationRequest: VerificationRequest; +} @replaceableComponent("views.dialogs.VerificationRequestDialog") -export default class VerificationRequestDialog extends React.Component { - static propTypes = { - verificationRequest: PropTypes.object, - verificationRequestPromise: PropTypes.object, - onFinished: PropTypes.func.isRequired, - member: PropTypes.string, - }; - - constructor(...args) { - super(...args); - this.state = {}; - if (this.props.verificationRequest) { - this.state.verificationRequest = this.props.verificationRequest; - } else if (this.props.verificationRequestPromise) { +export default class VerificationRequestDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + verificationRequest: this.props.verificationRequest, + }; + if (this.props.verificationRequestPromise) { this.props.verificationRequestPromise.then(r => { this.setState({ verificationRequest: r }); }); @@ -43,8 +49,6 @@ export default class VerificationRequestDialog extends React.Component { } render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); const request = this.state.verificationRequest; const otherUserId = request && request.otherUserId; const member = this.props.member || @@ -65,6 +69,7 @@ export default class VerificationRequestDialog extends React.Component { verificationRequestPromise={this.props.verificationRequestPromise} onClose={this.props.onFinished} member={member} + isRoomEncrypted={false} /> ; } diff --git a/src/components/views/elements/BlurhashPlaceholder.tsx b/src/components/views/elements/BlurhashPlaceholder.tsx new file mode 100644 index 0000000000..0e59253fe8 --- /dev/null +++ b/src/components/views/elements/BlurhashPlaceholder.tsx @@ -0,0 +1,56 @@ +/* + Copyright 2020 The Matrix.org Foundation C.I.C. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import React from 'react'; +import { decode } from "blurhash"; + +interface IProps { + blurhash: string; + width: number; + height: number; +} + +export default class BlurhashPlaceholder extends React.PureComponent { + private canvas: React.RefObject = React.createRef(); + + public componentDidMount() { + this.draw(); + } + + public componentDidUpdate() { + this.draw(); + } + + private draw() { + if (!this.canvas.current) return; + + try { + const { width, height } = this.props; + + const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height)); + const ctx = this.canvas.current.getContext("2d"); + const imgData = ctx.createImageData(width, height); + imgData.data.set(pixels); + ctx.putImageData(imgData, 0, 0); + } catch (e) { + console.error("Error rendering blurhash: ", e); + } + } + + public render() { + return ; + } +} diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index ab647db9ed..681817ca86 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -29,7 +29,7 @@ interface IProps { // The minimum number of events needed to trigger summarisation threshold?: number; // Whether or not to begin with state.expanded=true - startExpanded?: boolean, + startExpanded?: boolean; // The list of room members for which to show avatars next to the summary summaryMembers?: RoomMember[]; // The text to show as the summary of this event list diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 74538d2fa9..2628170f9c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -44,31 +44,31 @@ const ZOOM_COEFFICIENT = 0.0025; const ZOOM_DISTANCE = 10; interface IProps { - src: string, // the source of the image being displayed - name?: string, // the main title ('name') for the image - link?: string, // the link (if any) applied to the name of the image - width?: number, // width of the image src in pixels - height?: number, // height of the image src in pixels - fileSize?: number, // size of the image src in bytes - onFinished(): void, // callback when the lightbox is dismissed + src: string; // the source of the image being displayed + name?: string; // the main title ('name') for the image + link?: string; // the link (if any) applied to the name of the image + width?: number; // width of the image src in pixels + height?: number; // height of the image src in pixels + fileSize?: number; // size of the image src in bytes + onFinished(): void; // callback when the lightbox is dismissed // the event (if any) that the Image is displaying. Used for event-specific stuff like // redactions, senders, timestamps etc. Other descriptors are taken from the explicit // properties above, which let us use lightboxes to display images which aren't associated // with events. - mxEvent: MatrixEvent, - permalinkCreator: RoomPermalinkCreator, + mxEvent: MatrixEvent; + permalinkCreator: RoomPermalinkCreator; } interface IState { - zoom: number, - minZoom: number, - maxZoom: number, - rotation: number, - translationX: number, - translationY: number, - moving: boolean, - contextMenuDisplayed: boolean, + zoom: number; + minZoom: number; + maxZoom: number; + rotation: number; + translationX: number; + translationY: number; + moving: boolean; + contextMenuDisplayed: boolean; } @replaceableComponent("views.elements.ImageView") diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index d10a599d95..1678bdb33a 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -30,14 +30,14 @@ function languageMatchesSearchQuery(query, language) { } interface SpellCheckLanguagesDropdownIProps { - className: string, - value: string, - onOptionChange(language: string), + className: string; + value: string; + onOptionChange(language: string); } interface SpellCheckLanguagesDropdownIState { - searchQuery: string, - languages: any, + searchQuery: string; + languages: any; } @replaceableComponent("views.elements.SpellCheckLanguagesDropdown") diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx index 010801141a..ac39affdd9 100644 --- a/src/components/views/emojipicker/Header.tsx +++ b/src/components/views/emojipicker/Header.tsx @@ -25,7 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { categories: ICategory[]; - onAnchorClick(id: CategoryKey): void + onAnchorClick(id: CategoryKey): void; } @replaceableComponent("views.emojipicker.Header") diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 7e85f15898..5566f5aec0 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -29,6 +29,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import InlineSpinner from '../elements/InlineSpinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; +import BlurhashPlaceholder from "../elements/BlurhashPlaceholder"; +import { BLURHASH_FIELD } from "../../../ContentMessages"; @replaceableComponent("views.messages.MImageBody") export default class MImageBody extends React.Component { @@ -333,7 +335,8 @@ export default class MImageBody extends React.Component { infoWidth = content.info.w; infoHeight = content.info.h; } else { - // Whilst the image loads, display nothing. + // Whilst the image loads, display nothing. We also don't display a blurhash image + // because we don't really know what size of image we'll end up with. // // Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`. // @@ -368,12 +371,8 @@ export default class MImageBody extends React.Component { let placeholder = null; let gifLabel = null; - // e2e image hasn't been decrypted yet - if (content.file !== undefined && this.state.decryptedUrl === null) { - placeholder = ; - } else if (!this.state.imgLoaded) { - // Deliberately, getSpinner is left unimplemented here, MStickerBody overides - placeholder = this.getPlaceholder(); + if (!this.state.imgLoaded) { + placeholder = this.getPlaceholder(maxWidth, maxHeight); } let showPlaceholder = Boolean(placeholder); @@ -395,7 +394,7 @@ export default class MImageBody extends React.Component { if (!this.state.showImage) { img = ; - showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon. + showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) { @@ -411,9 +410,7 @@ export default class MImageBody extends React.Component { // Constrain width here so that spinner appears central to the loaded thumbnail maxWidth: infoWidth + "px", }}> -
- { placeholder } -
+ { placeholder } } @@ -437,9 +434,12 @@ export default class MImageBody extends React.Component { } // Overidden by MStickerBody - getPlaceholder() { - // MImageBody doesn't show a placeholder whilst the image loads, (but it could do) - return null; + getPlaceholder(width, height) { + const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; + if (blurhash) return ; + return
+ +
; } // Overidden by MStickerBody diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index d690513d55..fc550845e2 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -28,7 +28,7 @@ import EventTileBubble from "./EventTileBubble"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { - mxEvent: MatrixEvent + mxEvent: MatrixEvent; } @replaceableComponent("views.messages.MKeyVerificationRequest") @@ -154,7 +154,7 @@ export default class MKeyVerificationRequest extends React.Component { {_t("Decline")} - + {_t("Accept")} ); diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index eb3635b0c0..31af66baf5 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -18,6 +18,7 @@ import React from 'react'; import MImageBody from './MImageBody'; import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { BLURHASH_FIELD } from "../../../ContentMessages"; @replaceableComponent("views.messages.MStickerBody") export default class MStickerBody extends MImageBody { @@ -41,7 +42,8 @@ export default class MStickerBody extends MImageBody { // Placeholder to show in place of the sticker image if // img onLoad hasn't fired yet. - getPlaceholder() { + getPlaceholder(width, height) { + if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height); return ; } diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index ef79e96370..d882bb1eb0 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -16,6 +16,8 @@ limitations under the License. */ import React from 'react'; +import { decode } from "blurhash"; + import MFileBody from './MFileBody'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; @@ -23,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from '../elements/InlineSpinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; +import { BLURHASH_FIELD } from "../../../ContentMessages"; interface IProps { /* the MatrixEvent to show */ @@ -32,11 +35,13 @@ interface IProps { } interface IState { - decryptedUrl: string|null, - decryptedThumbnailUrl: string|null, - decryptedBlob: Blob|null, - error: any|null, - fetchingData: boolean, + decryptedUrl?: string; + decryptedThumbnailUrl?: string; + decryptedBlob?: Blob; + error?: any; + fetchingData: boolean; + posterLoading: boolean; + blurhashUrl: string; } @replaceableComponent("views.messages.MVideoBody") @@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent { decryptedThumbnailUrl: null, decryptedBlob: null, error: null, + posterLoading: false, + blurhashUrl: null, }; } - thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) { + thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) { if (!fullWidth || !fullHeight) { // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even // log this because it's spammy @@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent { private getThumbUrl(): string|null { const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); - if (media.isEncrypted) { + + if (media.isEncrypted && this.state.decryptedThumbnailUrl) { return this.state.decryptedThumbnailUrl; + } else if (this.state.posterLoading) { + return this.state.blurhashUrl; } else if (media.hasThumbnail) { return media.thumbnailHttp; } else { @@ -101,18 +111,57 @@ export default class MVideoBody extends React.PureComponent { } } + private loadBlurhash() { + const info = this.props.mxEvent.getContent()?.info; + if (!info[BLURHASH_FIELD]) return; + + const canvas = document.createElement("canvas"); + + let width = info.w; + let height = info.h; + const scale = this.thumbScale(info.w, info.h); + if (scale) { + width = Math.floor(info.w * scale); + height = Math.floor(info.h * scale); + } + + canvas.width = width; + canvas.height = height; + + const pixels = decode(info[BLURHASH_FIELD], width, height); + const ctx = canvas.getContext("2d"); + const imgData = ctx.createImageData(width, height); + imgData.data.set(pixels); + ctx.putImageData(imgData, 0, 0); + + this.setState({ + blurhashUrl: canvas.toDataURL(), + posterLoading: true, + }); + + const content = this.props.mxEvent.getContent(); + const media = mediaFromContent(content); + if (media.hasThumbnail) { + const image = new Image(); + image.onload = () => { + this.setState({ posterLoading: false }); + }; + image.src = media.thumbnailHttp; + } + } + async componentDidMount() { const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; const content = this.props.mxEvent.getContent(); + this.loadBlurhash(); + if (content.file !== undefined && this.state.decryptedUrl === null) { let thumbnailPromise = Promise.resolve(null); - if (content.info && content.info.thumbnail_file) { - thumbnailPromise = decryptFile( - content.info.thumbnail_file, - ).then(function(blob) { - return URL.createObjectURL(blob); - }); + if (content?.info?.thumbnail_file) { + thumbnailPromise = decryptFile(content.info.thumbnail_file) + .then(blob => URL.createObjectURL(blob)); } + try { const thumbnailUrl = await thumbnailPromise; if (autoplay) { @@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent { let poster = null; let preload = "metadata"; if (content.info) { - const scale = this.thumbScale(content.info.w, content.info.h, 480, 360); + const scale = this.thumbScale(content.info.w, content.info.h); if (scale) { width = Math.floor(content.info.w * scale); height = Math.floor(content.info.h * scale); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.tsx similarity index 80% rename from src/components/views/messages/TextualBody.js rename to src/components/views/messages/TextualBody.tsx index ffaaaada4d..6ba018c512 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.tsx @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,134 +14,151 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { createRef, SyntheticEvent } from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; import highlight from 'highlight.js'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { MsgType } from "matrix-js-sdk/src/@types/event"; + import * as HtmlUtils from '../../../HtmlUtils'; import { formatDate } from '../../../DateUtils'; -import * as sdk from '../../../index'; import Modal from '../../../Modal'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import * as ContextMenu from '../../structures/ContextMenu'; +import { toRightOf } from '../../structures/ContextMenu'; import SettingsStore from "../../../settings/SettingsStore"; import ReplyThread from "../elements/ReplyThread"; import { pillifyLinks, unmountPills } from '../../../utils/pillify'; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { isPermalinkHost } from "../../../utils/permalinks/Permalinks"; -import { toRightOf } from "../../structures/ContextMenu"; import { copyPlaintext } from "../../../utils/strings"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from "../../../stores/UIStore"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; +import { TileShape } from '../rooms/EventTile'; +import EditorStateTransfer from "../../../utils/EditorStateTransfer"; +import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; +import Spoiler from "../elements/Spoiler"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog"; +import EditMessageComposer from '../rooms/EditMessageComposer'; +import LinkPreviewWidget from '../rooms/LinkPreviewWidget'; + +interface IProps { + /* the MatrixEvent to show */ + mxEvent: MatrixEvent; + + /* a list of words to highlight */ + highlights?: string[]; + + /* link URL for the highlights */ + highlightLink?: string; + + /* should show URL previews for this event */ + showUrlPreview?: boolean; + + /* the shape of the tile, used */ + tileShape?: TileShape; + + editState?: EditorStateTransfer; + replacingEventId?: string; + + /* callback for when our widget has loaded */ + onHeightChanged(): void; +} + +interface IState { + // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. + links: string[]; + + // track whether the preview widget is hidden + widgetHidden: boolean; +} @replaceableComponent("views.messages.TextualBody") -export default class TextualBody extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, +export default class TextualBody extends React.Component { + private readonly contentRef = createRef(); - /* a list of words to highlight */ - highlights: PropTypes.array, - - /* link URL for the highlights */ - highlightLink: PropTypes.string, - - /* should show URL previews for this event */ - showUrlPreview: PropTypes.bool, - - /* callback for when our widget has loaded */ - onHeightChanged: PropTypes.func, - - /* the shape of the tile, used */ - tileShape: PropTypes.string, - }; + private unmounted = false; + private pills: Element[] = []; constructor(props) { super(props); - this._content = createRef(); - this.state = { - // the URLs (if any) to be previewed with a LinkPreviewWidget - // inside this TextualBody. links: [], - - // track whether the preview widget is hidden widgetHidden: false, }; } componentDidMount() { - this._unmounted = false; - this._pills = []; if (!this.props.editState) { - this._applyFormatting(); + this.applyFormatting(); } } - _applyFormatting() { + private applyFormatting(): void { const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); - this.activateSpoilers([this._content.current]); + this.activateSpoilers([this.contentRef.current]); // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. - pillifyLinks([this._content.current], this.props.mxEvent, this._pills); - HtmlUtils.linkifyElement(this._content.current); + pillifyLinks([this.contentRef.current], this.props.mxEvent, this.pills); + HtmlUtils.linkifyElement(this.contentRef.current); this.calculateUrlPreview(); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons - const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre"); + const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre"); if (pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. // This happens after the codeblock was edited. - if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue; + if (pres[i].parentElement.className == "mx_EventTile_pre_container") continue; // Add code element if it's missing since we depend on it if (pres[i].getElementsByTagName("code").length == 0) { - this._addCodeElement(pres[i]); + this.addCodeElement(pres[i]); } // Wrap a div around
 so that the copy button can be correctly positioned
                     // when the 
 overflows and is scrolled horizontally.
-                    const div = this._wrapInDiv(pres[i]);
-                    this._handleCodeBlockExpansion(pres[i]);
-                    this._addCodeExpansionButton(div, pres[i]);
-                    this._addCodeCopyButton(div);
+                    const div = this.wrapInDiv(pres[i]);
+                    this.handleCodeBlockExpansion(pres[i]);
+                    this.addCodeExpansionButton(div, pres[i]);
+                    this.addCodeCopyButton(div);
                     if (showLineNumbers) {
-                        this._addLineNumbers(pres[i]);
+                        this.addLineNumbers(pres[i]);
                     }
                 }
             }
             // Highlight code
-            const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
+            const codes = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("code");
             if (codes.length > 0) {
                 // Do this asynchronously: parsing code takes time and we don't
                 // need to block the DOM update on it.
                 setTimeout(() => {
-                    if (this._unmounted) return;
+                    if (this.unmounted) return;
                     for (let i = 0; i < codes.length; i++) {
                         // If the code already has the hljs class we want to skip this.
                         // This happens after the codeblock was edited.
                         if (codes[i].className.includes("hljs")) continue;
-                        this._highlightCode(codes[i]);
+                        this.highlightCode(codes[i]);
                     }
                 }, 10);
             }
         }
     }
 
-    _addCodeElement(pre) {
+    private addCodeElement(pre: HTMLPreElement): void {
         const code = document.createElement("code");
         code.append(...pre.childNodes);
         pre.appendChild(code);
     }
 
-    _addCodeExpansionButton(div, pre) {
+    private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
         // Calculate how many percent does the pre element take up.
         // If it's less than 30% we don't add the expansion button.
         const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
@@ -175,7 +190,7 @@ export default class TextualBody extends React.Component {
         div.appendChild(button);
     }
 
-    _addCodeCopyButton(div) {
+    private addCodeCopyButton(div: HTMLDivElement): void {
         const button = document.createElement("span");
         button.className = "mx_EventTile_button mx_EventTile_copyButton ";
 
@@ -185,11 +200,10 @@ export default class TextualBody extends React.Component {
         if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
 
         button.onclick = async () => {
-            const copyCode = button.parentNode.getElementsByTagName("code")[0];
+            const copyCode = button.parentElement.getElementsByTagName("code")[0];
             const successful = await copyPlaintext(copyCode.textContent);
 
             const buttonRect = button.getBoundingClientRect();
-            const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
             const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
                 ...toRightOf(buttonRect, 2),
                 message: successful ? _t('Copied!') : _t('Failed to copy'),
@@ -200,7 +214,7 @@ export default class TextualBody extends React.Component {
         div.appendChild(button);
     }
 
-    _wrapInDiv(pre) {
+    private wrapInDiv(pre: HTMLPreElement): HTMLDivElement {
         const div = document.createElement("div");
         div.className = "mx_EventTile_pre_container";
 
@@ -212,13 +226,13 @@ export default class TextualBody extends React.Component {
         return div;
     }
 
-    _handleCodeBlockExpansion(pre) {
+    private handleCodeBlockExpansion(pre: HTMLPreElement): void {
         if (!SettingsStore.getValue("expandCodeByDefault")) {
             pre.className = "mx_EventTile_collapsedCodeBlock";
         }
     }
 
-    _addLineNumbers(pre) {
+    private addLineNumbers(pre: HTMLPreElement): void {
         // Calculate number of lines in pre
         const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
         pre.innerHTML = '' + pre.innerHTML + '';
@@ -229,7 +243,7 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    _highlightCode(code) {
+    private highlightCode(code: HTMLElement): void {
         if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
             highlight.highlightBlock(code);
         } else {
@@ -249,14 +263,14 @@ export default class TextualBody extends React.Component {
             const stoppedEditing = prevProps.editState && !this.props.editState;
             const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
             if (messageWasEdited || stoppedEditing) {
-                this._applyFormatting();
+                this.applyFormatting();
             }
         }
     }
 
     componentWillUnmount() {
-        this._unmounted = true;
-        unmountPills(this._pills);
+        this.unmounted = true;
+        unmountPills(this.pills);
     }
 
     shouldComponentUpdate(nextProps, nextState) {
@@ -273,12 +287,12 @@ export default class TextualBody extends React.Component {
                 nextState.widgetHidden !== this.state.widgetHidden);
     }
 
-    calculateUrlPreview() {
+    private calculateUrlPreview(): void {
         //console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
 
         if (this.props.showUrlPreview) {
             // pass only the first child which is the event tile otherwise this recurses on edited events
-            let links = this.findLinks([this._content.current]);
+            let links = this.findLinks([this.contentRef.current]);
             if (links.length) {
                 // de-duplicate the links after stripping hashes as they don't affect the preview
                 // using a set here maintains the order
@@ -291,8 +305,8 @@ export default class TextualBody extends React.Component {
                 this.setState({ links });
 
                 // lazy-load the hidden state of the preview widget from localstorage
-                if (global.localStorage) {
-                    const hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
+                if (window.localStorage) {
+                    const hidden = !!window.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
                     this.setState({ widgetHidden: hidden });
                 }
             } else if (this.state.links.length) {
@@ -301,19 +315,15 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    activateSpoilers(nodes) {
+    private activateSpoilers(nodes: ArrayLike): void {
         let node = nodes[0];
         while (node) {
             if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
                 const spoilerContainer = document.createElement('span');
 
                 const reason = node.getAttribute("data-mx-spoiler");
-                const Spoiler = sdk.getComponent('elements.Spoiler');
                 node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
-                const spoiler = ;
+                const spoiler = ;
 
                 ReactDOM.render(spoiler, spoilerContainer);
                 node.parentNode.replaceChild(spoilerContainer, node);
@@ -322,15 +332,15 @@ export default class TextualBody extends React.Component {
             }
 
             if (node.childNodes && node.childNodes.length) {
-                this.activateSpoilers(node.childNodes);
+                this.activateSpoilers(node.childNodes as NodeListOf);
             }
 
-            node = node.nextSibling;
+            node = node.nextSibling as Element;
         }
     }
 
-    findLinks(nodes) {
-        let links = [];
+    private findLinks(nodes: ArrayLike): string[] {
+        let links: string[] = [];
 
         for (let i = 0; i < nodes.length; i++) {
             const node = nodes[i];
@@ -348,7 +358,7 @@ export default class TextualBody extends React.Component {
         return links;
     }
 
-    isLinkPreviewable(node) {
+    private isLinkPreviewable(node: Element): boolean {
         // don't try to preview relative links
         if (!node.getAttribute("href").startsWith("http://") &&
             !node.getAttribute("href").startsWith("https://")) {
@@ -381,7 +391,7 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    onCancelClick = event => {
+    private onCancelClick = (): void => {
         this.setState({ widgetHidden: true });
         // FIXME: persist this somewhere smarter than local storage
         if (global.localStorage) {
@@ -390,7 +400,7 @@ export default class TextualBody extends React.Component {
         this.forceUpdate();
     };
 
-    onEmoteSenderClick = event => {
+    private onEmoteSenderClick = (): void => {
         const mxEvent = this.props.mxEvent;
         dis.dispatch({
             action: Action.ComposerInsert,
@@ -398,7 +408,7 @@ export default class TextualBody extends React.Component {
         });
     };
 
-    getEventTileOps = () => ({
+    public getEventTileOps = () => ({
         isWidgetHidden: () => {
             return this.state.widgetHidden;
         },
@@ -411,7 +421,7 @@ export default class TextualBody extends React.Component {
         },
     });
 
-    onStarterLinkClick = (starterLink, ev) => {
+    private onStarterLinkClick = (starterLink: string, ev: SyntheticEvent): void => {
         ev.preventDefault();
         // We need to add on our scalar token to the starter link, but we may not have one!
         // In addition, we can't fetch one on click and then go to it immediately as that
@@ -431,7 +441,6 @@ export default class TextualBody extends React.Component {
         const scalarClient = integrationManager.getScalarClient();
         scalarClient.connect().then(() => {
             const completeUrl = scalarClient.getStarterLink(starterLink);
-            const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
             const integrationsUrl = integrationManager.uiUrl;
             Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
                 title: _t("Add an Integration"),
@@ -458,12 +467,11 @@ export default class TextualBody extends React.Component {
         });
     };
 
-    _openHistoryDialog = async () => {
-        const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
+    private openHistoryDialog = async (): Promise => {
         Modal.createDialog(MessageEditHistoryDialog, { mxEvent: this.props.mxEvent });
     };
 
-    _renderEditedMarker() {
+    private renderEditedMarker() {
         const date = this.props.mxEvent.replacingEventDate();
         const dateString = date && formatDate(date);
 
@@ -479,7 +487,7 @@ export default class TextualBody extends React.Component {
         return (
             
@@ -490,24 +498,25 @@ export default class TextualBody extends React.Component {
 
     render() {
         if (this.props.editState) {
-            const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
             return ;
         }
         const mxEvent = this.props.mxEvent;
         const content = mxEvent.getContent();
 
         // only strip reply if this is the original replying event, edits thereafter do not have the fallback
-        const stripReply = !mxEvent.replacingEvent() && ReplyThread.getParentEventId(mxEvent);
+        const stripReply = !mxEvent.replacingEvent() && !!ReplyThread.getParentEventId(mxEvent);
         let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
-            disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
+            disableBigEmoji: content.msgtype === MsgType.Emote
+                || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
             // Part of Replies fallback support
             stripReplyFallback: stripReply,
-            ref: this._content,
+            ref: this.contentRef,
+            returnString: false,
         });
         if (this.props.replacingEventId) {
             body = <>
                 {body}
-                {this._renderEditedMarker()}
+                {this.renderEditedMarker()}
             ;
         }
 
@@ -521,7 +530,6 @@ export default class TextualBody extends React.Component {
 
         let widgets;
         if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
-            const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
             widgets = this.state.links.map((link)=>{
                 return 
                         * 
@@ -549,7 +557,7 @@ export default class TextualBody extends React.Component {
                         { widgets }
                     
                 );
-            case "m.notice":
+            case MsgType.Notice:
                 return (
                     
                         { body }
diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.tsx
similarity index 72%
rename from src/components/views/messages/TextualEvent.js
rename to src/components/views/messages/TextualEvent.tsx
index 663f47dd2a..70f90a33e4 100644
--- a/src/components/views/messages/TextualEvent.js
+++ b/src/components/views/messages/TextualEvent.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,20 +15,20 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+
 import * as TextForEvent from "../../../TextForEvent";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
-@replaceableComponent("views.messages.TextualEvent")
-export default class TextualEvent extends React.Component {
-    static propTypes = {
-        /* the MatrixEvent to show */
-        mxEvent: PropTypes.object.isRequired,
-    };
+interface IProps {
+    mxEvent: MatrixEvent;
+}
 
+@replaceableComponent("views.messages.TextualEvent")
+export default class TextualEvent extends React.Component {
     render() {
         const text = TextForEvent.textForEvent(this.props.mxEvent, true);
-        if (text == null || text.length === 0) return null;
+        if (!text || (text as string).length === 0) return null;
         return (
             
{ text }
); diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index 3a26427246..251c04d3cc 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -39,9 +39,8 @@ interface IProps { member: RoomMember | User; onClose: () => void; verificationRequest: VerificationRequest; - verificationRequestPromise: Promise; + verificationRequestPromise?: Promise; layout: string; - inDialog: boolean; isRoomEncrypted: boolean; } diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 1c817140fa..f142328895 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -21,7 +21,6 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AppsDrawer from './AppsDrawer'; -import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { UIFeature } from "../../../settings/UIFeature"; @@ -29,35 +28,36 @@ import ResizeNotifier from "../../../utils/ResizeNotifier"; import CallViewForRoom from '../voip/CallViewForRoom'; import { objectHasDiff } from "../../../utils/objects"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { throttle } from 'lodash'; interface IProps { // js-sdk room object - room: Room, - userId: string, - showApps: boolean, // Render apps + room: Room; + userId: string; + showApps: boolean; // Render apps // maxHeight attribute for the aux panel and the video // therein - maxHeight: number, + maxHeight: number; // a callback which is called when the content of the aux panel changes // content in a way that is likely to make it change size. - onResize: () => void, - fullHeight: boolean, + onResize: () => void; + fullHeight: boolean; - resizeNotifier: ResizeNotifier, + resizeNotifier: ResizeNotifier; } interface Counter { - title: string, - value: number, - link: string, - severity: string, - stateKey: string, + title: string; + value: number; + link: string; + severity: string; + stateKey: string; } interface IState { - counters: Counter[], + counters: Counter[]; } @replaceableComponent("views.rooms.AuxPanel") @@ -99,9 +99,9 @@ export default class AuxPanel extends React.Component { } } - private rateLimitedUpdate = new RateLimitedFunc(() => { + private rateLimitedUpdate = throttle(() => { this.setState({ counters: this.computeCounters() }); - }, 500); + }, 500, { leading: true, trailing: true }); private computeCounters() { const counters = []; diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 94a292afe7..d317aa409b 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -41,7 +41,7 @@ import { Key } from "../../../Keyboard"; import { EMOTICON_TO_EMOJI } from "../../../emoji"; import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands"; import Range from "../../../editor/range"; -import MessageComposerFormatBar from "./MessageComposerFormatBar"; +import MessageComposerFormatBar, { Formatting } from "./MessageComposerFormatBar"; import DocumentOffset from "../../../editor/offset"; import { IDiff } from "../../../editor/diff"; import AutocompleteWrapperModel from "../../../editor/autocomplete"; @@ -55,7 +55,7 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc const IS_MAC = navigator.platform.indexOf("Mac") !== -1; -function ctrlShortcutLabel(key) { +function ctrlShortcutLabel(key: string): string { return (IS_MAC ? "⌘" : "Ctrl") + "+" + key; } @@ -81,14 +81,6 @@ function selectionEquals(a: Partial, b: Selection): boolean { a.type === b.type; } -enum Formatting { - Bold = "bold", - Italics = "italics", - Strikethrough = "strikethrough", - Code = "code", - Quote = "quote", -} - interface IProps { model: EditorModel; room: Room; @@ -111,9 +103,9 @@ interface IState { @replaceableComponent("views.rooms.BasicMessageEditor") export default class BasicMessageEditor extends React.Component { - private editorRef = createRef(); + public readonly editorRef = createRef(); private autocompleteRef = createRef(); - private formatBarRef = createRef(); + private formatBarRef = createRef(); private modifiedFlag = false; private isIMEComposing = false; @@ -156,7 +148,7 @@ export default class BasicMessageEditor extends React.Component } } - private replaceEmoticon = (caretPosition: DocumentPosition) => { + private replaceEmoticon = (caretPosition: DocumentPosition): number => { const { model } = this.props; const range = model.startRange(caretPosition); // expand range max 8 characters backwards from caretPosition, @@ -188,7 +180,7 @@ export default class BasicMessageEditor extends React.Component } }; - private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff) => { + private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => { renderModel(this.editorRef.current, this.props.model); if (selection) { // set the caret/selection try { @@ -230,25 +222,25 @@ export default class BasicMessageEditor extends React.Component } }; - private showPlaceholder() { + private showPlaceholder(): void { // escape single quotes const placeholder = this.props.placeholder.replace(/'/g, '\\\''); this.editorRef.current.style.setProperty("--placeholder", `'${placeholder}'`); this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty"); } - private hidePlaceholder() { + private hidePlaceholder(): void { this.editorRef.current.classList.remove("mx_BasicMessageComposer_inputEmpty"); this.editorRef.current.style.removeProperty("--placeholder"); } - private onCompositionStart = () => { + private onCompositionStart = (): void => { this.isIMEComposing = true; // even if the model is empty, the composition text shouldn't be mixed with the placeholder this.hidePlaceholder(); }; - private onCompositionEnd = () => { + private onCompositionEnd = (): void => { this.isIMEComposing = false; // some browsers (Chrome) don't fire an input event after ending a composition, // so trigger a model update after the composition is done by calling the input handler. @@ -271,14 +263,14 @@ export default class BasicMessageEditor extends React.Component } }; - isComposing(event: React.KeyboardEvent) { + public isComposing(event: React.KeyboardEvent): boolean { // checking the event.isComposing flag just in case any browser out there // emits events related to the composition after compositionend // has been fired return !!(this.isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing)); } - private onCutCopy = (event: ClipboardEvent, type: string) => { + private onCutCopy = (event: ClipboardEvent, type: string): void => { const selection = document.getSelection(); const text = selection.toString(); if (text) { @@ -296,15 +288,15 @@ export default class BasicMessageEditor extends React.Component } }; - private onCopy = (event: ClipboardEvent) => { + private onCopy = (event: ClipboardEvent): void => { this.onCutCopy(event, "copy"); }; - private onCut = (event: ClipboardEvent) => { + private onCut = (event: ClipboardEvent): void => { this.onCutCopy(event, "cut"); }; - private onPaste = (event: ClipboardEvent) => { + private onPaste = (event: ClipboardEvent): boolean => { event.preventDefault(); // we always handle the paste ourselves if (this.props.onPaste && this.props.onPaste(event, this.props.model)) { // to prevent double handling, allow props.onPaste to skip internal onPaste @@ -328,7 +320,7 @@ export default class BasicMessageEditor extends React.Component replaceRangeAndMoveCaret(range, parts); }; - private onInput = (event: Partial) => { + private onInput = (event: Partial): void => { // ignore any input while doing IME compositions if (this.isIMEComposing) { return; @@ -339,7 +331,7 @@ export default class BasicMessageEditor extends React.Component this.props.model.update(text, event.inputType, caret); }; - private insertText(textToInsert: string, inputType = "insertText") { + private insertText(textToInsert: string, inputType = "insertText"): void { const sel = document.getSelection(); const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel); const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset); @@ -353,14 +345,14 @@ export default class BasicMessageEditor extends React.Component // we don't need to. But if the user is navigating the caret without input // we need to recalculate it, to be able to know where to insert content after // losing focus - private setLastCaretFromPosition(position: DocumentPosition) { + private setLastCaretFromPosition(position: DocumentPosition): void { const { model } = this.props; this._isCaretAtEnd = position.isAtEnd(model); this.lastCaret = position.asOffset(model); this.lastSelection = cloneSelection(document.getSelection()); } - private refreshLastCaretIfNeeded() { + private refreshLastCaretIfNeeded(): DocumentOffset { // XXX: needed when going up and down in editing messages ... not sure why yet // because the editors should stop doing this when when blurred ... // maybe it's on focus and the _editorRef isn't available yet or something. @@ -377,38 +369,38 @@ export default class BasicMessageEditor extends React.Component return this.lastCaret; } - clearUndoHistory() { + public clearUndoHistory(): void { this.historyManager.clear(); } - getCaret() { + public getCaret(): DocumentOffset { return this.lastCaret; } - isSelectionCollapsed() { + public isSelectionCollapsed(): boolean { return !this.lastSelection || this.lastSelection.isCollapsed; } - isCaretAtStart() { + public isCaretAtStart(): boolean { return this.getCaret().offset === 0; } - isCaretAtEnd() { + public isCaretAtEnd(): boolean { return this._isCaretAtEnd; } - private onBlur = () => { + private onBlur = (): void => { document.removeEventListener("selectionchange", this.onSelectionChange); }; - private onFocus = () => { + private onFocus = (): void => { document.addEventListener("selectionchange", this.onSelectionChange); // force to recalculate this.lastSelection = null; this.refreshLastCaretIfNeeded(); }; - private onSelectionChange = () => { + private onSelectionChange = (): void => { const { isEmpty } = this.props.model; this.refreshLastCaretIfNeeded(); @@ -427,7 +419,7 @@ export default class BasicMessageEditor extends React.Component } }; - private onKeyDown = (event: React.KeyboardEvent) => { + private onKeyDown = (event: React.KeyboardEvent): void => { const model = this.props.model; let handled = false; const action = getKeyBindingsManager().getMessageComposerAction(event); @@ -523,7 +515,7 @@ export default class BasicMessageEditor extends React.Component } }; - private async tabCompleteName() { + private async tabCompleteName(): Promise { try { await new Promise(resolve => this.setState({ showVisualBell: false }, resolve)); const { model } = this.props; @@ -557,27 +549,27 @@ export default class BasicMessageEditor extends React.Component } } - isModified() { + public isModified(): boolean { return this.modifiedFlag; } - private onAutoCompleteConfirm = (completion: ICompletion) => { + private onAutoCompleteConfirm = (completion: ICompletion): void => { this.modifiedFlag = true; this.props.model.autoComplete.onComponentConfirm(completion); }; - private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { + private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number): void => { this.modifiedFlag = true; this.props.model.autoComplete.onComponentSelectionChange(completion); this.setState({ completionIndex }); }; - private configureEmoticonAutoReplace = () => { + private configureEmoticonAutoReplace = (): void => { const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji'); this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null); }; - private configureShouldShowPillAvatar = () => { + private configureShouldShowPillAvatar = (): void => { const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); this.setState({ showPillAvatar }); }; @@ -611,8 +603,8 @@ export default class BasicMessageEditor extends React.Component this.editorRef.current.focus(); } - private getInitialCaretPosition() { - let caretPosition; + private getInitialCaretPosition(): DocumentPosition { + let caretPosition: DocumentPosition; if (this.props.initialCaret) { // if restoring state from a previous editor, // restore caret position from the state @@ -625,7 +617,7 @@ export default class BasicMessageEditor extends React.Component return caretPosition; } - private onFormatAction = (action: Formatting) => { + private onFormatAction = (action: Formatting): void => { const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); // trim the range as we want it to exclude leading/trailing spaces range.trim(); @@ -680,9 +672,9 @@ export default class BasicMessageEditor extends React.Component }); const shortcuts = { - bold: ctrlShortcutLabel("B"), - italics: ctrlShortcutLabel("I"), - quote: ctrlShortcutLabel(">"), + [Formatting.Bold]: ctrlShortcutLabel("B"), + [Formatting.Italics]: ctrlShortcutLabel("I"), + [Formatting.Quote]: ctrlShortcutLabel(">"), }; const { completionIndex } = this.state; @@ -714,11 +706,11 @@ export default class BasicMessageEditor extends React.Component ); } - focus() { + public focus(): void { this.editorRef.current.focus(); } - public insertMention(userId: string) { + public insertMention(userId: string): void { const { model } = this.props; const { partCreator } = model; const member = this.props.room.getMember(userId); @@ -736,7 +728,7 @@ export default class BasicMessageEditor extends React.Component this.focus(); } - public insertQuotedMessage(event: MatrixEvent) { + public insertQuotedMessage(event: MatrixEvent): void { const { model } = this.props; const { partCreator } = model; const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true }); @@ -751,7 +743,7 @@ export default class BasicMessageEditor extends React.Component this.focus(); } - public insertPlaintext(text: string) { + public insertPlaintext(text: string): void { const { model } = this.props; const { partCreator } = model; const caret = this.getCaret(); diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.tsx similarity index 69% rename from src/components/views/rooms/EditMessageComposer.js rename to src/components/views/rooms/EditMessageComposer.tsx index 0ab972b5f1..4e51a0105b 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,37 +13,42 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import * as sdk from '../../../index'; + +import React, { createRef, KeyboardEvent } from 'react'; +import classNames from 'classnames'; +import { EventStatus, IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; + import { _t, _td } from '../../../languageHandler'; -import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { getCaretOffsetAndText } from '../../../editor/dom'; import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; import { findEditableEvent } from '../../../utils/EventUtils'; import { parseEvent } from '../../../editor/deserialize'; -import { CommandPartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; -import classNames from 'classnames'; -import { EventStatus } from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { CommandCategories, getCommand } from '../../../SlashCommands'; +import { Command, CommandCategories, getCommand } from '../../../SlashCommands'; import { Action } from "../../../dispatcher/actions"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SendHistoryManager from '../../../SendHistoryManager'; import Modal from '../../../Modal'; +import { MsgType } from 'matrix-js-sdk/src/@types/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import ErrorDialog from "../dialogs/ErrorDialog"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import { ActionPayload } from "../../../dispatcher/payloads"; +import AccessibleButton from '../elements/AccessibleButton'; -function _isReply(mxEvent) { +function eventIsReply(mxEvent: MatrixEvent): boolean { const relatesTo = mxEvent.getContent()["m.relates_to"]; - const isReply = !!(relatesTo && relatesTo["m.in_reply_to"]); - return isReply; + return !!(relatesTo && relatesTo["m.in_reply_to"]); } -function getHtmlReplyFallback(mxEvent) { +function getHtmlReplyFallback(mxEvent: MatrixEvent): string { const html = mxEvent.getContent().formatted_body; if (!html) { return ""; @@ -54,7 +58,7 @@ function getHtmlReplyFallback(mxEvent) { return (mxReply && mxReply.outerHTML) || ""; } -function getTextReplyFallback(mxEvent) { +function getTextReplyFallback(mxEvent: MatrixEvent): string { const body = mxEvent.getContent().body; const lines = body.split("\n").map(l => l.trim()); if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { @@ -63,12 +67,12 @@ function getTextReplyFallback(mxEvent) { return ""; } -function createEditContent(model, editedEvent) { +function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); } - const isReply = _isReply(editedEvent); + const isReply = eventIsReply(editedEvent); let plainPrefix = ""; let htmlPrefix = ""; @@ -79,11 +83,11 @@ function createEditContent(model, editedEvent) { const body = textSerialize(model); - const newContent = { - "msgtype": isEmote ? "m.emote" : "m.text", + const newContent: IContent = { + "msgtype": isEmote ? MsgType.Emote : MsgType.Text, "body": body, }; - const contentBody = { + const contentBody: IContent = { msgtype: newContent.msgtype, body: `${plainPrefix} * ${body}`, }; @@ -105,55 +109,60 @@ function createEditContent(model, editedEvent) { }, contentBody); } +interface IProps { + editState: EditorStateTransfer; + className?: string; +} + +interface IState { + saveDisabled: boolean; +} + @replaceableComponent("views.rooms.EditMessageComposer") -export default class EditMessageComposer extends React.Component { - static propTypes = { - // the message event being edited - editState: PropTypes.instanceOf(EditorStateTransfer).isRequired, - }; - +export default class EditMessageComposer extends React.Component { static contextType = MatrixClientContext; + context!: React.ContextType; - constructor(props, context) { - super(props, context); - this.model = null; - this._editorRef = null; + private readonly editorRef = createRef(); + private readonly dispatcherRef: string; + private model: EditorModel = null; + + constructor(props: IProps, context: React.ContextType) { + super(props); + this.context = context; // otherwise React will only set it prior to render due to type def above this.state = { saveDisabled: true, }; - this._createEditorModel(); - window.addEventListener("beforeunload", this._saveStoredEditorState); + + this.createEditorModel(); + window.addEventListener("beforeunload", this.saveStoredEditorState); this.dispatcherRef = dis.register(this.onAction); } - _setEditorRef = ref => { - this._editorRef = ref; - }; - - _getRoom() { + private getRoom(): Room { return this.context.getRoom(this.props.editState.getEvent().getRoomId()); } - _onKeyDown = (event) => { + private onKeyDown = (event: KeyboardEvent): void => { // ignore any keypress while doing IME compositions - if (this._editorRef.isComposing(event)) { + if (this.editorRef.current?.isComposing(event)) { return; } const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.Send: - this._sendEdit(); + this.sendEdit(); event.preventDefault(); break; case MessageComposerAction.CancelEditing: - this._cancelEdit(); + this.cancelEdit(); break; case MessageComposerAction.EditPrevMessage: { - if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) { + if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) { return; } - const previousEvent = findEditableEvent(this._getRoom(), false, + const previousEvent = findEditableEvent(this.getRoom(), false, this.props.editState.getEvent().getId()); if (previousEvent) { dis.dispatch({ action: 'edit_event', event: previousEvent }); @@ -162,14 +171,14 @@ export default class EditMessageComposer extends React.Component { break; } case MessageComposerAction.EditNextMessage: { - if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) { + if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) { return; } - const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId()); + const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId()); if (nextEvent) { dis.dispatch({ action: 'edit_event', event: nextEvent }); } else { - this._clearStoredEditorState(); + this.clearStoredEditorState(); dis.dispatch({ action: 'edit_event', event: null }); dis.fire(Action.FocusComposer); } @@ -177,32 +186,32 @@ export default class EditMessageComposer extends React.Component { break; } } + }; + + private get editorRoomKey(): string { + return `mx_edit_room_${this.getRoom().roomId}`; } - get _editorRoomKey() { - return `mx_edit_room_${this._getRoom().roomId}`; - } - - get _editorStateKey() { + private get editorStateKey(): string { return `mx_edit_state_${this.props.editState.getEvent().getId()}`; } - _cancelEdit = () => { - this._clearStoredEditorState(); + private cancelEdit = (): void => { + this.clearStoredEditorState(); dis.dispatch({ action: "edit_event", event: null }); dis.fire(Action.FocusComposer); + }; + + private get shouldSaveStoredEditorState(): boolean { + return localStorage.getItem(this.editorRoomKey) !== null; } - get _shouldSaveStoredEditorState() { - return localStorage.getItem(this._editorRoomKey) !== null; - } - - _restoreStoredEditorState(partCreator) { - const json = localStorage.getItem(this._editorStateKey); + private restoreStoredEditorState(partCreator: PartCreator): Part[] { + const json = localStorage.getItem(this.editorStateKey); if (json) { try { const { parts: serializedParts } = JSON.parse(json); - const parts = serializedParts.map(p => partCreator.deserializePart(p)); + const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p)); return parts; } catch (e) { console.error("Error parsing editing state: ", e); @@ -210,25 +219,25 @@ export default class EditMessageComposer extends React.Component { } } - _clearStoredEditorState() { - localStorage.removeItem(this._editorRoomKey); - localStorage.removeItem(this._editorStateKey); + private clearStoredEditorState(): void { + localStorage.removeItem(this.editorRoomKey); + localStorage.removeItem(this.editorStateKey); } - _clearPreviousEdit() { - if (localStorage.getItem(this._editorRoomKey)) { - localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`); + private clearPreviousEdit(): void { + if (localStorage.getItem(this.editorRoomKey)) { + localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this.editorRoomKey)}`); } } - _saveStoredEditorState() { + private saveStoredEditorState(): void { const item = SendHistoryManager.createItem(this.model); - this._clearPreviousEdit(); - localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId()); - localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + this.clearPreviousEdit(); + localStorage.setItem(this.editorRoomKey, this.props.editState.getEvent().getId()); + localStorage.setItem(this.editorStateKey, JSON.stringify(item)); } - _isSlashCommand() { + private isSlashCommand(): boolean { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { @@ -244,10 +253,10 @@ export default class EditMessageComposer extends React.Component { return false; } - _isContentModified(newContent) { + private isContentModified(newContent: IContent): boolean { // if nothing has changed then bail const oldContent = this.props.editState.getEvent().getContent(); - if (!this._editorRef.isModified() || + if (!this.editorRef.current?.isModified() || (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] && oldContent["format"] === newContent["format"] && oldContent["formatted_body"] === newContent["formatted_body"])) { @@ -256,7 +265,7 @@ export default class EditMessageComposer extends React.Component { return true; } - _getSlashCommand() { + private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -268,7 +277,7 @@ export default class EditMessageComposer extends React.Component { return [cmd, args, commandText]; } - async _runSlashCommand(cmd, args, roomId) { + private async runSlashCommand(cmd: Command, args: string, roomId: string): Promise { const result = cmd.run(roomId, args); let messageContent; let error = result.error; @@ -285,7 +294,6 @@ export default class EditMessageComposer extends React.Component { } if (error) { console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // assume the error is a server error when the command is async const isServerError = !!result.promise; const title = isServerError ? _td("Server error") : _td("Command error"); @@ -309,7 +317,7 @@ export default class EditMessageComposer extends React.Component { } } - _sendEdit = async () => { + private sendEdit = async (): Promise => { const startTime = CountlyAnalytics.getTimestamp(); const editedEvent = this.props.editState.getEvent(); const editContent = createEditContent(this.model, editedEvent); @@ -318,20 +326,19 @@ export default class EditMessageComposer extends React.Component { let shouldSend = true; // If content is modified then send an updated event into the room - if (this._isContentModified(newContent)) { + if (this.isContentModified(newContent)) { const roomId = editedEvent.getRoomId(); - if (!containsEmote(this.model) && this._isSlashCommand()) { - const [cmd, args, commandText] = this._getSlashCommand(); + if (!containsEmote(this.model) && this.isSlashCommand()) { + const [cmd, args, commandText] = this.getSlashCommand(); if (cmd) { if (cmd.category === CommandCategories.messages) { - editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId); + editContent["m.new_content"] = await this.runSlashCommand(cmd, args, roomId); } else { - this._runSlashCommand(cmd, args, roomId); + this.runSlashCommand(cmd, args, roomId); shouldSend = false; } } else { // ask the user if their unknown command should be sent as a message - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), description:
@@ -358,9 +365,9 @@ export default class EditMessageComposer extends React.Component { } } if (shouldSend) { - this._cancelPreviousPendingEdit(); + this.cancelPreviousPendingEdit(); const prom = this.context.sendMessage(roomId, editContent); - this._clearStoredEditorState(); + this.clearStoredEditorState(); dis.dispatch({ action: "message_sent" }); CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); } @@ -371,7 +378,7 @@ export default class EditMessageComposer extends React.Component { dis.fire(Action.FocusComposer); }; - _cancelPreviousPendingEdit() { + private cancelPreviousPendingEdit(): void { const originalEvent = this.props.editState.getEvent(); const previousEdit = originalEvent.replacingEvent(); if (previousEdit && ( @@ -389,23 +396,23 @@ export default class EditMessageComposer extends React.Component { const sel = document.getSelection(); let caret; if (sel.focusNode) { - caret = getCaretOffsetAndText(this._editorRef, sel).caret; + caret = getCaretOffsetAndText(this.editorRef.current?.editorRef.current, sel).caret; } const parts = this.model.serializeParts(); // if caret is undefined because for some reason there isn't a valid selection, // then when mounting the editor again with the same editor state, // it will set the cursor at the end. this.props.editState.setEditorState(caret, parts); - window.removeEventListener("beforeunload", this._saveStoredEditorState); - if (this._shouldSaveStoredEditorState) { - this._saveStoredEditorState(); + window.removeEventListener("beforeunload", this.saveStoredEditorState); + if (this.shouldSaveStoredEditorState) { + this.saveStoredEditorState(); } dis.unregister(this.dispatcherRef); } - _createEditorModel() { + private createEditorModel(): void { const { editState } = this.props; - const room = this._getRoom(); + const room = this.getRoom(); const partCreator = new CommandPartCreator(room, this.context); let parts; if (editState.hasEditorState()) { @@ -414,13 +421,13 @@ export default class EditMessageComposer extends React.Component { parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); } else { //otherwise, either restore serialized parts from localStorage or parse the body of the event - parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); + parts = this.restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); } this.model = new EditorModel(parts, partCreator); - this._saveStoredEditorState(); + this.saveStoredEditorState(); } - _getInitialCaretPosition() { + private getInitialCaretPosition(): CaretPosition { const { editState } = this.props; let caretPosition; if (editState.hasEditorState() && editState.getCaret()) { @@ -435,8 +442,8 @@ export default class EditMessageComposer extends React.Component { return caretPosition; } - _onChange = () => { - if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) { + private onChange = (): void => { + if (!this.state.saveDisabled || !this.editorRef.current?.isModified()) { return; } @@ -445,33 +452,34 @@ export default class EditMessageComposer extends React.Component { }); }; - onAction = payload => { - if (payload.action === "edit_composer_insert" && this._editorRef) { + private onAction = (payload: ActionPayload) => { + if (payload.action === "edit_composer_insert" && this.editorRef.current) { if (payload.userId) { - this._editorRef.insertMention(payload.userId); + this.editorRef.current?.insertMention(payload.userId); } else if (payload.event) { - this._editorRef.insertQuotedMessage(payload.event); + this.editorRef.current?.insertQuotedMessage(payload.event); } else if (payload.text) { - this._editorRef.insertPlaintext(payload.text); + this.editorRef.current?.insertPlaintext(payload.text); } } }; render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return (
+ return (
- {_t("Cancel")} - - {_t("Save")} + + { _t("Cancel") } + + + { _t("Save") }
); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 277f3ccb7c..c9d1040433 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -267,7 +267,7 @@ interface IProps { showReactions?: boolean; // which layout to use - layout: Layout; + layout?: Layout; // whether or not to show flair at all enableFlair?: boolean; @@ -287,10 +287,10 @@ interface IProps { permalinkCreator?: RoomPermalinkCreator; // Symbol of the root node - as?: string + as?: string; // whether or not to always show timestamps - alwaysShowTimestamps?: boolean + alwaysShowTimestamps?: boolean; } interface IState { @@ -321,6 +321,7 @@ export default class EventTile extends React.Component { static defaultProps = { // no-op function because onHeightChanged is optional yet some sub-components assume its existence onHeightChanged: function() {}, + layout: Layout.Group, }; static contextType = MatrixClientContext; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 68f87580df..f4df70c7ee 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher/dispatcher'; import { isValid3pidInvite } from "../../../RoomInvite"; -import rateLimitedFunction from "../../../ratelimitedfunc"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import BaseCard from "../right_panel/BaseCard"; @@ -43,6 +42,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import EntityTile from "./EntityTile"; import MemberTile from "./MemberTile"; import BaseAvatar from '../avatars/BaseAvatar'; +import { throttle } from 'lodash'; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -133,7 +133,7 @@ export default class MemberList extends React.Component { } // cancel any pending calls to the rate_limited_funcs - this.updateList.cancelPendingCall(); + this.updateList.cancel(); } /** @@ -237,9 +237,9 @@ export default class MemberList extends React.Component { if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); }; - private updateList = rateLimitedFunction(() => { + private updateList = throttle(() => { this.updateListNow(); - }, 500); + }, 500, { leading: true, trailing: true }); private updateListNow(): void { const members = this.roomMembers(); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index db57f98025..7d61ba5ec6 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -43,6 +43,7 @@ import { E2EStatus } from '../../../utils/ShieldUtils'; import SendMessageComposer from "./SendMessageComposer"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; +import EditorModel from "../../../editor/model"; interface IComposerAvatarProps { me: object; @@ -318,14 +319,14 @@ export default class MessageComposer extends React.Component { } }; - addEmoji(emoji: string) { + private addEmoji(emoji: string) { dis.dispatch({ action: Action.ComposerInsert, text: emoji, }); } - sendMessage = async () => { + private sendMessage = async () => { if (this.state.haveRecording && this.voiceRecordingButton) { // There shouldn't be any text message to send when a voice recording is active, so // just send out the voice recording. @@ -333,11 +334,10 @@ export default class MessageComposer extends React.Component { return; } - // XXX: Private function access - this.messageComposerInput._sendMessage(); + this.messageComposerInput.sendMessage(); }; - onChange = (model) => { + private onChange = (model: EditorModel) => { this.setState({ isComposerEmpty: model.isEmpty, }); diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.tsx similarity index 55% rename from src/components/views/rooms/MessageComposerFormatBar.js rename to src/components/views/rooms/MessageComposerFormatBar.tsx index c31538c6cd..75bca8aac7 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,21 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +import React, { createRef } from 'react'; import classNames from 'classnames'; + +import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.rooms.MessageComposerFormatBar") -export default class MessageComposerFormatBar extends React.PureComponent { - static propTypes = { - onAction: PropTypes.func.isRequired, - shortcuts: PropTypes.object.isRequired, - }; +export enum Formatting { + Bold = "bold", + Italics = "italics", + Strikethrough = "strikethrough", + Code = "code", + Quote = "quote", +} - constructor(props) { +interface IProps { + shortcuts: Partial>; + onAction(action: Formatting): void; +} + +interface IState { + visible: boolean; +} + +@replaceableComponent("views.rooms.MessageComposerFormatBar") +export default class MessageComposerFormatBar extends React.PureComponent { + private readonly formatBarRef = createRef(); + + constructor(props: IProps) { super(props); this.state = { visible: false }; } @@ -37,49 +51,53 @@ export default class MessageComposerFormatBar extends React.PureComponent { const classes = classNames("mx_MessageComposerFormatBar", { "mx_MessageComposerFormatBar_shown": this.state.visible, }); - return (
this._formatBarRef = ref}> - this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} /> - this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} /> - this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} /> - this.props.onAction("code")} icon="Code" visible={this.state.visible} /> - this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} /> + return (
+ this.props.onAction(Formatting.Bold)} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} /> + this.props.onAction(Formatting.Italics)} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} /> + this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} /> + this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} /> + this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
); } - showAt(selectionRect) { + public showAt(selectionRect: DOMRect): void { + if (!this.formatBarRef.current) return; + this.setState({ visible: true }); - const parentRect = this._formatBarRef.parentElement.getBoundingClientRect(); - this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`; + const parentRect = this.formatBarRef.current.parentElement.getBoundingClientRect(); + this.formatBarRef.current.style.left = `${selectionRect.left - parentRect.left}px`; // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. - this._formatBarRef.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`; + this.formatBarRef.current.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`; } - hide() { + public hide(): void { this.setState({ visible: false }); } } -class FormatButton extends React.PureComponent { - static propTypes = { - label: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - icon: PropTypes.string.isRequired, - shortcut: PropTypes.string, - visible: PropTypes.bool, - }; +interface IFormatButtonProps { + label: string; + icon: string; + shortcut?: string; + visible?: boolean; + onClick(): void; +} +class FormatButton extends React.PureComponent { render() { const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; let shortcut; if (this.props.shortcut) { - shortcut =
{this.props.shortcut}
; + shortcut =
+ { this.props.shortcut } +
; } const tooltip =
- {this.props.label} + { this.props.label }
- {shortcut} + { shortcut }
; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.tsx similarity index 84% rename from src/components/views/rooms/RoomHeader.js rename to src/components/views/rooms/RoomHeader.tsx index 886317f2bf..af5daed5bc 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.tsx @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,11 +16,9 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; @@ -31,54 +29,65 @@ import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { throttle } from 'lodash'; +import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src'; +import { E2EStatus } from '../../../utils/ShieldUtils'; +import { IOOBData } from '../../../stores/ThreepidInviteStore'; +import { SearchScope } from './SearchBar'; + +export interface ISearchInfo { + searchTerm: string; + searchScope: SearchScope; + searchCount: number; +} + +interface IProps { + room: Room; + oobData?: IOOBData; + inRoom: boolean; + onSettingsClick: () => void; + onSearchClick: () => void; + onForgetClick: () => void; + onCallPlaced: (type: PlaceCallType) => void; + onAppsClick: () => void; + e2eStatus: E2EStatus; + appsShown: boolean; + searchInfo: ISearchInfo; +} @replaceableComponent("views.rooms.RoomHeader") -export default class RoomHeader extends React.Component { - static propTypes = { - room: PropTypes.object, - oobData: PropTypes.object, - inRoom: PropTypes.bool, - onSettingsClick: PropTypes.func, - onSearchClick: PropTypes.func, - onLeaveClick: PropTypes.func, - e2eStatus: PropTypes.string, - onAppsClick: PropTypes.func, - appsShown: PropTypes.bool, - onCallPlaced: PropTypes.func, // (PlaceCallType) => void; - }; - +export default class RoomHeader extends React.Component { static defaultProps = { editing: false, inRoom: false, }; - componentDidMount() { + public componentDidMount() { const cli = MatrixClientPeg.get(); - cli.on("RoomState.events", this._onRoomStateEvents); + cli.on("RoomState.events", this.onRoomStateEvents); } - componentWillUnmount() { + public componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener("RoomState.events", this._onRoomStateEvents); + cli.removeListener("RoomState.events", this.onRoomStateEvents); } } - _onRoomStateEvents = (event, state) => { + private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } // redisplay the room name, topic, etc. - this._rateLimitedUpdate(); + this.rateLimitedUpdate(); }; - _rateLimitedUpdate = new RateLimitedFunc(function() { - /* eslint-disable @babel/no-invalid-this */ + private rateLimitedUpdate = throttle(() => { this.forceUpdate(); - }, 500); + }, 500, { leading: true, trailing: true }); - render() { + public render() { let searchStatus = null; // don't display the search count until the search completes and diff --git a/src/components/views/rooms/RoomListNumResults.tsx b/src/components/views/rooms/RoomListNumResults.tsx index a05db89f61..95c8c6590f 100644 --- a/src/components/views/rooms/RoomListNumResults.tsx +++ b/src/components/views/rooms/RoomListNumResults.tsx @@ -22,7 +22,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter"; import SpaceStore from "../../../stores/SpaceStore"; interface IProps { - onVisibilityChange?: () => void + onVisibilityChange?: () => void; } const RoomListNumResults: React.FC = ({ onVisibilityChange }) => { diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.tsx similarity index 76% rename from src/components/views/rooms/SearchResultTile.js rename to src/components/views/rooms/SearchResultTile.tsx index 3581a26351..980e8835f8 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -16,31 +16,28 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import { haveTileForEvent } from "./EventTile"; +import { SearchResult } from "matrix-js-sdk/src/models/search-result"; +import EventTile, { haveTileForEvent } from "./EventTile"; +import DateSeparator from '../messages/DateSeparator'; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; +import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + // a matrix-js-sdk SearchResult containing the details of this result + searchResult: SearchResult; + // a list of strings to be highlighted in the results + searchHighlights?: string[]; + // href for the highlights in this result + resultLink?: string; + onHeightChanged?: () => void; + permalinkCreator?: RoomPermalinkCreator; +} + @replaceableComponent("views.rooms.SearchResultTile") -export default class SearchResultTile extends React.Component { - static propTypes = { - // a matrix-js-sdk SearchResult containing the details of this result - searchResult: PropTypes.object.isRequired, - - // a list of strings to be highlighted in the results - searchHighlights: PropTypes.array, - - // href for the highlights in this result - resultLink: PropTypes.string, - - onHeightChanged: PropTypes.func, - }; - - render() { - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - const EventTile = sdk.getComponent('rooms.EventTile'); +export default class SearchResultTile extends React.Component { + public render() { const result = this.props.searchResult; const mxEv = result.context.getEvent(); const eventId = mxEv.getId(); diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.tsx similarity index 78% rename from src/components/views/rooms/SendMessageComposer.js rename to src/components/views/rooms/SendMessageComposer.tsx index cc819e05e1..ec190c829a 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,8 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; + +import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react'; +import EMOJI_REGEX from 'emojibase-regex'; +import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; + import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { @@ -27,29 +29,36 @@ import { startsWith, stripPrefix, } from '../../../editor/serialize'; -import { CommandPartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; import { findEditableEvent } from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import { CommandCategories, getCommand } from '../../../SlashCommands'; -import * as sdk from '../../../index'; +import { Command, CommandCategories, getCommand } from '../../../SlashCommands'; import Modal from '../../../Modal'; import { _t, _td } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RateLimitedFunc from '../../../ratelimitedfunc'; import { Action } from "../../../dispatcher/actions"; import { containsEmoji } from "../../../effects/utils"; import { CHAT_EFFECTS } from '../../../effects'; import CountlyAnalytics from "../../../CountlyAnalytics"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import EMOJI_REGEX from 'emojibase-regex'; import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from '../../../settings/SettingsStore'; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { Room } from 'matrix-js-sdk/src/models/room'; +import ErrorDialog from "../dialogs/ErrorDialog"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import { ActionPayload } from "../../../dispatcher/payloads"; +import { DebouncedFunc, throttle } from 'lodash'; -function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { +function addReplyToMessageContent( + content: IContent, + repliedToEvent: MatrixEvent, + permalinkCreator: RoomPermalinkCreator, +): void { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); Object.assign(content, replyContent); @@ -65,7 +74,11 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } // exported for tests -export function createMessageContent(model, permalinkCreator, replyToEvent) { +export function createMessageContent( + model: EditorModel, + permalinkCreator: RoomPermalinkCreator, + replyToEvent: MatrixEvent, +): IContent { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -76,7 +89,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) { model = unescapeMessage(model); const body = textSerialize(model); - const content = { + const content: IContent = { msgtype: isEmote ? "m.emote" : "m.text", body: body, }; @@ -94,7 +107,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) { } // exported for tests -export function isQuickReaction(model) { +export function isQuickReaction(model: EditorModel): boolean { const parts = model.parts; if (parts.length == 0) return false; const text = textSerialize(model); @@ -111,46 +124,48 @@ export function isQuickReaction(model) { return false; } +interface IProps { + room: Room; + placeholder?: string; + permalinkCreator: RoomPermalinkCreator; + replyToEvent?: MatrixEvent; + disabled?: boolean; + onChange?(model: EditorModel): void; +} + @replaceableComponent("views.rooms.SendMessageComposer") -export default class SendMessageComposer extends React.Component { - static propTypes = { - room: PropTypes.object.isRequired, - placeholder: PropTypes.string, - permalinkCreator: PropTypes.object.isRequired, - replyToEvent: PropTypes.object, - onChange: PropTypes.func, - disabled: PropTypes.bool, - }; - +export default class SendMessageComposer extends React.Component { static contextType = MatrixClientContext; + context!: React.ContextType; - constructor(props, context) { - super(props, context); - this.model = null; - this._editorRef = null; - this.currentlyComposedEditorState = null; + private readonly prepareToEncrypt?: DebouncedFunc<() => void>; + private readonly editorRef = createRef(); + private model: EditorModel = null; + private currentlyComposedEditorState: SerializedPart[] = null; + private dispatcherRef: string; + private sendHistoryManager: SendHistoryManager; + + constructor(props: IProps, context: React.ContextType) { + super(props); + this.context = context; // otherwise React will only set it prior to render due to type def above if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) { - this._prepareToEncrypt = new RateLimitedFunc(() => { + this.prepareToEncrypt = throttle(() => { this.context.prepareToEncrypt(this.props.room); - }, 60000); + }, 60000, { leading: true, trailing: false }); } - window.addEventListener("beforeunload", this._saveStoredEditorState); + window.addEventListener("beforeunload", this.saveStoredEditorState); } - _setEditorRef = ref => { - this._editorRef = ref; - }; - - _onKeyDown = (event) => { + private onKeyDown = (event: KeyboardEvent): void => { // ignore any keypress while doing IME compositions - if (this._editorRef.isComposing(event)) { + if (this.editorRef.current?.isComposing(event)) { return; } const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.Send: - this._sendMessage(); + this.sendMessage(); event.preventDefault(); break; case MessageComposerAction.SelectPrevSendHistory: @@ -165,7 +180,7 @@ export default class SendMessageComposer extends React.Component { } case MessageComposerAction.EditPrevMessage: // selection must be collapsed and caret at start - if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) { + if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) { const editEvent = findEditableEvent(this.props.room, false); if (editEvent) { // We're selecting history, so prevent the key event from doing anything else @@ -184,29 +199,29 @@ export default class SendMessageComposer extends React.Component { }); break; default: - if (this._prepareToEncrypt) { + if (this.prepareToEncrypt) { // This needs to be last! - this._prepareToEncrypt(); + this.prepareToEncrypt(); } } }; // we keep sent messages/commands in a separate history (separate from undo history) // so you can alt+up/down in them - selectSendHistory(up) { + private selectSendHistory(up: boolean): boolean { const delta = up ? -1 : 1; // True if we are not currently selecting history, but composing a message if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { // We can't go any further - there isn't any more history, so nop. if (!up) { - return; + return false; } this.currentlyComposedEditorState = this.model.serializeParts(); } else if (this.sendHistoryManager.currentIndex + delta === this.sendHistoryManager.history.length) { // True when we return to the message being composed currently this.model.reset(this.currentlyComposedEditorState); this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length; - return; + return true; } const { parts, replyEventId } = this.sendHistoryManager.getItem(delta); dis.dispatch({ @@ -215,11 +230,12 @@ export default class SendMessageComposer extends React.Component { }); if (parts) { this.model.reset(parts); - this._editorRef.focus(); + this.editorRef.current?.focus(); } + return true; } - _isSlashCommand() { + private isSlashCommand(): boolean { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { @@ -237,7 +253,7 @@ export default class SendMessageComposer extends React.Component { return false; } - _sendQuickReaction() { + private sendQuickReaction(): void { const timeline = this.props.room.getLiveTimeline(); const events = timeline.getEvents(); const reaction = this.model.parts[1].text; @@ -272,7 +288,7 @@ export default class SendMessageComposer extends React.Component { } } - _getSlashCommand() { + private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -284,7 +300,7 @@ export default class SendMessageComposer extends React.Component { return [cmd, args, commandText]; } - async _runSlashCommand(cmd, args) { + private async runSlashCommand(cmd: Command, args: string): Promise { const result = cmd.run(this.props.room.roomId, args); let messageContent; let error = result.error; @@ -302,7 +318,6 @@ export default class SendMessageComposer extends React.Component { } if (error) { console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // assume the error is a server error when the command is async const isServerError = !!result.promise; const title = isServerError ? _td("Server error") : _td("Command error"); @@ -326,7 +341,7 @@ export default class SendMessageComposer extends React.Component { } } - async _sendMessage() { + public async sendMessage(): Promise { if (this.model.isEmpty) { return; } @@ -335,21 +350,20 @@ export default class SendMessageComposer extends React.Component { let shouldSend = true; let content; - if (!containsEmote(this.model) && this._isSlashCommand()) { - const [cmd, args, commandText] = this._getSlashCommand(); + if (!containsEmote(this.model) && this.isSlashCommand()) { + const [cmd, args, commandText] = this.getSlashCommand(); if (cmd) { if (cmd.category === CommandCategories.messages) { - content = await this._runSlashCommand(cmd, args); + content = await this.runSlashCommand(cmd, args); if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); } } else { - this._runSlashCommand(cmd, args); + this.runSlashCommand(cmd, args); shouldSend = false; } } else { // ask the user if their unknown command should be sent as a message - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), description:
@@ -378,7 +392,7 @@ export default class SendMessageComposer extends React.Component { if (isQuickReaction(this.model)) { shouldSend = false; - this._sendQuickReaction(); + this.sendQuickReaction(); } if (shouldSend) { @@ -411,9 +425,9 @@ export default class SendMessageComposer extends React.Component { this.sendHistoryManager.save(this.model, replyToEvent); // clear composer this.model.reset([]); - this._editorRef.clearUndoHistory(); - this._editorRef.focus(); - this._clearStoredEditorState(); + this.editorRef.current?.clearUndoHistory(); + this.editorRef.current?.focus(); + this.clearStoredEditorState(); if (SettingsStore.getValue("scrollToBottomOnMessageSent")) { dis.dispatch({ action: "scroll_to_bottom" }); } @@ -421,33 +435,33 @@ export default class SendMessageComposer extends React.Component { componentWillUnmount() { dis.unregister(this.dispatcherRef); - window.removeEventListener("beforeunload", this._saveStoredEditorState); - this._saveStoredEditorState(); + window.removeEventListener("beforeunload", this.saveStoredEditorState); + this.saveStoredEditorState(); } // TODO: [REACT-WARNING] Move this to constructor UNSAFE_componentWillMount() { // eslint-disable-line camelcase const partCreator = new CommandPartCreator(this.props.room, this.context); - const parts = this._restoreStoredEditorState(partCreator) || []; + const parts = this.restoreStoredEditorState(partCreator) || []; this.model = new EditorModel(parts, partCreator); this.dispatcherRef = dis.register(this.onAction); this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_'); } - get _editorStateKey() { + private get editorStateKey() { return `mx_cider_state_${this.props.room.roomId}`; } - _clearStoredEditorState() { - localStorage.removeItem(this._editorStateKey); + private clearStoredEditorState(): void { + localStorage.removeItem(this.editorStateKey); } - _restoreStoredEditorState(partCreator) { - const json = localStorage.getItem(this._editorStateKey); + private restoreStoredEditorState(partCreator: PartCreator): Part[] { + const json = localStorage.getItem(this.editorStateKey); if (json) { try { const { parts: serializedParts, replyEventId } = JSON.parse(json); - const parts = serializedParts.map(p => partCreator.deserializePart(p)); + const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p)); if (replyEventId) { dis.dispatch({ action: 'reply_to_event', @@ -462,20 +476,20 @@ export default class SendMessageComposer extends React.Component { } // should save state when editor has contents or reply is open - _shouldSaveStoredEditorState = () => { - return !this.model.isEmpty || this.props.replyToEvent; - } + private shouldSaveStoredEditorState = (): boolean => { + return !this.model.isEmpty || !!this.props.replyToEvent; + }; - _saveStoredEditorState = () => { - if (this._shouldSaveStoredEditorState()) { + private saveStoredEditorState = (): void => { + if (this.shouldSaveStoredEditorState()) { const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent); - localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + localStorage.setItem(this.editorStateKey, JSON.stringify(item)); } else { - this._clearStoredEditorState(); + this.clearStoredEditorState(); } - } + }; - onAction = (payload) => { + private onAction = (payload: ActionPayload): void => { // don't let the user into the composer if it is disabled - all of these branches lead // to the cursor being in the composer if (this.props.disabled) return; @@ -483,21 +497,21 @@ export default class SendMessageComposer extends React.Component { switch (payload.action) { case 'reply_to_event': case Action.FocusComposer: - this._editorRef && this._editorRef.focus(); + this.editorRef.current?.focus(); break; case "send_composer_insert": if (payload.userId) { - this._editorRef && this._editorRef.insertMention(payload.userId); + this.editorRef.current?.insertMention(payload.userId); } else if (payload.event) { - this._editorRef && this._editorRef.insertQuotedMessage(payload.event); + this.editorRef.current?.insertQuotedMessage(payload.event); } else if (payload.text) { - this._editorRef && this._editorRef.insertPlaintext(payload.text); + this.editorRef.current?.insertPlaintext(payload.text); } break; } }; - _onPaste = (event) => { + private onPaste = (event: ClipboardEvent): boolean => { const { clipboardData } = event; // Prioritize text on the clipboard over files as Office on macOS puts a bitmap // in the clipboard as well as the content being copied. @@ -511,23 +525,27 @@ export default class SendMessageComposer extends React.Component { ); return true; // to skip internal onPaste handler } - } + }; - onChange = () => { + private onChange = (): void => { if (this.props.onChange) this.props.onChange(this.model); - } + }; + + private focusComposer = (): void => { + this.editorRef.current?.focus(); + }; render() { return ( -
+
diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx index 0876f07142..1858412dac 100644 --- a/src/components/views/settings/SpellCheckSettings.tsx +++ b/src/components/views/settings/SpellCheckSettings.tsx @@ -21,17 +21,17 @@ import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface ExistingSpellCheckLanguageIProps { - language: string, - onRemoved(language: string), + language: string; + onRemoved(language: string); } interface SpellCheckLanguagesIProps { - languages: Array, - onLanguagesChange(languages: Array), + languages: Array; + onLanguagesChange(languages: Array); } interface SpellCheckLanguagesIState { - newLanguage: string, + newLanguage: string; } export class ExistingSpellCheckLanguage extends React.Component { diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index f6b98eedec..6d2cc1f5db 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -23,7 +23,7 @@ import Field from "../elements/Field"; interface IProps { avatarUrl?: string; avatarDisabled?: boolean; - name?: string, + name?: string; nameDisabled?: boolean; topic?: string; topicDisabled?: boolean; diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 8f6b552334..75254d7c62 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; -import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; @@ -30,6 +29,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import VerificationRequestDialog from "../dialogs/VerificationRequestDialog"; interface IProps { toastKey: string; @@ -123,7 +123,6 @@ export default class VerificationRequestToast extends React.PureComponent { diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index 272d8a06a3..a2ab760c86 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -20,7 +20,7 @@ import { logger } from 'matrix-js-sdk/src/logger'; import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler"; interface IProps { - feed: CallFeed, + feed: CallFeed; } export default class AudioFeed extends React.Component { diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index c522116e0a..dd0e8cb138 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -35,10 +35,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // The call for us to display - call: MatrixCall, + call: MatrixCall; // Another ongoing call to display information about - secondaryCall?: MatrixCall, + secondaryCall?: MatrixCall; // a callback which is called when the content in the CallView changes // in a way that is likely to cause a resize. @@ -52,15 +52,15 @@ interface IProps { } interface IState { - isLocalOnHold: boolean, - isRemoteOnHold: boolean, - micMuted: boolean, - vidMuted: boolean, - callState: CallState, - controlsVisible: boolean, - showMoreMenu: boolean, - showDialpad: boolean, - feeds: CallFeed[], + isLocalOnHold: boolean; + isRemoteOnHold: boolean; + micMuted: boolean; + vidMuted: boolean; + callState: CallState; + controlsVisible: boolean; + showMoreMenu: boolean; + showDialpad: boolean; + feeds: CallFeed[]; } function getFullScreenElement() { diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index 9557fe0e7e..a5aa3e7734 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -25,16 +25,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // What room we should display the call for - roomId: string, + roomId: string; // maxHeight style attribute for the video panel maxVideoHeight?: number; - resizeNotifier: ResizeNotifier, + resizeNotifier: ResizeNotifier; } interface IState { - call: MatrixCall, + call: MatrixCall; } /* diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 2f88abe6fb..e5461eb1b4 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -24,9 +24,9 @@ import MemberAvatar from "../avatars/MemberAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { - call: MatrixCall, + call: MatrixCall; - feed: CallFeed, + feed: CallFeed; // Whether this call view is for picture-in-picture mode // otherwise, it's the larger call view when viewing the room the call is in. @@ -36,7 +36,7 @@ interface IProps { // a callback which is called when the video element is resized // due to a change in video metadata - onResize?: (e: Event) => void, + onResize?: (e: Event) => void; } interface IState { diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts index e215c5cb24..c2262e5f71 100644 --- a/src/customisations/Security.ts +++ b/src/customisations/Security.ts @@ -69,11 +69,11 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean { export interface ISecurityCustomisations { examineLoginResponse?: typeof examineLoginResponse; persistCredentials?: typeof persistCredentials; - createSecretStorageKey?: typeof createSecretStorageKey, - getSecretStorageKey?: typeof getSecretStorageKey, - catchAccessSecretStorageError?: typeof catchAccessSecretStorageError, - setupEncryptionNeeded?: typeof setupEncryptionNeeded, - getDehydrationKey?: typeof getDehydrationKey, + createSecretStorageKey?: typeof createSecretStorageKey; + getSecretStorageKey?: typeof getSecretStorageKey; + catchAccessSecretStorageError?: typeof catchAccessSecretStorageError; + setupEncryptionNeeded?: typeof setupEncryptionNeeded; + getDehydrationKey?: typeof getDehydrationKey; /** * When false, disables the post-login UI from showing. If there's @@ -83,7 +83,7 @@ export interface ISecurityCustomisations { * encryption is set up some other way which would circumvent the default * UI, such as by presenting alternative UI. */ - SHOW_ENCRYPTION_SETUP_UI?: boolean, // default true + SHOW_ENCRYPTION_SETUP_UI?: boolean; // default true } // A real customisation module will define and export one or more of the diff --git a/src/dispatcher/payloads/ComposerInsertPayload.ts b/src/dispatcher/payloads/ComposerInsertPayload.ts index 9702855432..ea5d8a0c53 100644 --- a/src/dispatcher/payloads/ComposerInsertPayload.ts +++ b/src/dispatcher/payloads/ComposerInsertPayload.ts @@ -20,7 +20,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; interface IBaseComposerInsertPayload extends ActionPayload { - action: Action.ComposerInsert, + action: Action.ComposerInsert; } interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload { diff --git a/src/editor/model.ts b/src/editor/model.ts index 1e8498a69e..da1c2f47f5 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -70,7 +70,7 @@ export default class EditorModel { * on the model that can span multiple parts. Also see `startRange()`. * @param {TransformCallback} transformCallback */ - setTransformCallback(transformCallback: TransformCallback) { + public setTransformCallback(transformCallback: TransformCallback): void { this.transformCallback = transformCallback; } @@ -78,23 +78,23 @@ export default class EditorModel { * Set a callback for rerendering the model after it has been updated. * @param {ModelCallback} updateCallback */ - setUpdateCallback(updateCallback: UpdateCallback) { + public setUpdateCallback(updateCallback: UpdateCallback): void { this.updateCallback = updateCallback; } - get partCreator() { + public get partCreator(): PartCreator { return this._partCreator; } - get isEmpty() { + public get isEmpty(): boolean { return this._parts.reduce((len, part) => len + part.text.length, 0) === 0; } - clone() { + public clone(): EditorModel { return new EditorModel(this._parts, this._partCreator, this.updateCallback); } - private insertPart(index: number, part: Part) { + private insertPart(index: number, part: Part): void { this._parts.splice(index, 0, part); if (this.activePartIdx >= index) { ++this.activePartIdx; @@ -104,7 +104,7 @@ export default class EditorModel { } } - private removePart(index: number) { + private removePart(index: number): void { this._parts.splice(index, 1); if (index === this.activePartIdx) { this.activePartIdx = null; @@ -118,22 +118,22 @@ export default class EditorModel { } } - private replacePart(index: number, part: Part) { + private replacePart(index: number, part: Part): void { this._parts.splice(index, 1, part); } - get parts() { + public get parts(): Part[] { return this._parts; } - get autoComplete() { + public get autoComplete(): AutocompleteWrapperModel { if (this.activePartIdx === this.autoCompletePartIdx) { return this._autoComplete; } return null; } - getPositionAtEnd() { + public getPositionAtEnd(): DocumentPosition { if (this._parts.length) { const index = this._parts.length - 1; const part = this._parts[index]; @@ -144,11 +144,11 @@ export default class EditorModel { } } - serializeParts() { + public serializeParts(): SerializedPart[] { return this._parts.map(p => p.serialize()); } - private diff(newValue: string, inputType: string, caret: DocumentOffset) { + private diff(newValue: string, inputType: string, caret: DocumentOffset): IDiff { const previousValue = this.parts.reduce((text, p) => text + p.text, ""); // can't use caret position with drag and drop if (inputType === "deleteByDrag") { @@ -158,7 +158,7 @@ export default class EditorModel { } } - reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string) { + public reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string): void { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); if (!caret) { caret = this.getPositionAtEnd(); @@ -180,7 +180,7 @@ export default class EditorModel { * @param {DocumentPosition} position the position to start inserting at * @return {Number} the amount of characters added */ - insert(parts: Part[], position: IPosition) { + public insert(parts: Part[], position: IPosition): number { const insertIndex = this.splitAt(position); let newTextLength = 0; for (let i = 0; i < parts.length; ++i) { @@ -191,7 +191,7 @@ export default class EditorModel { return newTextLength; } - update(newValue: string, inputType: string, caret: DocumentOffset) { + public update(newValue: string, inputType: string, caret: DocumentOffset): Promise { const diff = this.diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; @@ -220,7 +220,7 @@ export default class EditorModel { return Number.isFinite(result) ? result as number : 0; } - private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean) { + private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean): Promise { const { index } = pos; const part = this._parts[index]; if (part) { @@ -250,7 +250,7 @@ export default class EditorModel { return Promise.resolve(); } - private onAutoComplete = ({ replaceParts, close }: ICallback) => { + private onAutoComplete = ({ replaceParts, close }: ICallback): void => { let pos; if (replaceParts) { this._parts.splice(this.autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts); @@ -270,7 +270,7 @@ export default class EditorModel { this.updateCallback(pos); }; - private mergeAdjacentParts() { + private mergeAdjacentParts(): void { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -294,7 +294,7 @@ export default class EditorModel { * @return {Number} how many characters before pos were also removed, * usually because of non-editable parts that can only be removed in their entirety. */ - removeText(pos: IPosition, len: number) { + public removeText(pos: IPosition, len: number): number { let { index, offset } = pos; let removedOffsetDecrease = 0; while (len > 0) { @@ -329,7 +329,7 @@ export default class EditorModel { } // return part index where insertion will insert between at offset - private splitAt(pos: IPosition) { + private splitAt(pos: IPosition): number { if (pos.index === -1) { return 0; } @@ -356,7 +356,7 @@ export default class EditorModel { * @return {Number} how far from position (in characters) the insertion ended. * This can be more than the length of `str` when crossing non-editable parts, which are skipped. */ - private addText(pos: IPosition, str: string, inputType: string) { + private addText(pos: IPosition, str: string, inputType: string): number { let { index } = pos; const { offset } = pos; let addLen = str.length; @@ -390,7 +390,7 @@ export default class EditorModel { return addLen; } - positionForOffset(totalOffset: number, atPartEnd = false) { + public positionForOffset(totalOffset: number, atPartEnd = false): DocumentPosition { let currentOffset = 0; const index = this._parts.findIndex(part => { const partLen = part.text.length; @@ -416,11 +416,11 @@ export default class EditorModel { * @param {DocumentPosition?} positionB the other boundary of the range, optional * @return {Range} */ - startRange(positionA: DocumentPosition, positionB = positionA) { + public startRange(positionA: DocumentPosition, positionB = positionA): Range { return new Range(this, positionA, positionB); } - replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]) { + public replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]): void { // convert end position to offset, so it is independent of how the document is split into parts // which we'll change when splitting up at the start position const endOffset = endPosition.asOffset(this); @@ -445,9 +445,9 @@ export default class EditorModel { * @param {ManualTransformCallback} callback to run the transformations in * @return {Promise} a promise when auto-complete (if applicable) is done updating */ - transform(callback: ManualTransformCallback) { + public transform(callback: ManualTransformCallback): Promise { const pos = callback(); - let acPromise = null; + let acPromise: Promise = null; if (!(pos instanceof Range)) { acPromise = this.setActivePart(pos, true); } else { diff --git a/src/editor/parts.ts b/src/editor/parts.ts index c16a95dbc9..351df5062f 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -552,7 +552,7 @@ export class PartCreator { // part creator that support auto complete for /commands, // used in SendMessageComposer export class CommandPartCreator extends PartCreator { - createPartForInput(text: string, partIndex: number) { + public createPartForInput(text: string, partIndex: number): Part { // at beginning and starts with /? create if (partIndex === 0 && text[0] === "/") { // text will be inserted by model, so pass empty string @@ -562,11 +562,11 @@ export class CommandPartCreator extends PartCreator { } } - command(text: string) { + public command(text: string): CommandPart { return new CommandPart(text, this.autoCompleteCreator); } - deserializePart(part: Part): Part { + public deserializePart(part: SerializedPart): Part { if (part.type === "command") { return this.command(part.text); } else { @@ -576,7 +576,7 @@ export class CommandPartCreator extends PartCreator { } class CommandPart extends PillCandidatePart { - get type(): IPillCandidatePart["type"] { + public get type(): IPillCandidatePart["type"] { return Type.Command; } } diff --git a/src/effects/confetti/index.ts b/src/effects/confetti/index.ts index 53e5dda5d2..ae2bb822c2 100644 --- a/src/effects/confetti/index.ts +++ b/src/effects/confetti/index.ts @@ -20,34 +20,34 @@ export type ConfettiOptions = { /** * max confetti count */ - maxCount: number, + maxCount: number; /** * particle animation speed */ - speed: number, + speed: number; /** * the confetti animation frame interval in milliseconds */ - frameInterval: number, + frameInterval: number; /** * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible) */ - alpha: number, + alpha: number; /** * use gradient instead of solid particle color */ - gradient: boolean, + gradient: boolean; }; type ConfettiParticle = { - color: string, - color2: string, - x: number, - y: number, - diameter: number, - tilt: number, - tiltAngleIncrement: number, - tiltAngle: number, + color: string; + color2: string; + x: number; + y: number; + diameter: number; + tilt: number; + tiltAngleIncrement: number; + tiltAngle: number; }; export const DefaultOptions: ConfettiOptions = { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6f06e3d6a4..618d5763fa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2342,15 +2342,6 @@ "Message edits": "Message edits", "Modal Widget": "Modal Widget", "Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s", - "Your account is not secure": "Your account is not secure", - "Your password": "Your password", - "This session, or the other session": "This session, or the other session", - "The internet connection either session is using": "The internet connection either session is using", - "We recommend you change your password and Security Key in Settings immediately": "We recommend you change your password and Security Key in Settings immediately", - "New session": "New session", - "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", - "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", - "This wasn't me": "This wasn't me", "Doesn't look like a valid email address": "Doesn't look like a valid email address", "Continuing without email": "Continuing without email", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", diff --git a/src/performance/index.ts b/src/performance/index.ts index 1e24839370..cb808f9173 100644 --- a/src/performance/index.ts +++ b/src/performance/index.ts @@ -17,15 +17,15 @@ limitations under the License. import { PerformanceEntryNames } from "./entry-names"; interface GetEntriesOptions { - name?: string, - type?: string, + name?: string; + type?: string; } type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void; interface PerformanceDataListener { - entryNames?: string[], - callback: PerformanceCallbackFunction + entryNames?: string[]; + callback: PerformanceCallbackFunction; } export default class PerformanceMonitor { diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js deleted file mode 100644 index 3df3db615e..0000000000 --- a/src/ratelimitedfunc.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/** - * 'debounces' a function to only execute every n milliseconds. - * Useful when react-sdk gets many, many events but only wants - * to update the interface once for all of them. - * - * Note that the function must not take arguments, since the args - * could be different for each invocation of the function. - * - * The returned function has a 'cancelPendingCall' property which can be called - * on unmount or similar to cancel any pending update. - */ - -import {throttle} from "lodash"; - -export default function ratelimitedfunc(fn, time) { - const throttledFn = throttle(fn, time, { - leading: true, - trailing: true, - }); - const _bind = throttledFn.bind; - throttledFn.bind = function() { - const boundFn = _bind.apply(throttledFn, arguments); - boundFn.cancelPendingCall = throttledFn.cancelPendingCall; - return boundFn; - }; - - throttledFn.cancelPendingCall = function() { - throttledFn.cancel(); - }; - return throttledFn; -} diff --git a/src/stores/ThreepidInviteStore.ts b/src/stores/ThreepidInviteStore.ts index 74a5f5f8ec..d0cf40941c 100644 --- a/src/stores/ThreepidInviteStore.ts +++ b/src/stores/ThreepidInviteStore.ts @@ -45,6 +45,16 @@ export interface IThreepidInvite { inviterName: string; } +// Any data about the room that would normally come from the homeserver +// but has been passed out-of-band, eg. the room name and avatar URL +// from an email invite (a workaround for the fact that we can't +// get this information from the HS using an email invite). +export interface IOOBData { + name?: string; // The room's name + avatarUrl?: string; // The mxc:// avatar URL for the room + inviterName?: string; // The display name of the person who invited us to the room +} + const STORAGE_PREFIX = "mx_threepid_invite_"; export default class ThreepidInviteStore extends EventEmitter { diff --git a/src/stores/TypingStore.ts b/src/stores/TypingStore.ts index 447f41c7ae..9781c93eb4 100644 --- a/src/stores/TypingStore.ts +++ b/src/stores/TypingStore.ts @@ -27,10 +27,10 @@ const TYPING_SERVER_TIMEOUT = 30000; export default class TypingStore { private typingStates: { [roomId: string]: { - isTyping: boolean, - userTimer: Timer, - serverTimer: Timer, - }, + isTyping: boolean; + userTimer: Timer; + serverTimer: Timer; + }; }; constructor() { diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts index 0b0be50541..d3ef9df023 100644 --- a/src/stores/WidgetEchoStore.ts +++ b/src/stores/WidgetEchoStore.ts @@ -26,8 +26,8 @@ import { WidgetType } from "../widgets/WidgetType"; class WidgetEchoStore extends EventEmitter { private roomWidgetEcho: { [roomId: string]: { - [widgetId: string]: IWidget, - }, + [widgetId: string]: IWidget; + }; }; constructor() { diff --git a/src/utils/EditorStateTransfer.ts b/src/utils/EditorStateTransfer.ts index ba303f9b73..d2ce58f7dc 100644 --- a/src/utils/EditorStateTransfer.ts +++ b/src/utils/EditorStateTransfer.ts @@ -17,7 +17,7 @@ limitations under the License. import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SerializedPart } from "../editor/parts"; -import { Caret } from "../editor/caret"; +import DocumentOffset from "../editor/offset"; /** * Used while editing, to pass the event, and to preserve editor state @@ -26,28 +26,28 @@ import { Caret } from "../editor/caret"; */ export default class EditorStateTransfer { private serializedParts: SerializedPart[] = null; - private caret: Caret = null; + private caret: DocumentOffset = null; constructor(private readonly event: MatrixEvent) {} - public setEditorState(caret: Caret, serializedParts: SerializedPart[]) { + public setEditorState(caret: DocumentOffset, serializedParts: SerializedPart[]) { this.caret = caret; this.serializedParts = serializedParts; } - public hasEditorState() { + public hasEditorState(): boolean { return !!this.serializedParts; } - public getSerializedParts() { + public getSerializedParts(): SerializedPart[] { return this.serializedParts; } - public getCaret() { + public getCaret(): DocumentOffset { return this.caret; } - public getEvent() { + public getEvent(): MatrixEvent { return this.event; } } diff --git a/src/utils/objects.ts b/src/utils/objects.ts index 561e68e8c5..c2ee6ce100 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -152,7 +152,7 @@ export function objectClone(obj: O): O { export function objectFromEntries(entries: Iterable<[K, V]>): {[k: K]: V} { const obj: { // @ts-ignore - same as return type - [k: K]: V} = {}; + [k: K]: V;} = {}; for (const e of entries) { // @ts-ignore - same as return type obj[e[0]] = e[1]; diff --git a/src/utils/pillify.js b/src/utils/pillify.tsx similarity index 91% rename from src/utils/pillify.js rename to src/utils/pillify.tsx index 489ba5d504..22240fcda5 100644 --- a/src/utils/pillify.js +++ b/src/utils/pillify.tsx @@ -16,9 +16,11 @@ limitations under the License. import React from "react"; import ReactDOM from 'react-dom'; +import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import { MatrixClientPeg } from '../MatrixClientPeg'; import SettingsStore from "../settings/SettingsStore"; -import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; import Pill from "../components/views/elements/Pill"; import { parseAppLocalLink } from "./permalinks/Permalinks"; @@ -27,15 +29,15 @@ import { parseAppLocalLink } from "./permalinks/Permalinks"; * into pills based on the context of a given room. Returns a list of * the resulting React nodes so they can be unmounted rather than leaking. * - * @param {Node[]} nodes - a list of sibling DOM nodes to traverse to try + * @param {Element[]} nodes - a list of sibling DOM nodes to traverse to try * to turn into pills. * @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are * part of representing. - * @param {Node[]} pills: an accumulator of the DOM nodes which contain + * @param {Element[]} pills: an accumulator of the DOM nodes which contain * React components which have been mounted as part of this. * The initial caller should pass in an empty array to seed the accumulator. */ -export function pillifyLinks(nodes, mxEvent, pills) { +export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pills: Element[]) { const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); let node = nodes[0]; @@ -73,7 +75,7 @@ export function pillifyLinks(nodes, mxEvent, pills) { // to clear the pills from the last run of pillifyLinks !node.parentElement.classList.contains("mx_AtRoomPill") ) { - let currentTextNode = node; + let currentTextNode = node as Node as Text; const roomNotifTextNodes = []; // Take a textNode and break it up to make all the instances of @room their @@ -125,10 +127,10 @@ export function pillifyLinks(nodes, mxEvent, pills) { } if (node.childNodes && node.childNodes.length && !pillified) { - pillifyLinks(node.childNodes, mxEvent, pills); + pillifyLinks(node.childNodes as NodeListOf, mxEvent, pills); } - node = node.nextSibling; + node = node.nextSibling as Element; } } @@ -140,10 +142,10 @@ export function pillifyLinks(nodes, mxEvent, pills) { * emitter on BaseAvatar as per * https://github.com/vector-im/element-web/issues/12417 * - * @param {Node[]} pills - array of pill containers whose React + * @param {Element[]} pills - array of pill containers whose React * components should be unmounted. */ -export function unmountPills(pills) { +export function unmountPills(pills: Element[]) { for (const pillContainer of pills) { ReactDOM.unmountComponentAtNode(pillContainer); } diff --git a/test/KeyBindingsManager-test.ts b/test/KeyBindingsManager-test.ts index 694efac7b5..eab1bea2b0 100644 --- a/test/KeyBindingsManager-test.ts +++ b/test/KeyBindingsManager-test.ts @@ -17,10 +17,10 @@ limitations under the License. import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager'; function mockKeyEvent(key: string, modifiers?: { - ctrlKey?: boolean, - altKey?: boolean, - shiftKey?: boolean, - metaKey?: boolean + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; }): KeyboardEvent { return { key, diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index d32970a278..f415b85105 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,11 +26,11 @@ import { EventEmitter } from "events"; import sdk from '../../skinned-sdk'; const MessagePanel = sdk.getComponent('structures.MessagePanel'); -import {MatrixClientPeg} from '../../../src/MatrixClientPeg'; +import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; import Matrix from 'matrix-js-sdk'; -const test_utils = require('../../test-utils'); -const mockclock = require('../../mock-clock'); +const TestUtilsMatrix = require('../../test-utils'); +import FakeTimers from '@sinonjs/fake-timers'; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import { configure, mount } from "enzyme"; @@ -72,14 +72,14 @@ class WrappedMessagePanel extends React.Component { } describe('MessagePanel', function() { - const clock = mockclock.clock(); + let clock = null; const realSetTimeout = window.setTimeout; const events = mkEvents(); beforeEach(function() { - test_utils.stubClient(); + TestUtilsMatrix.stubClient(); client = MatrixClientPeg.get(); - client.credentials = {userId: '@me:here'}; + client.credentials = { userId: '@me:here' }; // HACK: We assume all settings want to be disabled SettingsStore.getValue = jest.fn((arg) => { @@ -90,14 +90,17 @@ describe('MessagePanel', function() { }); afterEach(function() { - clock.uninstall(); + if (clock) { + clock.uninstall(); + clock = null; + } }); function mkEvents() { const events = []; const ts0 = Date.now(); for (let i = 0; i < 10; i++) { - events.push(test_utils.mkMessage( + events.push(TestUtilsMatrix.mkMessage( { event: true, room: "!room:id", user: "@user:id", ts: ts0 + i * 1000, @@ -111,7 +114,7 @@ describe('MessagePanel', function() { const events = []; const ts0 = Date.parse('09 May 2004 00:12:00 GMT'); for (let i = 0; i < 10; i++) { - events.push(test_utils.mkMessage( + events.push(TestUtilsMatrix.mkMessage( { event: true, room: "!room:id", user: "@user:id", ts: ts0 + i * 1000, @@ -120,7 +123,6 @@ describe('MessagePanel', function() { return events; } - // make a collection of events with some member events that should be collapsed // with a MemberEventListSummary function mkMelsEvents() { @@ -128,13 +130,13 @@ describe('MessagePanel', function() { const ts0 = Date.now(); let i = 0; - events.push(test_utils.mkMessage({ + events.push(TestUtilsMatrix.mkMessage({ event: true, room: "!room:id", user: "@user:id", ts: ts0 + ++i * 1000, })); for (i = 0; i < 10; i++) { - events.push(test_utils.mkMembership({ + events.push(TestUtilsMatrix.mkMembership({ event: true, room: "!room:id", user: "@user:id", target: { userId: "@user:id", @@ -151,7 +153,7 @@ describe('MessagePanel', function() { })); } - events.push(test_utils.mkMessage({ + events.push(TestUtilsMatrix.mkMessage({ event: true, room: "!room:id", user: "@user:id", ts: ts0 + ++i*1000, })); @@ -167,7 +169,7 @@ describe('MessagePanel', function() { let i = 0; for (i = 0; i < 10; i++) { - events.push(test_utils.mkMembership({ + events.push(TestUtilsMatrix.mkMembership({ event: true, room: "!room:id", user: "@user:id", target: { userId: "@user:id", @@ -189,8 +191,8 @@ describe('MessagePanel', function() { // A list of room creation, encryption, and invite events. function mkCreationEvents() { - const mkEvent = test_utils.mkEvent; - const mkMembership = test_utils.mkMembership; + const mkEvent = TestUtilsMatrix.mkEvent; + const mkMembership = TestUtilsMatrix.mkMembership; const roomId = "!someroom"; const alice = "@alice:example.org"; const ts0 = Date.now(); @@ -363,8 +365,7 @@ describe('MessagePanel', function() { it('shows a ghost read-marker when the read-marker moves', function(done) { // fake the clock so that we can test the velocity animation. - clock.install(); - clock.mockDate(); + clock = FakeTimers.install(); const parentDiv = document.createElement('div'); diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index fa44fc8d92..86fa562b60 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -20,10 +20,10 @@ import ReactTestUtils from 'react-dom/test-utils'; import MatrixReactTestUtils from 'matrix-react-test-utils'; import sdk from '../../../skinned-sdk'; -import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; +import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import * as test_utils from '../../../test-utils'; -import {sleep} from "../../../../src/utils/promise"; +import * as TestUtilsMatrix from '../../../test-utils'; +import { sleep } from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -33,7 +33,7 @@ describe('InteractiveAuthDialog', function() { let parentDiv; beforeEach(function() { - test_utils.stubClient(); + TestUtilsMatrix.stubClient(); parentDiv = document.createElement('div'); document.body.appendChild(parentDiv); }); @@ -45,11 +45,11 @@ describe('InteractiveAuthDialog', function() { it('Should successfully complete a password flow', function() { const onFinished = jest.fn(); - const doRequest = jest.fn().mockResolvedValue({a: 1}); + const doRequest = jest.fn().mockResolvedValue({ a: 1 }); // tell the stub matrixclient to return a real userid const client = MatrixClientPeg.get(); - client.credentials = {userId: "@user:id"}; + client.credentials = { userId: "@user:id" }; const dlg = ReactDOM.render( { expect(onFinished).toBeCalledTimes(1); - expect(onFinished).toBeCalledWith(true, {a: 1}); + expect(onFinished).toBeCalledWith(true, { a: 1 }); }); }); }); diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js index 2fddf8b691..2947f0fe60 100644 --- a/test/components/views/rooms/SendMessageComposer-test.js +++ b/test/components/views/rooms/SendMessageComposer-test.js @@ -147,7 +147,7 @@ describe('', () => { wrapper.update(); }); - const key = wrapper.find(SendMessageComposer).instance()._editorStateKey; + const key = wrapper.find(SendMessageComposer).instance().editorStateKey; expect(wrapper.text()).toBe("Test Text"); expect(localStorage.getItem(key)).toBeNull(); @@ -188,7 +188,7 @@ describe('', () => { wrapper.update(); }); - const key = wrapper.find(SendMessageComposer).instance()._editorStateKey; + const key = wrapper.find(SendMessageComposer).instance().editorStateKey; expect(wrapper.text()).toBe("Hello World"); expect(localStorage.getItem(key)).toBeNull(); diff --git a/test/mock-clock.js b/test/mock-clock.js deleted file mode 100644 index 1a4d6086de..0000000000 --- a/test/mock-clock.js +++ /dev/null @@ -1,421 +0,0 @@ -/* -Copyright (c) 2008-2015 Pivotal Labs -Copyright 2019 The Matrix.org Foundation C.I.C. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -/* This is jasmine's implementation of a mock clock, lifted from the depths of - * jasmine-core and exposed as a standalone module. The interface is just the - * same as that of jasmine.clock. For example: - * - * var mock_clock = require("../../mock-clock").clock(); - * mock_clock.install(); - * setTimeout(function() { - * timerCallback(); - * }, 100); - * - * expect(timerCallback).not.toHaveBeenCalled(); - * mock_clock.tick(101); - * expect(timerCallback).toHaveBeenCalled(); - * - * mock_clock.uninstall(); - * - * - * The reason for C&Ping jasmine's clock here is that jasmine itself is - * difficult to webpack, and we don't really want all of it. Sinon also has a - * mock-clock implementation, but again, it is difficult to webpack. - */ - -const j$ = {}; - -j$.Clock = function() { - function Clock(global, delayedFunctionSchedulerFactory, mockDate) { - let self = this, - realTimingFunctions = { - setTimeout: global.setTimeout, - clearTimeout: global.clearTimeout, - setInterval: global.setInterval, - clearInterval: global.clearInterval, - }, - fakeTimingFunctions = { - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval, - }, - installed = false, - delayedFunctionScheduler, - timer; - - - self.install = function() { - if(!originalTimingFunctionsIntact()) { - throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); - } - replace(global, fakeTimingFunctions); - timer = fakeTimingFunctions; - delayedFunctionScheduler = delayedFunctionSchedulerFactory(); - installed = true; - - return self; - }; - - self.uninstall = function() { - delayedFunctionScheduler = null; - mockDate.uninstall(); - replace(global, realTimingFunctions); - - timer = realTimingFunctions; - installed = false; - }; - - self.withMock = function(closure) { - this.install(); - try { - closure(); - } finally { - this.uninstall(); - } - }; - - self.mockDate = function(initialDate) { - mockDate.install(initialDate); - }; - - self.setTimeout = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill'); - } - return timer.setTimeout(fn, delay); - } - return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]); - }; - - self.setInterval = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill'); - } - return timer.setInterval(fn, delay); - } - return Function.prototype.apply.apply(timer.setInterval, [global, arguments]); - }; - - self.clearTimeout = function(id) { - return Function.prototype.call.apply(timer.clearTimeout, [global, id]); - }; - - self.clearInterval = function(id) { - return Function.prototype.call.apply(timer.clearInterval, [global, id]); - }; - - self.tick = function(millis) { - if (installed) { - mockDate.tick(millis); - delayedFunctionScheduler.tick(millis); - } else { - throw new Error('Mock clock is not installed, use jasmine.clock().install()'); - } - }; - - return self; - - function originalTimingFunctionsIntact() { - return global.setTimeout === realTimingFunctions.setTimeout && - global.clearTimeout === realTimingFunctions.clearTimeout && - global.setInterval === realTimingFunctions.setInterval && - global.clearInterval === realTimingFunctions.clearInterval; - } - - function legacyIE() { - //if these methods are polyfilled, apply will be present - return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; - } - - function replace(dest, source) { - for (const prop in source) { - dest[prop] = source[prop]; - } - } - - function setTimeout(fn, delay) { - return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); - } - - function clearTimeout(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function setInterval(fn, interval) { - return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); - } - - function clearInterval(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function argSlice(argsObj, n) { - return Array.prototype.slice.call(argsObj, n); - } - } - - return Clock; -}(); - - -j$.DelayedFunctionScheduler = function() { - function DelayedFunctionScheduler() { - const self = this; - const scheduledLookup = []; - const scheduledFunctions = {}; - let currentTime = 0; - let delayedFnCount = 0; - - self.tick = function(millis) { - millis = millis || 0; - const endTime = currentTime + millis; - - runScheduledFunctions(endTime); - currentTime = endTime; - }; - - self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) { - let f; - if (typeof(funcToCall) === 'string') { - /* jshint evil: true */ - f = function() { return eval(funcToCall); }; - /* jshint evil: false */ - } else { - f = funcToCall; - } - - millis = millis || 0; - timeoutKey = timeoutKey || ++delayedFnCount; - runAtMillis = runAtMillis || (currentTime + millis); - - const funcToSchedule = { - runAtMillis: runAtMillis, - funcToCall: f, - recurring: recurring, - params: params, - timeoutKey: timeoutKey, - millis: millis, - }; - - if (runAtMillis in scheduledFunctions) { - scheduledFunctions[runAtMillis].push(funcToSchedule); - } else { - scheduledFunctions[runAtMillis] = [funcToSchedule]; - scheduledLookup.push(runAtMillis); - scheduledLookup.sort(function(a, b) { - return a - b; - }); - } - - return timeoutKey; - }; - - self.removeFunctionWithId = function(timeoutKey) { - for (const runAtMillis in scheduledFunctions) { - const funcs = scheduledFunctions[runAtMillis]; - const i = indexOfFirstToPass(funcs, function(func) { - return func.timeoutKey === timeoutKey; - }); - - if (i > -1) { - if (funcs.length === 1) { - delete scheduledFunctions[runAtMillis]; - deleteFromLookup(runAtMillis); - } else { - funcs.splice(i, 1); - } - - // intervals get rescheduled when executed, so there's never more - // than a single scheduled function with a given timeoutKey - break; - } - } - }; - - return self; - - function indexOfFirstToPass(array, testFn) { - let index = -1; - - for (let i = 0; i < array.length; ++i) { - if (testFn(array[i])) { - index = i; - break; - } - } - - return index; - } - - function deleteFromLookup(key) { - const value = Number(key); - const i = indexOfFirstToPass(scheduledLookup, function(millis) { - return millis === value; - }); - - if (i > -1) { - scheduledLookup.splice(i, 1); - } - } - - function reschedule(scheduledFn) { - self.scheduleFunction(scheduledFn.funcToCall, - scheduledFn.millis, - scheduledFn.params, - true, - scheduledFn.timeoutKey, - scheduledFn.runAtMillis + scheduledFn.millis); - } - - function forEachFunction(funcsToRun, callback) { - for (let i = 0; i < funcsToRun.length; ++i) { - callback(funcsToRun[i]); - } - } - - function runScheduledFunctions(endTime) { - if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { - return; - } - - do { - currentTime = scheduledLookup.shift(); - - const funcsToRun = scheduledFunctions[currentTime]; - delete scheduledFunctions[currentTime]; - - forEachFunction(funcsToRun, function(funcToRun) { - if (funcToRun.recurring) { - reschedule(funcToRun); - } - }); - - forEachFunction(funcsToRun, function(funcToRun) { - funcToRun.funcToCall.apply(null, funcToRun.params || []); - }); - } while (scheduledLookup.length > 0 && - // checking first if we're out of time prevents setTimeout(0) - // scheduled in a funcToRun from forcing an extra iteration - currentTime !== endTime && - scheduledLookup[0] <= endTime); - } - } - - return DelayedFunctionScheduler; -}(); - - -j$.MockDate = function() { - function MockDate(global) { - const self = this; - let currentTime = 0; - - if (!global || !global.Date) { - self.install = function() {}; - self.tick = function() {}; - self.uninstall = function() {}; - return self; - } - - const GlobalDate = global.Date; - - self.install = function(mockDate) { - if (mockDate instanceof GlobalDate) { - currentTime = mockDate.getTime(); - } else { - currentTime = new GlobalDate().getTime(); - } - - global.Date = FakeDate; - }; - - self.tick = function(millis) { - millis = millis || 0; - currentTime = currentTime + millis; - }; - - self.uninstall = function() { - currentTime = 0; - global.Date = GlobalDate; - }; - - createDateProperties(); - - return self; - - function FakeDate() { - switch(arguments.length) { - case 0: - return new GlobalDate(currentTime); - case 1: - return new GlobalDate(arguments[0]); - case 2: - return new GlobalDate(arguments[0], arguments[1]); - case 3: - return new GlobalDate(arguments[0], arguments[1], arguments[2]); - case 4: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]); - case 5: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4]); - case 6: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5]); - default: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5], arguments[6]); - } - } - - function createDateProperties() { - FakeDate.prototype = GlobalDate.prototype; - - FakeDate.now = function() { - if (GlobalDate.now) { - return currentTime; - } else { - throw new Error('Browser does not support Date.now()'); - } - }; - - FakeDate.toSource = GlobalDate.toSource; - FakeDate.toString = GlobalDate.toString; - FakeDate.parse = GlobalDate.parse; - FakeDate.UTC = GlobalDate.UTC; - } - } - - return MockDate; -}(); - -const _clock = new j$.Clock(global, function() { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global)); - -export function clock() { - return _clock; -} - - diff --git a/yarn.lock b/yarn.lock index 89e11fcea5..c8c3315855 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1478,6 +1478,11 @@ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf" integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw== +"@types/commonmark@^0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@types/commonmark/-/commonmark-0.27.4.tgz#8f42990e5cf3b6b95bd99eaa452e157aab679b82" + integrity sha512-7koSjp08QxKoS1/+3T15+kD7+vqOUvZRHvM8PutF3Xsk5aAEkdlIGRsHJ3/XsC3izoqTwBdRW/vH7rzCKkIicA== + "@types/counterpart@^0.18.1": version "0.18.1" resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" @@ -2190,6 +2195,11 @@ bluebird@^3.5.0: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +blurhash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e" + integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -2345,9 +2355,9 @@ camelcase@^6.0.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001173: - version "1.0.30001178" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001178.tgz#3ad813b2b2c7d585b0be0a2440e1e233c6eabdbc" - integrity sha512-VtdZLC0vsXykKni8Uztx45xynytOi71Ufx9T8kHptSw9AL4dpqailUJJHavttuzUe1KYuBYtChiWv+BAb7mPmQ== + version "1.0.30001241" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001241.tgz" + integrity sha512-1uoSZ1Pq1VpH0WerIMqwptXHNNGfdl7d1cJUFs80CwQ/lVzdhTvsFZCeNFslze7AjsQnb4C85tzclPa1VShbeQ== capture-exit@^2.0.0: version "2.0.0" @@ -3222,7 +3232,7 @@ eslint-config-google@^0.14.0: "eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main": version "0.3.2" - resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/28d392822533a7468be0dd806d0a4ba573a45d74" + resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/e8197938dca66849ffdac4baca7c05275df12835" eslint-plugin-react-hooks@^4.2.0: version "4.2.0"