Merge branch 'develop' into t3chguy/ts/11
This commit is contained in:
commit
82119a08f7
61 changed files with 467 additions and 766 deletions
|
@ -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/
|
|
|
@ -45,7 +45,7 @@
|
||||||
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"",
|
"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\"",
|
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
"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:types": "tsc --noEmit --jsx react",
|
||||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
@ -54,7 +54,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/commonmark": "^0.27.4",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
|
"blurhash": "^1.1.3",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"cheerio": "^1.0.0-rc.9",
|
"cheerio": "^1.0.0-rc.9",
|
||||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
.mx_MImageBody {
|
.mx_MImageBody {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 34px;
|
margin-right: 34px;
|
||||||
|
@ -25,7 +27,11 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: 4px;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail_container {
|
.mx_MImageBody_thumbnail_container {
|
||||||
|
|
|
@ -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 <<EOF
|
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
./node_modules/.bin/eslint -f json src test |
|
|
||||||
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
|
|
||||||
sed -e 's/.*matrix-react-sdk\///';
|
|
||||||
} > "$out"
|
|
||||||
# also append rules from eslintignore file
|
|
||||||
cat .eslintignore >> $out
|
|
|
@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThirdpartyLookupResponse {
|
interface ThirdpartyLookupResponse {
|
||||||
userid: string,
|
userid: string;
|
||||||
protocol: string,
|
protocol: string;
|
||||||
fields: ThirdpartyLookupResponseFields,
|
fields: ThirdpartyLookupResponseFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
||||||
|
|
|
@ -17,9 +17,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import dis from './dispatcher/dispatcher';
|
import { encode } from "blurhash";
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import dis from './dispatcher/dispatcher';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
@ -47,6 +48,10 @@ const MAX_HEIGHT = 600;
|
||||||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||||
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
||||||
|
|
||||||
|
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
||||||
|
const BLURHASH_X_COMPONENTS = 6;
|
||||||
|
const BLURHASH_Y_COMPONENTS = 6;
|
||||||
|
|
||||||
export class UploadCanceledError extends Error {}
|
export class UploadCanceledError extends Error {}
|
||||||
|
|
||||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||||
|
@ -77,6 +82,7 @@ interface IThumbnail {
|
||||||
};
|
};
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
|
[BLURHASH_FIELD]: string;
|
||||||
};
|
};
|
||||||
thumbnail: Blob;
|
thumbnail: Blob;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +130,16 @@ function createThumbnail(
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = targetWidth;
|
canvas.width = targetWidth;
|
||||||
canvas.height = targetHeight;
|
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) {
|
canvas.toBlob(function(thumbnail) {
|
||||||
resolve({
|
resolve({
|
||||||
info: {
|
info: {
|
||||||
|
@ -136,8 +151,9 @@ function createThumbnail(
|
||||||
},
|
},
|
||||||
w: inputWidth,
|
w: inputWidth,
|
||||||
h: inputHeight,
|
h: inputHeight,
|
||||||
|
[BLURHASH_FIELD]: blurhash,
|
||||||
},
|
},
|
||||||
thumbnail: thumbnail,
|
thumbnail,
|
||||||
});
|
});
|
||||||
}, mimeType);
|
}, mimeType);
|
||||||
});
|
});
|
||||||
|
@ -220,7 +236,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a file into a newly created video element.
|
* Load a file into a newly created video element and pull some strings
|
||||||
|
* in an attempt to guarantee the first frame will be showing.
|
||||||
*
|
*
|
||||||
* @param {File} videoFile The file to load in an video element.
|
* @param {File} videoFile The file to load in an video element.
|
||||||
* @return {Promise} A promise that resolves with the video image element.
|
* @return {Promise} A promise that resolves with the video image element.
|
||||||
|
@ -229,20 +246,25 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
video.preload = "metadata";
|
||||||
|
video.playsInline = true;
|
||||||
|
video.muted = true;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function(ev) {
|
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.
|
// Wait until we have enough data to thumbnail the first frame.
|
||||||
video.onloadeddata = function() {
|
video.onloadeddata = async function() {
|
||||||
resolve(video);
|
resolve(video);
|
||||||
|
video.pause();
|
||||||
};
|
};
|
||||||
video.onerror = function(e) {
|
video.onerror = function(e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
video.src = ev.target.result as string;
|
||||||
|
video.load();
|
||||||
|
video.play();
|
||||||
};
|
};
|
||||||
reader.onerror = function(e) {
|
reader.onerror = function(e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
@ -347,7 +369,7 @@ export function uploadFile(
|
||||||
});
|
});
|
||||||
(prom as IAbortablePromise<any>).abort = () => {
|
(prom as IAbortablePromise<any>).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
|
||||||
};
|
};
|
||||||
return prom;
|
return prom;
|
||||||
} else {
|
} else {
|
||||||
|
@ -357,11 +379,11 @@ export function uploadFile(
|
||||||
const promise1 = basePromise.then(function(url) {
|
const promise1 = basePromise.then(function(url) {
|
||||||
if (canceled) throw new UploadCanceledError();
|
if (canceled) throw new UploadCanceledError();
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return { "url": url };
|
return { url };
|
||||||
});
|
});
|
||||||
(promise1 as any).abort = () => {
|
(promise1 as any).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
matrixClient.cancelUpload(basePromise);
|
||||||
};
|
};
|
||||||
return promise1;
|
return promise1;
|
||||||
}
|
}
|
||||||
|
@ -373,7 +395,7 @@ export default class ContentMessages {
|
||||||
|
|
||||||
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
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);
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
@ -415,7 +437,7 @@ export default class ContentMessages {
|
||||||
|
|
||||||
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
|
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');
|
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||||
await this.ensureMediaConfigFetched();
|
await this.ensureMediaConfigFetched(matrixClient);
|
||||||
modal.close();
|
modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,7 +492,7 @@ export default class ContentMessages {
|
||||||
return this.inprogress.filter(u => !u.canceled);
|
return this.inprogress.filter(u => !u.canceled);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload(promise: Promise<any>) {
|
cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
|
||||||
let upload: IUpload;
|
let upload: IUpload;
|
||||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||||
if (this.inprogress[i].promise === promise) {
|
if (this.inprogress[i].promise === promise) {
|
||||||
|
@ -480,7 +502,7 @@ export default class ContentMessages {
|
||||||
}
|
}
|
||||||
if (upload) {
|
if (upload) {
|
||||||
upload.canceled = true;
|
upload.canceled = true;
|
||||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
matrixClient.cancelUpload(upload.promise);
|
||||||
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -621,11 +643,11 @@ export default class ContentMessages {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureMediaConfigFetched() {
|
private ensureMediaConfigFetched(matrixClient: MatrixClient) {
|
||||||
if (this.mediaConfig !== null) return;
|
if (this.mediaConfig !== null) return;
|
||||||
|
|
||||||
console.log("[Media Config] Fetching");
|
console.log("[Media Config] Fetching");
|
||||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
return matrixClient.getMediaConfig().then((config) => {
|
||||||
console.log("[Media Config] Fetched config:", config);
|
console.log("[Media Config] Fetched config:", config);
|
||||||
return config;
|
return config;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|
|
@ -256,7 +256,7 @@ interface ICreateRoomEvent extends IEvent {
|
||||||
num_users: number;
|
num_users: number;
|
||||||
is_encrypted: boolean;
|
is_encrypted: boolean;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IJoinRoomEvent extends IEvent {
|
interface IJoinRoomEvent extends IEvent {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,16 +16,24 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as commonmark from 'commonmark';
|
import * as commonmark from 'commonmark';
|
||||||
import {escape} from "lodash";
|
import { escape } from "lodash";
|
||||||
|
|
||||||
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||||
|
|
||||||
// These types of node are definitely text
|
// These types of node are definitely text
|
||||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||||
|
|
||||||
function is_allowed_html_tag(node) {
|
// As far as @types/commonmark is concerned, these are not public, so add them
|
||||||
|
interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer {
|
||||||
|
paragraph: (node: commonmark.Node, entering: boolean) => void;
|
||||||
|
link: (node: commonmark.Node, entering: boolean) => void;
|
||||||
|
html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||||
|
html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedHtmlTag(node: commonmark.Node): boolean {
|
||||||
if (node.literal != null &&
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,21 +48,12 @@ function is_allowed_html_tag(node) {
|
||||||
return false;
|
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
|
* Returns true if the parse output containing the node
|
||||||
* comprises multiple block level elements (ie. lines),
|
* comprises multiple block level elements (ie. lines),
|
||||||
* or false if it is only a single line.
|
* or false if it is only a single line.
|
||||||
*/
|
*/
|
||||||
function is_multi_line(node) {
|
function isMultiLine(node: commonmark.Node): boolean {
|
||||||
let par = node;
|
let par = node;
|
||||||
while (par.parent) {
|
while (par.parent) {
|
||||||
par = par.parent;
|
par = par.parent;
|
||||||
|
@ -67,6 +67,9 @@ function is_multi_line(node) {
|
||||||
* it's plain text.
|
* it's plain text.
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
|
private input: string;
|
||||||
|
private parsed: commonmark.Node;
|
||||||
|
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
|
|
||||||
|
@ -74,7 +77,7 @@ export default class Markdown {
|
||||||
this.parsed = parser.parse(this.input);
|
this.parsed = parser.parse(this.input);
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlainText() {
|
isPlainText(): boolean {
|
||||||
const walker = this.parsed.walker();
|
const walker = this.parsed.walker();
|
||||||
|
|
||||||
let ev;
|
let ev;
|
||||||
|
@ -87,7 +90,7 @@ export default class Markdown {
|
||||||
// if it's an allowed html tag, we need to render it and therefore
|
// 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 will need to use HTML. If it's not allowed, it's not HTML since
|
||||||
// we'll just be treating it as text.
|
// we'll just be treating it as text.
|
||||||
if (is_allowed_html_tag(node)) {
|
if (isAllowedHtmlTag(node)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -97,7 +100,7 @@ export default class Markdown {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML({ externalLinks = false } = {}) {
|
toHTML({ externalLinks = false } = {}): string {
|
||||||
const renderer = new commonmark.HtmlRenderer({
|
const renderer = new commonmark.HtmlRenderer({
|
||||||
safe: false,
|
safe: false,
|
||||||
|
|
||||||
|
@ -107,7 +110,7 @@ export default class Markdown {
|
||||||
// block quote ends up all on one line
|
// block quote ends up all on one line
|
||||||
// (https://github.com/vector-im/element-web/issues/3154)
|
// (https://github.com/vector-im/element-web/issues/3154)
|
||||||
softbreak: '<br />',
|
softbreak: '<br />',
|
||||||
});
|
}) as CommonmarkHtmlRendererInternal;
|
||||||
|
|
||||||
// Trying to strip out the wrapping <p/> causes a lot more complication
|
// Trying to strip out the wrapping <p/> causes a lot more complication
|
||||||
// than it's worth, i think. For instance, this code will go and strip
|
// than it's worth, i think. For instance, this code will go and strip
|
||||||
|
@ -118,16 +121,16 @@ export default class Markdown {
|
||||||
//
|
//
|
||||||
// Let's try sending with <p/>s anyway for now, though.
|
// Let's try sending with <p/>s anyway for now, though.
|
||||||
|
|
||||||
const real_paragraph = renderer.paragraph;
|
const realParagraph = renderer.paragraph;
|
||||||
|
|
||||||
renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
|
||||||
// If there is only one top level node, just return the
|
// If there is only one top level node, just return the
|
||||||
// bare text: it's a single line of text and so should be
|
// bare text: it's a single line of text and so should be
|
||||||
// 'inline', rather than unnecessarily wrapped in its own
|
// 'inline', rather than unnecessarily wrapped in its own
|
||||||
// p tag. If, however, we have multiple nodes, each gets
|
// p tag. If, however, we have multiple nodes, each gets
|
||||||
// its own p tag to keep them as separate paragraphs.
|
// its own p tag to keep them as separate paragraphs.
|
||||||
if (is_multi_line(node)) {
|
if (isMultiLine(node)) {
|
||||||
real_paragraph.call(this, node, entering);
|
realParagraph.call(this, node, entering);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -150,19 +153,26 @@ export default class Markdown {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.html_inline = html_if_tag_allowed;
|
renderer.html_inline = function(node: commonmark.Node) {
|
||||||
|
if (isAllowedHtmlTag(node)) {
|
||||||
|
this.lit(node.literal);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.lit(escape(node.literal));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
renderer.html_block = function(node) {
|
renderer.html_block = function(node: commonmark.Node) {
|
||||||
/*
|
/*
|
||||||
// as with `paragraph`, we only insert line breaks
|
// as with `paragraph`, we only insert line breaks
|
||||||
// if there are multiple lines in the markdown.
|
// if there are multiple lines in the markdown.
|
||||||
const isMultiLine = is_multi_line(node);
|
const isMultiLine = is_multi_line(node);
|
||||||
if (isMultiLine) this.cr();
|
if (isMultiLine) this.cr();
|
||||||
*/
|
*/
|
||||||
html_if_tag_allowed.call(this, node);
|
renderer.html_inline(node);
|
||||||
/*
|
/*
|
||||||
if (isMultiLine) this.cr();
|
if (isMultiLine) this.cr();
|
||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
return renderer.render(this.parsed);
|
return renderer.render(this.parsed);
|
||||||
|
@ -177,23 +187,22 @@ export default class Markdown {
|
||||||
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
|
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
|
||||||
* which has no formatting. Otherwise it emits HTML(!).
|
* which has no formatting. Otherwise it emits HTML(!).
|
||||||
*/
|
*/
|
||||||
toPlaintext() {
|
toPlaintext(): string {
|
||||||
const renderer = new commonmark.HtmlRenderer({safe: false});
|
const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
|
||||||
const real_paragraph = renderer.paragraph;
|
|
||||||
|
|
||||||
renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
|
||||||
// as with toHTML, only append lines to paragraphs if there are
|
// as with toHTML, only append lines to paragraphs if there are
|
||||||
// multiple paragraphs
|
// multiple paragraphs
|
||||||
if (is_multi_line(node)) {
|
if (isMultiLine(node)) {
|
||||||
if (!entering && node.next) {
|
if (!entering && node.next) {
|
||||||
this.lit('\n\n');
|
this.lit('\n\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.html_block = function(node) {
|
renderer.html_block = function(node: commonmark.Node) {
|
||||||
this.lit(node.literal);
|
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);
|
return renderer.render(this.parsed);
|
|
@ -42,8 +42,8 @@ let secretStorageBeingAccessed = false;
|
||||||
let nonInteractive = false;
|
let nonInteractive = false;
|
||||||
|
|
||||||
let dehydrationCache: {
|
let dehydrationCache: {
|
||||||
key?: Uint8Array,
|
key?: Uint8Array;
|
||||||
keyInfo?: ISecretStorageKeyInfo,
|
keyInfo?: ISecretStorageKeyInfo;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
function isCachingAllowed(): boolean {
|
function isCachingAllowed(): boolean {
|
||||||
|
|
10
src/Terms.ts
10
src/Terms.ts
|
@ -49,13 +49,13 @@ export interface Policy {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Policies = {
|
export type Policies = {
|
||||||
[policy: string]: Policy,
|
[policy: string]: Policy;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TermsInteractionCallback = (
|
export type TermsInteractionCallback = (
|
||||||
policiesAndServicePairs: {
|
policiesAndServicePairs: {
|
||||||
service: Service,
|
service: Service;
|
||||||
policies: Policies,
|
policies: Policies;
|
||||||
}[],
|
}[],
|
||||||
agreedUrls: string[],
|
agreedUrls: string[],
|
||||||
extraClassNames?: string,
|
extraClassNames?: string,
|
||||||
|
@ -181,8 +181,8 @@ export async function startTermsFlow(
|
||||||
|
|
||||||
export function dialogTermsInteractionCallback(
|
export function dialogTermsInteractionCallback(
|
||||||
policiesAndServicePairs: {
|
policiesAndServicePairs: {
|
||||||
service: Service,
|
service: Service;
|
||||||
policies: { [policy: string]: Policy },
|
policies: { [policy: string]: Policy };
|
||||||
}[],
|
}[],
|
||||||
agreedUrls: string[],
|
agreedUrls: string[],
|
||||||
extraClassNames?: string,
|
extraClassNames?: string,
|
||||||
|
|
|
@ -21,8 +21,8 @@ interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
|
||||||
className?: string;
|
className?: string;
|
||||||
onScroll?: (event: Event) => void;
|
onScroll?: (event: Event) => void;
|
||||||
onWheel?: (event: WheelEvent) => void;
|
onWheel?: (event: WheelEvent) => void;
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties;
|
||||||
tabIndex?: number,
|
tabIndex?: number;
|
||||||
wrappedRef?: (ref: HTMLDivElement) => void;
|
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
resizeNotifier: ResizeNotifier
|
resizeNotifier: ResizeNotifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -48,7 +48,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
|
||||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||||
|
@ -81,14 +81,14 @@ interface IProps {
|
||||||
page_type: string;
|
page_type: string;
|
||||||
autoJoin: boolean;
|
autoJoin: boolean;
|
||||||
threepidInvite?: IThreepidInvite;
|
threepidInvite?: IThreepidInvite;
|
||||||
roomOobData?: object;
|
roomOobData?: IOOBData;
|
||||||
currentRoomId: string;
|
currentRoomId: string;
|
||||||
collapseLhs: boolean;
|
collapseLhs: boolean;
|
||||||
config: {
|
config: {
|
||||||
piwik: {
|
piwik: {
|
||||||
policyUrl: string;
|
policyUrl: string;
|
||||||
},
|
};
|
||||||
[key: string]: any,
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
currentGroupId?: string;
|
currentGroupId?: string;
|
||||||
|
|
|
@ -204,7 +204,7 @@ interface IState {
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
serverConfig?: ValidatedServerConfig;
|
serverConfig?: ValidatedServerConfig;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
threepidInvite?: IThreepidInvite,
|
threepidInvite?: IThreepidInvite;
|
||||||
roomOobData?: object;
|
roomOobData?: object;
|
||||||
pendingInitialSync?: boolean;
|
pendingInitialSync?: boolean;
|
||||||
justRegistered?: boolean;
|
justRegistered?: boolean;
|
||||||
|
|
|
@ -24,7 +24,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
toasts: ComponentClass[],
|
toasts: ComponentClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.NonUrgentToastContainer")
|
@replaceableComponent("structures.NonUrgentToastContainer")
|
||||||
|
|
|
@ -370,7 +370,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onFilterChange = (alias: string) => {
|
private onFilterChange = (alias: string) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: alias || null,
|
filterString: alias || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// don't send the request for a little bit,
|
// don't send the request for a little bit,
|
||||||
|
@ -389,7 +389,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
private onFilterClear = () => {
|
private onFilterClear = () => {
|
||||||
// update immediately
|
// update immediately
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString: null,
|
filterString: "",
|
||||||
}, this.refreshRoomList);
|
}, this.refreshRoomList);
|
||||||
|
|
||||||
if (this.filterTimeout) {
|
if (this.filterTimeout) {
|
||||||
|
|
|
@ -63,7 +63,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||||
import { containsEmoji } from '../../effects/utils';
|
import { containsEmoji } from '../../effects/utils';
|
||||||
import { CHAT_EFFECTS } from '../../effects';
|
import { CHAT_EFFECTS } from '../../effects';
|
||||||
|
@ -94,22 +94,8 @@ if (DEBUG) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
threepidInvite: IThreepidInvite,
|
threepidInvite: IThreepidInvite;
|
||||||
|
oobData?: IOOBData;
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
justCreatedOpts?: IOpts;
|
justCreatedOpts?: IOpts;
|
||||||
|
@ -1261,7 +1247,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private injectSticker(url, info, text) {
|
private injectSticker(url: string, info: object, text: string) {
|
||||||
if (this.context.isGuest()) {
|
if (this.context.isGuest()) {
|
||||||
dis.dispatch({ action: 'require_registration' });
|
dis.dispatch({ action: 'require_registration' });
|
||||||
return;
|
return;
|
||||||
|
@ -1460,13 +1446,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLeaveClick = () => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'leave_room',
|
|
||||||
room_id: this.state.room.roomId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onForgetClick = () => {
|
private onForgetClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'forget_room',
|
action: 'forget_room',
|
||||||
|
@ -2106,7 +2085,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
||||||
appsShown={this.state.showApps}
|
appsShown={this.state.showApps}
|
||||||
|
|
|
@ -58,7 +58,7 @@ export interface ISpaceSummaryRoom {
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
guest_can_join: boolean;
|
guest_can_join: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
num_joined_members: number
|
num_joined_members: number;
|
||||||
room_id: string;
|
room_id: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
world_readable: boolean;
|
world_readable: boolean;
|
||||||
|
|
|
@ -125,7 +125,7 @@ interface IProps {
|
||||||
onReadMarkerUpdated?(): void;
|
onReadMarkerUpdated?(): void;
|
||||||
|
|
||||||
// callback which is called when we wish to paginate the timeline window.
|
// callback which is called when we wish to paginate the timeline window.
|
||||||
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>,
|
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import { IUpload } from "../../models/IUpload";
|
import { IUpload } from "../../models/IUpload";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -38,6 +39,8 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("structures.UploadBar")
|
@replaceableComponent("structures.UploadBar")
|
||||||
export default class UploadBar extends React.Component<IProps, IState> {
|
export default class UploadBar extends React.Component<IProps, IState> {
|
||||||
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private mounted: boolean;
|
private mounted: boolean;
|
||||||
|
|
||||||
|
@ -82,7 +85,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onCancelClick = (ev) => {
|
private onCancelClick = (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
|
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -49,7 +49,7 @@ interface IProps {
|
||||||
// for operations like uploading cross-signing keys).
|
// for operations like uploading cross-signing keys).
|
||||||
onLoggedIn(params: {
|
onLoggedIn(params: {
|
||||||
userId: string;
|
userId: string;
|
||||||
deviceId: string
|
deviceId: string;
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
identityServerUrl?: string;
|
identityServerUrl?: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
|
|
@ -49,7 +49,7 @@ interface IProps {
|
||||||
fragmentAfterLogin?: string;
|
fragmentAfterLogin?: string;
|
||||||
|
|
||||||
// Called when the SSO login completes
|
// Called when the SSO login completes
|
||||||
onTokenLoginCompleted: () => void,
|
onTokenLoginCompleted: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -52,8 +52,8 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
fieldValid: Partial<Record<LoginField, boolean>>;
|
fieldValid: Partial<Record<LoginField, boolean>>;
|
||||||
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone,
|
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone;
|
||||||
password: "",
|
password: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LoginField {
|
enum LoginField {
|
||||||
|
|
|
@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler";
|
||||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
avatarSize: number;
|
avatarSize: number;
|
||||||
displayBadge?: boolean;
|
displayBadge?: boolean;
|
||||||
forceCount?: boolean;
|
forceCount?: boolean;
|
||||||
oobData?: object;
|
oobData?: IOOBData;
|
||||||
viewAvatarOnClick?: boolean;
|
viewAvatarOnClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,14 +24,14 @@ import Modal from '../../../Modal';
|
||||||
import * as Avatar from '../../../Avatar';
|
import * as Avatar from '../../../Avatar';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
room?: Room;
|
room?: Room;
|
||||||
// TODO: type when js-sdk has types
|
oobData?: IOOBData;
|
||||||
oobData?: any;
|
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
resizeMethod?: ResizeMethod;
|
resizeMethod?: ResizeMethod;
|
||||||
|
|
|
@ -29,7 +29,7 @@ interface IProps {
|
||||||
// group member object. Supply either this or 'member'
|
// group member object. Supply either this or 'member'
|
||||||
groupMember: GroupMemberType;
|
groupMember: GroupMemberType;
|
||||||
// needed if a group member is specified
|
// needed if a group member is specified
|
||||||
matrixClient?: MatrixClient,
|
matrixClient?: MatrixClient;
|
||||||
action: string; // eg. 'Ban'
|
action: string; // eg. 'Ban'
|
||||||
title: string; // eg. 'Ban this user?'
|
title: string; // eg. 'Ban this user?'
|
||||||
|
|
||||||
|
|
|
@ -70,9 +70,9 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
interface IRecentUser {
|
interface IRecentUser {
|
||||||
userId: string,
|
userId: string;
|
||||||
user: RoomMember,
|
user: RoomMember;
|
||||||
lastActive: number,
|
lastActive: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
|
@ -330,16 +330,16 @@ interface IInviteDialogProps {
|
||||||
|
|
||||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||||
// not provided.
|
// not provided.
|
||||||
kind: string,
|
kind: string;
|
||||||
|
|
||||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
// 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.
|
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||||
call: MatrixCall,
|
call: MatrixCall;
|
||||||
|
|
||||||
// Initial value to populate the filter with
|
// Initial value to populate the filter with
|
||||||
initialText: string,
|
initialText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IInviteDialogState {
|
interface IInviteDialogState {
|
||||||
|
@ -356,8 +356,8 @@ interface IInviteDialogState {
|
||||||
consultFirst: boolean;
|
consultFirst: boolean;
|
||||||
|
|
||||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||||
busy: boolean,
|
busy: boolean;
|
||||||
errorText: string,
|
errorText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.InviteDialog")
|
@replaceableComponent("views.dialogs.InviteDialog")
|
||||||
|
|
|
@ -46,19 +46,19 @@ interface ITermsDialogProps {
|
||||||
* Array of [Service, policies] pairs, where policies is the response from the
|
* Array of [Service, policies] pairs, where policies is the response from the
|
||||||
* /terms endpoint for that service
|
* /terms endpoint for that service
|
||||||
*/
|
*/
|
||||||
policiesAndServicePairs: any[],
|
policiesAndServicePairs: any[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* urls that the user has already agreed to
|
* urls that the user has already agreed to
|
||||||
*/
|
*/
|
||||||
agreedUrls?: string[],
|
agreedUrls?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called with:
|
* Called with:
|
||||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||||
* * agreedUrls {string[]} List of agreed URLs
|
* * agreedUrls {string[]} List of agreed URLs
|
||||||
*/
|
*/
|
||||||
onFinished: (success: boolean, agreedUrls?: string[]) => void,
|
onFinished: (success: boolean, agreedUrls?: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
56
src/components/views/elements/BlurhashPlaceholder.tsx
Normal file
56
src/components/views/elements/BlurhashPlaceholder.tsx
Normal file
|
@ -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<IProps> {
|
||||||
|
private canvas: React.RefObject<HTMLCanvasElement> = 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 <canvas height={this.props.height} width={this.props.width} ref={this.canvas} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ interface IProps {
|
||||||
// The minimum number of events needed to trigger summarisation
|
// The minimum number of events needed to trigger summarisation
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
// Whether or not to begin with state.expanded=true
|
// 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
|
// The list of room members for which to show avatars next to the summary
|
||||||
summaryMembers?: RoomMember[];
|
summaryMembers?: RoomMember[];
|
||||||
// The text to show as the summary of this event list
|
// The text to show as the summary of this event list
|
||||||
|
|
|
@ -44,31 +44,31 @@ const ZOOM_COEFFICIENT = 0.0025;
|
||||||
const ZOOM_DISTANCE = 10;
|
const ZOOM_DISTANCE = 10;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
src: string, // the source of the image being displayed
|
src: string; // the source of the image being displayed
|
||||||
name?: string, // the main title ('name') for the image
|
name?: string; // the main title ('name') for the image
|
||||||
link?: string, // the link (if any) applied to the name of the image
|
link?: string; // the link (if any) applied to the name of the image
|
||||||
width?: number, // width of the image src in pixels
|
width?: number; // width of the image src in pixels
|
||||||
height?: number, // height of the image src in pixels
|
height?: number; // height of the image src in pixels
|
||||||
fileSize?: number, // size of the image src in bytes
|
fileSize?: number; // size of the image src in bytes
|
||||||
onFinished(): void, // callback when the lightbox is dismissed
|
onFinished(): void; // callback when the lightbox is dismissed
|
||||||
|
|
||||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
// 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
|
// 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
|
// properties above, which let us use lightboxes to display images which aren't associated
|
||||||
// with events.
|
// with events.
|
||||||
mxEvent: MatrixEvent,
|
mxEvent: MatrixEvent;
|
||||||
permalinkCreator: RoomPermalinkCreator,
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
zoom: number,
|
zoom: number;
|
||||||
minZoom: number,
|
minZoom: number;
|
||||||
maxZoom: number,
|
maxZoom: number;
|
||||||
rotation: number,
|
rotation: number;
|
||||||
translationX: number,
|
translationX: number;
|
||||||
translationY: number,
|
translationY: number;
|
||||||
moving: boolean,
|
moving: boolean;
|
||||||
contextMenuDisplayed: boolean,
|
contextMenuDisplayed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.ImageView")
|
@replaceableComponent("views.elements.ImageView")
|
||||||
|
|
|
@ -30,14 +30,14 @@ function languageMatchesSearchQuery(query, language) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpellCheckLanguagesDropdownIProps {
|
interface SpellCheckLanguagesDropdownIProps {
|
||||||
className: string,
|
className: string;
|
||||||
value: string,
|
value: string;
|
||||||
onOptionChange(language: string),
|
onOptionChange(language: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpellCheckLanguagesDropdownIState {
|
interface SpellCheckLanguagesDropdownIState {
|
||||||
searchQuery: string,
|
searchQuery: string;
|
||||||
languages: any,
|
languages: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
|
@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
categories: ICategory[];
|
categories: ICategory[];
|
||||||
onAnchorClick(id: CategoryKey): void
|
onAnchorClick(id: CategoryKey): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.emojipicker.Header")
|
@replaceableComponent("views.emojipicker.Header")
|
||||||
|
|
|
@ -29,6 +29,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { mediaFromContent } from "../../../customisations/Media";
|
||||||
|
import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
|
||||||
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MImageBody")
|
@replaceableComponent("views.messages.MImageBody")
|
||||||
export default class MImageBody extends React.Component {
|
export default class MImageBody extends React.Component {
|
||||||
|
@ -333,7 +335,8 @@ export default class MImageBody extends React.Component {
|
||||||
infoWidth = content.info.w;
|
infoWidth = content.info.w;
|
||||||
infoHeight = content.info.h;
|
infoHeight = content.info.h;
|
||||||
} else {
|
} 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`.
|
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||||
//
|
//
|
||||||
|
@ -368,12 +371,8 @@ export default class MImageBody extends React.Component {
|
||||||
let placeholder = null;
|
let placeholder = null;
|
||||||
let gifLabel = null;
|
let gifLabel = null;
|
||||||
|
|
||||||
// e2e image hasn't been decrypted yet
|
if (!this.state.imgLoaded) {
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||||
placeholder = <InlineSpinner w={32} h={32} />;
|
|
||||||
} else if (!this.state.imgLoaded) {
|
|
||||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
|
||||||
placeholder = this.getPlaceholder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let showPlaceholder = Boolean(placeholder);
|
let showPlaceholder = Boolean(placeholder);
|
||||||
|
@ -395,7 +394,7 @@ export default class MImageBody extends React.Component {
|
||||||
|
|
||||||
if (!this.state.showImage) {
|
if (!this.state.showImage) {
|
||||||
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
|
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
|
||||||
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) {
|
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
|
||||||
|
@ -411,9 +410,7 @@ export default class MImageBody extends React.Component {
|
||||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||||
maxWidth: infoWidth + "px",
|
maxWidth: infoWidth + "px",
|
||||||
}}>
|
}}>
|
||||||
<div className="mx_MImageBody_thumbnail_spinner">
|
{ placeholder }
|
||||||
{ placeholder }
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,9 +434,12 @@ export default class MImageBody extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
getPlaceholder() {
|
getPlaceholder(width, height) {
|
||||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||||
return null;
|
if (blurhash) return <BlurhashPlaceholder blurhash={blurhash} width={width} height={height} />;
|
||||||
|
return <div className="mx_MImageBody_thumbnail_spinner">
|
||||||
|
<InlineSpinner w={32} h={32} />
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
|
|
|
@ -28,7 +28,7 @@ import EventTileBubble from "./EventTileBubble";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent
|
mxEvent: MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MKeyVerificationRequest")
|
@replaceableComponent("views.messages.MKeyVerificationRequest")
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import MImageBody from './MImageBody';
|
import MImageBody from './MImageBody';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MStickerBody")
|
@replaceableComponent("views.messages.MStickerBody")
|
||||||
export default class MStickerBody extends MImageBody {
|
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
|
// Placeholder to show in place of the sticker image if
|
||||||
// img onLoad hasn't fired yet.
|
// img onLoad hasn't fired yet.
|
||||||
getPlaceholder() {
|
getPlaceholder(width, height) {
|
||||||
|
if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
|
||||||
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
|
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { decode } from "blurhash";
|
||||||
|
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
import { decryptFile } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -23,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { mediaFromContent } from "../../../customisations/Media";
|
||||||
|
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
|
@ -32,11 +35,13 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
decryptedUrl: string|null,
|
decryptedUrl?: string;
|
||||||
decryptedThumbnailUrl: string|null,
|
decryptedThumbnailUrl?: string;
|
||||||
decryptedBlob: Blob|null,
|
decryptedBlob?: Blob;
|
||||||
error: any|null,
|
error?: any;
|
||||||
fetchingData: boolean,
|
fetchingData: boolean;
|
||||||
|
posterLoading: boolean;
|
||||||
|
blurhashUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MVideoBody")
|
@replaceableComponent("views.messages.MVideoBody")
|
||||||
|
@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
decryptedThumbnailUrl: null,
|
decryptedThumbnailUrl: null,
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
error: 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) {
|
if (!fullWidth || !fullHeight) {
|
||||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||||
// log this because it's spammy
|
// log this because it's spammy
|
||||||
|
@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
private getThumbUrl(): string|null {
|
private getThumbUrl(): string|null {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
const media = mediaFromContent(content);
|
const media = mediaFromContent(content);
|
||||||
if (media.isEncrypted) {
|
|
||||||
|
if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
|
} else if (this.state.posterLoading) {
|
||||||
|
return this.state.blurhashUrl;
|
||||||
} else if (media.hasThumbnail) {
|
} else if (media.hasThumbnail) {
|
||||||
return media.thumbnailHttp;
|
return media.thumbnailHttp;
|
||||||
} else {
|
} else {
|
||||||
|
@ -101,18 +111,57 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async componentDidMount() {
|
||||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
|
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
this.loadBlurhash();
|
||||||
|
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
let thumbnailPromise = Promise.resolve(null);
|
let thumbnailPromise = Promise.resolve(null);
|
||||||
if (content.info && content.info.thumbnail_file) {
|
if (content?.info?.thumbnail_file) {
|
||||||
thumbnailPromise = decryptFile(
|
thumbnailPromise = decryptFile(content.info.thumbnail_file)
|
||||||
content.info.thumbnail_file,
|
.then(blob => URL.createObjectURL(blob));
|
||||||
).then(function(blob) {
|
|
||||||
return URL.createObjectURL(blob);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thumbnailUrl = await thumbnailPromise;
|
const thumbnailUrl = await thumbnailPromise;
|
||||||
if (autoplay) {
|
if (autoplay) {
|
||||||
|
@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
let poster = null;
|
let poster = null;
|
||||||
let preload = "metadata";
|
let preload = "metadata";
|
||||||
if (content.info) {
|
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) {
|
if (scale) {
|
||||||
width = Math.floor(content.info.w * scale);
|
width = Math.floor(content.info.w * scale);
|
||||||
height = Math.floor(content.info.h * scale);
|
height = Math.floor(content.info.h * scale);
|
||||||
|
|
|
@ -67,7 +67,7 @@ interface IProps {
|
||||||
replacingEventId?: string;
|
replacingEventId?: string;
|
||||||
|
|
||||||
/* callback for when our widget has loaded */
|
/* callback for when our widget has loaded */
|
||||||
onHeightChanged(): void,
|
onHeightChanged(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -32,32 +32,32 @@ import { throttle } from 'lodash';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// js-sdk room object
|
// js-sdk room object
|
||||||
room: Room,
|
room: Room;
|
||||||
userId: string,
|
userId: string;
|
||||||
showApps: boolean, // Render apps
|
showApps: boolean; // Render apps
|
||||||
|
|
||||||
// maxHeight attribute for the aux panel and the video
|
// maxHeight attribute for the aux panel and the video
|
||||||
// therein
|
// therein
|
||||||
maxHeight: number,
|
maxHeight: number;
|
||||||
|
|
||||||
// a callback which is called when the content of the aux panel changes
|
// 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.
|
// content in a way that is likely to make it change size.
|
||||||
onResize: () => void,
|
onResize: () => void;
|
||||||
fullHeight: boolean,
|
fullHeight: boolean;
|
||||||
|
|
||||||
resizeNotifier: ResizeNotifier,
|
resizeNotifier: ResizeNotifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Counter {
|
interface Counter {
|
||||||
title: string,
|
title: string;
|
||||||
value: number,
|
value: number;
|
||||||
link: string,
|
link: string;
|
||||||
severity: string,
|
severity: string;
|
||||||
stateKey: string,
|
stateKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
counters: Counter[],
|
counters: Counter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.AuxPanel")
|
@replaceableComponent("views.rooms.AuxPanel")
|
||||||
|
|
|
@ -287,10 +287,10 @@ interface IProps {
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
|
||||||
// Symbol of the root node
|
// Symbol of the root node
|
||||||
as?: string
|
as?: string;
|
||||||
|
|
||||||
// whether or not to always show timestamps
|
// whether or not to always show timestamps
|
||||||
alwaysShowTimestamps?: boolean
|
alwaysShowTimestamps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
@ -31,53 +30,64 @@ import RoomName from "../elements/RoomName";
|
||||||
import { PlaceCallType } from "../../../CallHandler";
|
import { PlaceCallType } from "../../../CallHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
|
||||||
|
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||||
|
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||||
|
import { SearchScope } from './SearchBar';
|
||||||
|
|
||||||
|
export interface ISearchInfo {
|
||||||
|
searchTerm: string;
|
||||||
|
searchScope: SearchScope;
|
||||||
|
searchCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
oobData?: IOOBData;
|
||||||
|
inRoom: boolean;
|
||||||
|
onSettingsClick: () => void;
|
||||||
|
onSearchClick: () => void;
|
||||||
|
onForgetClick: () => void;
|
||||||
|
onCallPlaced: (type: PlaceCallType) => void;
|
||||||
|
onAppsClick: () => void;
|
||||||
|
e2eStatus: E2EStatus;
|
||||||
|
appsShown: boolean;
|
||||||
|
searchInfo: ISearchInfo;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.RoomHeader")
|
@replaceableComponent("views.rooms.RoomHeader")
|
||||||
export default class RoomHeader extends React.Component {
|
export default class RoomHeader extends React.Component<IProps> {
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
editing: false,
|
editing: false,
|
||||||
inRoom: false,
|
inRoom: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomStateEvents = (event, state) => {
|
private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => {
|
||||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// redisplay the room name, topic, etc.
|
// redisplay the room name, topic, etc.
|
||||||
this._rateLimitedUpdate();
|
this.rateLimitedUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
_rateLimitedUpdate = throttle(() => {
|
private rateLimitedUpdate = throttle(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500, { leading: true, trailing: true });
|
}, 500, { leading: true, trailing: true });
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
let searchStatus = null;
|
let searchStatus = null;
|
||||||
|
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
|
@ -22,7 +22,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onVisibilityChange?: () => void
|
onVisibilityChange?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||||
|
|
|
@ -21,17 +21,17 @@ import { _t } from "../../../languageHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface ExistingSpellCheckLanguageIProps {
|
interface ExistingSpellCheckLanguageIProps {
|
||||||
language: string,
|
language: string;
|
||||||
onRemoved(language: string),
|
onRemoved(language: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpellCheckLanguagesIProps {
|
interface SpellCheckLanguagesIProps {
|
||||||
languages: Array<string>,
|
languages: Array<string>;
|
||||||
onLanguagesChange(languages: Array<string>),
|
onLanguagesChange(languages: Array<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpellCheckLanguagesIState {
|
interface SpellCheckLanguagesIState {
|
||||||
newLanguage: string,
|
newLanguage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import Field from "../elements/Field";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
avatarDisabled?: boolean;
|
avatarDisabled?: boolean;
|
||||||
name?: string,
|
name?: string;
|
||||||
nameDisabled?: boolean;
|
nameDisabled?: boolean;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
topicDisabled?: boolean;
|
topicDisabled?: boolean;
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
|
import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
feed: CallFeed,
|
feed: CallFeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AudioFeed extends React.Component<IProps> {
|
export default class AudioFeed extends React.Component<IProps> {
|
||||||
|
|
|
@ -35,10 +35,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// The call for us to display
|
// The call for us to display
|
||||||
call: MatrixCall,
|
call: MatrixCall;
|
||||||
|
|
||||||
// Another ongoing call to display information about
|
// Another ongoing call to display information about
|
||||||
secondaryCall?: MatrixCall,
|
secondaryCall?: MatrixCall;
|
||||||
|
|
||||||
// a callback which is called when the content in the CallView changes
|
// a callback which is called when the content in the CallView changes
|
||||||
// in a way that is likely to cause a resize.
|
// in a way that is likely to cause a resize.
|
||||||
|
@ -52,15 +52,15 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
isLocalOnHold: boolean,
|
isLocalOnHold: boolean;
|
||||||
isRemoteOnHold: boolean,
|
isRemoteOnHold: boolean;
|
||||||
micMuted: boolean,
|
micMuted: boolean;
|
||||||
vidMuted: boolean,
|
vidMuted: boolean;
|
||||||
callState: CallState,
|
callState: CallState;
|
||||||
controlsVisible: boolean,
|
controlsVisible: boolean;
|
||||||
showMoreMenu: boolean,
|
showMoreMenu: boolean;
|
||||||
showDialpad: boolean,
|
showDialpad: boolean;
|
||||||
feeds: CallFeed[],
|
feeds: CallFeed[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFullScreenElement() {
|
function getFullScreenElement() {
|
||||||
|
|
|
@ -25,16 +25,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// What room we should display the call for
|
// What room we should display the call for
|
||||||
roomId: string,
|
roomId: string;
|
||||||
|
|
||||||
// maxHeight style attribute for the video panel
|
// maxHeight style attribute for the video panel
|
||||||
maxVideoHeight?: number;
|
maxVideoHeight?: number;
|
||||||
|
|
||||||
resizeNotifier: ResizeNotifier,
|
resizeNotifier: ResizeNotifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
call: MatrixCall,
|
call: MatrixCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -24,9 +24,9 @@ import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
call: MatrixCall,
|
call: MatrixCall;
|
||||||
|
|
||||||
feed: CallFeed,
|
feed: CallFeed;
|
||||||
|
|
||||||
// Whether this call view is for picture-in-picture mode
|
// 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.
|
// 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
|
// a callback which is called when the video element is resized
|
||||||
// due to a change in video metadata
|
// due to a change in video metadata
|
||||||
onResize?: (e: Event) => void,
|
onResize?: (e: Event) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -69,11 +69,11 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
|
||||||
export interface ISecurityCustomisations {
|
export interface ISecurityCustomisations {
|
||||||
examineLoginResponse?: typeof examineLoginResponse;
|
examineLoginResponse?: typeof examineLoginResponse;
|
||||||
persistCredentials?: typeof persistCredentials;
|
persistCredentials?: typeof persistCredentials;
|
||||||
createSecretStorageKey?: typeof createSecretStorageKey,
|
createSecretStorageKey?: typeof createSecretStorageKey;
|
||||||
getSecretStorageKey?: typeof getSecretStorageKey,
|
getSecretStorageKey?: typeof getSecretStorageKey;
|
||||||
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
|
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError;
|
||||||
setupEncryptionNeeded?: typeof setupEncryptionNeeded,
|
setupEncryptionNeeded?: typeof setupEncryptionNeeded;
|
||||||
getDehydrationKey?: typeof getDehydrationKey,
|
getDehydrationKey?: typeof getDehydrationKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When false, disables the post-login UI from showing. If there's
|
* 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
|
* encryption is set up some other way which would circumvent the default
|
||||||
* UI, such as by presenting alternative UI.
|
* 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
|
// A real customisation module will define and export one or more of the
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { ActionPayload } from "../payloads";
|
||||||
import { Action } from "../actions";
|
import { Action } from "../actions";
|
||||||
|
|
||||||
interface IBaseComposerInsertPayload extends ActionPayload {
|
interface IBaseComposerInsertPayload extends ActionPayload {
|
||||||
action: Action.ComposerInsert,
|
action: Action.ComposerInsert;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
|
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
|
||||||
|
|
|
@ -20,34 +20,34 @@ export type ConfettiOptions = {
|
||||||
/**
|
/**
|
||||||
* max confetti count
|
* max confetti count
|
||||||
*/
|
*/
|
||||||
maxCount: number,
|
maxCount: number;
|
||||||
/**
|
/**
|
||||||
* particle animation speed
|
* particle animation speed
|
||||||
*/
|
*/
|
||||||
speed: number,
|
speed: number;
|
||||||
/**
|
/**
|
||||||
* the confetti animation frame interval in milliseconds
|
* 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)
|
* 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
|
* use gradient instead of solid particle color
|
||||||
*/
|
*/
|
||||||
gradient: boolean,
|
gradient: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfettiParticle = {
|
type ConfettiParticle = {
|
||||||
color: string,
|
color: string;
|
||||||
color2: string,
|
color2: string;
|
||||||
x: number,
|
x: number;
|
||||||
y: number,
|
y: number;
|
||||||
diameter: number,
|
diameter: number;
|
||||||
tilt: number,
|
tilt: number;
|
||||||
tiltAngleIncrement: number,
|
tiltAngleIncrement: number;
|
||||||
tiltAngle: number,
|
tiltAngle: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultOptions: ConfettiOptions = {
|
export const DefaultOptions: ConfettiOptions = {
|
||||||
|
|
|
@ -17,15 +17,15 @@ limitations under the License.
|
||||||
import { PerformanceEntryNames } from "./entry-names";
|
import { PerformanceEntryNames } from "./entry-names";
|
||||||
|
|
||||||
interface GetEntriesOptions {
|
interface GetEntriesOptions {
|
||||||
name?: string,
|
name?: string;
|
||||||
type?: string,
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void;
|
type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void;
|
||||||
|
|
||||||
interface PerformanceDataListener {
|
interface PerformanceDataListener {
|
||||||
entryNames?: string[],
|
entryNames?: string[];
|
||||||
callback: PerformanceCallbackFunction
|
callback: PerformanceCallbackFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PerformanceMonitor {
|
export default class PerformanceMonitor {
|
||||||
|
|
|
@ -45,6 +45,16 @@ export interface IThreepidInvite {
|
||||||
inviterName: string;
|
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_";
|
const STORAGE_PREFIX = "mx_threepid_invite_";
|
||||||
|
|
||||||
export default class ThreepidInviteStore extends EventEmitter {
|
export default class ThreepidInviteStore extends EventEmitter {
|
||||||
|
|
|
@ -27,10 +27,10 @@ const TYPING_SERVER_TIMEOUT = 30000;
|
||||||
export default class TypingStore {
|
export default class TypingStore {
|
||||||
private typingStates: {
|
private typingStates: {
|
||||||
[roomId: string]: {
|
[roomId: string]: {
|
||||||
isTyping: boolean,
|
isTyping: boolean;
|
||||||
userTimer: Timer,
|
userTimer: Timer;
|
||||||
serverTimer: Timer,
|
serverTimer: Timer;
|
||||||
},
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -26,8 +26,8 @@ import { WidgetType } from "../widgets/WidgetType";
|
||||||
class WidgetEchoStore extends EventEmitter {
|
class WidgetEchoStore extends EventEmitter {
|
||||||
private roomWidgetEcho: {
|
private roomWidgetEcho: {
|
||||||
[roomId: string]: {
|
[roomId: string]: {
|
||||||
[widgetId: string]: IWidget,
|
[widgetId: string]: IWidget;
|
||||||
},
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -152,7 +152,7 @@ export function objectClone<O extends {}>(obj: O): O {
|
||||||
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
|
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
|
||||||
const obj: {
|
const obj: {
|
||||||
// @ts-ignore - same as return type
|
// @ts-ignore - same as return type
|
||||||
[k: K]: V} = {};
|
[k: K]: V;} = {};
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
// @ts-ignore - same as return type
|
// @ts-ignore - same as return type
|
||||||
obj[e[0]] = e[1];
|
obj[e[0]] = e[1];
|
||||||
|
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
||||||
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
|
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
|
||||||
|
|
||||||
function mockKeyEvent(key: string, modifiers?: {
|
function mockKeyEvent(key: string, modifiers?: {
|
||||||
ctrlKey?: boolean,
|
ctrlKey?: boolean;
|
||||||
altKey?: boolean,
|
altKey?: boolean;
|
||||||
shiftKey?: boolean,
|
shiftKey?: boolean;
|
||||||
metaKey?: boolean
|
metaKey?: boolean;
|
||||||
}): KeyboardEvent {
|
}): KeyboardEvent {
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -26,11 +26,11 @@ import { EventEmitter } from "events";
|
||||||
import sdk from '../../skinned-sdk';
|
import sdk from '../../skinned-sdk';
|
||||||
|
|
||||||
const MessagePanel = sdk.getComponent('structures.MessagePanel');
|
const MessagePanel = sdk.getComponent('structures.MessagePanel');
|
||||||
import {MatrixClientPeg} from '../../../src/MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
|
|
||||||
const test_utils = require('../../test-utils');
|
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 Adapter from "@wojtekmaj/enzyme-adapter-react-17";
|
||||||
import { configure, mount } from "enzyme";
|
import { configure, mount } from "enzyme";
|
||||||
|
@ -72,14 +72,14 @@ class WrappedMessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('MessagePanel', function() {
|
describe('MessagePanel', function() {
|
||||||
const clock = mockclock.clock();
|
let clock = null;
|
||||||
const realSetTimeout = window.setTimeout;
|
const realSetTimeout = window.setTimeout;
|
||||||
const events = mkEvents();
|
const events = mkEvents();
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
test_utils.stubClient();
|
TestUtilsMatrix.stubClient();
|
||||||
client = MatrixClientPeg.get();
|
client = MatrixClientPeg.get();
|
||||||
client.credentials = {userId: '@me:here'};
|
client.credentials = { userId: '@me:here' };
|
||||||
|
|
||||||
// HACK: We assume all settings want to be disabled
|
// HACK: We assume all settings want to be disabled
|
||||||
SettingsStore.getValue = jest.fn((arg) => {
|
SettingsStore.getValue = jest.fn((arg) => {
|
||||||
|
@ -90,14 +90,17 @@ describe('MessagePanel', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
clock.uninstall();
|
if (clock) {
|
||||||
|
clock.uninstall();
|
||||||
|
clock = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function mkEvents() {
|
function mkEvents() {
|
||||||
const events = [];
|
const events = [];
|
||||||
const ts0 = Date.now();
|
const ts0 = Date.now();
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
events.push(test_utils.mkMessage(
|
events.push(TestUtilsMatrix.mkMessage(
|
||||||
{
|
{
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
ts: ts0 + i * 1000,
|
ts: ts0 + i * 1000,
|
||||||
|
@ -111,7 +114,7 @@ describe('MessagePanel', function() {
|
||||||
const events = [];
|
const events = [];
|
||||||
const ts0 = Date.parse('09 May 2004 00:12:00 GMT');
|
const ts0 = Date.parse('09 May 2004 00:12:00 GMT');
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
events.push(test_utils.mkMessage(
|
events.push(TestUtilsMatrix.mkMessage(
|
||||||
{
|
{
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
ts: ts0 + i * 1000,
|
ts: ts0 + i * 1000,
|
||||||
|
@ -120,7 +123,6 @@ describe('MessagePanel', function() {
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// make a collection of events with some member events that should be collapsed
|
// make a collection of events with some member events that should be collapsed
|
||||||
// with a MemberEventListSummary
|
// with a MemberEventListSummary
|
||||||
function mkMelsEvents() {
|
function mkMelsEvents() {
|
||||||
|
@ -128,13 +130,13 @@ describe('MessagePanel', function() {
|
||||||
const ts0 = Date.now();
|
const ts0 = Date.now();
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
events.push(test_utils.mkMessage({
|
events.push(TestUtilsMatrix.mkMessage({
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
ts: ts0 + ++i * 1000,
|
ts: ts0 + ++i * 1000,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (i = 0; i < 10; i++) {
|
for (i = 0; i < 10; i++) {
|
||||||
events.push(test_utils.mkMembership({
|
events.push(TestUtilsMatrix.mkMembership({
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
target: {
|
target: {
|
||||||
userId: "@user:id",
|
userId: "@user:id",
|
||||||
|
@ -151,7 +153,7 @@ describe('MessagePanel', function() {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
events.push(test_utils.mkMessage({
|
events.push(TestUtilsMatrix.mkMessage({
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
ts: ts0 + ++i*1000,
|
ts: ts0 + ++i*1000,
|
||||||
}));
|
}));
|
||||||
|
@ -167,7 +169,7 @@ describe('MessagePanel', function() {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
for (i = 0; i < 10; i++) {
|
for (i = 0; i < 10; i++) {
|
||||||
events.push(test_utils.mkMembership({
|
events.push(TestUtilsMatrix.mkMembership({
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
target: {
|
target: {
|
||||||
userId: "@user:id",
|
userId: "@user:id",
|
||||||
|
@ -189,8 +191,8 @@ describe('MessagePanel', function() {
|
||||||
|
|
||||||
// A list of room creation, encryption, and invite events.
|
// A list of room creation, encryption, and invite events.
|
||||||
function mkCreationEvents() {
|
function mkCreationEvents() {
|
||||||
const mkEvent = test_utils.mkEvent;
|
const mkEvent = TestUtilsMatrix.mkEvent;
|
||||||
const mkMembership = test_utils.mkMembership;
|
const mkMembership = TestUtilsMatrix.mkMembership;
|
||||||
const roomId = "!someroom";
|
const roomId = "!someroom";
|
||||||
const alice = "@alice:example.org";
|
const alice = "@alice:example.org";
|
||||||
const ts0 = Date.now();
|
const ts0 = Date.now();
|
||||||
|
@ -363,8 +365,7 @@ describe('MessagePanel', function() {
|
||||||
|
|
||||||
it('shows a ghost read-marker when the read-marker moves', function(done) {
|
it('shows a ghost read-marker when the read-marker moves', function(done) {
|
||||||
// fake the clock so that we can test the velocity animation.
|
// fake the clock so that we can test the velocity animation.
|
||||||
clock.install();
|
clock = FakeTimers.install();
|
||||||
clock.mockDate();
|
|
||||||
|
|
||||||
const parentDiv = document.createElement('div');
|
const parentDiv = document.createElement('div');
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,10 @@ import MatrixReactTestUtils from 'matrix-react-test-utils';
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import sdk from '../../../skinned-sdk';
|
import sdk from '../../../skinned-sdk';
|
||||||
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
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(
|
const InteractiveAuthDialog = sdk.getComponent(
|
||||||
'views.dialogs.InteractiveAuthDialog',
|
'views.dialogs.InteractiveAuthDialog',
|
||||||
|
@ -33,7 +34,7 @@ describe('InteractiveAuthDialog', function() {
|
||||||
let parentDiv;
|
let parentDiv;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
test_utils.stubClient();
|
TestUtilsMatrix.stubClient();
|
||||||
parentDiv = document.createElement('div');
|
parentDiv = document.createElement('div');
|
||||||
document.body.appendChild(parentDiv);
|
document.body.appendChild(parentDiv);
|
||||||
});
|
});
|
||||||
|
@ -45,11 +46,11 @@ describe('InteractiveAuthDialog', function() {
|
||||||
|
|
||||||
it('Should successfully complete a password flow', function() {
|
it('Should successfully complete a password flow', function() {
|
||||||
const onFinished = jest.fn();
|
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
|
// tell the stub matrixclient to return a real userid
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
client.credentials = {userId: "@user:id"};
|
client.credentials = { userId: "@user:id" };
|
||||||
|
|
||||||
const dlg = ReactDOM.render(
|
const dlg = ReactDOM.render(
|
||||||
<InteractiveAuthDialog
|
<InteractiveAuthDialog
|
||||||
|
@ -57,7 +58,7 @@ describe('InteractiveAuthDialog', function() {
|
||||||
authData={{
|
authData={{
|
||||||
session: "sess",
|
session: "sess",
|
||||||
flows: [
|
flows: [
|
||||||
{"stages": ["m.login.password"]},
|
{ "stages": ["m.login.password"] },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
makeRequest={doRequest}
|
makeRequest={doRequest}
|
||||||
|
@ -105,7 +106,7 @@ describe('InteractiveAuthDialog', function() {
|
||||||
return sleep(1);
|
return sleep(1);
|
||||||
}).then(sleep(1)).then(() => {
|
}).then(sleep(1)).then(() => {
|
||||||
expect(onFinished).toBeCalledTimes(1);
|
expect(onFinished).toBeCalledTimes(1);
|
||||||
expect(onFinished).toBeCalledWith(true, {a: 1});
|
expect(onFinished).toBeCalledWith(true, { a: 1 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,421 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright (c) 2008-2015 Pivotal Labs
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* This is jasmine's implementation of a mock clock, lifted from the depths of
|
|
||||||
* jasmine-core and exposed as a standalone module. The interface is just the
|
|
||||||
* same as that of jasmine.clock. For example:
|
|
||||||
*
|
|
||||||
* var mock_clock = require("../../mock-clock").clock();
|
|
||||||
* mock_clock.install();
|
|
||||||
* setTimeout(function() {
|
|
||||||
* timerCallback();
|
|
||||||
* }, 100);
|
|
||||||
*
|
|
||||||
* expect(timerCallback).not.toHaveBeenCalled();
|
|
||||||
* mock_clock.tick(101);
|
|
||||||
* expect(timerCallback).toHaveBeenCalled();
|
|
||||||
*
|
|
||||||
* mock_clock.uninstall();
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* The reason for C&Ping jasmine's clock here is that jasmine itself is
|
|
||||||
* difficult to webpack, and we don't really want all of it. Sinon also has a
|
|
||||||
* mock-clock implementation, but again, it is difficult to webpack.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const j$ = {};
|
|
||||||
|
|
||||||
j$.Clock = function() {
|
|
||||||
function Clock(global, delayedFunctionSchedulerFactory, mockDate) {
|
|
||||||
let self = this,
|
|
||||||
realTimingFunctions = {
|
|
||||||
setTimeout: global.setTimeout,
|
|
||||||
clearTimeout: global.clearTimeout,
|
|
||||||
setInterval: global.setInterval,
|
|
||||||
clearInterval: global.clearInterval,
|
|
||||||
},
|
|
||||||
fakeTimingFunctions = {
|
|
||||||
setTimeout: setTimeout,
|
|
||||||
clearTimeout: clearTimeout,
|
|
||||||
setInterval: setInterval,
|
|
||||||
clearInterval: clearInterval,
|
|
||||||
},
|
|
||||||
installed = false,
|
|
||||||
delayedFunctionScheduler,
|
|
||||||
timer;
|
|
||||||
|
|
||||||
|
|
||||||
self.install = function() {
|
|
||||||
if(!originalTimingFunctionsIntact()) {
|
|
||||||
throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?');
|
|
||||||
}
|
|
||||||
replace(global, fakeTimingFunctions);
|
|
||||||
timer = fakeTimingFunctions;
|
|
||||||
delayedFunctionScheduler = delayedFunctionSchedulerFactory();
|
|
||||||
installed = true;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.uninstall = function() {
|
|
||||||
delayedFunctionScheduler = null;
|
|
||||||
mockDate.uninstall();
|
|
||||||
replace(global, realTimingFunctions);
|
|
||||||
|
|
||||||
timer = realTimingFunctions;
|
|
||||||
installed = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.withMock = function(closure) {
|
|
||||||
this.install();
|
|
||||||
try {
|
|
||||||
closure();
|
|
||||||
} finally {
|
|
||||||
this.uninstall();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.mockDate = function(initialDate) {
|
|
||||||
mockDate.install(initialDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.setTimeout = function(fn, delay, params) {
|
|
||||||
if (legacyIE()) {
|
|
||||||
if (arguments.length > 2) {
|
|
||||||
throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill');
|
|
||||||
}
|
|
||||||
return timer.setTimeout(fn, delay);
|
|
||||||
}
|
|
||||||
return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.setInterval = function(fn, delay, params) {
|
|
||||||
if (legacyIE()) {
|
|
||||||
if (arguments.length > 2) {
|
|
||||||
throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill');
|
|
||||||
}
|
|
||||||
return timer.setInterval(fn, delay);
|
|
||||||
}
|
|
||||||
return Function.prototype.apply.apply(timer.setInterval, [global, arguments]);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.clearTimeout = function(id) {
|
|
||||||
return Function.prototype.call.apply(timer.clearTimeout, [global, id]);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.clearInterval = function(id) {
|
|
||||||
return Function.prototype.call.apply(timer.clearInterval, [global, id]);
|
|
||||||
};
|
|
||||||
|
|
||||||
self.tick = function(millis) {
|
|
||||||
if (installed) {
|
|
||||||
mockDate.tick(millis);
|
|
||||||
delayedFunctionScheduler.tick(millis);
|
|
||||||
} else {
|
|
||||||
throw new Error('Mock clock is not installed, use jasmine.clock().install()');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return self;
|
|
||||||
|
|
||||||
function originalTimingFunctionsIntact() {
|
|
||||||
return global.setTimeout === realTimingFunctions.setTimeout &&
|
|
||||||
global.clearTimeout === realTimingFunctions.clearTimeout &&
|
|
||||||
global.setInterval === realTimingFunctions.setInterval &&
|
|
||||||
global.clearInterval === realTimingFunctions.clearInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
function legacyIE() {
|
|
||||||
//if these methods are polyfilled, apply will be present
|
|
||||||
return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply;
|
|
||||||
}
|
|
||||||
|
|
||||||
function replace(dest, source) {
|
|
||||||
for (const prop in source) {
|
|
||||||
dest[prop] = source[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTimeout(fn, delay) {
|
|
||||||
return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearTimeout(id) {
|
|
||||||
return delayedFunctionScheduler.removeFunctionWithId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setInterval(fn, interval) {
|
|
||||||
return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearInterval(id) {
|
|
||||||
return delayedFunctionScheduler.removeFunctionWithId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function argSlice(argsObj, n) {
|
|
||||||
return Array.prototype.slice.call(argsObj, n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Clock;
|
|
||||||
}();
|
|
||||||
|
|
||||||
|
|
||||||
j$.DelayedFunctionScheduler = function() {
|
|
||||||
function DelayedFunctionScheduler() {
|
|
||||||
const self = this;
|
|
||||||
const scheduledLookup = [];
|
|
||||||
const scheduledFunctions = {};
|
|
||||||
let currentTime = 0;
|
|
||||||
let delayedFnCount = 0;
|
|
||||||
|
|
||||||
self.tick = function(millis) {
|
|
||||||
millis = millis || 0;
|
|
||||||
const endTime = currentTime + millis;
|
|
||||||
|
|
||||||
runScheduledFunctions(endTime);
|
|
||||||
currentTime = endTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
|
|
||||||
let f;
|
|
||||||
if (typeof(funcToCall) === 'string') {
|
|
||||||
/* jshint evil: true */
|
|
||||||
f = function() { return eval(funcToCall); };
|
|
||||||
/* jshint evil: false */
|
|
||||||
} else {
|
|
||||||
f = funcToCall;
|
|
||||||
}
|
|
||||||
|
|
||||||
millis = millis || 0;
|
|
||||||
timeoutKey = timeoutKey || ++delayedFnCount;
|
|
||||||
runAtMillis = runAtMillis || (currentTime + millis);
|
|
||||||
|
|
||||||
const funcToSchedule = {
|
|
||||||
runAtMillis: runAtMillis,
|
|
||||||
funcToCall: f,
|
|
||||||
recurring: recurring,
|
|
||||||
params: params,
|
|
||||||
timeoutKey: timeoutKey,
|
|
||||||
millis: millis,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (runAtMillis in scheduledFunctions) {
|
|
||||||
scheduledFunctions[runAtMillis].push(funcToSchedule);
|
|
||||||
} else {
|
|
||||||
scheduledFunctions[runAtMillis] = [funcToSchedule];
|
|
||||||
scheduledLookup.push(runAtMillis);
|
|
||||||
scheduledLookup.sort(function(a, b) {
|
|
||||||
return a - b;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeoutKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.removeFunctionWithId = function(timeoutKey) {
|
|
||||||
for (const runAtMillis in scheduledFunctions) {
|
|
||||||
const funcs = scheduledFunctions[runAtMillis];
|
|
||||||
const i = indexOfFirstToPass(funcs, function(func) {
|
|
||||||
return func.timeoutKey === timeoutKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (i > -1) {
|
|
||||||
if (funcs.length === 1) {
|
|
||||||
delete scheduledFunctions[runAtMillis];
|
|
||||||
deleteFromLookup(runAtMillis);
|
|
||||||
} else {
|
|
||||||
funcs.splice(i, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// intervals get rescheduled when executed, so there's never more
|
|
||||||
// than a single scheduled function with a given timeoutKey
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return self;
|
|
||||||
|
|
||||||
function indexOfFirstToPass(array, testFn) {
|
|
||||||
let index = -1;
|
|
||||||
|
|
||||||
for (let i = 0; i < array.length; ++i) {
|
|
||||||
if (testFn(array[i])) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFromLookup(key) {
|
|
||||||
const value = Number(key);
|
|
||||||
const i = indexOfFirstToPass(scheduledLookup, function(millis) {
|
|
||||||
return millis === value;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (i > -1) {
|
|
||||||
scheduledLookup.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reschedule(scheduledFn) {
|
|
||||||
self.scheduleFunction(scheduledFn.funcToCall,
|
|
||||||
scheduledFn.millis,
|
|
||||||
scheduledFn.params,
|
|
||||||
true,
|
|
||||||
scheduledFn.timeoutKey,
|
|
||||||
scheduledFn.runAtMillis + scheduledFn.millis);
|
|
||||||
}
|
|
||||||
|
|
||||||
function forEachFunction(funcsToRun, callback) {
|
|
||||||
for (let i = 0; i < funcsToRun.length; ++i) {
|
|
||||||
callback(funcsToRun[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runScheduledFunctions(endTime) {
|
|
||||||
if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
currentTime = scheduledLookup.shift();
|
|
||||||
|
|
||||||
const funcsToRun = scheduledFunctions[currentTime];
|
|
||||||
delete scheduledFunctions[currentTime];
|
|
||||||
|
|
||||||
forEachFunction(funcsToRun, function(funcToRun) {
|
|
||||||
if (funcToRun.recurring) {
|
|
||||||
reschedule(funcToRun);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
forEachFunction(funcsToRun, function(funcToRun) {
|
|
||||||
funcToRun.funcToCall.apply(null, funcToRun.params || []);
|
|
||||||
});
|
|
||||||
} while (scheduledLookup.length > 0 &&
|
|
||||||
// checking first if we're out of time prevents setTimeout(0)
|
|
||||||
// scheduled in a funcToRun from forcing an extra iteration
|
|
||||||
currentTime !== endTime &&
|
|
||||||
scheduledLookup[0] <= endTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DelayedFunctionScheduler;
|
|
||||||
}();
|
|
||||||
|
|
||||||
|
|
||||||
j$.MockDate = function() {
|
|
||||||
function MockDate(global) {
|
|
||||||
const self = this;
|
|
||||||
let currentTime = 0;
|
|
||||||
|
|
||||||
if (!global || !global.Date) {
|
|
||||||
self.install = function() {};
|
|
||||||
self.tick = function() {};
|
|
||||||
self.uninstall = function() {};
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GlobalDate = global.Date;
|
|
||||||
|
|
||||||
self.install = function(mockDate) {
|
|
||||||
if (mockDate instanceof GlobalDate) {
|
|
||||||
currentTime = mockDate.getTime();
|
|
||||||
} else {
|
|
||||||
currentTime = new GlobalDate().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
global.Date = FakeDate;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.tick = function(millis) {
|
|
||||||
millis = millis || 0;
|
|
||||||
currentTime = currentTime + millis;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.uninstall = function() {
|
|
||||||
currentTime = 0;
|
|
||||||
global.Date = GlobalDate;
|
|
||||||
};
|
|
||||||
|
|
||||||
createDateProperties();
|
|
||||||
|
|
||||||
return self;
|
|
||||||
|
|
||||||
function FakeDate() {
|
|
||||||
switch(arguments.length) {
|
|
||||||
case 0:
|
|
||||||
return new GlobalDate(currentTime);
|
|
||||||
case 1:
|
|
||||||
return new GlobalDate(arguments[0]);
|
|
||||||
case 2:
|
|
||||||
return new GlobalDate(arguments[0], arguments[1]);
|
|
||||||
case 3:
|
|
||||||
return new GlobalDate(arguments[0], arguments[1], arguments[2]);
|
|
||||||
case 4:
|
|
||||||
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]);
|
|
||||||
case 5:
|
|
||||||
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
|
||||||
arguments[4]);
|
|
||||||
case 6:
|
|
||||||
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
|
||||||
arguments[4], arguments[5]);
|
|
||||||
default:
|
|
||||||
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
|
||||||
arguments[4], arguments[5], arguments[6]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDateProperties() {
|
|
||||||
FakeDate.prototype = GlobalDate.prototype;
|
|
||||||
|
|
||||||
FakeDate.now = function() {
|
|
||||||
if (GlobalDate.now) {
|
|
||||||
return currentTime;
|
|
||||||
} else {
|
|
||||||
throw new Error('Browser does not support Date.now()');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
FakeDate.toSource = GlobalDate.toSource;
|
|
||||||
FakeDate.toString = GlobalDate.toString;
|
|
||||||
FakeDate.parse = GlobalDate.parse;
|
|
||||||
FakeDate.UTC = GlobalDate.UTC;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MockDate;
|
|
||||||
}();
|
|
||||||
|
|
||||||
const _clock = new j$.Clock(global, function() { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global));
|
|
||||||
|
|
||||||
export function clock() {
|
|
||||||
return _clock;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -1478,6 +1478,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf"
|
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf"
|
||||||
integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw==
|
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":
|
"@types/counterpart@^0.18.1":
|
||||||
version "0.18.1"
|
version "0.18.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
|
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
|
||||||
|
@ -2190,6 +2195,11 @@ bluebird@^3.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
|
blurhash@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||||
|
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||||
|
|
||||||
boolbase@^1.0.0:
|
boolbase@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||||
|
@ -3222,7 +3232,7 @@ eslint-config-google@^0.14.0:
|
||||||
|
|
||||||
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
|
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
|
||||||
version "0.3.2"
|
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:
|
eslint-plugin-react-hooks@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
|
|
Loading…
Reference in a new issue