From 53db386731422c88d4e24e299e9e56a150dad3cf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 10 Aug 2020 22:06:30 -0600 Subject: [PATCH 01/29] Add support for blurhash (MSC2448) MSC: https://github.com/matrix-org/matrix-doc/pull/2448 While the image loads, we can show a blurred version of it (calculated at upload time) so we don't have a blank space in the timeline. --- package.json | 1 + src/ContentMessages.tsx | 10 +++- .../views/elements/BlurhashPlaceholder.tsx | 56 +++++++++++++++++++ src/components/views/messages/MImageBody.js | 18 +++--- yarn.lock | 5 ++ 5 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 src/components/views/elements/BlurhashPlaceholder.tsx diff --git a/package.json b/package.json index 548b33f353..989672d414 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@babel/runtime": "^7.10.5", "await-lock": "^2.0.1", "blueimp-canvas-to-blob": "^3.27.0", + "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "classnames": "^2.2.6", diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 6f55a75d0c..7e57b34ff7 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -334,6 +334,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo if (file.type) { encryptInfo.mimetype = file.type; } + // TODO: Blurhash for encrypted media? return {"file": encryptInfo}; }); (prom as IAbortablePromise).abort = () => { @@ -344,11 +345,15 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo } else { const basePromise = matrixClient.uploadContent(file, { progressHandler: progressHandler, + onlyContentUri: false, }); - const promise1 = basePromise.then(function(url) { + const promise1 = basePromise.then(function(body) { if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. - return {"url": url}; + return { + "url": body.content_uri, + "blurhash": body["xyz.amorgan.blurhash"], // TODO: Use `body.blurhash` when MSC2448 lands + }; }); promise1.abort = () => { canceled = true; @@ -550,6 +555,7 @@ export default class ContentMessages { return upload.promise.then(function(result) { content.file = result.file; content.url = result.url; + content.info['xyz.amorgan.blurhash'] = result.blurhash; // TODO: Use `blurhash` when MSC2448 lands }); }).then(() => { // Await previous message being sent into the room diff --git a/src/components/views/elements/BlurhashPlaceholder.tsx b/src/components/views/elements/BlurhashPlaceholder.tsx new file mode 100644 index 0000000000..bed45bfe5a --- /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/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index c92ae475bf..e0ac24c641 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -27,6 +27,7 @@ import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import InlineSpinner from '../elements/InlineSpinner'; +import BlurhashPlaceholder from "../elements/BlurhashPlaceholder"; export default class MImageBody extends React.Component { static propTypes = { @@ -53,6 +54,8 @@ export default class MImageBody extends React.Component { this.onClick = this.onClick.bind(this); this._isGif = this._isGif.bind(this); + const imageInfo = this.props.mxEvent.getContent().info; + this.state = { decryptedUrl: null, decryptedThumbnailUrl: null, @@ -63,6 +66,7 @@ export default class MImageBody extends React.Component { loadedImageDimensions: null, hover: false, showImage: SettingsStore.getValue("showImages"), + blurhash: imageInfo ? imageInfo['xyz.amorgan.blurhash'] : null, // TODO: Use `blurhash` when MSC2448 lands. }; this._image = createRef(); @@ -329,7 +333,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,8 +373,7 @@ export default class MImageBody extends React.Component { 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(); + placeholder = this.getPlaceholder(maxWidth, maxHeight); } let showPlaceholder = Boolean(placeholder); @@ -391,7 +395,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) { @@ -433,9 +437,9 @@ 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) { + if (!this.state.blurhash) return null; + return ; } // Overidden by MStickerBody diff --git a/yarn.lock b/yarn.lock index 98fe42ef13..f1cace67a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2493,6 +2493,11 @@ blueimp-canvas-to-blob@^3.27.0: resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.27.0.tgz#a2bd5c43587b95dedf0f6998603452d1bfcc9b9e" integrity sha512-AcIj+hCw6WquxzJuzC6KzgYmqxLFeTWe88KuY2BEIsW1zbEOfoinDAGlhyvFNGt+U3JElkVSK7anA1FaSdmmfA== +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== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: version "4.11.9" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" From 00d093b4ffb83e4d100b706757e0e44a8f1fb376 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 08:56:14 +0100 Subject: [PATCH 02/29] yarn lock --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index 19c0646d32..57b1198019 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2180,6 +2180,11 @@ blueimp-canvas-to-blob@^3.28.0: resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.28.0.tgz#c8ab4dc6bb08774a7f273798cdf94b0776adf6c8" integrity sha512-5q+YHzgGsuHQ01iouGgJaPJXod2AzTxJXmVv90PpGrRxU7G7IqgPqWXz+PBmt3520jKKi6irWbNV87DicEa7wg== +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" From c21445c406fd077e17fd17661590739069a82995 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 13:23:17 +0100 Subject: [PATCH 03/29] switch from MatrixClientPeg in ContentMessages for consistency --- src/ContentMessages.tsx | 19 +++++++++---------- src/components/structures/UploadBar.tsx | 5 ++++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 65b6f1aba4..ba84b30733 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -18,7 +18,6 @@ limitations under the License. import React from "react"; import dis from './dispatcher/dispatcher'; -import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClient} from "matrix-js-sdk/src/client"; import * as sdk from './index'; import { _t } from './languageHandler'; @@ -344,7 +343,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo }); (prom as IAbortablePromise).abort = () => { canceled = true; - if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); + if (uploadPromise) matrixClient.cancelUpload(uploadPromise); }; return prom; } else { @@ -362,7 +361,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo }); promise1.abort = () => { canceled = true; - MatrixClientPeg.get().cancelUpload(basePromise); + matrixClient.cancelUpload(basePromise); }; return promise1; } @@ -372,9 +371,9 @@ export default class ContentMessages { private inprogress: IUpload[] = []; private mediaConfig: IMediaConfig = null; - sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) { + sendStickerContentToRoom(url: string, roomId: string, info: object, 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; }); @@ -416,7 +415,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(); } @@ -471,7 +470,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) { @@ -481,7 +480,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}); } } @@ -623,11 +622,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/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index e19e312f58..269c615698 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() { From ba7604fd44679d020335160ed5f30f20f294184b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 13:24:19 +0100 Subject: [PATCH 04/29] fix types around sending stickers --- src/components/structures/RoomView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c0f3c59457..1595bc6c53 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1196,7 +1196,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; From 563e6433b9eec30ec404abed1cbfcf2bb2279119 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 15:47:11 +0100 Subject: [PATCH 05/29] Don't store blurhash in state, its immutable and pointless --- src/components/views/messages/MImageBody.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 14043b95f3..34320f1b31 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -60,7 +60,6 @@ export default class MImageBody extends React.Component { this.onClick = this.onClick.bind(this); this._isGif = this._isGif.bind(this); - const imageInfo = this.props.mxEvent.getContent().info; this.state = { decryptedUrl: null, @@ -72,7 +71,6 @@ export default class MImageBody extends React.Component { loadedImageDimensions: null, hover: false, showImage: SettingsStore.getValue("showImages"), - blurhash: imageInfo ? imageInfo['xyz.amorgan.blurhash'] : null, // TODO: Use `blurhash` when MSC2448 lands. }; this._image = createRef(); @@ -442,8 +440,9 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getPlaceholder(width, height) { - if (!this.state.blurhash) return null; - return ; + const blurhash = this.props.mxEvent.getContent().info['xyz.amorgan.blurhash']; + if (!blurhash) return null; + return ; } // Overidden by MStickerBody From 8368b864406453d35c3d47b096a2febd9a83273c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 14:52:27 +0100 Subject: [PATCH 06/29] Wire up local blurhash encoding --- src/ContentMessages.tsx | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index ba84b30733..9d9709a04d 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -17,6 +17,8 @@ limitations under the License. */ import React from "react"; +import { encode } from "blurhash"; + import dis from './dispatcher/dispatcher'; import {MatrixClient} from "matrix-js-sdk/src/client"; import * as sdk from './index'; @@ -47,6 +49,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]; +const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC 2448 +const BLURHASH_X_COMPONENTS = 8; +const BLURHASH_Y_COMPONENTS = 8; + export class UploadCanceledError extends Error {} type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; @@ -77,6 +83,7 @@ interface IThumbnail { }; w: number; h: number; + [BLURHASH_FIELD]: string; }; thumbnail: Blob; } @@ -124,7 +131,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 +152,9 @@ function createThumbnail( }, w: inputWidth, h: inputHeight, + [BLURHASH_FIELD]: blurhash, }, - thumbnail: thumbnail, + thumbnail, }); }, mimeType); }); @@ -338,7 +355,6 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo if (file.type) { encryptInfo.mimetype = file.type; } - // TODO: Blurhash for encrypted media? return {"file": encryptInfo}; }); (prom as IAbortablePromise).abort = () => { @@ -349,15 +365,11 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo } else { const basePromise = matrixClient.uploadContent(file, { progressHandler: progressHandler, - onlyContentUri: false, }); - const promise1 = basePromise.then(function(body) { + const promise1 = basePromise.then(function(url) { if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. - return { - "url": body.content_uri, - "blurhash": body["xyz.amorgan.blurhash"], // TODO: Use `body.blurhash` when MSC2448 lands - }; + return { url }; }); promise1.abort = () => { canceled = true; @@ -565,7 +577,6 @@ export default class ContentMessages { return upload.promise.then(function(result) { content.file = result.file; content.url = result.url; - content.info['xyz.amorgan.blurhash'] = result.blurhash; // TODO: Use `blurhash` when MSC2448 lands }); }).then(() => { // Await previous message being sent into the room From 6672cbcf8029f93a3e5b89724ec4346404fb5de0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 15:29:17 +0100 Subject: [PATCH 07/29] fix typo --- src/ContentMessages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 9d9709a04d..626e5cbf69 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -49,7 +49,7 @@ 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]; -const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC 2448 +const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 const BLURHASH_X_COMPONENTS = 8; const BLURHASH_Y_COMPONENTS = 8; From 3ed421dacf5e9df2f346dcfd549b69555bf6707b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:04:22 +0100 Subject: [PATCH 08/29] Tweak blurhash size --- src/ContentMessages.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 626e5cbf69..0914bff41a 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -50,8 +50,8 @@ const MAX_HEIGHT = 600; const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 -const BLURHASH_X_COMPONENTS = 8; -const BLURHASH_Y_COMPONENTS = 8; +const BLURHASH_X_COMPONENTS = 6; +const BLURHASH_Y_COMPONENTS = 6; export class UploadCanceledError extends Error {} From 254697644e956e032451b11c500de46d49410564 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:04:36 +0100 Subject: [PATCH 09/29] Fix video thumbnailer --- src/ContentMessages.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 0914bff41a..98a4554a07 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -237,7 +237,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. @@ -246,20 +247,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); From dbca9b4625fbde6c32edef7522f2a2668b484829 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:19:41 +0100 Subject: [PATCH 10/29] use const for blurhash field --- src/ContentMessages.tsx | 2 +- src/components/views/messages/MImageBody.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 98a4554a07..44bb3edca9 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -49,7 +49,7 @@ 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]; -const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 +export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 const BLURHASH_X_COMPONENTS = 6; const BLURHASH_Y_COMPONENTS = 6; diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 34320f1b31..6062375243 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -30,6 +30,7 @@ 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 { @@ -440,7 +441,7 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getPlaceholder(width, height) { - const blurhash = this.props.mxEvent.getContent().info['xyz.amorgan.blurhash']; + const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; if (!blurhash) return null; return ; } From 3a2e5389f6ccad7fc1ed3e039544d3fe596a104a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:20:00 +0100 Subject: [PATCH 11/29] Support blurhash for video posters --- src/components/views/messages/MVideoBody.tsx | 67 +++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 2efdce506e..44b246f0ad 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 */ @@ -37,6 +40,8 @@ interface IState { decryptedBlob: Blob|null, error: any|null, 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); From 9c599b567df828ed5e888c846aa080b93c535ae2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:39:38 +0100 Subject: [PATCH 12/29] Fix alignment of image blurhashes and interplay with spinners --- src/components/views/messages/MImageBody.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 6062375243..8d3f1c4c10 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -372,10 +372,7 @@ 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) { + if (!this.state.imgLoaded) { placeholder = this.getPlaceholder(maxWidth, maxHeight); } @@ -414,9 +411,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 } } @@ -442,8 +437,10 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getPlaceholder(width, height) { const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; - if (!blurhash) return null; - return ; + if (blurhash) return ; + return
+ +
; } // Overidden by MStickerBody From 3f9b6caacd76608cb1d22403442460223ab7090a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:46:18 +0100 Subject: [PATCH 13/29] fix missing rounded corners on blurhash placeholder --- res/css/views/messages/_MImageBody.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 1c773c2f06..d04b3d5c93 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 { From 44e14a7d4305639b928e3a78a0b6f368f33f8424 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 May 2021 10:06:17 +0100 Subject: [PATCH 14/29] Support blurhashes on stickers --- src/components/views/messages/MStickerBody.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index 54eb7649b4..d3dea2bfa4 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); const TintableSVG = sdk.getComponent('elements.TintableSvg'); return ; } From d354a0a23b966549a5fab0b8c7c38dbd6a5a3a3d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 09:57:33 +0100 Subject: [PATCH 15/29] Prevent RoomDirectory from exploding when filterString is wrongly nullified --- src/components/structures/RoomDirectory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) { From 8bbb768825ab1154e0685b3557cebe35d623eae6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 19:54:05 +0100 Subject: [PATCH 16/29] Convert RoomHeader to TS Also add a type of OOBData... possibly this should be in the js-sdk, but threepid invites themselves have a type in react-sdk so it can live alongside it I guess (also I'm not entirely sure we actually use it anymore...) --- src/components/structures/LoggedInView.tsx | 4 +- src/components/structures/RoomView.tsx | 26 +-------- .../views/avatars/DecoratedRoomAvatar.tsx | 3 +- src/components/views/avatars/RoomAvatar.tsx | 4 +- .../rooms/{RoomHeader.js => RoomHeader.tsx} | 58 +++++++++++-------- src/stores/ThreepidInviteStore.ts | 10 ++++ 6 files changed, 52 insertions(+), 53 deletions(-) rename src/components/views/rooms/{RoomHeader.js => RoomHeader.tsx} (86%) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 26bb0fe24a..65b26cb505 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,7 +81,7 @@ interface IProps { page_type: string; autoJoin: boolean; threepidInvite?: IThreepidInvite; - roomOobData?: object; + roomOobData?: IOOBData; currentRoomId: string; collapseLhs: boolean; config: { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d08eaa2ecd..a8eb8d662a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -63,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'; @@ -95,21 +95,7 @@ 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; - }; + oobData?: IOOBData; resizeNotifier: ResizeNotifier; justCreatedOpts?: IOpts; @@ -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/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/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.tsx similarity index 86% rename from src/components/views/rooms/RoomHeader.js rename to src/components/views/rooms/RoomHeader.tsx index b05b709e36..c20b68cb9b 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,7 +16,6 @@ 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'; @@ -31,53 +30,64 @@ import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { throttle } from 'lodash'; +import { Room } 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: (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, state) => { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } // redisplay the room name, topic, etc. - this._rateLimitedUpdate(); + this.rateLimitedUpdate(); }; - _rateLimitedUpdate = throttle(() => { + private rateLimitedUpdate = throttle(() => { this.forceUpdate(); }, 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/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 { From e121e5bf8a6ebf3fc7a8d75ababfb54a0ffb8e6a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 19:58:25 +0100 Subject: [PATCH 17/29] nice try, vscode --- src/components/views/rooms/RoomHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c20b68cb9b..48b8951c81 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -30,7 +30,7 @@ import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { throttle } from 'lodash'; -import { Room } from '../../../../../matrix-js-sdk/src'; +import { Room } from 'matrix-js-sdk/src'; import { E2EStatus } from '../../../utils/ShieldUtils'; import { IOOBData } from '../../../stores/ThreepidInviteStore'; import { SearchScope } from './SearchBar'; From e6b754f8dd511d6f0bd77357e23121d6c3c5f04c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:33:25 +0100 Subject: [PATCH 18/29] Convert Markdown to TypeScript --- src/{Markdown.js => Markdown.ts} | 57 +++++++++++++++++--------------- 1 file changed, 30 insertions(+), 27 deletions(-) rename src/{Markdown.js => Markdown.ts} (85%) diff --git a/src/Markdown.js b/src/Markdown.ts similarity index 85% rename from src/Markdown.js rename to src/Markdown.ts index f670bded12..64037369db 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,16 @@ 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) { +function isAllowedHtmlTag(node: any) { 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 +40,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) { let par = node; while (par.parent) { par = par.parent; @@ -65,8 +57,13 @@ function is_multi_line(node) { * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether * it's plain text. + * + * Types are a bit of a struggle here as commonmark doesn't have types */ export default class Markdown { + private input: string; + private parsed: any; + constructor(input) { this.input = input; @@ -87,7 +84,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 { @@ -118,7 +115,7 @@ 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) { // If there is only one top level node, just return the @@ -126,8 +123,8 @@ export default class Markdown { // '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 +147,26 @@ export default class Markdown { } }; - renderer.html_inline = html_if_tag_allowed; + renderer.html_inline = function(node: any) { + if (isAllowedHtmlTag(node)) { + this.lit(node.literal); + return; + } else { + this.lit(escape(node.literal)); + } + }; renderer.html_block = function(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); @@ -178,13 +182,12 @@ export default class Markdown { * which has no formatting. Otherwise it emits HTML(!). */ toPlaintext() { - const renderer = new commonmark.HtmlRenderer({safe: false}); - const real_paragraph = renderer.paragraph; + const renderer = new commonmark.HtmlRenderer({ safe: false }); renderer.paragraph = function(node, entering) { // 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'); } @@ -193,7 +196,7 @@ export default class Markdown { renderer.html_block = function(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); From ee9be5438ebc094edee0055151d1e70feb095890 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:43:05 +0100 Subject: [PATCH 19/29] eslint --fix --- .eslintignore.errorfiles | 15 - package.json | 2 +- scripts/generate-eslint-error-ignore-file | 23 - .../structures/MessagePanel-test.js | 5 +- .../dialogs/InteractiveAuthDialog-test.js | 12 +- test/mock-clock.js | 588 +++++++++--------- 6 files changed, 301 insertions(+), 344 deletions(-) delete mode 100644 .eslintignore.errorfiles delete mode 100755 scripts/generate-eslint-error-ignore-file diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles deleted file mode 100644 index ea66720529..0000000000 --- a/.eslintignore.errorfiles +++ /dev/null @@ -1,15 +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/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..bd997f8559 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", 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/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index d32970a278..d836de0785 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -26,7 +26,7 @@ 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'); @@ -79,7 +79,7 @@ describe('MessagePanel', function() { beforeEach(function() { test_utils.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) => { @@ -120,7 +120,6 @@ describe('MessagePanel', function() { return events; } - // make a collection of events with some member events that should be collapsed // with a MemberEventListSummary function mkMelsEvents() { diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index fa44fc8d92..c75b2fdcc8 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 { sleep } from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -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/mock-clock.js b/test/mock-clock.js index 1a4d6086de..4306433b6f 100644 --- a/test/mock-clock.js +++ b/test/mock-clock.js @@ -47,369 +47,366 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. const j$ = {}; j$.Clock = function() { - function Clock(global, delayedFunctionSchedulerFactory, mockDate) { - let self = this, - realTimingFunctions = { + function Clock(global, delayedFunctionSchedulerFactory, mockDate) { + const self = this; + const realTimingFunctions = { setTimeout: global.setTimeout, clearTimeout: global.clearTimeout, setInterval: global.setInterval, clearInterval: global.clearInterval, - }, - fakeTimingFunctions = { + }; + const fakeTimingFunctions = { setTimeout: setTimeout, clearTimeout: clearTimeout, setInterval: setInterval, clearInterval: clearInterval, - }, - installed = false, - delayedFunctionScheduler, - timer; + }; + let installed = false; + let delayedFunctionScheduler; + let 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; - 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; + }; - return self; - }; + self.uninstall = function() { + delayedFunctionScheduler = null; + mockDate.uninstall(); + replace(global, realTimingFunctions); - self.uninstall = function() { - delayedFunctionScheduler = null; - mockDate.uninstall(); - replace(global, realTimingFunctions); + timer = realTimingFunctions; + installed = false; + }; - timer = realTimingFunctions; - installed = false; - }; + self.withMock = function(closure) { + this.install(); + try { + closure(); + } finally { + this.uninstall(); + } + }; - self.withMock = function(closure) { - this.install(); - try { - closure(); - } finally { - this.uninstall(); - } - }; + self.mockDate = function(initialDate) { + mockDate.install(initialDate); + }; - 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.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.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.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.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()'); + } + }; - 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; - return self; - - function originalTimingFunctionsIntact() { - return global.setTimeout === realTimingFunctions.setTimeout && + 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); + } } - 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; + return Clock; }(); - j$.DelayedFunctionScheduler = function() { - function DelayedFunctionScheduler() { - const self = this; - const scheduledLookup = []; - const scheduledFunctions = {}; - let currentTime = 0; - let delayedFnCount = 0; + 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; + self.tick = function(millis) { + millis = millis || 0; + const endTime = currentTime + millis; - runScheduledFunctions(endTime); - currentTime = endTime; - }; + 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; - } + 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); + millis = millis || 0; + timeoutKey = timeoutKey || ++delayedFnCount; + runAtMillis = runAtMillis || (currentTime + millis); - const funcToSchedule = { + 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; - }); - } + 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; - }; + 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; - }); + 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); - } + 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; + // 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; } - } - }; - return self; + function deleteFromLookup(key) { + const value = Number(key); + const i = indexOfFirstToPass(scheduledLookup, function(millis) { + return millis === value; + }); - function indexOfFirstToPass(array, testFn) { - let index = -1; - - for (let i = 0; i < array.length; ++i) { - if (testFn(array[i])) { - index = i; - break; + if (i > -1) { + scheduledLookup.splice(i, 1); + } } - } - return index; - } + function reschedule(scheduledFn) { + self.scheduleFunction(scheduledFn.funcToCall, + scheduledFn.millis, + scheduledFn.params, + true, + scheduledFn.timeoutKey, + scheduledFn.runAtMillis + scheduledFn.millis); + } - function deleteFromLookup(key) { - const value = Number(key); - const i = indexOfFirstToPass(scheduledLookup, function(millis) { - return millis === value; - }); + function forEachFunction(funcsToRun, callback) { + for (let i = 0; i < funcsToRun.length; ++i) { + callback(funcsToRun[i]); + } + } - if (i > -1) { - scheduledLookup.splice(i, 1); - } - } + function runScheduledFunctions(endTime) { + if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { + return; + } - function reschedule(scheduledFn) { - self.scheduleFunction(scheduledFn.funcToCall, - scheduledFn.millis, - scheduledFn.params, - true, - scheduledFn.timeoutKey, - scheduledFn.runAtMillis + scheduledFn.millis); - } + do { + currentTime = scheduledLookup.shift(); - function forEachFunction(funcsToRun, callback) { - for (let i = 0; i < funcsToRun.length; ++i) { - callback(funcsToRun[i]); - } - } + const funcsToRun = scheduledFunctions[currentTime]; + delete scheduledFunctions[currentTime]; - function runScheduledFunctions(endTime) { - if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { - return; - } + forEachFunction(funcsToRun, function(funcToRun) { + if (funcToRun.recurring) { + reschedule(funcToRun); + } + }); - 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 + 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; + return DelayedFunctionScheduler; }(); - j$.MockDate = function() { - function MockDate(global) { - const self = this; - let currentTime = 0; + 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()'); + if (!global || !global.Date) { + self.install = function() {}; + self.tick = function() {}; + self.uninstall = function() {}; + return self; } - }; - FakeDate.toSource = GlobalDate.toSource; - FakeDate.toString = GlobalDate.toString; - FakeDate.parse = GlobalDate.parse; - FakeDate.UTC = GlobalDate.UTC; + 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; + return MockDate; }(); const _clock = new j$.Clock(global, function() { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global)); @@ -418,4 +415,3 @@ export function clock() { return _clock; } - From 12cce2a93884046d39eae98df1b9fc214a2b3e58 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 20:51:13 +0100 Subject: [PATCH 20/29] delint --- src/components/views/elements/BlurhashPlaceholder.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/BlurhashPlaceholder.tsx b/src/components/views/elements/BlurhashPlaceholder.tsx index bed45bfe5a..0e59253fe8 100644 --- a/src/components/views/elements/BlurhashPlaceholder.tsx +++ b/src/components/views/elements/BlurhashPlaceholder.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import {decode} from "blurhash"; +import { decode } from "blurhash"; interface IProps { blurhash: string; @@ -38,7 +38,7 @@ export default class BlurhashPlaceholder extends React.PureComponent { if (!this.canvas.current) return; try { - const {width, height} = this.props; + const { width, height } = this.props; const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height)); const ctx = this.canvas.current.getContext("2d"); From 543fe6382d2d4e8f7feb643627667ca5b78aa2c0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:54:04 +0100 Subject: [PATCH 21/29] Fix linting in tests Or in the case of one test, disbale just the one rule it breaks --- .../structures/MessagePanel-test.js | 22 +++++++++---------- .../dialogs/InteractiveAuthDialog-test.js | 4 ++-- test/mock-clock.js | 11 +++++++++- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index d836de0785..ed2b7a6a36 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. @@ -29,7 +29,7 @@ const MessagePanel = sdk.getComponent('structures.MessagePanel'); import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; import Matrix from 'matrix-js-sdk'; -const test_utils = require('../../test-utils'); +const TestUtilsMatrix = require('../../test-utils'); const mockclock = require('../../mock-clock'); import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; @@ -77,7 +77,7 @@ describe('MessagePanel', function() { const events = mkEvents(); beforeEach(function() { - test_utils.stubClient(); + TestUtilsMatrix.stubClient(); client = MatrixClientPeg.get(); client.credentials = { userId: '@me:here' }; @@ -97,7 +97,7 @@ describe('MessagePanel', function() { 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 +111,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, @@ -127,13 +127,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", @@ -150,7 +150,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, })); @@ -166,7 +166,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", @@ -188,8 +188,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(); diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index c75b2fdcc8..86fa562b60 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -22,7 +22,7 @@ import MatrixReactTestUtils from 'matrix-react-test-utils'; import sdk from '../../../skinned-sdk'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import * as test_utils from '../../../test-utils'; +import * as TestUtilsMatrix from '../../../test-utils'; import { sleep } from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( @@ -33,7 +33,7 @@ describe('InteractiveAuthDialog', function() { let parentDiv; beforeEach(function() { - test_utils.stubClient(); + TestUtilsMatrix.stubClient(); parentDiv = document.createElement('div'); document.body.appendChild(parentDiv); }); diff --git a/test/mock-clock.js b/test/mock-clock.js index 4306433b6f..134bd107dd 100644 --- a/test/mock-clock.js +++ b/test/mock-clock.js @@ -42,8 +42,14 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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. + * + * Hopefully the reasons for c+ping this no longer apply, but until we can + * verify this / remove it, disable the eslint rule that it breaks (impresssively + * only one, having fixed the long line). */ +/* eslint-disable prefer-rest-params */ + const j$ = {}; j$.Clock = function() { @@ -67,7 +73,10 @@ j$.Clock = function() { self.install = function() { if (!originalTimingFunctionsIntact()) { - throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); + throw new Error( + 'Jasmine Clock was unable to install over custom global timer functions. ' + + 'Is the clock already installed?', + ); } replace(global, fakeTimingFunctions); timer = fakeTimingFunctions; From 3db95d607d5bd8108a687574f5805a5a6e72c370 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:57:56 +0100 Subject: [PATCH 22/29] Use semicolons Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 48b8951c81..0871c9e400 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -42,9 +42,9 @@ export interface ISearchInfo { } interface IProps { - room: Room, - oobData?: IOOBData, - inRoom: boolean, + room: Room; + oobData?: IOOBData; + inRoom: boolean; onSettingsClick: () => void; onSearchClick: () => void; onForgetClick: () => void; From 38ed53d5d19e2d6f3122b358b840e790dbc32f0d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:58:51 +0100 Subject: [PATCH 23/29] Oops, don't just blindly insert the commented type Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 0871c9e400..2b398b442a 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -48,7 +48,7 @@ interface IProps { onSettingsClick: () => void; onSearchClick: () => void; onForgetClick: () => void; - onCallPlaced: (PlaceCallType) => void; + onCallPlaced: (type: PlaceCallType) => void; onAppsClick: () => void; e2eStatus: E2EStatus; appsShown: boolean; From eb1a22ab19e2dd86000c8809dea36fb7bf25d03e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 21:00:54 +0100 Subject: [PATCH 24/29] Add types --- src/components/views/rooms/RoomHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 2b398b442a..af5daed5bc 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -30,7 +30,7 @@ import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { throttle } from 'lodash'; -import { Room } from 'matrix-js-sdk/src'; +import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src'; import { E2EStatus } from '../../../utils/ShieldUtils'; import { IOOBData } from '../../../stores/ThreepidInviteStore'; import { SearchScope } from './SearchBar'; @@ -74,7 +74,7 @@ export default class RoomHeader extends React.Component { } } - private onRoomStateEvents = (event, state) => { + private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } From c1310bcd9f11faeb5414443cc9ece721582e19eb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 21:31:17 +0100 Subject: [PATCH 25/29] Better types --- package.json | 1 + src/Markdown.ts | 26 ++++++++++++++++---------- yarn.lock | 5 +++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4ad585ba7d..c505e16dce 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", + "@types/commonmark": "^0.27.4", "await-lock": "^2.1.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", diff --git a/src/Markdown.ts b/src/Markdown.ts index 64037369db..bb6d6d8df0 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -23,7 +23,15 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; -function isAllowedHtmlTag(node: any) { +// 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) { return true; @@ -45,7 +53,7 @@ function isAllowedHtmlTag(node: any) { * comprises multiple block level elements (ie. lines), * or false if it is only a single line. */ -function isMultiLine(node) { +function isMultiLine(node: commonmark.Node): boolean { let par = node; while (par.parent) { par = par.parent; @@ -57,12 +65,10 @@ function isMultiLine(node) { * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether * it's plain text. - * - * Types are a bit of a struggle here as commonmark doesn't have types */ export default class Markdown { private input: string; - private parsed: any; + private parsed: commonmark.Node; constructor(input) { this.input = input; @@ -71,7 +77,7 @@ export default class Markdown { this.parsed = parser.parse(this.input); } - isPlainText() { + isPlainText(): boolean { const walker = this.parsed.walker(); let ev; @@ -94,7 +100,7 @@ export default class Markdown { return true; } - toHTML({ externalLinks = false } = {}) { + toHTML({ externalLinks = false } = {}): string { const renderer = new commonmark.HtmlRenderer({ safe: false, @@ -104,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 @@ -181,8 +187,8 @@ 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 }); + toPlaintext(): string { + const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal; renderer.paragraph = function(node, entering) { // as with toHTML, only append lines to paragraphs if there are diff --git a/yarn.lock b/yarn.lock index 6f08372e18..d59dc9d79f 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" From 08176beb58163f3695a16ff757d838886fea5026 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 21:52:15 +0100 Subject: [PATCH 26/29] Kill mock-clock We already import sinon's one in UserActivitytest, so use that here too. --- .../structures/MessagePanel-test.js | 12 +- test/mock-clock.js | 426 ------------------ 2 files changed, 7 insertions(+), 431 deletions(-) delete mode 100644 test/mock-clock.js diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index ed2b7a6a36..f415b85105 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -30,7 +30,7 @@ import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; import Matrix from 'matrix-js-sdk'; const TestUtilsMatrix = require('../../test-utils'); -const mockclock = require('../../mock-clock'); +import FakeTimers from '@sinonjs/fake-timers'; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import { configure, mount } from "enzyme"; @@ -72,7 +72,7 @@ class WrappedMessagePanel extends React.Component { } describe('MessagePanel', function() { - const clock = mockclock.clock(); + let clock = null; const realSetTimeout = window.setTimeout; const events = mkEvents(); @@ -90,7 +90,10 @@ describe('MessagePanel', function() { }); afterEach(function() { - clock.uninstall(); + if (clock) { + clock.uninstall(); + clock = null; + } }); function mkEvents() { @@ -362,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/mock-clock.js b/test/mock-clock.js deleted file mode 100644 index 134bd107dd..0000000000 --- a/test/mock-clock.js +++ /dev/null @@ -1,426 +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. - * - * Hopefully the reasons for c+ping this no longer apply, but until we can - * verify this / remove it, disable the eslint rule that it breaks (impresssively - * only one, having fixed the long line). - */ - -/* eslint-disable prefer-rest-params */ - -const j$ = {}; - -j$.Clock = function() { - function Clock(global, delayedFunctionSchedulerFactory, mockDate) { - const self = this; - const realTimingFunctions = { - setTimeout: global.setTimeout, - clearTimeout: global.clearTimeout, - setInterval: global.setInterval, - clearInterval: global.clearInterval, - }; - const fakeTimingFunctions = { - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval, - }; - let installed = false; - let delayedFunctionScheduler; - let 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; -} - From fda17b4959b90de2db2884b126bcb7fc14dd4007 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 23:02:51 +0100 Subject: [PATCH 27/29] Add types to the implementations too Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Markdown.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Markdown.ts b/src/Markdown.ts index bb6d6d8df0..96169d4011 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -123,7 +123,7 @@ export default class Markdown { 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 @@ -153,7 +153,7 @@ export default class Markdown { } }; - renderer.html_inline = function(node: any) { + renderer.html_inline = function(node: commonmark.Node) { if (isAllowedHtmlTag(node)) { this.lit(node.literal); return; @@ -162,7 +162,7 @@ export default class Markdown { } }; - 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. @@ -190,7 +190,7 @@ export default class Markdown { 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 (isMultiLine(node)) { @@ -200,7 +200,7 @@ export default class Markdown { } }; - renderer.html_block = function(node) { + renderer.html_block = function(node: commonmark.Node) { this.lit(node.literal); if (isMultiLine(node) && node.next) this.lit('\n\n'); }; From 80f52322176d988f38cc5f9319e2ce4aaf9eed4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 23:23:03 +0100 Subject: [PATCH 28/29] Comply with new member-delimiter-style rule Just `eslint --fix` with rule from https://github.com/matrix-org/eslint-plugin-matrix-org/pull/9 in place --- src/CallHandler.tsx | 6 ++-- src/CountlyAnalytics.ts | 2 +- src/SecurityManager.ts | 4 +-- src/Terms.ts | 10 +++--- .../structures/AutoHideScrollbar.tsx | 4 +-- src/components/structures/FilePanel.tsx | 2 +- src/components/structures/LoggedInView.tsx | 4 +-- src/components/structures/MatrixChat.tsx | 2 +- .../structures/NonUrgentToastContainer.tsx | 2 +- src/components/structures/RoomView.tsx | 2 +- .../structures/SpaceRoomDirectory.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 2 +- .../structures/auth/Registration.tsx | 2 +- src/components/structures/auth/SoftLogout.tsx | 2 +- src/components/views/auth/PasswordLogin.tsx | 4 +-- .../views/dialogs/ConfirmUserActionDialog.tsx | 2 +- src/components/views/dialogs/InviteDialog.tsx | 18 +++++----- src/components/views/dialogs/TermsDialog.tsx | 6 ++-- .../views/elements/EventListSummary.tsx | 2 +- src/components/views/elements/ImageView.tsx | 34 +++++++++---------- .../elements/SpellCheckLanguagesDropdown.tsx | 10 +++--- src/components/views/emojipicker/Header.tsx | 2 +- .../messages/MKeyVerificationRequest.tsx | 2 +- src/components/views/messages/TextualBody.tsx | 2 +- src/components/views/rooms/AuxPanel.tsx | 26 +++++++------- src/components/views/rooms/EventTile.tsx | 4 +-- .../views/rooms/RoomListNumResults.tsx | 2 +- .../views/settings/SpellCheckSettings.tsx | 10 +++--- .../views/spaces/SpaceBasicSettings.tsx | 2 +- src/components/views/voip/AudioFeed.tsx | 2 +- src/components/views/voip/CallView.tsx | 22 ++++++------ src/components/views/voip/CallViewForRoom.tsx | 6 ++-- src/components/views/voip/VideoFeed.tsx | 6 ++-- src/customisations/Security.ts | 12 +++---- .../payloads/ComposerInsertPayload.ts | 2 +- src/effects/confetti/index.ts | 26 +++++++------- src/performance/index.ts | 8 ++--- src/stores/TypingStore.ts | 8 ++--- src/stores/WidgetEchoStore.ts | 4 +-- src/utils/objects.ts | 2 +- test/KeyBindingsManager-test.ts | 8 ++--- 41 files changed, 139 insertions(+), 139 deletions(-) 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/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 3ad56fe3bf..d4a340ddaf 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -256,7 +256,7 @@ interface ICreateRoomEvent extends IEvent { num_users: number; is_encrypted: boolean; is_public: boolean; - } + }; } interface IJoinRoomEvent extends IEvent { 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/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 65b26cb505..1f870da900 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -87,8 +87,8 @@ interface IProps { 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/RoomView.tsx b/src/components/structures/RoomView.tsx index 6428b7cdc2..2c8fc08dac 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -94,7 +94,7 @@ if (DEBUG) { } interface IProps { - threepidInvite: IThreepidInvite, + threepidInvite: IThreepidInvite; oobData?: IOOBData; resizeNotifier: ResizeNotifier; 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/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/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/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/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/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/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index 8b8eb25f6b..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") diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index f9e0f695ab..6ba018c512 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -67,7 +67,7 @@ interface IProps { replacingEventId?: string; /* callback for when our widget has loaded */ - onHeightChanged(): void, + onHeightChanged(): void; } interface IState { diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 99286dfa07..f142328895 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -32,32 +32,32 @@ 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") diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index cebb631708..c9d1040433 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -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 { 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/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/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/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/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/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/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/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, From 1fe0c2ab881f579453c576b19ee017a8433837fb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 23:45:08 +0100 Subject: [PATCH 29/29] Bump eslint plugin version --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index fcb9a46b37..c8c3315855 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3232,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"