Merge branch 'develop' into t3chguy/ts/11

This commit is contained in:
Michael Telatynski 2021-07-02 11:03:54 +01:00 committed by GitHub
commit 82119a08f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 467 additions and 766 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ interface IProps {
} }
interface IState { interface IState {
toasts: ComponentClass[], toasts: ComponentClass[];
} }
@replaceableComponent("structures.NonUrgentToastContainer") @replaceableComponent("structures.NonUrgentToastContainer")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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?'

View file

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

View file

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

View 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} />;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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" />;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
} }
/* /*

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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