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:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||
"lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||
"lint:js": "eslint --max-warnings 0 src test",
|
||||
"lint:types": "tsc --noEmit --jsx react",
|
||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||
"test": "jest",
|
||||
|
@ -54,7 +54,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/commonmark": "^0.27.4",
|
||||
"await-lock": "^2.1.0",
|
||||
"blurhash": "^1.1.3",
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"cheerio": "^1.0.0-rc.9",
|
||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
$timelineImageBorderRadius: 4px;
|
||||
|
||||
.mx_MImageBody {
|
||||
display: block;
|
||||
margin-right: 34px;
|
||||
|
@ -25,7 +27,11 @@ limitations under the License.
|
|||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
|
||||
> canvas {
|
||||
border-radius: $timelineImageBorderRadius;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MImageBody_thumbnail_container {
|
||||
|
|
|
@ -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 {
|
||||
userid: string,
|
||||
protocol: string,
|
||||
fields: ThirdpartyLookupResponseFields,
|
||||
userid: string;
|
||||
protocol: string;
|
||||
fields: ThirdpartyLookupResponseFields;
|
||||
}
|
||||
|
||||
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
||||
|
|
|
@ -17,9 +17,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { encode } from "blurhash";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
|
@ -47,6 +48,10 @@ const MAX_HEIGHT = 600;
|
|||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
||||
|
||||
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
||||
const BLURHASH_X_COMPONENTS = 6;
|
||||
const BLURHASH_Y_COMPONENTS = 6;
|
||||
|
||||
export class UploadCanceledError extends Error {}
|
||||
|
||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||
|
@ -77,6 +82,7 @@ interface IThumbnail {
|
|||
};
|
||||
w: number;
|
||||
h: number;
|
||||
[BLURHASH_FIELD]: string;
|
||||
};
|
||||
thumbnail: Blob;
|
||||
}
|
||||
|
@ -124,7 +130,16 @@ function createThumbnail(
|
|||
const canvas = document.createElement("canvas");
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||
const context = canvas.getContext("2d");
|
||||
context.drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
||||
const blurhash = encode(
|
||||
imageData.data,
|
||||
imageData.width,
|
||||
imageData.height,
|
||||
BLURHASH_X_COMPONENTS,
|
||||
BLURHASH_Y_COMPONENTS,
|
||||
);
|
||||
canvas.toBlob(function(thumbnail) {
|
||||
resolve({
|
||||
info: {
|
||||
|
@ -136,8 +151,9 @@ function createThumbnail(
|
|||
},
|
||||
w: inputWidth,
|
||||
h: inputHeight,
|
||||
[BLURHASH_FIELD]: blurhash,
|
||||
},
|
||||
thumbnail: thumbnail,
|
||||
thumbnail,
|
||||
});
|
||||
}, mimeType);
|
||||
});
|
||||
|
@ -220,7 +236,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load a file into a newly created video element.
|
||||
* Load a file into a newly created video element and pull some strings
|
||||
* in an attempt to guarantee the first frame will be showing.
|
||||
*
|
||||
* @param {File} videoFile The file to load in an video element.
|
||||
* @return {Promise} A promise that resolves with the video image element.
|
||||
|
@ -229,20 +246,25 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
|
|||
return new Promise((resolve, reject) => {
|
||||
// Load the file into an html element
|
||||
const video = document.createElement("video");
|
||||
video.preload = "metadata";
|
||||
video.playsInline = true;
|
||||
video.muted = true;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(ev) {
|
||||
video.src = ev.target.result as string;
|
||||
|
||||
// Once ready, returns its size
|
||||
// Wait until we have enough data to thumbnail the first frame.
|
||||
video.onloadeddata = function() {
|
||||
video.onloadeddata = async function() {
|
||||
resolve(video);
|
||||
video.pause();
|
||||
};
|
||||
video.onerror = function(e) {
|
||||
reject(e);
|
||||
};
|
||||
|
||||
video.src = ev.target.result as string;
|
||||
video.load();
|
||||
video.play();
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
reject(e);
|
||||
|
@ -347,7 +369,7 @@ export function uploadFile(
|
|||
});
|
||||
(prom as IAbortablePromise<any>).abort = () => {
|
||||
canceled = true;
|
||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
||||
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
|
||||
};
|
||||
return prom;
|
||||
} else {
|
||||
|
@ -357,11 +379,11 @@ export function uploadFile(
|
|||
const promise1 = basePromise.then(function(url) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// If the attachment isn't encrypted then include the URL directly.
|
||||
return { "url": url };
|
||||
return { url };
|
||||
});
|
||||
(promise1 as any).abort = () => {
|
||||
canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
||||
matrixClient.cancelUpload(basePromise);
|
||||
};
|
||||
return promise1;
|
||||
}
|
||||
|
@ -373,7 +395,7 @@ export default class ContentMessages {
|
|||
|
||||
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||
const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||
throw e;
|
||||
});
|
||||
|
@ -415,7 +437,7 @@ export default class ContentMessages {
|
|||
|
||||
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
|
||||
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
await this.ensureMediaConfigFetched();
|
||||
await this.ensureMediaConfigFetched(matrixClient);
|
||||
modal.close();
|
||||
}
|
||||
|
||||
|
@ -470,7 +492,7 @@ export default class ContentMessages {
|
|||
return this.inprogress.filter(u => !u.canceled);
|
||||
}
|
||||
|
||||
cancelUpload(promise: Promise<any>) {
|
||||
cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
|
||||
let upload: IUpload;
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
if (this.inprogress[i].promise === promise) {
|
||||
|
@ -480,7 +502,7 @@ export default class ContentMessages {
|
|||
}
|
||||
if (upload) {
|
||||
upload.canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||
matrixClient.cancelUpload(upload.promise);
|
||||
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
||||
}
|
||||
}
|
||||
|
@ -621,11 +643,11 @@ export default class ContentMessages {
|
|||
return true;
|
||||
}
|
||||
|
||||
private ensureMediaConfigFetched() {
|
||||
private ensureMediaConfigFetched(matrixClient: MatrixClient) {
|
||||
if (this.mediaConfig !== null) return;
|
||||
|
||||
console.log("[Media Config] Fetching");
|
||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
return matrixClient.getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
|
|
|
@ -256,7 +256,7 @@ interface ICreateRoomEvent extends IEvent {
|
|||
num_users: number;
|
||||
is_encrypted: boolean;
|
||||
is_public: boolean;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface IJoinRoomEvent extends IEvent {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,16 +16,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as commonmark from 'commonmark';
|
||||
import {escape} from "lodash";
|
||||
import { escape } from "lodash";
|
||||
|
||||
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||
|
||||
// These types of node are definitely text
|
||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||
|
||||
function is_allowed_html_tag(node) {
|
||||
// As far as @types/commonmark is concerned, these are not public, so add them
|
||||
interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer {
|
||||
paragraph: (node: commonmark.Node, entering: boolean) => void;
|
||||
link: (node: commonmark.Node, entering: boolean) => void;
|
||||
html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||
html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
function isAllowedHtmlTag(node: commonmark.Node): boolean {
|
||||
if (node.literal != null &&
|
||||
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
|
||||
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -39,21 +48,12 @@ function is_allowed_html_tag(node) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function html_if_tag_allowed(node) {
|
||||
if (is_allowed_html_tag(node)) {
|
||||
this.lit(node.literal);
|
||||
return;
|
||||
} else {
|
||||
this.lit(escape(node.literal));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if the parse output containing the node
|
||||
* comprises multiple block level elements (ie. lines),
|
||||
* or false if it is only a single line.
|
||||
*/
|
||||
function is_multi_line(node) {
|
||||
function isMultiLine(node: commonmark.Node): boolean {
|
||||
let par = node;
|
||||
while (par.parent) {
|
||||
par = par.parent;
|
||||
|
@ -67,6 +67,9 @@ function is_multi_line(node) {
|
|||
* it's plain text.
|
||||
*/
|
||||
export default class Markdown {
|
||||
private input: string;
|
||||
private parsed: commonmark.Node;
|
||||
|
||||
constructor(input) {
|
||||
this.input = input;
|
||||
|
||||
|
@ -74,7 +77,7 @@ export default class Markdown {
|
|||
this.parsed = parser.parse(this.input);
|
||||
}
|
||||
|
||||
isPlainText() {
|
||||
isPlainText(): boolean {
|
||||
const walker = this.parsed.walker();
|
||||
|
||||
let ev;
|
||||
|
@ -87,7 +90,7 @@ export default class Markdown {
|
|||
// if it's an allowed html tag, we need to render it and therefore
|
||||
// we will need to use HTML. If it's not allowed, it's not HTML since
|
||||
// we'll just be treating it as text.
|
||||
if (is_allowed_html_tag(node)) {
|
||||
if (isAllowedHtmlTag(node)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
|
@ -97,7 +100,7 @@ export default class Markdown {
|
|||
return true;
|
||||
}
|
||||
|
||||
toHTML({ externalLinks = false } = {}) {
|
||||
toHTML({ externalLinks = false } = {}): string {
|
||||
const renderer = new commonmark.HtmlRenderer({
|
||||
safe: false,
|
||||
|
||||
|
@ -107,7 +110,7 @@ export default class Markdown {
|
|||
// block quote ends up all on one line
|
||||
// (https://github.com/vector-im/element-web/issues/3154)
|
||||
softbreak: '<br />',
|
||||
});
|
||||
}) as CommonmarkHtmlRendererInternal;
|
||||
|
||||
// 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
|
||||
|
@ -118,16 +121,16 @@ export default class Markdown {
|
|||
//
|
||||
// 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
|
||||
// bare text: it's a single line of text and so should be
|
||||
// 'inline', rather than unnecessarily wrapped in its own
|
||||
// p tag. If, however, we have multiple nodes, each gets
|
||||
// its own p tag to keep them as separate paragraphs.
|
||||
if (is_multi_line(node)) {
|
||||
real_paragraph.call(this, node, entering);
|
||||
if (isMultiLine(node)) {
|
||||
realParagraph.call(this, node, entering);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -150,19 +153,26 @@ export default class Markdown {
|
|||
}
|
||||
};
|
||||
|
||||
renderer.html_inline = html_if_tag_allowed;
|
||||
renderer.html_inline = function(node: commonmark.Node) {
|
||||
if (isAllowedHtmlTag(node)) {
|
||||
this.lit(node.literal);
|
||||
return;
|
||||
} else {
|
||||
this.lit(escape(node.literal));
|
||||
}
|
||||
};
|
||||
|
||||
renderer.html_block = function(node) {
|
||||
/*
|
||||
renderer.html_block = function(node: commonmark.Node) {
|
||||
/*
|
||||
// as with `paragraph`, we only insert line breaks
|
||||
// if there are multiple lines in the markdown.
|
||||
const isMultiLine = is_multi_line(node);
|
||||
if (isMultiLine) this.cr();
|
||||
*/
|
||||
html_if_tag_allowed.call(this, node);
|
||||
/*
|
||||
*/
|
||||
renderer.html_inline(node);
|
||||
/*
|
||||
if (isMultiLine) this.cr();
|
||||
*/
|
||||
*/
|
||||
};
|
||||
|
||||
return renderer.render(this.parsed);
|
||||
|
@ -177,23 +187,22 @@ export default class Markdown {
|
|||
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
|
||||
* which has no formatting. Otherwise it emits HTML(!).
|
||||
*/
|
||||
toPlaintext() {
|
||||
const renderer = new commonmark.HtmlRenderer({safe: false});
|
||||
const real_paragraph = renderer.paragraph;
|
||||
toPlaintext(): string {
|
||||
const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
|
||||
|
||||
renderer.paragraph = function(node, entering) {
|
||||
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
|
||||
// as with toHTML, only append lines to paragraphs if there are
|
||||
// multiple paragraphs
|
||||
if (is_multi_line(node)) {
|
||||
if (isMultiLine(node)) {
|
||||
if (!entering && node.next) {
|
||||
this.lit('\n\n');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderer.html_block = function(node) {
|
||||
renderer.html_block = function(node: commonmark.Node) {
|
||||
this.lit(node.literal);
|
||||
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
||||
if (isMultiLine(node) && node.next) this.lit('\n\n');
|
||||
};
|
||||
|
||||
return renderer.render(this.parsed);
|
|
@ -42,8 +42,8 @@ let secretStorageBeingAccessed = false;
|
|||
let nonInteractive = false;
|
||||
|
||||
let dehydrationCache: {
|
||||
key?: Uint8Array,
|
||||
keyInfo?: ISecretStorageKeyInfo,
|
||||
key?: Uint8Array;
|
||||
keyInfo?: ISecretStorageKeyInfo;
|
||||
} = {};
|
||||
|
||||
function isCachingAllowed(): boolean {
|
||||
|
|
10
src/Terms.ts
10
src/Terms.ts
|
@ -49,13 +49,13 @@ export interface Policy {
|
|||
}
|
||||
|
||||
export type Policies = {
|
||||
[policy: string]: Policy,
|
||||
[policy: string]: Policy;
|
||||
};
|
||||
|
||||
export type TermsInteractionCallback = (
|
||||
policiesAndServicePairs: {
|
||||
service: Service,
|
||||
policies: Policies,
|
||||
service: Service;
|
||||
policies: Policies;
|
||||
}[],
|
||||
agreedUrls: string[],
|
||||
extraClassNames?: string,
|
||||
|
@ -181,8 +181,8 @@ export async function startTermsFlow(
|
|||
|
||||
export function dialogTermsInteractionCallback(
|
||||
policiesAndServicePairs: {
|
||||
service: Service,
|
||||
policies: { [policy: string]: Policy },
|
||||
service: Service;
|
||||
policies: { [policy: string]: Policy };
|
||||
}[],
|
||||
agreedUrls: string[],
|
||||
extraClassNames?: string,
|
||||
|
|
|
@ -21,8 +21,8 @@ interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
|
|||
className?: string;
|
||||
onScroll?: (event: Event) => void;
|
||||
onWheel?: (event: WheelEvent) => void;
|
||||
style?: React.CSSProperties
|
||||
tabIndex?: number,
|
||||
style?: React.CSSProperties;
|
||||
tabIndex?: number;
|
||||
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
|
|||
interface IProps {
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
resizeNotifier: ResizeNotifier
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -48,7 +48,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
|
|||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||
|
@ -81,14 +81,14 @@ interface IProps {
|
|||
page_type: string;
|
||||
autoJoin: boolean;
|
||||
threepidInvite?: IThreepidInvite;
|
||||
roomOobData?: object;
|
||||
roomOobData?: IOOBData;
|
||||
currentRoomId: string;
|
||||
collapseLhs: boolean;
|
||||
config: {
|
||||
piwik: {
|
||||
policyUrl: string;
|
||||
},
|
||||
[key: string]: any,
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
currentUserId?: string;
|
||||
currentGroupId?: string;
|
||||
|
|
|
@ -204,7 +204,7 @@ interface IState {
|
|||
resizeNotifier: ResizeNotifier;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ready: boolean;
|
||||
threepidInvite?: IThreepidInvite,
|
||||
threepidInvite?: IThreepidInvite;
|
||||
roomOobData?: object;
|
||||
pendingInitialSync?: boolean;
|
||||
justRegistered?: boolean;
|
||||
|
|
|
@ -24,7 +24,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
toasts: ComponentClass[],
|
||||
toasts: ComponentClass[];
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.NonUrgentToastContainer")
|
||||
|
|
|
@ -370,7 +370,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
|
||||
private onFilterChange = (alias: string) => {
|
||||
this.setState({
|
||||
filterString: alias || null,
|
||||
filterString: alias || "",
|
||||
});
|
||||
|
||||
// don't send the request for a little bit,
|
||||
|
@ -389,7 +389,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
private onFilterClear = () => {
|
||||
// update immediately
|
||||
this.setState({
|
||||
filterString: null,
|
||||
filterString: "",
|
||||
}, this.refreshRoomList);
|
||||
|
||||
if (this.filterTimeout) {
|
||||
|
|
|
@ -63,7 +63,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
|||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import { XOR } from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
|
@ -94,22 +94,8 @@ if (DEBUG) {
|
|||
}
|
||||
|
||||
interface IProps {
|
||||
threepidInvite: IThreepidInvite,
|
||||
|
||||
// Any data about the room that would normally come from the homeserver
|
||||
// but has been passed out-of-band, eg. the room name and avatar URL
|
||||
// from an email invite (a workaround for the fact that we can't
|
||||
// get this information from the HS using an email invite).
|
||||
// Fields:
|
||||
// * name (string) The room's name
|
||||
// * avatarUrl (string) The mxc:// avatar URL for the room
|
||||
// * inviterName (string) The display name of the person who
|
||||
// * invited us to the room
|
||||
oobData?: {
|
||||
name?: string;
|
||||
avatarUrl?: string;
|
||||
inviterName?: string;
|
||||
};
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
resizeNotifier: ResizeNotifier;
|
||||
justCreatedOpts?: IOpts;
|
||||
|
@ -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()) {
|
||||
dis.dispatch({ action: 'require_registration' });
|
||||
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 = () => {
|
||||
dis.dispatch({
|
||||
action: 'forget_room',
|
||||
|
@ -2106,7 +2085,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
||||
appsShown={this.state.showApps}
|
||||
|
|
|
@ -58,7 +58,7 @@ export interface ISpaceSummaryRoom {
|
|||
avatar_url?: string;
|
||||
guest_can_join: boolean;
|
||||
name?: string;
|
||||
num_joined_members: number
|
||||
num_joined_members: number;
|
||||
room_id: string;
|
||||
topic?: string;
|
||||
world_readable: boolean;
|
||||
|
|
|
@ -125,7 +125,7 @@ interface IProps {
|
|||
onReadMarkerUpdated?(): void;
|
||||
|
||||
// callback which is called when we wish to paginate the timeline window.
|
||||
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>,
|
||||
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { IUpload } from "../../models/IUpload";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -38,6 +39,8 @@ interface IState {
|
|||
|
||||
@replaceableComponent("structures.UploadBar")
|
||||
export default class UploadBar extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private dispatcherRef: string;
|
||||
private mounted: boolean;
|
||||
|
||||
|
@ -82,7 +85,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
|
|||
|
||||
private onCancelClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
|
||||
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -49,7 +49,7 @@ interface IProps {
|
|||
// for operations like uploading cross-signing keys).
|
||||
onLoggedIn(params: {
|
||||
userId: string;
|
||||
deviceId: string
|
||||
deviceId: string;
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
accessToken: string;
|
||||
|
|
|
@ -49,7 +49,7 @@ interface IProps {
|
|||
fragmentAfterLogin?: string;
|
||||
|
||||
// Called when the SSO login completes
|
||||
onTokenLoginCompleted: () => void,
|
||||
onTokenLoginCompleted: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -52,8 +52,8 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
fieldValid: Partial<Record<LoginField, boolean>>;
|
||||
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone,
|
||||
password: "",
|
||||
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone;
|
||||
password: "";
|
||||
}
|
||||
|
||||
enum LoginField {
|
||||
|
|
|
@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler";
|
|||
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
avatarSize: number;
|
||||
displayBadge?: boolean;
|
||||
forceCount?: boolean;
|
||||
oobData?: object;
|
||||
oobData?: IOOBData;
|
||||
viewAvatarOnClick?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,14 +24,14 @@ import Modal from '../../../Modal';
|
|||
import * as Avatar from '../../../Avatar';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
room?: Room;
|
||||
// TODO: type when js-sdk has types
|
||||
oobData?: any;
|
||||
oobData?: IOOBData;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: ResizeMethod;
|
||||
|
|
|
@ -29,7 +29,7 @@ interface IProps {
|
|||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType;
|
||||
// needed if a group member is specified
|
||||
matrixClient?: MatrixClient,
|
||||
matrixClient?: MatrixClient;
|
||||
action: string; // eg. 'Ban'
|
||||
title: string; // eg. 'Ban this user?'
|
||||
|
||||
|
|
|
@ -70,9 +70,9 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
|||
/* eslint-disable camelcase */
|
||||
|
||||
interface IRecentUser {
|
||||
userId: string,
|
||||
user: RoomMember,
|
||||
lastActive: number,
|
||||
userId: string;
|
||||
user: RoomMember;
|
||||
lastActive: number;
|
||||
}
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
|
@ -330,16 +330,16 @@ interface IInviteDialogProps {
|
|||
|
||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||
// not provided.
|
||||
kind: string,
|
||||
kind: string;
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: string,
|
||||
roomId: string;
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
call: MatrixCall,
|
||||
call: MatrixCall;
|
||||
|
||||
// Initial value to populate the filter with
|
||||
initialText: string,
|
||||
initialText: string;
|
||||
}
|
||||
|
||||
interface IInviteDialogState {
|
||||
|
@ -356,8 +356,8 @@ interface IInviteDialogState {
|
|||
consultFirst: boolean;
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: boolean,
|
||||
errorText: string,
|
||||
busy: boolean;
|
||||
errorText: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.InviteDialog")
|
||||
|
|
|
@ -46,19 +46,19 @@ interface ITermsDialogProps {
|
|||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: any[],
|
||||
policiesAndServicePairs: any[];
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls?: string[],
|
||||
agreedUrls?: string[];
|
||||
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: (success: boolean, agreedUrls?: string[]) => void,
|
||||
onFinished: (success: boolean, agreedUrls?: string[]) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
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
|
||||
threshold?: number;
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded?: boolean,
|
||||
startExpanded?: boolean;
|
||||
// The list of room members for which to show avatars next to the summary
|
||||
summaryMembers?: RoomMember[];
|
||||
// The text to show as the summary of this event list
|
||||
|
|
|
@ -44,31 +44,31 @@ const ZOOM_COEFFICIENT = 0.0025;
|
|||
const ZOOM_DISTANCE = 10;
|
||||
|
||||
interface IProps {
|
||||
src: string, // the source of the image being displayed
|
||||
name?: string, // the main title ('name') for the image
|
||||
link?: string, // the link (if any) applied to the name of the image
|
||||
width?: number, // width of the image src in pixels
|
||||
height?: number, // height of the image src in pixels
|
||||
fileSize?: number, // size of the image src in bytes
|
||||
onFinished(): void, // callback when the lightbox is dismissed
|
||||
src: string; // the source of the image being displayed
|
||||
name?: string; // the main title ('name') for the image
|
||||
link?: string; // the link (if any) applied to the name of the image
|
||||
width?: number; // width of the image src in pixels
|
||||
height?: number; // height of the image src in pixels
|
||||
fileSize?: number; // size of the image src in bytes
|
||||
onFinished(): void; // callback when the lightbox is dismissed
|
||||
|
||||
// the event (if any) that the Image is displaying. Used for event-specific stuff like
|
||||
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit
|
||||
// properties above, which let us use lightboxes to display images which aren't associated
|
||||
// with events.
|
||||
mxEvent: MatrixEvent,
|
||||
permalinkCreator: RoomPermalinkCreator,
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
zoom: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
rotation: number,
|
||||
translationX: number,
|
||||
translationY: number,
|
||||
moving: boolean,
|
||||
contextMenuDisplayed: boolean,
|
||||
zoom: number;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
rotation: number;
|
||||
translationX: number;
|
||||
translationY: number;
|
||||
moving: boolean;
|
||||
contextMenuDisplayed: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.ImageView")
|
||||
|
|
|
@ -30,14 +30,14 @@ function languageMatchesSearchQuery(query, language) {
|
|||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIProps {
|
||||
className: string,
|
||||
value: string,
|
||||
onOptionChange(language: string),
|
||||
className: string;
|
||||
value: string;
|
||||
onOptionChange(language: string);
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIState {
|
||||
searchQuery: string,
|
||||
languages: any,
|
||||
searchQuery: string;
|
||||
languages: any;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
|
||||
|
|
|
@ -25,7 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
interface IProps {
|
||||
categories: ICategory[];
|
||||
onAnchorClick(id: CategoryKey): void
|
||||
onAnchorClick(id: CategoryKey): void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.emojipicker.Header")
|
||||
|
|
|
@ -29,6 +29,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
|
||||
@replaceableComponent("views.messages.MImageBody")
|
||||
export default class MImageBody extends React.Component {
|
||||
|
@ -333,7 +335,8 @@ export default class MImageBody extends React.Component {
|
|||
infoWidth = content.info.w;
|
||||
infoHeight = content.info.h;
|
||||
} else {
|
||||
// Whilst the image loads, display nothing.
|
||||
// Whilst the image loads, display nothing. We also don't display a blurhash image
|
||||
// because we don't really know what size of image we'll end up with.
|
||||
//
|
||||
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||
//
|
||||
|
@ -368,12 +371,8 @@ export default class MImageBody extends React.Component {
|
|||
let placeholder = null;
|
||||
let gifLabel = null;
|
||||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
placeholder = <InlineSpinner w={32} h={32} />;
|
||||
} else if (!this.state.imgLoaded) {
|
||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||
placeholder = this.getPlaceholder();
|
||||
if (!this.state.imgLoaded) {
|
||||
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
let showPlaceholder = Boolean(placeholder);
|
||||
|
@ -395,7 +394,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
if (!this.state.showImage) {
|
||||
img = <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) {
|
||||
|
@ -411,9 +410,7 @@ export default class MImageBody extends React.Component {
|
|||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: infoWidth + "px",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail_spinner">
|
||||
{ placeholder }
|
||||
</div>
|
||||
{ placeholder }
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -437,9 +434,12 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getPlaceholder() {
|
||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
||||
return null;
|
||||
getPlaceholder(width, height) {
|
||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||
if (blurhash) return <BlurhashPlaceholder blurhash={blurhash} width={width} height={height} />;
|
||||
return <div className="mx_MImageBody_thumbnail_spinner">
|
||||
<InlineSpinner w={32} h={32} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
|
|
|
@ -28,7 +28,7 @@ import EventTileBubble from "./EventTileBubble";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationRequest")
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import MImageBody from './MImageBody';
|
||||
import * as sdk from '../../../index';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
|
||||
@replaceableComponent("views.messages.MStickerBody")
|
||||
export default class MStickerBody extends MImageBody {
|
||||
|
@ -41,7 +42,8 @@ export default class MStickerBody extends MImageBody {
|
|||
|
||||
// Placeholder to show in place of the sticker image if
|
||||
// img onLoad hasn't fired yet.
|
||||
getPlaceholder() {
|
||||
getPlaceholder(width, height) {
|
||||
if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
|
||||
return <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 { decode } from "blurhash";
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -23,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -32,11 +35,13 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
decryptedUrl: string|null,
|
||||
decryptedThumbnailUrl: string|null,
|
||||
decryptedBlob: Blob|null,
|
||||
error: any|null,
|
||||
fetchingData: boolean,
|
||||
decryptedUrl?: string;
|
||||
decryptedThumbnailUrl?: string;
|
||||
decryptedBlob?: Blob;
|
||||
error?: any;
|
||||
fetchingData: boolean;
|
||||
posterLoading: boolean;
|
||||
blurhashUrl: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MVideoBody")
|
||||
|
@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
posterLoading: false,
|
||||
blurhashUrl: null,
|
||||
};
|
||||
}
|
||||
|
||||
thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
|
||||
thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) {
|
||||
if (!fullWidth || !fullHeight) {
|
||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||
// log this because it's spammy
|
||||
|
@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
private getThumbUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
|
||||
if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
} else if (this.state.posterLoading) {
|
||||
return this.state.blurhashUrl;
|
||||
} else if (media.hasThumbnail) {
|
||||
return media.thumbnailHttp;
|
||||
} else {
|
||||
|
@ -101,18 +111,57 @@ export default class MVideoBody extends React.PureComponent<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() {
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
this.loadBlurhash();
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info && content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
return URL.createObjectURL(blob);
|
||||
});
|
||||
if (content?.info?.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(content.info.thumbnail_file)
|
||||
.then(blob => URL.createObjectURL(blob));
|
||||
}
|
||||
|
||||
try {
|
||||
const thumbnailUrl = await thumbnailPromise;
|
||||
if (autoplay) {
|
||||
|
@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
let poster = null;
|
||||
let preload = "metadata";
|
||||
if (content.info) {
|
||||
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
|
||||
const scale = this.thumbScale(content.info.w, content.info.h);
|
||||
if (scale) {
|
||||
width = Math.floor(content.info.w * scale);
|
||||
height = Math.floor(content.info.h * scale);
|
||||
|
|
|
@ -67,7 +67,7 @@ interface IProps {
|
|||
replacingEventId?: string;
|
||||
|
||||
/* callback for when our widget has loaded */
|
||||
onHeightChanged(): void,
|
||||
onHeightChanged(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -32,32 +32,32 @@ import { throttle } from 'lodash';
|
|||
|
||||
interface IProps {
|
||||
// js-sdk room object
|
||||
room: Room,
|
||||
userId: string,
|
||||
showApps: boolean, // Render apps
|
||||
room: Room;
|
||||
userId: string;
|
||||
showApps: boolean; // Render apps
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: number,
|
||||
maxHeight: number;
|
||||
|
||||
// a callback which is called when the content of the aux panel changes
|
||||
// content in a way that is likely to make it change size.
|
||||
onResize: () => void,
|
||||
fullHeight: boolean,
|
||||
onResize: () => void;
|
||||
fullHeight: boolean;
|
||||
|
||||
resizeNotifier: ResizeNotifier,
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface Counter {
|
||||
title: string,
|
||||
value: number,
|
||||
link: string,
|
||||
severity: string,
|
||||
stateKey: string,
|
||||
title: string;
|
||||
value: number;
|
||||
link: string;
|
||||
severity: string;
|
||||
stateKey: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
counters: Counter[],
|
||||
counters: Counter[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.AuxPanel")
|
||||
|
|
|
@ -287,10 +287,10 @@ interface IProps {
|
|||
permalinkCreator?: RoomPermalinkCreator;
|
||||
|
||||
// Symbol of the root node
|
||||
as?: string
|
||||
as?: string;
|
||||
|
||||
// whether or not to always show timestamps
|
||||
alwaysShowTimestamps?: boolean
|
||||
alwaysShowTimestamps?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
|
@ -31,53 +30,64 @@ import RoomName from "../elements/RoomName";
|
|||
import { PlaceCallType } from "../../../CallHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { throttle } from 'lodash';
|
||||
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||
import { SearchScope } from './SearchBar';
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchTerm: string;
|
||||
searchScope: SearchScope;
|
||||
searchCount: number;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
oobData?: IOOBData;
|
||||
inRoom: boolean;
|
||||
onSettingsClick: () => void;
|
||||
onSearchClick: () => void;
|
||||
onForgetClick: () => void;
|
||||
onCallPlaced: (type: PlaceCallType) => void;
|
||||
onAppsClick: () => void;
|
||||
e2eStatus: E2EStatus;
|
||||
appsShown: boolean;
|
||||
searchInfo: ISearchInfo;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.RoomHeader")
|
||||
export default class RoomHeader extends React.Component {
|
||||
static propTypes = {
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
inRoom: PropTypes.bool,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
e2eStatus: PropTypes.string,
|
||||
onAppsClick: PropTypes.func,
|
||||
appsShown: PropTypes.bool,
|
||||
onCallPlaced: PropTypes.func, // (PlaceCallType) => void;
|
||||
};
|
||||
|
||||
export default class RoomHeader extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
editing: false,
|
||||
inRoom: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomStateEvents = (event, state) => {
|
||||
private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => {
|
||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// redisplay the room name, topic, etc.
|
||||
this._rateLimitedUpdate();
|
||||
this.rateLimitedUpdate();
|
||||
};
|
||||
|
||||
_rateLimitedUpdate = throttle(() => {
|
||||
private rateLimitedUpdate = throttle(() => {
|
||||
this.forceUpdate();
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
let searchStatus = null;
|
||||
|
||||
// don't display the search count until the search completes and
|
|
@ -22,7 +22,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
|||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
onVisibilityChange?: () => void
|
||||
onVisibilityChange?: () => void;
|
||||
}
|
||||
|
||||
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
|
|
|
@ -21,17 +21,17 @@ import { _t } from "../../../languageHandler";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface ExistingSpellCheckLanguageIProps {
|
||||
language: string,
|
||||
onRemoved(language: string),
|
||||
language: string;
|
||||
onRemoved(language: string);
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIProps {
|
||||
languages: Array<string>,
|
||||
onLanguagesChange(languages: Array<string>),
|
||||
languages: Array<string>;
|
||||
onLanguagesChange(languages: Array<string>);
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIState {
|
||||
newLanguage: string,
|
||||
newLanguage: string;
|
||||
}
|
||||
|
||||
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||
|
|
|
@ -23,7 +23,7 @@ import Field from "../elements/Field";
|
|||
interface IProps {
|
||||
avatarUrl?: string;
|
||||
avatarDisabled?: boolean;
|
||||
name?: string,
|
||||
name?: string;
|
||||
nameDisabled?: boolean;
|
||||
topic?: string;
|
||||
topicDisabled?: boolean;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { logger } from 'matrix-js-sdk/src/logger';
|
|||
import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
|
||||
|
||||
interface IProps {
|
||||
feed: CallFeed,
|
||||
feed: CallFeed;
|
||||
}
|
||||
|
||||
export default class AudioFeed extends React.Component<IProps> {
|
||||
|
|
|
@ -35,10 +35,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
interface IProps {
|
||||
// The call for us to display
|
||||
call: MatrixCall,
|
||||
call: MatrixCall;
|
||||
|
||||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall,
|
||||
secondaryCall?: MatrixCall;
|
||||
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
|
@ -52,15 +52,15 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
isLocalOnHold: boolean,
|
||||
isRemoteOnHold: boolean,
|
||||
micMuted: boolean,
|
||||
vidMuted: boolean,
|
||||
callState: CallState,
|
||||
controlsVisible: boolean,
|
||||
showMoreMenu: boolean,
|
||||
showDialpad: boolean,
|
||||
feeds: CallFeed[],
|
||||
isLocalOnHold: boolean;
|
||||
isRemoteOnHold: boolean;
|
||||
micMuted: boolean;
|
||||
vidMuted: boolean;
|
||||
callState: CallState;
|
||||
controlsVisible: boolean;
|
||||
showMoreMenu: boolean;
|
||||
showDialpad: boolean;
|
||||
feeds: CallFeed[];
|
||||
}
|
||||
|
||||
function getFullScreenElement() {
|
||||
|
|
|
@ -25,16 +25,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|||
|
||||
interface IProps {
|
||||
// What room we should display the call for
|
||||
roomId: string,
|
||||
roomId: string;
|
||||
|
||||
// maxHeight style attribute for the video panel
|
||||
maxVideoHeight?: number;
|
||||
|
||||
resizeNotifier: ResizeNotifier,
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
call: MatrixCall,
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -24,9 +24,9 @@ import MemberAvatar from "../avatars/MemberAvatar";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall,
|
||||
call: MatrixCall;
|
||||
|
||||
feed: CallFeed,
|
||||
feed: CallFeed;
|
||||
|
||||
// Whether this call view is for picture-in-picture mode
|
||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||
|
@ -36,7 +36,7 @@ interface IProps {
|
|||
|
||||
// a callback which is called when the video element is resized
|
||||
// due to a change in video metadata
|
||||
onResize?: (e: Event) => void,
|
||||
onResize?: (e: Event) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -69,11 +69,11 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
|
|||
export interface ISecurityCustomisations {
|
||||
examineLoginResponse?: typeof examineLoginResponse;
|
||||
persistCredentials?: typeof persistCredentials;
|
||||
createSecretStorageKey?: typeof createSecretStorageKey,
|
||||
getSecretStorageKey?: typeof getSecretStorageKey,
|
||||
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
|
||||
setupEncryptionNeeded?: typeof setupEncryptionNeeded,
|
||||
getDehydrationKey?: typeof getDehydrationKey,
|
||||
createSecretStorageKey?: typeof createSecretStorageKey;
|
||||
getSecretStorageKey?: typeof getSecretStorageKey;
|
||||
catchAccessSecretStorageError?: typeof catchAccessSecretStorageError;
|
||||
setupEncryptionNeeded?: typeof setupEncryptionNeeded;
|
||||
getDehydrationKey?: typeof getDehydrationKey;
|
||||
|
||||
/**
|
||||
* When false, disables the post-login UI from showing. If there's
|
||||
|
@ -83,7 +83,7 @@ export interface ISecurityCustomisations {
|
|||
* encryption is set up some other way which would circumvent the default
|
||||
* UI, such as by presenting alternative UI.
|
||||
*/
|
||||
SHOW_ENCRYPTION_SETUP_UI?: boolean, // default true
|
||||
SHOW_ENCRYPTION_SETUP_UI?: boolean; // default true
|
||||
}
|
||||
|
||||
// A real customisation module will define and export one or more of the
|
||||
|
|
|
@ -20,7 +20,7 @@ import { ActionPayload } from "../payloads";
|
|||
import { Action } from "../actions";
|
||||
|
||||
interface IBaseComposerInsertPayload extends ActionPayload {
|
||||
action: Action.ComposerInsert,
|
||||
action: Action.ComposerInsert;
|
||||
}
|
||||
|
||||
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
|
||||
|
|
|
@ -20,34 +20,34 @@ export type ConfettiOptions = {
|
|||
/**
|
||||
* max confetti count
|
||||
*/
|
||||
maxCount: number,
|
||||
maxCount: number;
|
||||
/**
|
||||
* particle animation speed
|
||||
*/
|
||||
speed: number,
|
||||
speed: number;
|
||||
/**
|
||||
* the confetti animation frame interval in milliseconds
|
||||
*/
|
||||
frameInterval: number,
|
||||
frameInterval: number;
|
||||
/**
|
||||
* the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
|
||||
*/
|
||||
alpha: number,
|
||||
alpha: number;
|
||||
/**
|
||||
* use gradient instead of solid particle color
|
||||
*/
|
||||
gradient: boolean,
|
||||
gradient: boolean;
|
||||
};
|
||||
|
||||
type ConfettiParticle = {
|
||||
color: string,
|
||||
color2: string,
|
||||
x: number,
|
||||
y: number,
|
||||
diameter: number,
|
||||
tilt: number,
|
||||
tiltAngleIncrement: number,
|
||||
tiltAngle: number,
|
||||
color: string;
|
||||
color2: string;
|
||||
x: number;
|
||||
y: number;
|
||||
diameter: number;
|
||||
tilt: number;
|
||||
tiltAngleIncrement: number;
|
||||
tiltAngle: number;
|
||||
};
|
||||
|
||||
export const DefaultOptions: ConfettiOptions = {
|
||||
|
|
|
@ -17,15 +17,15 @@ limitations under the License.
|
|||
import { PerformanceEntryNames } from "./entry-names";
|
||||
|
||||
interface GetEntriesOptions {
|
||||
name?: string,
|
||||
type?: string,
|
||||
name?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void;
|
||||
|
||||
interface PerformanceDataListener {
|
||||
entryNames?: string[],
|
||||
callback: PerformanceCallbackFunction
|
||||
entryNames?: string[];
|
||||
callback: PerformanceCallbackFunction;
|
||||
}
|
||||
|
||||
export default class PerformanceMonitor {
|
||||
|
|
|
@ -45,6 +45,16 @@ export interface IThreepidInvite {
|
|||
inviterName: string;
|
||||
}
|
||||
|
||||
// Any data about the room that would normally come from the homeserver
|
||||
// but has been passed out-of-band, eg. the room name and avatar URL
|
||||
// from an email invite (a workaround for the fact that we can't
|
||||
// get this information from the HS using an email invite).
|
||||
export interface IOOBData {
|
||||
name?: string; // The room's name
|
||||
avatarUrl?: string; // The mxc:// avatar URL for the room
|
||||
inviterName?: string; // The display name of the person who invited us to the room
|
||||
}
|
||||
|
||||
const STORAGE_PREFIX = "mx_threepid_invite_";
|
||||
|
||||
export default class ThreepidInviteStore extends EventEmitter {
|
||||
|
|
|
@ -27,10 +27,10 @@ const TYPING_SERVER_TIMEOUT = 30000;
|
|||
export default class TypingStore {
|
||||
private typingStates: {
|
||||
[roomId: string]: {
|
||||
isTyping: boolean,
|
||||
userTimer: Timer,
|
||||
serverTimer: Timer,
|
||||
},
|
||||
isTyping: boolean;
|
||||
userTimer: Timer;
|
||||
serverTimer: Timer;
|
||||
};
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
|
|
@ -26,8 +26,8 @@ import { WidgetType } from "../widgets/WidgetType";
|
|||
class WidgetEchoStore extends EventEmitter {
|
||||
private roomWidgetEcho: {
|
||||
[roomId: string]: {
|
||||
[widgetId: string]: IWidget,
|
||||
},
|
||||
[widgetId: string]: IWidget;
|
||||
};
|
||||
};
|
||||
|
||||
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} {
|
||||
const obj: {
|
||||
// @ts-ignore - same as return type
|
||||
[k: K]: V} = {};
|
||||
[k: K]: V;} = {};
|
||||
for (const e of entries) {
|
||||
// @ts-ignore - same as return type
|
||||
obj[e[0]] = e[1];
|
||||
|
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
|||
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
|
||||
|
||||
function mockKeyEvent(key: string, modifiers?: {
|
||||
ctrlKey?: boolean,
|
||||
altKey?: boolean,
|
||||
shiftKey?: boolean,
|
||||
metaKey?: boolean
|
||||
ctrlKey?: boolean;
|
||||
altKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
}): KeyboardEvent {
|
||||
return {
|
||||
key,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -26,11 +26,11 @@ import { EventEmitter } from "events";
|
|||
import sdk from '../../skinned-sdk';
|
||||
|
||||
const MessagePanel = sdk.getComponent('structures.MessagePanel');
|
||||
import {MatrixClientPeg} from '../../../src/MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
const test_utils = require('../../test-utils');
|
||||
const mockclock = require('../../mock-clock');
|
||||
const TestUtilsMatrix = require('../../test-utils');
|
||||
import FakeTimers from '@sinonjs/fake-timers';
|
||||
|
||||
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
|
||||
import { configure, mount } from "enzyme";
|
||||
|
@ -72,14 +72,14 @@ class WrappedMessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
describe('MessagePanel', function() {
|
||||
const clock = mockclock.clock();
|
||||
let clock = null;
|
||||
const realSetTimeout = window.setTimeout;
|
||||
const events = mkEvents();
|
||||
|
||||
beforeEach(function() {
|
||||
test_utils.stubClient();
|
||||
TestUtilsMatrix.stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
client.credentials = {userId: '@me:here'};
|
||||
client.credentials = { userId: '@me:here' };
|
||||
|
||||
// HACK: We assume all settings want to be disabled
|
||||
SettingsStore.getValue = jest.fn((arg) => {
|
||||
|
@ -90,14 +90,17 @@ describe('MessagePanel', function() {
|
|||
});
|
||||
|
||||
afterEach(function() {
|
||||
clock.uninstall();
|
||||
if (clock) {
|
||||
clock.uninstall();
|
||||
clock = null;
|
||||
}
|
||||
});
|
||||
|
||||
function mkEvents() {
|
||||
const events = [];
|
||||
const ts0 = Date.now();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
events.push(test_utils.mkMessage(
|
||||
events.push(TestUtilsMatrix.mkMessage(
|
||||
{
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
ts: ts0 + i * 1000,
|
||||
|
@ -111,7 +114,7 @@ describe('MessagePanel', function() {
|
|||
const events = [];
|
||||
const ts0 = Date.parse('09 May 2004 00:12:00 GMT');
|
||||
for (let i = 0; i < 10; i++) {
|
||||
events.push(test_utils.mkMessage(
|
||||
events.push(TestUtilsMatrix.mkMessage(
|
||||
{
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
ts: ts0 + i * 1000,
|
||||
|
@ -120,7 +123,6 @@ describe('MessagePanel', function() {
|
|||
return events;
|
||||
}
|
||||
|
||||
|
||||
// make a collection of events with some member events that should be collapsed
|
||||
// with a MemberEventListSummary
|
||||
function mkMelsEvents() {
|
||||
|
@ -128,13 +130,13 @@ describe('MessagePanel', function() {
|
|||
const ts0 = Date.now();
|
||||
|
||||
let i = 0;
|
||||
events.push(test_utils.mkMessage({
|
||||
events.push(TestUtilsMatrix.mkMessage({
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
ts: ts0 + ++i * 1000,
|
||||
}));
|
||||
|
||||
for (i = 0; i < 10; i++) {
|
||||
events.push(test_utils.mkMembership({
|
||||
events.push(TestUtilsMatrix.mkMembership({
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
target: {
|
||||
userId: "@user:id",
|
||||
|
@ -151,7 +153,7 @@ describe('MessagePanel', function() {
|
|||
}));
|
||||
}
|
||||
|
||||
events.push(test_utils.mkMessage({
|
||||
events.push(TestUtilsMatrix.mkMessage({
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
ts: ts0 + ++i*1000,
|
||||
}));
|
||||
|
@ -167,7 +169,7 @@ describe('MessagePanel', function() {
|
|||
let i = 0;
|
||||
|
||||
for (i = 0; i < 10; i++) {
|
||||
events.push(test_utils.mkMembership({
|
||||
events.push(TestUtilsMatrix.mkMembership({
|
||||
event: true, room: "!room:id", user: "@user:id",
|
||||
target: {
|
||||
userId: "@user:id",
|
||||
|
@ -189,8 +191,8 @@ describe('MessagePanel', function() {
|
|||
|
||||
// A list of room creation, encryption, and invite events.
|
||||
function mkCreationEvents() {
|
||||
const mkEvent = test_utils.mkEvent;
|
||||
const mkMembership = test_utils.mkMembership;
|
||||
const mkEvent = TestUtilsMatrix.mkEvent;
|
||||
const mkMembership = TestUtilsMatrix.mkMembership;
|
||||
const roomId = "!someroom";
|
||||
const alice = "@alice:example.org";
|
||||
const ts0 = Date.now();
|
||||
|
@ -363,8 +365,7 @@ describe('MessagePanel', function() {
|
|||
|
||||
it('shows a ghost read-marker when the read-marker moves', function(done) {
|
||||
// fake the clock so that we can test the velocity animation.
|
||||
clock.install();
|
||||
clock.mockDate();
|
||||
clock = FakeTimers.install();
|
||||
|
||||
const parentDiv = document.createElement('div');
|
||||
|
||||
|
|
|
@ -21,9 +21,10 @@ import MatrixReactTestUtils from 'matrix-react-test-utils';
|
|||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
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(
|
||||
'views.dialogs.InteractiveAuthDialog',
|
||||
|
@ -33,7 +34,7 @@ describe('InteractiveAuthDialog', function() {
|
|||
let parentDiv;
|
||||
|
||||
beforeEach(function() {
|
||||
test_utils.stubClient();
|
||||
TestUtilsMatrix.stubClient();
|
||||
parentDiv = document.createElement('div');
|
||||
document.body.appendChild(parentDiv);
|
||||
});
|
||||
|
@ -45,11 +46,11 @@ describe('InteractiveAuthDialog', function() {
|
|||
|
||||
it('Should successfully complete a password flow', function() {
|
||||
const onFinished = jest.fn();
|
||||
const doRequest = jest.fn().mockResolvedValue({a: 1});
|
||||
const doRequest = jest.fn().mockResolvedValue({ a: 1 });
|
||||
|
||||
// tell the stub matrixclient to return a real userid
|
||||
const client = MatrixClientPeg.get();
|
||||
client.credentials = {userId: "@user:id"};
|
||||
client.credentials = { userId: "@user:id" };
|
||||
|
||||
const dlg = ReactDOM.render(
|
||||
<InteractiveAuthDialog
|
||||
|
@ -57,7 +58,7 @@ describe('InteractiveAuthDialog', function() {
|
|||
authData={{
|
||||
session: "sess",
|
||||
flows: [
|
||||
{"stages": ["m.login.password"]},
|
||||
{ "stages": ["m.login.password"] },
|
||||
],
|
||||
}}
|
||||
makeRequest={doRequest}
|
||||
|
@ -105,7 +106,7 @@ describe('InteractiveAuthDialog', function() {
|
|||
return sleep(1);
|
||||
}).then(sleep(1)).then(() => {
|
||||
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"
|
||||
integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw==
|
||||
|
||||
"@types/commonmark@^0.27.4":
|
||||
version "0.27.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/commonmark/-/commonmark-0.27.4.tgz#8f42990e5cf3b6b95bd99eaa452e157aab679b82"
|
||||
integrity sha512-7koSjp08QxKoS1/+3T15+kD7+vqOUvZRHvM8PutF3Xsk5aAEkdlIGRsHJ3/XsC3izoqTwBdRW/vH7rzCKkIicA==
|
||||
|
||||
"@types/counterpart@^0.18.1":
|
||||
version "0.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
|
||||
|
@ -2190,6 +2195,11 @@ bluebird@^3.5.0:
|
|||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
blurhash@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
|
@ -3222,7 +3232,7 @@ eslint-config-google@^0.14.0:
|
|||
|
||||
"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
|
||||
version "0.3.2"
|
||||
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/28d392822533a7468be0dd806d0a4ba573a45d74"
|
||||
resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/e8197938dca66849ffdac4baca7c05275df12835"
|
||||
|
||||
eslint-plugin-react-hooks@^4.2.0:
|
||||
version "4.2.0"
|
||||
|
|
Loading…
Reference in a new issue