Move from browser-request to fetch (#9345)

This commit is contained in:
Michael Telatynski 2022-10-12 18:59:07 +01:00 committed by GitHub
parent ae883bb94b
commit 8b54be6f48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1474 additions and 607 deletions

View file

@ -1,81 +0,0 @@
/*
Copyright 2022 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.
*/
const en = require("../src/i18n/strings/en_EN");
const de = require("../src/i18n/strings/de_DE");
const lv = {
"Save": "Saglabāt",
"Uploading %(filename)s and %(count)s others|one": "Качване на %(filename)s и %(count)s друг",
};
function weblateToCounterpart(inTrs) {
const outTrs = {};
for (const key of Object.keys(inTrs)) {
const keyParts = key.split('|', 2);
if (keyParts.length === 2) {
let obj = outTrs[keyParts[0]];
if (obj === undefined) {
obj = outTrs[keyParts[0]] = {};
} else if (typeof obj === "string") {
// This is a transitional edge case if a string went from singular to pluralised and both still remain
// in the translation json file. Use the singular translation as `other` and merge pluralisation atop.
obj = outTrs[keyParts[0]] = {
"other": inTrs[key],
};
console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]);
}
obj[keyParts[1]] = inTrs[key];
} else {
outTrs[key] = inTrs[key];
}
}
return outTrs;
}
// Mock the browser-request for the languageHandler tests to return
// Fake languages.json containing references to en_EN, de_DE and lv
// en_EN.json
// de_DE.json
// lv.json - mock version with few translations, used to test fallback translation
module.exports = jest.fn((opts, cb) => {
const url = opts.url || opts.uri;
if (url && url.endsWith("languages.json")) {
cb(undefined, { status: 200 }, JSON.stringify({
"en": {
"fileName": "en_EN.json",
"label": "English",
},
"de": {
"fileName": "de_DE.json",
"label": "German",
},
"lv": {
"fileName": "lv.json",
"label": "Latvian",
},
}));
} else if (url && url.endsWith("en_EN.json")) {
cb(undefined, { status: 200 }, JSON.stringify(weblateToCounterpart(en)));
} else if (url && url.endsWith("de_DE.json")) {
cb(undefined, { status: 200 }, JSON.stringify(weblateToCounterpart(de)));
} else if (url && url.endsWith("lv.json")) {
cb(undefined, { status: 200 }, JSON.stringify(weblateToCounterpart(lv)));
} else {
cb(true, { status: 404 }, "");
}
});

View file

@ -124,7 +124,8 @@ Cypress.Commands.add("startDM", (name: string) => {
cy.get(".mx_BasicMessageComposer_input")
.should("have.focus")
.type("Hey!{enter}");
cy.contains(".mx_EventTile_body", "Hey!");
// The DM room is created at this point, this can take a little bit of time
cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 });
cy.contains(".mx_RoomSublist[aria-label=People]", name);
});
@ -217,7 +218,7 @@ describe("Spotlight", () => {
it("should find joined rooms", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightSearch().clear().type(room1Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).click();
@ -231,7 +232,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room1Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).should("contain", "View");
@ -246,7 +247,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room2Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room2Name);
cy.spotlightResults().eq(0).should("contain", "Join");
@ -262,7 +263,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room3Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room3Name);
cy.spotlightResults().eq(0).should("contain", "View");
@ -301,7 +302,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot1Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot1Name);
cy.spotlightResults().eq(0).click();
@ -314,7 +315,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click();
@ -331,7 +332,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click();
@ -345,7 +346,7 @@ describe("Spotlight", () => {
.type("Hey!{enter}");
// Assert DM exists by checking for the first message and the room being in the room list
cy.contains(".mx_EventTile_body", "Hey!");
cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 });
cy.get(".mx_RoomSublist[aria-label=People]").should("contain", bot2Name);
// Invite BotBob into existing DM with ByteBot
@ -409,7 +410,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
@ -431,7 +432,7 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot1Name);
cy.wait(1000); // wait for the dialog code to settle
cy.wait(3000); // wait for the dialog code to settle
cy.get(".mx_Spinner").should("not.exist");
cy.spotlightResults().should("have.length", 1);
});

View file

@ -91,11 +91,11 @@ describe("Timeline", () => {
describe("useOnlyCurrentProfiles", () => {
beforeEach(() => {
cy.uploadContent(OLD_AVATAR).then((url) => {
cy.uploadContent(OLD_AVATAR).then(({ content_uri: url }) => {
oldAvatarUrl = url;
cy.setAvatarUrl(url);
});
cy.uploadContent(NEW_AVATAR).then((url) => {
cy.uploadContent(NEW_AVATAR).then(({ content_uri: url }) => {
newAvatarUrl = url;
});
});
@ -271,7 +271,7 @@ describe("Timeline", () => {
cy.get(".mx_RoomHeader_searchButton").click();
cy.get(".mx_SearchBar_input input").type("Message{enter}");
cy.get(".mx_EventTile:not(.mx_EventTile_contextual)").find(".mx_EventTile_searchHighlight").should("exist");
cy.get(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight").should("exist");
cy.get(".mx_RoomView_searchResultsPanel").percySnapshotElement("Highlighted search results");
});

View file

@ -16,8 +16,6 @@ limitations under the License.
/// <reference types="cypress" />
import request from "browser-request";
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { SynapseInstance } from "../plugins/synapsedocker";
import Chainable = Cypress.Chainable;
@ -86,7 +84,6 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
userId: credentials.userId,
deviceId: credentials.deviceId,
accessToken: credentials.accessToken,
request,
store: new win.matrixcs.MemoryStore(),
scheduler: new win.matrixcs.MatrixScheduler(),
cryptoStore: new win.matrixcs.MemoryCryptoStore(),

View file

@ -16,9 +16,8 @@ limitations under the License.
/// <reference types="cypress" />
import type { FileType, UploadContentResponseType } from "matrix-js-sdk/src/http-api";
import type { IAbortablePromise } from "matrix-js-sdk/src/@types/partials";
import type { ICreateRoomOpts, ISendEventResponse, IUploadOpts } from "matrix-js-sdk/src/@types/requests";
import type { FileType, Upload, UploadOpts } from "matrix-js-sdk/src/http-api";
import type { ICreateRoomOpts, ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { IContent } from "matrix-js-sdk/src/models/event";
@ -90,10 +89,10 @@ declare global {
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a a Buffer, String or ReadStream.
*/
uploadContent<O extends IUploadOpts>(
uploadContent(
file: FileType,
opts?: O,
): IAbortablePromise<UploadContentResponseType<O>>;
opts?: UploadOpts,
): Chainable<Awaited<Upload["promise"]>>;
/**
* Turn an MXC URL into an HTTP one. <strong>This method is experimental and
* may change.</strong>
@ -203,9 +202,9 @@ Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => {
});
});
Cypress.Commands.add("uploadContent", (file: FileType): Chainable<{}> => {
Cypress.Commands.add("uploadContent", (file: FileType, opts?: UploadOpts): Chainable<Awaited<Upload["promise"]>> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.uploadContent(file);
return cli.uploadContent(file, opts);
});
});

View file

@ -65,7 +65,6 @@
"@types/ua-parser-js": "^0.7.36",
"await-lock": "^2.1.0",
"blurhash": "^1.1.3",
"browser-request": "^0.3.3",
"cheerio": "^1.0.0-rc.9",
"classnames": "^2.2.6",
"commonmark": "^0.29.3",
@ -190,16 +189,16 @@
"eslint-plugin-matrix-org": "^0.6.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"fetch-mock-jest": "^1.5.1",
"fs-extra": "^10.0.1",
"glob": "^7.1.6",
"jest": "^27.4.0",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^27.0.6",
"jest-fetch-mock": "^3.0.3",
"jest-mock": "^27.5.1",
"jest-raw-loader": "^1.0.1",
"jest-sonar-reporter": "^2.0.0",
"matrix-mock-request": "^2.0.0",
"matrix-mock-request": "^2.5.0",
"matrix-react-test-utils": "^0.2.3",
"matrix-web-i18n": "^1.3.0",
"postcss-scss": "^4.0.4",

View file

@ -85,7 +85,7 @@ export default class AddThreepid {
const identityAccessToken = await authClient.getAccessToken();
return MatrixClientPeg.get().requestEmailToken(
emailAddress, this.clientSecret, 1,
undefined, undefined, identityAccessToken,
undefined, identityAccessToken,
).then((res) => {
this.sessionId = res.sid;
return res;
@ -142,7 +142,7 @@ export default class AddThreepid {
const identityAccessToken = await authClient.getAccessToken();
return MatrixClientPeg.get().requestMsisdnToken(
phoneCountry, phoneNumber, this.clientSecret, 1,
undefined, undefined, identityAccessToken,
undefined, identityAccessToken,
).then((res) => {
this.sessionId = res.sid;
return res;

View file

@ -17,16 +17,16 @@ limitations under the License.
*/
import { MatrixClient } from "matrix-js-sdk/src/client";
import { IUploadOpts } from "matrix-js-sdk/src/@types/requests";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import encrypt from "matrix-encrypt-attachment";
import extractPngChunks from "png-chunks-extract";
import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials";
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
import { logger } from "matrix-js-sdk/src/logger";
import { IEventRelation, ISendEventResponse, MatrixError, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { IEventRelation, ISendEventResponse, MatrixEvent, UploadOpts, UploadProgress } from "matrix-js-sdk/src/matrix";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import { removeElement } from "matrix-js-sdk/src/utils";
import { IEncryptedFile, IMediaEventInfo } from "./customisations/models/IMediaEventContent";
import { IEncryptedFile, IMediaEventContent, IMediaEventInfo } from "./customisations/models/IMediaEventContent";
import dis from './dispatcher/dispatcher';
import { _t } from './languageHandler';
import Modal from './Modal';
@ -39,7 +39,7 @@ import {
UploadProgressPayload,
UploadStartedPayload,
} from "./dispatcher/payloads/UploadPayload";
import { IUpload } from "./models/IUpload";
import { RoomUpload } from "./models/RoomUpload";
import SettingsStore from "./settings/SettingsStore";
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
import { TimelineRenderingType } from "./contexts/RoomContext";
@ -62,14 +62,6 @@ interface IMediaConfig {
"m.upload.size"?: number;
}
interface IContent {
body: string;
msgtype: string;
info: IMediaEventInfo;
file?: string;
url?: string;
}
/**
* Load a file into a newly created image element.
*
@ -78,7 +70,7 @@ interface IContent {
*/
async function loadImageElement(imageFile: File) {
// Load the file into an html element
const img = document.createElement("img");
const img = new Image();
const objectUrl = URL.createObjectURL(imageFile);
const imgPromise = new Promise((resolve, reject) => {
img.onload = function() {
@ -93,7 +85,7 @@ async function loadImageElement(imageFile: File) {
// check for hi-dpi PNGs and fudge display resolution as needed.
// this is mainly needed for macOS screencaps
let parsePromise;
let parsePromise: Promise<boolean>;
if (imageFile.type === "image/png") {
// in practice macOS happens to order the chunks so they fall in
// the first 0x1000 bytes (thanks to a massive ICC header).
@ -277,71 +269,58 @@ function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
* @param {File} file The file to upload.
* @param {Function?} progressHandler optional callback to be called when a chunk of
* data is uploaded.
* @param {AbortController?} controller optional abortController to use for this upload.
* @return {Promise} A promise that resolves with an object.
* If the file is unencrypted then the object will have a "url" key.
* If the file is encrypted then the object will have a "file" key.
*/
export function uploadFile(
export async function uploadFile(
matrixClient: MatrixClient,
roomId: string,
file: File | Blob,
progressHandler?: IUploadOpts["progressHandler"],
): IAbortablePromise<{ url?: string, file?: IEncryptedFile }> {
let canceled = false;
if (matrixClient.isRoomEncrypted(roomId)) {
progressHandler?: UploadOpts["progressHandler"],
controller?: AbortController,
): Promise<{ url?: string, file?: IEncryptedFile }> {
const abortController = controller ?? new AbortController();
// If the room is encrypted then encrypt the file before uploading it.
if (matrixClient.isRoomEncrypted(roomId)) {
// First read the file into memory.
let uploadPromise: IAbortablePromise<string>;
const prom = readFileAsArrayBuffer(file).then(function(data) {
if (canceled) throw new UploadCanceledError();
const data = await readFileAsArrayBuffer(file);
if (abortController.signal.aborted) throw new UploadCanceledError();
// Then encrypt the file.
return encrypt.encryptAttachment(data);
}).then(function(encryptResult) {
if (canceled) throw new UploadCanceledError();
const encryptResult = await encrypt.encryptAttachment(data);
if (abortController.signal.aborted) throw new UploadCanceledError();
// Pass the encrypted data as a Blob to the uploader.
const blob = new Blob([encryptResult.data]);
uploadPromise = matrixClient.uploadContent(blob, {
const { content_uri: url } = await matrixClient.uploadContent(blob, {
progressHandler,
abortController,
includeFilename: false,
});
if (abortController.signal.aborted) throw new UploadCanceledError();
return uploadPromise.then(url => {
if (canceled) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and
// add it under a file key.
// If the attachment is encrypted then bundle the URL along with the information
// needed to decrypt the attachment and add it under a file key.
return {
file: {
...encryptResult.info,
url,
},
} as IEncryptedFile,
};
});
}) as IAbortablePromise<{ file: IEncryptedFile }>;
prom.abort = () => {
canceled = true;
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
};
return prom;
} else {
const basePromise = matrixClient.uploadContent(file, { progressHandler });
const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError();
const { content_uri: url } = await matrixClient.uploadContent(file, { progressHandler, abortController });
if (abortController.signal.aborted) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
return { url };
}) as IAbortablePromise<{ url: string }>;
promise1.abort = () => {
canceled = true;
matrixClient.cancelUpload(basePromise);
};
return promise1;
}
}
export default class ContentMessages {
private inprogress: IUpload[] = [];
private inprogress: RoomUpload[] = [];
private mediaConfig: IMediaConfig = null;
public sendStickerContentToRoom(
@ -460,36 +439,33 @@ export default class ContentMessages {
});
}
public getCurrentUploads(relation?: IEventRelation): IUpload[] {
return this.inprogress.filter(upload => {
const noRelation = !relation && !upload.relation;
const matchingRelation = relation && upload.relation
&& relation.rel_type === upload.relation.rel_type
&& relation.event_id === upload.relation.event_id;
public getCurrentUploads(relation?: IEventRelation): RoomUpload[] {
return this.inprogress.filter(roomUpload => {
const noRelation = !relation && !roomUpload.relation;
const matchingRelation = relation && roomUpload.relation
&& relation.rel_type === roomUpload.relation.rel_type
&& relation.event_id === roomUpload.relation.event_id;
return (noRelation || matchingRelation) && !upload.canceled;
return (noRelation || matchingRelation) && !roomUpload.cancelled;
});
}
public cancelUpload(promise: IAbortablePromise<any>, matrixClient: MatrixClient): void {
const upload = this.inprogress.find(item => item.promise === promise);
if (upload) {
upload.canceled = true;
matrixClient.cancelUpload(upload.promise);
public cancelUpload(upload: RoomUpload): void {
upload.abort();
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
}
}
private sendContentToRoom(
public async sendContentToRoom(
file: File,
roomId: string,
relation: IEventRelation | undefined,
matrixClient: MatrixClient,
replyToEvent: MatrixEvent | undefined,
promBefore: Promise<any>,
promBefore?: Promise<any>,
) {
const content: Omit<IContent, "info"> & { info: Partial<IMediaEventInfo> } = {
body: file.name || 'Attachment',
const fileName = file.name || _t("Attachment");
const content: Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo> } = {
body: fileName,
info: {
size: file.size,
},
@ -512,91 +488,72 @@ export default class ContentMessages {
content.info.mimetype = file.type;
}
const prom = new Promise<void>((resolve) => {
if (file.type.indexOf('image/') === 0) {
content.msgtype = MsgType.Image;
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
Object.assign(content.info, imageInfo);
resolve();
}, (e) => {
// Failed to thumbnail, fall back to uploading an m.file
logger.error(e);
content.msgtype = MsgType.File;
resolve();
});
} else if (file.type.indexOf('audio/') === 0) {
content.msgtype = MsgType.Audio;
resolve();
} else if (file.type.indexOf('video/') === 0) {
content.msgtype = MsgType.Video;
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
Object.assign(content.info, videoInfo);
resolve();
}, (e) => {
// Failed to thumbnail, fall back to uploading an m.file
logger.error(e);
content.msgtype = MsgType.File;
resolve();
});
} else {
content.msgtype = MsgType.File;
resolve();
}
}) as IAbortablePromise<void>;
// create temporary abort handler for before the actual upload gets passed off to js-sdk
prom.abort = () => {
upload.canceled = true;
};
const upload: IUpload = {
fileName: file.name || 'Attachment',
roomId,
relation,
total: file.size,
loaded: 0,
promise: prom,
};
const upload = new RoomUpload(roomId, fileName, relation, file.size);
this.inprogress.push(upload);
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
function onProgress(ev) {
upload.total = ev.total;
upload.loaded = ev.loaded;
function onProgress(progress: UploadProgress) {
upload.onProgress(progress);
dis.dispatch<UploadProgressPayload>({ action: Action.UploadProgress, upload });
}
let error: MatrixError;
return prom.then(() => {
if (upload.canceled) throw new UploadCanceledError();
// XXX: upload.promise must be the promise that
// is returned by uploadFile as it has an abort()
// method hacked onto it.
upload.promise = uploadFile(matrixClient, roomId, file, onProgress);
return upload.promise.then(function(result) {
try {
if (file.type.startsWith('image/')) {
content.msgtype = MsgType.Image;
try {
const imageInfo = await infoForImageFile(matrixClient, roomId, file);
Object.assign(content.info, imageInfo);
} catch (e) {
// Failed to thumbnail, fall back to uploading an m.file
logger.error(e);
content.msgtype = MsgType.File;
}
} else if (file.type.indexOf('audio/') === 0) {
content.msgtype = MsgType.Audio;
} else if (file.type.indexOf('video/') === 0) {
content.msgtype = MsgType.Video;
try {
const videoInfo = await infoForVideoFile(matrixClient, roomId, file);
Object.assign(content.info, videoInfo);
} catch (e) {
// Failed to thumbnail, fall back to uploading an m.file
logger.error(e);
content.msgtype = MsgType.File;
}
} else {
content.msgtype = MsgType.File;
}
if (upload.cancelled) throw new UploadCanceledError();
const result = await uploadFile(matrixClient, roomId, file, onProgress, upload.abortController);
content.file = result.file;
content.url = result.url;
});
}).then(() => {
if (upload.cancelled) throw new UploadCanceledError();
// Await previous message being sent into the room
return promBefore;
}).then(function() {
if (upload.canceled) throw new UploadCanceledError();
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name
? relation.event_id
: null;
const prom = matrixClient.sendMessage(roomId, threadId, content);
if (promBefore) await promBefore;
if (upload.cancelled) throw new UploadCanceledError();
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
const response = await matrixClient.sendMessage(roomId, threadId, content);
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
prom.then(resp => {
sendRoundTripMetric(matrixClient, roomId, resp.event_id);
});
sendRoundTripMetric(matrixClient, roomId, response.event_id);
}
return prom;
}, function(err: MatrixError) {
error = err;
if (!upload.canceled) {
dis.dispatch<UploadFinishedPayload>({ action: Action.UploadFinished, upload });
dis.dispatch({ action: 'message_sent' });
} catch (error) {
// 413: File was too big or upset the server in some way:
// clear the media size limit so we fetch it again next time we try to upload
if (error?.httpStatus === 413) {
this.mediaConfig = null;
}
if (!upload.cancelled) {
let desc = _t("The file '%(fileName)s' failed to upload.", { fileName: upload.fileName });
if (err.httpStatus === 413) {
if (error.httpStatus === 413) {
desc = _t(
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
{ fileName: upload.fileName },
@ -606,27 +563,11 @@ export default class ContentMessages {
title: _t('Upload Failed'),
description: desc,
});
}
}).finally(() => {
for (let i = 0; i < this.inprogress.length; ++i) {
if (this.inprogress[i].promise === upload.promise) {
this.inprogress.splice(i, 1);
break;
}
}
if (error) {
// 413: File was too big or upset the server in some way:
// clear the media size limit so we fetch it again next time
// we try to upload
if (error?.httpStatus === 413) {
this.mediaConfig = null;
}
dis.dispatch<UploadErrorPayload>({ action: Action.UploadFailed, upload, error });
} else {
dis.dispatch<UploadFinishedPayload>({ action: Action.UploadFinished, upload });
dis.dispatch({ action: 'message_sent' });
}
});
} finally {
removeElement(this.inprogress, e => e.promise === upload.promise);
}
}
private isFileSizeAcceptable(file: File) {

View file

@ -739,7 +739,7 @@ export function logout(): void {
_isLoggingOut = true;
const client = MatrixClientPeg.get();
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
client.logout(undefined, true).then(onLoggedOut, (err) => {
client.logout(true).then(onLoggedOut, (err) => {
// Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and
// you want to log into a different server, so just forget the

View file

@ -169,7 +169,7 @@ export default class Login {
* @param {string} loginType the type of login to do
* @param {ILoginParams} loginParams the parameters for the login
*
* @returns {MatrixClientCreds}
* @returns {IMatrixClientCreds}
*/
export async function sendLoginRequest(
hsUrl: string,

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import url from 'url';
import request from "browser-request";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { IOpenIDToken } from 'matrix-js-sdk/src/matrix';
import SettingsStore from "./settings/SettingsStore";
import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms';
@ -103,29 +103,29 @@ export default class ScalarAuthClient {
}
}
private getAccountName(token: string): Promise<string> {
const url = this.apiUrl + "/account";
private async getAccountName(token: string): Promise<string> {
const url = new URL(this.apiUrl + "/account");
url.searchParams.set("scalar_token", token);
url.searchParams.set("v", imApiVersion);
return new Promise(function(resolve, reject) {
request({
const res = await fetch(url, {
method: "GET",
uri: url,
qs: { scalar_token: token, v: imApiVersion },
json: true,
}, (err, response, body) => {
if (err) {
reject(err);
} else if (body && body.errcode === 'M_TERMS_NOT_SIGNED') {
reject(new TermsNotSignedError());
} else if (response.statusCode / 100 !== 2) {
reject(body);
} else if (!body || !body.user_id) {
reject(new Error("Missing user_id in response"));
} else {
resolve(body.user_id);
});
const body = await res.json();
if (body?.errcode === "M_TERMS_NOT_SIGNED") {
throw new TermsNotSignedError();
}
});
});
if (!res.ok) {
throw body;
}
if (!body?.user_id) {
throw new Error("Missing user_id in response");
}
return body.user_id;
}
private checkToken(token: string): Promise<string> {
@ -183,56 +183,41 @@ export default class ScalarAuthClient {
});
}
exchangeForScalarToken(openidTokenObject: any): Promise<string> {
const scalarRestUrl = this.apiUrl;
public async exchangeForScalarToken(openidTokenObject: IOpenIDToken): Promise<string> {
const scalarRestUrl = new URL(this.apiUrl + "/register");
scalarRestUrl.searchParams.set("v", imApiVersion);
return new Promise(function(resolve, reject) {
request({
method: 'POST',
uri: scalarRestUrl + '/register',
qs: { v: imApiVersion },
body: openidTokenObject,
json: true,
}, (err, response, body) => {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body || !body.scalar_token) {
reject(new Error("Missing scalar_token in response"));
} else {
resolve(body.scalar_token);
}
});
const res = await fetch(scalarRestUrl, {
method: "POST",
body: JSON.stringify(openidTokenObject),
});
if (!res.ok) {
throw new Error(`Scalar request failed: ${res.status}`);
}
getScalarPageTitle(url: string): Promise<string> {
let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup';
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
const body = await res.json();
if (!body?.scalar_token) {
throw new Error("Missing scalar_token in response");
}
return new Promise(function(resolve, reject) {
request({
method: 'GET',
uri: scalarPageLookupUrl,
json: true,
}, (err, response, body) => {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body) {
reject(new Error("Missing page title in response"));
} else {
let title = "";
if (body.page_title_cache_item && body.page_title_cache_item.cached_title) {
title = body.page_title_cache_item.cached_title;
return body.scalar_token;
}
resolve(title);
public async getScalarPageTitle(url: string): Promise<string> {
const scalarPageLookupUrl = new URL(this.getStarterLink(this.apiUrl + '/widgets/title_lookup'));
scalarPageLookupUrl.searchParams.set("curl", encodeURIComponent(url));
const res = await fetch(scalarPageLookupUrl, {
method: "GET",
});
if (!res.ok) {
throw new Error(`Scalar request failed: ${res.status}`);
}
});
});
const body = await res.json();
return body?.page_title_cache_item?.cached_title;
}
/**
@ -243,31 +228,24 @@ export default class ScalarAuthClient {
* @param {string} widgetId The widget ID to disable assets for
* @return {Promise} Resolves on completion
*/
disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise<void> {
let url = this.apiUrl + '/widgets/set_assets_state';
url = this.getStarterLink(url);
return new Promise<void>((resolve, reject) => {
request({
method: 'GET', // XXX: Actions shouldn't be GET requests
uri: url,
json: true,
qs: {
'widget_type': widgetType.preferred,
'widget_id': widgetId,
'state': 'disable',
},
}, (err, response, body) => {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body) {
reject(new Error("Failed to set widget assets state"));
} else {
resolve();
public async disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise<void> {
const url = new URL(this.getStarterLink(this.apiUrl + "/widgets/set_assets_state"));
url.searchParams.set("widget_type", widgetType.preferred);
url.searchParams.set("widget_id", widgetId);
url.searchParams.set("state", "disable");
const res = await fetch(url, {
method: "GET", // XXX: Actions shouldn't be GET requests
});
if (!res.ok) {
throw new Error(`Scalar request failed: ${res.status}`);
}
const body = await res.text();
if (!body) {
throw new Error("Failed to set widget assets state");
}
});
});
}
getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string {

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import request from 'browser-request';
import sanitizeHtml from 'sanitize-html';
import classnames from 'classnames';
import { logger } from "matrix-js-sdk/src/logger";
@ -61,31 +60,27 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
return sanitizeHtml(_t(s));
}
public componentDidMount(): void {
this.unmounted = false;
private async fetchEmbed() {
let res: Response;
if (!this.props.url) {
return;
}
// we use request() to inline the page into the react component
// so that it can inherit CSS and theming easily rather than mess around
// with iframes and trying to synchronise document.stylesheets.
request(
{ method: "GET", url: this.props.url },
(err, response, body) => {
if (this.unmounted) {
return;
}
if (err || response.status < 200 || response.status >= 300) {
try {
res = await fetch(this.props.url, { method: "GET" });
} catch (err) {
if (this.unmounted) return;
logger.warn(`Error loading page: ${err}`);
this.setState({ page: _t("Couldn't load page") });
return;
}
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1) => this.translate(g1));
if (this.unmounted) return;
if (!res.ok) {
logger.warn(`Error loading page: ${res.status}`);
this.setState({ page: _t("Couldn't load page") });
return;
}
let body = (await res.text()).replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1) => this.translate(g1));
if (this.props.replaceMap) {
Object.keys(this.props.replaceMap).forEach(key => {
@ -94,8 +89,19 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
}
this.setState({ page: body });
},
);
}
public componentDidMount(): void {
this.unmounted = false;
if (!this.props.url) {
return;
}
// We use fetch to inline the page into the react component
// so that it can inherit CSS and theming easily rather than mess around
// with iframes and trying to synchronise document.stylesheets.
this.fetchEmbed();
this.dispatcherRef = dis.register(this.onAction);
}

View file

@ -1362,7 +1362,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
if (this.unmounted) return;
this.setState({ timelineLoading: false });
logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}: ${error}`);
logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}`, error);
let onFinished: () => void;

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import { Room } from "matrix-js-sdk/src/models/room";
import filesize from "filesize";
import { IAbortablePromise, IEventRelation } from 'matrix-js-sdk/src/matrix';
import { IEventRelation } from 'matrix-js-sdk/src/matrix';
import { Optional } from "matrix-events-sdk";
import ContentMessages from '../../ContentMessages';
@ -26,8 +26,7 @@ import { _t } from '../../languageHandler';
import { Action } from "../../dispatcher/actions";
import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomUpload } from "../../models/RoomUpload";
import { ActionPayload } from '../../dispatcher/payloads';
import { UploadPayload } from "../../dispatcher/payloads/UploadPayload";
@ -38,7 +37,7 @@ interface IProps {
interface IState {
currentFile?: string;
currentPromise?: IAbortablePromise<any>;
currentUpload?: RoomUpload;
currentLoaded?: number;
currentTotal?: number;
countFiles: number;
@ -55,8 +54,6 @@ function isUploadPayload(payload: ActionPayload): payload is UploadPayload {
}
export default class UploadBar extends React.PureComponent<IProps, IState> {
static contextType = MatrixClientContext;
private dispatcherRef: Optional<string>;
private mounted = false;
@ -78,7 +75,7 @@ export default class UploadBar extends React.PureComponent<IProps, IState> {
dis.unregister(this.dispatcherRef!);
}
private getUploadsInRoom(): IUpload[] {
private getUploadsInRoom(): RoomUpload[] {
const uploads = ContentMessages.sharedInstance().getCurrentUploads(this.props.relation);
return uploads.filter(u => u.roomId === this.props.room.roomId);
}
@ -86,8 +83,8 @@ export default class UploadBar extends React.PureComponent<IProps, IState> {
private calculateState(): IState {
const [currentUpload, ...otherUploads] = this.getUploadsInRoom();
return {
currentUpload,
currentFile: currentUpload?.fileName,
currentPromise: currentUpload?.promise,
currentLoaded: currentUpload?.loaded,
currentTotal: currentUpload?.total,
countFiles: otherUploads.length + 1,
@ -103,7 +100,7 @@ export default class UploadBar extends React.PureComponent<IProps, IState> {
private onCancelClick = (ev: ButtonEvent) => {
ev.preventDefault();
ContentMessages.sharedInstance().cancelUpload(this.state.currentPromise!, this.context);
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload!);
};
render() {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React, { ReactNode } from 'react';
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { ConnectionError, MatrixError } from "matrix-js-sdk/src/http-api";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
@ -453,7 +453,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
let errorText: ReactNode = _t("There was a problem communicating with the homeserver, " +
"please try again later.") + (errCode ? " (" + errCode + ")" : "");
if (err["cors"] === 'rejected') { // browser-request specific error field
if (err instanceof ConnectionError) {
if (window.location.protocol === 'https:' &&
(this.props.serverConfig.hsUrl.startsWith("http:") ||
!this.props.serverConfig.hsUrl.startsWith("http"))

View file

@ -16,7 +16,6 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
*/
import React from 'react';
import request from 'browser-request';
import { _t } from '../../../languageHandler';
import QuestionDialog from "./QuestionDialog";
@ -37,22 +36,33 @@ export default class ChangelogDialog extends React.Component<IProps> {
this.state = {};
}
private async fetchChanges(repo: string, oldVersion: string, newVersion: string): Promise<void> {
const url = `https://riot.im/github/repos/${repo}/compare/${oldVersion}...${newVersion}`;
try {
const res = await fetch(url);
if (!res.ok) {
this.setState({ [repo]: res.statusText });
return;
}
const body = await res.json();
this.setState({ [repo]: body.commits });
} catch (err) {
this.setState({ [repo]: err.message });
}
}
public componentDidMount() {
const version = this.props.newVersion.split('-');
const version2 = this.props.version.split('-');
if (version == null || version2 == null) return;
// parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
for (let i=0; i<REPOS.length; i++) {
for (let i = 0; i < REPOS.length; i++) {
const oldVersion = version2[2*i];
const newVersion = version[2*i];
const url = `https://riot.im/github/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`;
request(url, (err, response, body) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
this.setState({ [REPOS[i]]: response.statusText });
return;
}
this.setState({ [REPOS[i]]: JSON.parse(body).commits });
});
this.fetchChanges(REPOS[i], oldVersion, newVersion);
}
}

View file

@ -654,12 +654,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
const token = await authClient.getAccessToken();
if (term !== this.state.filterText) return; // abandon hope
const lookup = await MatrixClientPeg.get().lookupThreePid(
'email',
term,
undefined, // callback
token,
);
const lookup = await MatrixClientPeg.get().lookupThreePid('email', term, token);
if (term !== this.state.filterText) return; // abandon hope
if (!lookup || !lookup.mxid) {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import React from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
import { MatrixClient, Method } from 'matrix-js-sdk/src/matrix';
import { logger } from 'matrix-js-sdk/src/logger';
import { _t } from '../../../languageHandler';
@ -33,17 +33,10 @@ import { SettingLevel } from "../../../settings/SettingLevel";
* @throws if the proxy server is unreachable or not configured to the given homeserver
*/
async function syncHealthCheck(cli: MatrixClient): Promise<void> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
const url = cli.http.getUrl("/sync", {}, "/_matrix/client/unstable/org.matrix.msc3575");
const res = await fetch(url, {
signal: controller.signal,
method: "POST",
await cli.http.authedRequest(Method.Post, "/sync", undefined, undefined, {
localTimeoutMs: 10 * 1000, // 10s
prefix: "/_matrix/client/unstable/org.matrix.msc3575",
});
clearTimeout(id);
if (res.status != 200) {
throw new Error(`syncHealthCheck: server returned HTTP ${res.status}`);
}
logger.info("server natively support sliding sync OK");
}

View file

@ -74,7 +74,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
if (!ev.target.files?.length) return;
setBusy(true);
const file = ev.target.files[0];
const uri = await cli.uploadContent(file);
const { content_uri: uri } = await cli.uploadContent(file);
await setAvatarUrl(uri);
setBusy(false);
}}

View file

@ -134,7 +134,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
}
if (this.state.avatarFile) {
const uri = await client.uploadContent(this.state.avatarFile);
const { content_uri: uri } = await client.uploadContent(this.state.avatarFile);
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
newState.originalAvatarUrl = newState.avatarUrl;

View file

@ -148,7 +148,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const result = await MatrixClientPeg.get().lookupThreePid(
'email',
this.props.invitedEmail,
undefined /* callback */,
identityAccessToken,
);
this.setState({ invitedEmailMxid: result.mxid });

View file

@ -115,13 +115,13 @@ export default class ChangeAvatar extends React.Component<IProps, IState> {
this.setState({
phase: Phases.Uploading,
});
const httpPromise = MatrixClientPeg.get().uploadContent(file).then((url) => {
const httpPromise = MatrixClientPeg.get().uploadContent(file).then(({ content_uri: url }) => {
newUrl = url;
if (this.props.room) {
return MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId,
'm.room.avatar',
{ url: url },
{ url },
'',
);
} else {

View file

@ -111,7 +111,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
logger.log(
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
` (${this.state.avatarFile.size}) bytes`);
const uri = await client.uploadContent(this.state.avatarFile);
const { content_uri: uri } = await client.uploadContent(this.state.avatarFile);
await client.setAvatarUrl(uri);
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
newState.originalAvatarUrl = newState.avatarUrl;

View file

@ -246,7 +246,7 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
if (opts.avatar) {
let url = opts.avatar;
if (opts.avatar instanceof File) {
url = await client.uploadContent(opts.avatar);
({ content_uri: url } = await client.uploadContent(opts.avatar));
}
createOpts.initial_state.push({

View file

@ -151,7 +151,7 @@ export class Media {
* @param {MatrixClient} client? Optional client to use.
* @returns {Media} The media object.
*/
export function mediaFromContent(content: IMediaEventContent, client?: MatrixClient): Media {
export function mediaFromContent(content: Partial<IMediaEventContent>, client?: MatrixClient): Media {
return new Media(prepEventContentAsMedia(content), client);
}

View file

@ -46,6 +46,7 @@ export interface IMediaEventInfo {
}
export interface IMediaEventContent {
msgtype: string;
body?: string;
filename?: string; // `m.file` optional field
url?: string; // required on unencrypted media
@ -69,7 +70,7 @@ export interface IMediaObject {
* @returns {IPreparedMedia} A prepared media object.
* @throws Throws if the given content cannot be packaged into a prepared media object.
*/
export function prepEventContentAsMedia(content: IMediaEventContent): IPreparedMedia {
export function prepEventContentAsMedia(content: Partial<IMediaEventContent>): IPreparedMedia {
let thumbnail: IMediaObject = null;
if (content?.info?.thumbnail_url) {
thumbnail = {

View file

@ -16,13 +16,13 @@ limitations under the License.
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
import { IUpload } from "../../models/IUpload";
import { RoomUpload } from "../../models/RoomUpload";
export interface UploadPayload extends ActionPayload {
/**
* The upload with fields representing the new upload state.
*/
upload: IUpload;
upload: RoomUpload;
}
export interface UploadStartedPayload extends UploadPayload {

View file

@ -16,6 +16,7 @@
"Error": "Error",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
"Attachment": "Attachment",
"The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.",
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
"Upload Failed": "Upload Failed",
@ -654,7 +655,6 @@
"This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.",
"Please <a>contact your service administrator</a> to continue using the service.": "Please <a>contact your service administrator</a> to continue using the service.",
"Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...",
"Attachment": "Attachment",
"%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
"%(items)s and %(count)s others|one": "%(items)s and one other",
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",

View file

@ -17,7 +17,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from 'browser-request';
import counterpart from 'counterpart';
import React from 'react';
import { logger } from "matrix-js-sdk/src/logger";
@ -386,6 +385,11 @@ export function setMissingEntryGenerator(f: (value: string) => void) {
counterpart.setMissingEntryGenerator(f);
}
type Language = {
fileName: string;
label: string;
};
export function setLanguage(preferredLangs: string | string[]) {
if (!Array.isArray(preferredLangs)) {
preferredLangs = [preferredLangs];
@ -396,8 +400,8 @@ export function setLanguage(preferredLangs: string | string[]) {
plaf.setLanguage(preferredLangs);
}
let langToUse;
let availLangs;
let langToUse: string;
let availLangs: { [lang: string]: Language };
return getLangsJson().then((result) => {
availLangs = result;
@ -532,29 +536,21 @@ export function pickBestLanguage(langs: string[]): string {
return langs[0];
}
function getLangsJson(): Promise<object> {
return new Promise((resolve, reject) => {
let url;
async function getLangsJson(): Promise<{ [lang: string]: Language }> {
let url: string;
if (typeof(webpackLangJsonUrl) === 'string') { // in Jest this 'url' isn't a URL, so just fall through
url = webpackLangJsonUrl;
} else {
url = i18nFolder + 'languages.json';
}
request(
{ method: "GET", url },
(err, response, body) => {
if (err) {
reject(err);
return;
const res = await fetch(url, { method: "GET" });
if (!res.ok) {
throw new Error(`Failed to load ${url}, got ${res.status}`);
}
if (response.status < 200 || response.status >= 300) {
reject(new Error(`Failed to load ${url}, got ${response.status}`));
return;
}
resolve(JSON.parse(body));
},
);
});
return res.json();
}
interface ICounterpartTranslation {
@ -571,23 +567,14 @@ async function getLanguageRetry(langPath: string, num = 3): Promise<ICounterpart
});
}
function getLanguage(langPath: string): Promise<ICounterpartTranslation> {
return new Promise((resolve, reject) => {
request(
{ method: "GET", url: langPath },
(err, response, body) => {
if (err) {
reject(err);
return;
async function getLanguage(langPath: string): Promise<ICounterpartTranslation> {
const res = await fetch(langPath, { method: "GET" });
if (!res.ok) {
throw new Error(`Failed to load ${langPath}, got ${res.status}`);
}
if (response.status < 200 || response.status >= 300) {
reject(new Error(`Failed to load ${langPath}, got ${response.status}`));
return;
}
resolve(JSON.parse(body));
},
);
});
return res.json();
}
export interface ICustomTranslations {

View file

@ -1,28 +0,0 @@
/*
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.
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 { IEventRelation } from "matrix-js-sdk/src/matrix";
import { IAbortablePromise } from "matrix-js-sdk/src/@types/partials";
export interface IUpload {
fileName: string;
roomId: string;
relation?: IEventRelation;
total: number;
loaded: number;
promise: IAbortablePromise<any>;
canceled?: boolean;
}

53
src/models/RoomUpload.ts Normal file
View file

@ -0,0 +1,53 @@
/*
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.
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 { IEventRelation, UploadProgress } from "matrix-js-sdk/src/matrix";
import { IEncryptedFile } from "../customisations/models/IMediaEventContent";
export class RoomUpload {
public readonly abortController = new AbortController();
public promise: Promise<{ url?: string, file?: IEncryptedFile }>;
private uploaded = 0;
constructor(
public readonly roomId: string,
public readonly fileName: string,
public readonly relation?: IEventRelation,
public fileSize = 0,
) {}
public onProgress(progress: UploadProgress) {
this.uploaded = progress.loaded;
this.fileSize = progress.total;
}
public abort(): void {
this.abortController.abort();
}
public get cancelled(): boolean {
return this.abortController.signal.aborted;
}
public get total(): number {
return this.fileSize;
}
public get loaded(): number {
return this.uploaded;
}
}

View file

@ -184,7 +184,7 @@ export default class MultiInviter {
}
}
return this.matrixClient.invite(roomId, addr, undefined, this.reason);
return this.matrixClient.invite(roomId, addr, this.reason);
} else {
throw new Error('Unsupported address');
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { IAbortablePromise, MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import {
@ -108,7 +108,7 @@ export class VoiceBroadcastRecording
await this.sendVoiceMessage(chunk, url, file);
};
private uploadFile(chunk: ChunkRecordedPayload): IAbortablePromise<{ url?: string, file?: IEncryptedFile }> {
private uploadFile(chunk: ChunkRecordedPayload): ReturnType<typeof uploadFile> {
return uploadFile(
this.client,
this.infoEvent.getRoomId(),

View file

@ -15,15 +15,31 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { IImageInfo, ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
import { IImageInfo, ISendEventResponse, MatrixClient, RelationType, UploadResponse } from "matrix-js-sdk/src/matrix";
import { defer } from "matrix-js-sdk/src/utils";
import encrypt, { IEncryptedFile } from "matrix-encrypt-attachment";
import ContentMessages from "../src/ContentMessages";
import ContentMessages, { UploadCanceledError, uploadFile } from "../src/ContentMessages";
import { doMaybeLocalRoomAction } from "../src/utils/local-room";
import { createTestClient } from "./test-utils";
import { BlurhashEncoder } from "../src/BlurhashEncoder";
jest.mock("matrix-encrypt-attachment", () => ({ encryptAttachment: jest.fn().mockResolvedValue({}) }));
jest.mock("../src/BlurhashEncoder", () => ({
BlurhashEncoder: {
instance: {
getBlurhash: jest.fn(),
},
},
}));
jest.mock("../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(),
}));
const createElement = document.createElement.bind(document);
describe("ContentMessages", () => {
const stickerUrl = "https://example.com/sticker";
const roomId = "!room:example.com";
@ -36,6 +52,9 @@ describe("ContentMessages", () => {
beforeEach(() => {
client = {
sendStickerMessage: jest.fn(),
sendMessage: jest.fn(),
isRoomEncrypted: jest.fn().mockReturnValue(false),
uploadContent: jest.fn().mockResolvedValue({ content_uri: "mxc://server/file" }),
} as unknown as MatrixClient;
contentMessages = new ContentMessages();
prom = Promise.resolve(null);
@ -65,4 +84,226 @@ describe("ContentMessages", () => {
expect(client.sendStickerMessage).toHaveBeenCalledWith(roomId, null, stickerUrl, imageInfo, text);
});
});
describe("sendContentToRoom", () => {
const roomId = "!roomId:server";
beforeEach(() => {
Object.defineProperty(global.Image.prototype, 'src', {
// Define the property setter
set(src) {
setTimeout(() => this.onload());
},
});
Object.defineProperty(global.Image.prototype, 'height', {
get() { return 600; },
});
Object.defineProperty(global.Image.prototype, 'width', {
get() { return 800; },
});
mocked(doMaybeLocalRoomAction).mockImplementation((
roomId: string,
fn: (actualRoomId: string) => Promise<ISendEventResponse>,
) => fn(roomId));
mocked(BlurhashEncoder.instance.getBlurhash).mockResolvedValue(undefined);
});
it("should use m.image for image files", async () => {
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const file = new File([], "fileName", { type: "image/jpeg" });
await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({
url: "mxc://server/file",
msgtype: "m.image",
}));
});
it("should fall back to m.file for invalid image files", async () => {
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const file = new File([], "fileName", { type: "image/png" });
await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({
url: "mxc://server/file",
msgtype: "m.file",
}));
});
it("should use m.video for video files", async () => {
jest.spyOn(document, "createElement").mockImplementation(tagName => {
const element = createElement(tagName);
if (tagName === "video") {
element.load = jest.fn();
element.play = () => element.onloadeddata(new Event("loadeddata"));
element.pause = jest.fn();
Object.defineProperty(element, 'videoHeight', {
get() { return 600; },
});
Object.defineProperty(element, 'videoWidth', {
get() { return 800; },
});
}
return element;
});
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const file = new File([], "fileName", { type: "video/mp4" });
await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({
url: "mxc://server/file",
msgtype: "m.video",
}));
});
it("should use m.audio for audio files", async () => {
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const file = new File([], "fileName", { type: "audio/mp3" });
await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({
url: "mxc://server/file",
msgtype: "m.audio",
}));
});
it("should default to name 'Attachment' if file doesn't have a name", async () => {
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const file = new File([], "", { type: "text/plain" });
await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
expect(client.sendMessage).toHaveBeenCalledWith(roomId, null, expect.objectContaining({
url: "mxc://server/file",
msgtype: "m.file",
body: "Attachment",
}));
});
it("should keep RoomUpload's total and loaded values up to date", async () => {
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const file = new File([], "", { type: "text/plain" });
const prom = contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined);
const [upload] = contentMessages.getCurrentUploads();
expect(upload.loaded).toBe(0);
expect(upload.total).toBe(file.size);
const { progressHandler } = mocked(client.uploadContent).mock.calls[0][1];
progressHandler({ loaded: 123, total: 1234 });
expect(upload.loaded).toBe(123);
expect(upload.total).toBe(1234);
await prom;
});
});
describe("getCurrentUploads", () => {
const file1 = new File([], "file1");
const file2 = new File([], "file2");
const roomId = "!roomId:server";
beforeEach(() => {
mocked(doMaybeLocalRoomAction).mockImplementation((
roomId: string,
fn: (actualRoomId: string) => Promise<ISendEventResponse>,
) => fn(roomId));
});
it("should return only uploads for the given relation", async () => {
const relation = {
rel_type: RelationType.Thread,
event_id: "!threadId:server",
};
const p1 = contentMessages.sendContentToRoom(file1, roomId, relation, client, undefined);
const p2 = contentMessages.sendContentToRoom(file2, roomId, undefined, client, undefined);
const uploads = contentMessages.getCurrentUploads(relation);
expect(uploads).toHaveLength(1);
expect(uploads[0].relation).toEqual(relation);
expect(uploads[0].fileName).toEqual("file1");
await Promise.all([p1, p2]);
});
it("should return only uploads for no relation when not passed one", async () => {
const relation = {
rel_type: RelationType.Thread,
event_id: "!threadId:server",
};
const p1 = contentMessages.sendContentToRoom(file1, roomId, relation, client, undefined);
const p2 = contentMessages.sendContentToRoom(file2, roomId, undefined, client, undefined);
const uploads = contentMessages.getCurrentUploads();
expect(uploads).toHaveLength(1);
expect(uploads[0].relation).toEqual(undefined);
expect(uploads[0].fileName).toEqual("file2");
await Promise.all([p1, p2]);
});
});
describe("cancelUpload", () => {
it("should cancel in-flight upload", async () => {
const deferred = defer<UploadResponse>();
mocked(client.uploadContent).mockReturnValue(deferred.promise);
const file1 = new File([], "file1");
const prom = contentMessages.sendContentToRoom(file1, roomId, undefined, client, undefined);
const { abortController } = mocked(client.uploadContent).mock.calls[0][1];
expect(abortController.signal.aborted).toBeFalsy();
const [upload] = contentMessages.getCurrentUploads();
contentMessages.cancelUpload(upload);
expect(abortController.signal.aborted).toBeTruthy();
deferred.resolve({} as UploadResponse);
await prom;
});
});
});
describe("uploadFile", () => {
beforeEach(() => {
jest.clearAllMocks();
});
const client = createTestClient();
it("should not encrypt the file if the room isn't encrypted", async () => {
mocked(client.isRoomEncrypted).mockReturnValue(false);
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
const progressHandler = jest.fn();
const file = new Blob([]);
const res = await uploadFile(client, "!roomId:server", file, progressHandler);
expect(res.url).toBe("mxc://server/file");
expect(res.file).toBeFalsy();
expect(encrypt.encryptAttachment).not.toHaveBeenCalled();
expect(client.uploadContent).toHaveBeenCalledWith(file, expect.objectContaining({ progressHandler }));
});
it("should encrypt the file if the room is encrypted", async () => {
mocked(client.isRoomEncrypted).mockReturnValue(true);
mocked(client.uploadContent).mockResolvedValue({ content_uri: "mxc://server/file" });
mocked(encrypt.encryptAttachment).mockResolvedValue({
data: new ArrayBuffer(123),
info: {} as IEncryptedFile,
});
const progressHandler = jest.fn();
const file = new Blob(["123"]);
const res = await uploadFile(client, "!roomId:server", file, progressHandler);
expect(res.url).toBeFalsy();
expect(res.file).toEqual(expect.objectContaining({
url: "mxc://server/file",
}));
expect(encrypt.encryptAttachment).toHaveBeenCalled();
expect(client.uploadContent).toHaveBeenCalledWith(expect.any(Blob), expect.objectContaining({
progressHandler,
includeFilename: false,
}));
expect(mocked(client.uploadContent).mock.calls[0][0]).not.toBe(file);
});
it("should throw UploadCanceledError upon aborting the upload", async () => {
mocked(client.isRoomEncrypted).mockReturnValue(false);
const deferred = defer<UploadResponse>();
mocked(client.uploadContent).mockReturnValue(deferred.promise);
const file = new Blob([]);
const prom = uploadFile(client, "!roomId:server", file);
mocked(client.uploadContent).mock.calls[0][1].abortController.abort();
deferred.resolve({ content_uri: "mxc://foo/bar" });
await expect(prom).rejects.toThrowError(UploadCanceledError);
});
});

View file

@ -14,47 +14,199 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import ScalarAuthClient from '../src/ScalarAuthClient';
import { MatrixClientPeg } from '../src/MatrixClientPeg';
import { stubClient } from './test-utils';
import SdkConfig from "../src/SdkConfig";
import { WidgetType } from "../src/widgets/WidgetType";
describe('ScalarAuthClient', function() {
const apiUrl = 'test.com/api';
const uiUrl = 'test.com/app';
const apiUrl = 'https://test.com/api';
const uiUrl = 'https:/test.com/app';
const tokenObject = {
access_token: "token",
token_type: "Bearer",
matrix_server_name: "localhost",
expires_in: 999,
};
let client;
beforeEach(function() {
window.localStorage.getItem = jest.fn((arg) => {
if (arg === "mx_scalar_token") return "brokentoken";
});
stubClient();
jest.clearAllMocks();
client = stubClient();
});
it('should request a new token if the old one fails', async function() {
const sac = new ScalarAuthClient(apiUrl, uiUrl);
const sac = new ScalarAuthClient(apiUrl + 0, uiUrl);
// @ts-ignore unhappy with Promise calls
jest.spyOn(sac, 'getAccountName').mockImplementation((arg: string) => {
switch (arg) {
case "brokentoken":
return Promise.reject({
message: "Invalid token",
});
case "wokentoken":
default:
return Promise.resolve(MatrixClientPeg.get().getUserId());
}
fetchMock.get("https://test.com/api0/account?scalar_token=brokentoken&v=1.1", {
body: { message: "Invalid token" },
});
MatrixClientPeg.get().getOpenIdToken = jest.fn().mockResolvedValue('this is your openid token');
fetchMock.get("https://test.com/api0/account?scalar_token=wokentoken&v=1.1", {
body: { user_id: client.getUserId() },
});
client.getOpenIdToken = jest.fn().mockResolvedValue(tokenObject);
sac.exchangeForScalarToken = jest.fn((arg) => {
if (arg === "this is your openid token") return Promise.resolve("wokentoken");
if (arg === tokenObject) return Promise.resolve("wokentoken");
});
await sac.connect();
expect(sac.exchangeForScalarToken).toBeCalledWith('this is your openid token');
expect(sac.exchangeForScalarToken).toBeCalledWith(tokenObject);
expect(sac.hasCredentials).toBeTruthy();
// @ts-ignore private property
expect(sac.scalarToken).toEqual('wokentoken');
});
describe("exchangeForScalarToken", () => {
it("should return `scalar_token` from API /register", async () => {
const sac = new ScalarAuthClient(apiUrl + 1, uiUrl);
fetchMock.postOnce("https://test.com/api1/register?v=1.1", {
body: { scalar_token: "stoken" },
});
await expect(sac.exchangeForScalarToken(tokenObject)).resolves.toBe("stoken");
});
it("should throw upon non-20x code", async () => {
const sac = new ScalarAuthClient(apiUrl + 2, uiUrl);
fetchMock.postOnce("https://test.com/api2/register?v=1.1", {
status: 500,
});
await expect(sac.exchangeForScalarToken(tokenObject)).rejects.toThrow("Scalar request failed: 500");
});
it("should throw if scalar_token is missing in response", async () => {
const sac = new ScalarAuthClient(apiUrl + 3, uiUrl);
fetchMock.postOnce("https://test.com/api3/register?v=1.1", {
body: {},
});
await expect(sac.exchangeForScalarToken(tokenObject)).rejects.toThrow("Missing scalar_token in response");
});
});
describe("registerForToken", () => {
it("should call `termsInteractionCallback` upon M_TERMS_NOT_SIGNED error", async () => {
const sac = new ScalarAuthClient(apiUrl + 4, uiUrl);
const termsInteractionCallback = jest.fn();
sac.setTermsInteractionCallback(termsInteractionCallback);
fetchMock.get("https://test.com/api4/account?scalar_token=testtoken1&v=1.1", {
body: { errcode: "M_TERMS_NOT_SIGNED" },
});
sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken1"));
mocked(client.getTerms).mockResolvedValue({ policies: [] });
await expect(sac.registerForToken()).resolves.toBe("testtoken1");
});
it("should throw upon non-20x code", async () => {
const sac = new ScalarAuthClient(apiUrl + 5, uiUrl);
fetchMock.get("https://test.com/api5/account?scalar_token=testtoken2&v=1.1", {
body: { errcode: "SERVER_IS_SAD" },
status: 500,
});
sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken2"));
await expect(sac.registerForToken()).rejects.toBeTruthy();
});
it("should throw if user_id is missing from response", async () => {
const sac = new ScalarAuthClient(apiUrl + 6, uiUrl);
fetchMock.get("https://test.com/api6/account?scalar_token=testtoken3&v=1.1", {
body: {},
});
sac.exchangeForScalarToken = jest.fn(() => Promise.resolve("testtoken3"));
await expect(sac.registerForToken()).rejects.toThrow("Missing user_id in response");
});
});
describe("getScalarPageTitle", () => {
let sac: ScalarAuthClient;
beforeEach(async () => {
SdkConfig.put({
integrations_rest_url: apiUrl + 7,
integrations_ui_url: uiUrl,
});
window.localStorage.setItem("mx_scalar_token_at_https://test.com/api7", "wokentoken1");
fetchMock.get("https://test.com/api7/account?scalar_token=wokentoken1&v=1.1", {
body: { user_id: client.getUserId() },
});
sac = new ScalarAuthClient(apiUrl + 7, uiUrl);
await sac.connect();
});
it("should return `cached_title` from API /widgets/title_lookup", async () => {
const url = "google.com";
fetchMock.get("https://test.com/api7/widgets/title_lookup?scalar_token=wokentoken1&curl=" + url, {
body: {
page_title_cache_item: {
cached_title: "Google",
},
},
});
await expect(sac.getScalarPageTitle(url)).resolves.toBe("Google");
});
it("should throw upon non-20x code", async () => {
const url = "yahoo.com";
fetchMock.get("https://test.com/api7/widgets/title_lookup?scalar_token=wokentoken1&curl=" + url, {
status: 500,
});
await expect(sac.getScalarPageTitle(url)).rejects.toThrow("Scalar request failed: 500");
});
});
describe("disableWidgetAssets", () => {
let sac: ScalarAuthClient;
beforeEach(async () => {
SdkConfig.put({
integrations_rest_url: apiUrl + 8,
integrations_ui_url: uiUrl,
});
window.localStorage.setItem("mx_scalar_token_at_https://test.com/api8", "wokentoken1");
fetchMock.get("https://test.com/api8/account?scalar_token=wokentoken1&v=1.1", {
body: { user_id: client.getUserId() },
});
sac = new ScalarAuthClient(apiUrl + 8, uiUrl);
await sac.connect();
});
it("should send state=disable to API /widgets/set_assets_state", async () => {
fetchMock.get("https://test.com/api8/widgets/set_assets_state?scalar_token=wokentoken1" +
"&widget_type=m.custom&widget_id=id1&state=disable", {
body: "OK",
});
await expect(sac.disableWidgetAssets(WidgetType.CUSTOM, "id1")).resolves.toBeUndefined();
});
it("should throw upon non-20x code", async () => {
fetchMock.get("https://test.com/api8/widgets/set_assets_state?scalar_token=wokentoken1" +
"&widget_type=m.custom&widget_id=id2&state=disable", {
status: 500,
});
await expect(sac.disableWidgetAssets(WidgetType.CUSTOM, "id2"))
.rejects.toThrow("Scalar request failed: 500");
});
});
});

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { IAbortablePromise, IEncryptedFile, IUploadOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
import { IEncryptedFile, UploadOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
import { createVoiceMessageRecording, VoiceMessageRecording } from "../../src/audio/VoiceMessageRecording";
import { RecordingState, VoiceRecording } from "../../src/audio/VoiceRecording";
@ -161,8 +161,8 @@ describe("VoiceMessageRecording", () => {
matrixClient: MatrixClient,
roomId: string,
file: File | Blob,
_progressHandler?: IUploadOpts["progressHandler"],
): IAbortablePromise<{ url?: string, file?: IEncryptedFile }> => {
_progressHandler?: UploadOpts["progressHandler"],
): Promise<{ url?: string, file?: IEncryptedFile }> => {
uploadFileClient = matrixClient;
uploadFileRoomId = roomId;
uploadBlob = file;

View file

@ -0,0 +1,58 @@
/*
Copyright 2022 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 fetchMock from "fetch-mock-jest";
import { render, screen } from "@testing-library/react";
import { mocked } from "jest-mock";
import { _t } from "../../../../src/languageHandler";
import EmbeddedPage from "../../../../src/components/structures/EmbeddedPage";
jest.mock("../../../../src/languageHandler", () => ({
_t: jest.fn(),
}));
describe("<EmbeddedPage />", () => {
it("should translate _t strings", async () => {
mocked(_t).mockReturnValue("Przeglądaj pokoje");
fetchMock.get("https://home.page", {
body: '<h1>_t("Explore rooms")</h1>',
});
const { asFragment } = render(<EmbeddedPage url="https://home.page" />);
await screen.findByText("Przeglądaj pokoje");
expect(_t).toHaveBeenCalledWith("Explore rooms");
expect(asFragment()).toMatchSnapshot();
});
it("should show error if unable to load", async () => {
mocked(_t).mockReturnValue("Couldn't load page");
fetchMock.get("https://other.page", {
status: 404,
});
const { asFragment } = render(<EmbeddedPage url="https://other.page" />);
await screen.findByText("Couldn't load page");
expect(_t).toHaveBeenCalledWith("Couldn't load page");
expect(asFragment()).toMatchSnapshot();
});
it("should render nothing if no url given", () => {
const { asFragment } = render(<EmbeddedPage />);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<EmbeddedPage /> should render nothing if no url given 1`] = `
<DocumentFragment>
<div
class="undefined undefined_guest"
>
<div
class="undefined_body"
/>
</div>
</DocumentFragment>
`;
exports[`<EmbeddedPage /> should show error if unable to load 1`] = `
<DocumentFragment>
<div
class="undefined undefined_guest"
>
<div
class="undefined_body"
>
Couldn't load page
</div>
</div>
</DocumentFragment>
`;
exports[`<EmbeddedPage /> should translate _t strings 1`] = `
<DocumentFragment>
<div
class="undefined undefined_guest"
>
<div
class="undefined_body"
>
<h1>
Przeglądaj pokoje
</h1>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,104 @@
/*
Copyright 2022 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 fetchMock from "fetch-mock-jest";
import { render, screen, waitForElementToBeRemoved } from "@testing-library/react";
import ChangelogDialog from "../../../../src/components/views/dialogs/ChangelogDialog";
describe("<ChangelogDialog />", () => {
it("should fetch github proxy url for each repo with old and new version strings", async () => {
const webUrl = "https://riot.im/github/repos/vector-im/element-web/compare/oldsha1...newsha1";
fetchMock.get(webUrl, {
url: "https://api.github.com/repos/vector-im/element-web/compare/master...develop",
html_url: "https://github.com/vector-im/element-web/compare/master...develop",
permalink_url: "https://github.com/vector-im/element-web/compare/vector-im:72ca95e...vector-im:8891698",
diff_url: "https://github.com/vector-im/element-web/compare/master...develop.diff",
patch_url: "https://github.com/vector-im/element-web/compare/master...develop.patch",
base_commit: {},
merge_base_commit: {},
status: "ahead",
ahead_by: 24,
behind_by: 0,
total_commits: 24,
commits: [{
sha: "commit-sha",
html_url: "https://api.github.com/repos/vector-im/element-web/commit/commit-sha",
commit: { message: "This is the first commit message" },
}],
files: [],
});
const reactUrl = "https://riot.im/github/repos/matrix-org/matrix-react-sdk/compare/oldsha2...newsha2";
fetchMock.get(reactUrl, {
url: "https://api.github.com/repos/matrix-org/matrix-react-sdk/compare/master...develop",
html_url: "https://github.com/matrix-org/matrix-react-sdk/compare/master...develop",
permalink_url: "https://github.com/matrix-org/matrix-react-sdk/compare/matrix-org:cdb00...matrix-org:4a926",
diff_url: "https://github.com/matrix-org/matrix-react-sdk/compare/master...develop.diff",
patch_url: "https://github.com/matrix-org/matrix-react-sdk/compare/master...develop.patch",
base_commit: {},
merge_base_commit: {},
status: "ahead",
ahead_by: 83,
behind_by: 0,
total_commits: 83,
commits: [{
sha: "commit-sha0",
html_url: "https://api.github.com/repos/matrix-org/matrix-react-sdk/commit/commit-sha",
commit: { message: "This is a commit message" },
}],
files: [],
});
const jsUrl = "https://riot.im/github/repos/matrix-org/matrix-js-sdk/compare/oldsha3...newsha3";
fetchMock.get(jsUrl, {
url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/compare/master...develop",
html_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop",
permalink_url: "https://github.com/matrix-org/matrix-js-sdk/compare/matrix-org:6166a8f...matrix-org:fec350",
diff_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.diff",
patch_url: "https://github.com/matrix-org/matrix-js-sdk/compare/master...develop.patch",
base_commit: {},
merge_base_commit: {},
status: "ahead",
ahead_by: 48,
behind_by: 0,
total_commits: 48,
commits: [{
sha: "commit-sha1",
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha1",
commit: { message: "This is a commit message" },
}, {
sha: "commit-sha2",
html_url: "https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha2",
commit: { message: "This is another commit message" },
}],
files: [],
});
const newVersion = "newsha1-react-newsha2-js-newsha3";
const oldVersion = "oldsha1-react-oldsha2-js-oldsha3";
const { asFragment } = render((
<ChangelogDialog newVersion={newVersion} version={oldVersion} onFinished={jest.fn()} />
));
// Wait for spinners to go away
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
expect(fetchMock).toHaveFetched(webUrl);
expect(fetchMock).toHaveFetched(reactUrl);
expect(fetchMock).toHaveFetched(jsUrl);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -26,6 +26,11 @@ import SdkConfig from "../../../../src/SdkConfig";
import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig";
import { IConfigOptions } from "../../../../src/IConfigOptions";
const mockGetAccessToken = jest.fn().mockResolvedValue("getAccessToken");
jest.mock("../../../../src/IdentityAuthClient", () => jest.fn().mockImplementation(() => ({
getAccessToken: mockGetAccessToken,
})));
describe("InviteDialog", () => {
const roomId = "!111111111111111111:example.org";
const aliceId = "@alice:example.org";
@ -42,6 +47,14 @@ describe("InviteDialog", () => {
getProfileInfo: jest.fn().mockRejectedValue({ errcode: "" }),
getIdentityServerUrl: jest.fn(),
searchUserDirectory: jest.fn().mockResolvedValue({}),
lookupThreePid: jest.fn(),
registerWithIdentityServer: jest.fn().mockResolvedValue({
access_token: "access_token",
token: "token",
}),
getOpenIdToken: jest.fn().mockResolvedValue({}),
getIdentityAccount: jest.fn().mockResolvedValue({}),
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
});
beforeEach(() => {
@ -85,7 +98,7 @@ describe("InviteDialog", () => {
expect(screen.queryByText("Invite to Room")).toBeTruthy();
});
it("should suggest valid MXIDs even if unknown", () => {
it("should suggest valid MXIDs even if unknown", async () => {
render((
<InviteDialog
kind={KIND_INVITE}
@ -95,7 +108,7 @@ describe("InviteDialog", () => {
/>
));
expect(screen.queryByText("@localpart:server.tld")).toBeFalsy();
await screen.findAllByText("@localpart:server.tld"); // Using findAllByText as the MXID is used for name too
});
it("should not suggest invalid MXIDs", () => {
@ -110,4 +123,48 @@ describe("InviteDialog", () => {
expect(screen.queryByText("@localpart:server:tld")).toBeFalsy();
});
it("should lookup inputs which look like email addresses", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({
address: "foobar@email.com",
medium: "email",
mxid: "@foobar:server",
});
mockClient.getProfileInfo.mockResolvedValue({
displayname: "Mr. Foo",
avatar_url: "mxc://foo/bar",
});
render((
<InviteDialog
kind={KIND_INVITE}
roomId={roomId}
onFinished={jest.fn()}
initialText="foobar@email.com"
/>
));
await screen.findByText("Mr. Foo");
await screen.findByText("@foobar:server");
expect(mockClient.lookupThreePid).toHaveBeenCalledWith("email", "foobar@email.com", expect.anything());
expect(mockClient.getProfileInfo).toHaveBeenCalledWith("@foobar:server");
});
it("should suggest e-mail even if lookup fails", async () => {
mockClient.getIdentityServerUrl.mockReturnValue("https://identity-server");
mockClient.lookupThreePid.mockResolvedValue({});
render((
<InviteDialog
kind={KIND_INVITE}
roomId={roomId}
onFinished={jest.fn()}
initialText="foobar@email.com"
/>
));
await screen.findByText("foobar@email.com");
await screen.findByText("Invite by email");
});
});

View file

@ -0,0 +1,135 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ChangelogDialog /> should fetch github proxy url for each repo with old and new version strings 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header mx_Dialog_headerWithCancel"
>
<h2
class="mx_Heading_h2 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Changelog
</h2>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div
class="mx_ChangelogDialog_content"
>
<div>
<h2>
vector-im/element-web
</h2>
<ul>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/vector-im/element-web/commit/commit-sha"
rel="noreferrer noopener"
target="_blank"
>
This is the first commit message
</a>
</li>
</ul>
</div>
<div>
<h2>
matrix-org/matrix-react-sdk
</h2>
<ul>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/matrix-org/matrix-react-sdk/commit/commit-sha"
rel="noreferrer noopener"
target="_blank"
>
This is a commit message
</a>
</li>
</ul>
</div>
<div>
<h2>
matrix-org/matrix-js-sdk
</h2>
<ul>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha1"
rel="noreferrer noopener"
target="_blank"
>
This is a commit message
</a>
</li>
<li
class="mx_ChangelogDialog_li"
>
<a
href="https://api.github.com/repos/matrix-org/matrix-js-sdk/commit/commit-sha2"
rel="noreferrer noopener"
target="_blank"
>
This is another commit message
</a>
</li>
</ul>
</div>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-test-id="dialog-cancel-button"
type="button"
>
Cancel
</button>
<button
class="mx_Dialog_primary"
data-test-id="dialog-primary-button"
type="button"
>
Update
</button>
</span>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -364,7 +364,7 @@ describe('<RoomPreviewBar />', () => {
expect(getMessage(component)).toMatchSnapshot();
expect(MatrixClientPeg.get().lookupThreePid).toHaveBeenCalledWith(
'email', invitedEmail, undefined, 'mock-token',
'email', invitedEmail, 'mock-token',
);
await testJoinButton({ inviterName, invitedEmail })();
});

View file

@ -130,6 +130,20 @@ describe("createRoom", () => {
expect(callPower).toBe(100);
expect(callMemberPower).toBe(100);
});
it("should upload avatar if one is passed", async () => {
client.uploadContent.mockResolvedValue({ content_uri: "mxc://foobar" });
const avatar = new File([], "avatar.png");
await createRoom({ avatar });
expect(client.createRoom).toHaveBeenCalledWith(expect.objectContaining({
initial_state: expect.arrayContaining([{
content: {
url: "mxc://foobar",
},
type: "m.room.avatar",
}]),
}));
});
});
describe("canEncryptToAllUsers", () => {

View file

@ -27,10 +27,7 @@ import {
import { stubClient } from '../test-utils';
describe('languageHandler', function() {
/*
See /__mocks__/browser-request.js/ for how we are stubbing out translations
to provide fixture data for these tests
*/
// See setupLanguage.ts for how we are stubbing out translations to provide fixture data for these tests
const basicString = 'Rooms';
const selfClosingTagSub = 'Accept <policyLink /> to continue:';
const textInTagSub = '<a>Upgrade</a> to your own domain';

View file

@ -14,7 +14,70 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import * as languageHandler from "../../src/languageHandler";
import en from "../../src/i18n/strings/en_EN.json";
import de from "../../src/i18n/strings/de_DE.json";
fetchMock.config.overwriteRoutes = false;
fetchMock.catch("");
window.fetch = fetchMock.sandbox();
const lv = {
"Save": "Saglabāt",
"Uploading %(filename)s and %(count)s others|one": "Качване на %(filename)s и %(count)s друг",
};
// Fake languages.json containing references to en_EN, de_DE and lv
// en_EN.json
// de_DE.json
// lv.json - mock version with few translations, used to test fallback translation
function weblateToCounterpart(inTrs: object): object {
const outTrs = {};
for (const key of Object.keys(inTrs)) {
const keyParts = key.split('|', 2);
if (keyParts.length === 2) {
let obj = outTrs[keyParts[0]];
if (obj === undefined) {
obj = outTrs[keyParts[0]] = {};
} else if (typeof obj === "string") {
// This is a transitional edge case if a string went from singular to pluralised and both still remain
// in the translation json file. Use the singular translation as `other` and merge pluralisation atop.
obj = outTrs[keyParts[0]] = {
"other": inTrs[key],
};
console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]);
}
obj[keyParts[1]] = inTrs[key];
} else {
outTrs[key] = inTrs[key];
}
}
return outTrs;
}
fetchMock
.get("/i18n/languages.json", {
"en": {
"fileName": "en_EN.json",
"label": "English",
},
"de": {
"fileName": "de_DE.json",
"label": "German",
},
"lv": {
"fileName": "lv.json",
"label": "Latvian",
},
})
.get("end:en_EN.json", weblateToCounterpart(en))
.get("end:de_DE.json", weblateToCounterpart(de))
.get("end:lv.json", weblateToCounterpart(lv));
languageHandler.setLanguage('en');
languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);

View file

@ -45,6 +45,7 @@ global.matchMedia = mockMatchMedia;
// maplibre requires a createObjectURL mock
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
// polyfilling TextEncoder as it is not available on JSDOM
// view https://github.com/facebook/jest/issues/9983

View file

@ -20,8 +20,7 @@ import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { configure } from "enzyme";
import "blob-polyfill"; // https://github.com/jsdom/jsdom/issues/2555
// Enable the jest & enzyme mocks
require('jest-fetch-mock').enableMocks();
// Enable the enzyme mocks
configure({ adapter: new Adapter() });
// Very carefully enable the mocks for everything else in

View file

@ -158,7 +158,7 @@ export function createTestClient(): MatrixClient {
getOpenIdToken: jest.fn().mockResolvedValue(undefined),
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
getIdentityAccount: jest.fn().mockResolvedValue({}),
getTerms: jest.fn().mockResolvedValueOnce(undefined),
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(undefined),
isVersionSupported: jest.fn().mockResolvedValue(undefined),
getPushRules: jest.fn().mockResolvedValue(undefined),
@ -182,6 +182,7 @@ export function createTestClient(): MatrixClient {
setVideoInput: jest.fn(),
setAudioInput: jest.fn(),
} as unknown as MediaHandler),
uploadContent: jest.fn(),
} as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client);

View file

@ -98,9 +98,9 @@ describe('MultiInviter', () => {
const result = await inviter.invite([MXID1, MXID2, MXID3]);
expect(client.invite).toHaveBeenCalledTimes(3);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined, undefined);
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined, undefined);
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined, undefined);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
expectAllInvitedResult(result);
});
@ -116,9 +116,9 @@ describe('MultiInviter', () => {
const result = await inviter.invite([MXID1, MXID2, MXID3]);
expect(client.invite).toHaveBeenCalledTimes(3);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined, undefined);
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined, undefined);
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined, undefined);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
expectAllInvitedResult(result);
});
@ -131,7 +131,7 @@ describe('MultiInviter', () => {
const result = await inviter.invite([MXID1, MXID2, MXID3]);
expect(client.invite).toHaveBeenCalledTimes(1);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined, undefined);
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
// The resolved state is 'invited' for all users.
// With the above client expectations, the test ensures that only the first user is invited.

219
yarn.lock
View file

@ -68,6 +68,32 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.13.tgz#6aff7b350a1e8c3e40b029e46cbe78e24a913483"
integrity sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==
"@babel/compat-data@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.3.tgz#707b939793f867f5a73b2666e6d9a3396eb03151"
integrity sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==
"@babel/core@^7.0.0":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.3.tgz#2519f62a51458f43b682d61583c3810e7dcee64c"
integrity sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==
dependencies:
"@ampproject/remapping" "^2.1.0"
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.19.3"
"@babel/helper-compilation-targets" "^7.19.3"
"@babel/helper-module-transforms" "^7.19.0"
"@babel/helpers" "^7.19.0"
"@babel/parser" "^7.19.3"
"@babel/template" "^7.18.10"
"@babel/traverse" "^7.19.3"
"@babel/types" "^7.19.3"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.1"
semver "^6.3.0"
"@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
version "7.18.13"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac"
@ -114,6 +140,15 @@
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
"@babel/generator@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.3.tgz#d7f4d1300485b4547cb6f94b27d10d237b42bf59"
integrity sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==
dependencies:
"@babel/types" "^7.19.3"
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
@ -139,6 +174,16 @@
browserslist "^4.20.2"
semver "^6.3.0"
"@babel/helper-compilation-targets@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz#a10a04588125675d7c7ae299af86fa1b2ee038ca"
integrity sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==
dependencies:
"@babel/compat-data" "^7.19.3"
"@babel/helper-validator-option" "^7.18.6"
browserslist "^4.21.3"
semver "^6.3.0"
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.18.9":
version "7.18.13"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz#63e771187bd06d234f95fdf8bd5f8b6429de6298"
@ -192,6 +237,14 @@
"@babel/template" "^7.18.6"
"@babel/types" "^7.18.9"
"@babel/helper-function-name@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c"
integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==
dependencies:
"@babel/template" "^7.18.10"
"@babel/types" "^7.19.0"
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
@ -227,6 +280,20 @@
"@babel/traverse" "^7.18.9"
"@babel/types" "^7.18.9"
"@babel/helper-module-transforms@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz#309b230f04e22c58c6a2c0c0c7e50b216d350c30"
integrity sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==
dependencies:
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-module-imports" "^7.18.6"
"@babel/helper-simple-access" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/helper-validator-identifier" "^7.18.6"
"@babel/template" "^7.18.10"
"@babel/traverse" "^7.19.0"
"@babel/types" "^7.19.0"
"@babel/helper-optimise-call-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe"
@ -291,6 +358,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
"@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-option@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
@ -315,6 +387,15 @@
"@babel/traverse" "^7.18.9"
"@babel/types" "^7.18.9"
"@babel/helpers@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.0.tgz#f30534657faf246ae96551d88dd31e9d1fa1fc18"
integrity sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==
dependencies:
"@babel/template" "^7.18.10"
"@babel/traverse" "^7.19.0"
"@babel/types" "^7.19.0"
"@babel/highlight@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
@ -329,6 +410,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4"
integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==
"@babel/parser@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.3.tgz#8dd36d17c53ff347f9e55c328710321b49479a9a"
integrity sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@ -1092,6 +1178,22 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/traverse@^7.19.0", "@babel/traverse@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.3.tgz#3a3c5348d4988ba60884e8494b0592b2f15a04b4"
integrity sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==
dependencies:
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.19.3"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.19.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.19.3"
"@babel/types" "^7.19.3"
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
version "7.18.13"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a"
@ -1101,6 +1203,15 @@
"@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0"
"@babel/types@^7.19.0", "@babel/types@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.3.tgz#fc420e6bbe54880bce6779ffaf315f5e43ec9624"
integrity sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==
dependencies:
"@babel/helper-string-parser" "^7.18.10"
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@ -3539,6 +3650,11 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==
core-js@^3.0.0:
version "3.25.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.5.tgz#e86f651a2ca8a0237a5f064c2fe56cef89646e27"
integrity sha512-nbm6eZSjm+ZuBQxCUPQKQCoUEfFOXjUZ8dTTyikyKaWrTYmAVbykQfwsKE5dBK88u3QCkCrzsx/PPlKfhsvgpw==
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -3576,13 +3692,6 @@ crc-32@^0.3.0:
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-0.3.0.tgz#6a3d3687f5baec41f7e9b99fe1953a2e5d19775e"
integrity sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==
cross-fetch@^3.0.4:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
node-fetch "2.6.7"
cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -4772,6 +4881,29 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fetch-mock-jest@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz#0e13df990d286d9239e284f12b279ed509bf53cd"
integrity sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ==
dependencies:
fetch-mock "^9.11.0"
fetch-mock@^9.11.0:
version "9.11.0"
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f"
integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==
dependencies:
"@babel/core" "^7.0.0"
"@babel/runtime" "^7.0.0"
core-js "^3.0.0"
debug "^4.1.1"
glob-to-regexp "^0.4.0"
is-subset "^0.1.1"
lodash.isequal "^4.5.0"
path-to-regexp "^2.2.1"
querystring "^0.2.0"
whatwg-url "^6.5.0"
fflate@^0.4.1:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
@ -5074,7 +5206,7 @@ glob-parent@^6.0.1:
dependencies:
is-glob "^4.0.3"
glob-to-regexp@^0.4.1:
glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
@ -6018,14 +6150,6 @@ jest-environment-node@^27.5.1:
jest-mock "^27.5.1"
jest-util "^27.5.1"
jest-fetch-mock@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
dependencies:
cross-fetch "^3.0.4"
promise-polyfill "^8.1.3"
jest-get-type@^26.3.0:
version "26.3.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
@ -6788,6 +6912,11 @@ lodash.once@^4.1.1:
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
@ -6951,10 +7080,10 @@ matrix-events-sdk@^0.0.1-beta.7:
request "^2.88.2"
unhomoglyph "^1.0.6"
matrix-mock-request@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.2.tgz#11e38ed1233dced88a6f2bfba1684d5c5b3aa2c2"
integrity sha512-/OXCIzDGSLPJ3fs+uzDrtaOHI/Sqp4iEuniRn31U8S06mPXbvAnXknHqJ4c6A/KVwJj/nPFbGXpK4wPM038I6A==
matrix-mock-request@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.5.0.tgz#78da2590e82be2e31edcf9814833af5e5f8d2f1a"
integrity sha512-7T3gklpW+4rfHsTnp/FDML7aWoBrXhAh8+1ltinQfAh9TDj6y382z/RUMR7i03d1WDzt/ed1UTihqO5GDoOq9Q==
dependencies:
expect "^28.1.0"
@ -7189,13 +7318,6 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-fetch@2.6.7, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -7204,6 +7326,13 @@ node-fetch@^1.0.1:
encoding "^0.1.11"
is-stream "^1.0.1"
node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@ -7549,6 +7678,11 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@^2.2.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704"
integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==
path-to-regexp@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5"
@ -7750,11 +7884,6 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
promise-polyfill@^8.1.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6"
integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==
promise@^7.0.3, promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@ -7854,6 +7983,11 @@ querystring@0.2.0:
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==
querystring@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd"
integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==
querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
@ -9075,6 +9209,13 @@ tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==
dependencies:
punycode "^2.1.0"
tr46@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
@ -9448,6 +9589,11 @@ webidl-conversions@^3.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
@ -9493,6 +9639,15 @@ whatwg-url@^5.0.0:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
whatwg-url@^8.0.0, whatwg-url@^8.5.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"