Merge branch 'develop' into kegan/lists-as-keys
commit
fcde4b7880
|
@ -1,3 +1,9 @@
|
||||||
|
Changes in [3.64.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.1) (2023-01-18)
|
||||||
|
=====================================================================================================
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix crash in older browsers (replace .at() with array.length-1) ([\#9933](https://github.com/matrix-org/matrix-react-sdk/pull/9933)). Fixes matrix-org/element-web-rageshakes#19281.
|
||||||
|
|
||||||
Changes in [3.64.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.0) (2023-01-18)
|
Changes in [3.64.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.0) (2023-01-18)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
import type { CypressBot } from "../../support/bot";
|
import type { CypressBot } from "../../support/bot";
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
type EmojiMapping = [emoji: string, name: string];
|
type EmojiMapping = [emoji: string, name: string];
|
||||||
interface CryptoTestContext extends Mocha.Context {
|
interface CryptoTestContext extends Mocha.Context {
|
||||||
|
@ -154,11 +155,15 @@ const verify = function (this: CryptoTestContext) {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Cryptography", function () {
|
describe("Cryptography", function () {
|
||||||
|
let aliceCredentials: UserCredentials;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
cy.startHomeserver("default")
|
cy.startHomeserver("default")
|
||||||
.as("homeserver")
|
.as("homeserver")
|
||||||
.then((homeserver: HomeserverInstance) => {
|
.then((homeserver: HomeserverInstance) => {
|
||||||
cy.initTestUser(homeserver, "Alice", undefined, "alice_");
|
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
|
||||||
|
aliceCredentials = credentials;
|
||||||
|
});
|
||||||
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
|
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -183,7 +188,7 @@ describe("Cryptography", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
|
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
|
||||||
cy.bootstrapCrossSigning();
|
cy.bootstrapCrossSigning(aliceCredentials);
|
||||||
startDMWithBob.call(this);
|
startDMWithBob.call(this);
|
||||||
// send first message
|
// send first message
|
||||||
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
|
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
|
||||||
|
@ -194,7 +199,7 @@ describe("Cryptography", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
|
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
|
||||||
cy.bootstrapCrossSigning();
|
cy.bootstrapCrossSigning(aliceCredentials);
|
||||||
autoJoin(this.bob);
|
autoJoin(this.bob);
|
||||||
|
|
||||||
// we need to have a room with the other user present, so we can open the verification panel
|
// we need to have a room with the other user present, so we can open the verification panel
|
||||||
|
@ -212,7 +217,7 @@ describe("Cryptography", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
|
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
|
||||||
cy.bootstrapCrossSigning();
|
cy.bootstrapCrossSigning(aliceCredentials);
|
||||||
|
|
||||||
// bob has a second, not cross-signed, device
|
// bob has a second, not cross-signed, device
|
||||||
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
|
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
|
||||||
|
|
|
@ -105,15 +105,9 @@ describe("Decryption Failure Bar", () => {
|
||||||
"and there are other verified devices or backups",
|
"and there are other verified devices or backups",
|
||||||
() => {
|
() => {
|
||||||
let otherDevice: MatrixClient | undefined;
|
let otherDevice: MatrixClient | undefined;
|
||||||
cy.loginBot(homeserver, testUser.username, testUser.password, {})
|
cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true })
|
||||||
.then(async (cli) => {
|
.then(async (cli) => {
|
||||||
otherDevice = cli;
|
otherDevice = cli;
|
||||||
await otherDevice.bootstrapCrossSigning({
|
|
||||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
|
||||||
await makeRequest({});
|
|
||||||
},
|
|
||||||
setupNewCrossSigning: true,
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cy.botSendMessage(bot, roomId, "test");
|
cy.botSendMessage(bot, roomId, "test");
|
||||||
|
@ -169,15 +163,11 @@ describe("Decryption Failure Bar", () => {
|
||||||
"should prompt the user to reset keys, if this device isn't verified " +
|
"should prompt the user to reset keys, if this device isn't verified " +
|
||||||
"and there are no other verified devices or backups",
|
"and there are no other verified devices or backups",
|
||||||
() => {
|
() => {
|
||||||
cy.loginBot(homeserver, testUser.username, testUser.password, {}).then(async (cli) => {
|
cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true }).then(
|
||||||
await cli.bootstrapCrossSigning({
|
async (cli) => {
|
||||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
await cli.logout(true);
|
||||||
await makeRequest({});
|
},
|
||||||
},
|
);
|
||||||
setupNewCrossSigning: true,
|
|
||||||
});
|
|
||||||
await cli.logout(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.botSendMessage(bot, roomId, "test");
|
cy.botSendMessage(bot, roomId, "test");
|
||||||
cy.wait(5000);
|
cy.wait(5000);
|
||||||
|
|
|
@ -150,7 +150,14 @@ function setupBotClient(
|
||||||
if (opts.bootstrapCrossSigning) {
|
if (opts.bootstrapCrossSigning) {
|
||||||
await cli.bootstrapCrossSigning({
|
await cli.bootstrapCrossSigning({
|
||||||
authUploadDeviceSigningKeys: async (func) => {
|
authUploadDeviceSigningKeys: async (func) => {
|
||||||
await func({});
|
await func({
|
||||||
|
type: "m.login.password",
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: credentials.userId,
|
||||||
|
},
|
||||||
|
password: credentials.password,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import type { IContent } from "matrix-js-sdk/src/models/event";
|
import type { IContent } from "matrix-js-sdk/src/models/event";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
import { UserCredentials } from "./login";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
@ -119,7 +120,7 @@ declare global {
|
||||||
/**
|
/**
|
||||||
* Boostraps cross-signing.
|
* Boostraps cross-signing.
|
||||||
*/
|
*/
|
||||||
bootstrapCrossSigning(): Chainable<void>;
|
bootstrapCrossSigning(credendtials: UserCredentials): Chainable<void>;
|
||||||
/**
|
/**
|
||||||
* Joins the given room by alias or ID
|
* Joins the given room by alias or ID
|
||||||
* @param roomIdOrAlias the id or alias of the room to join
|
* @param roomIdOrAlias the id or alias of the room to join
|
||||||
|
@ -210,11 +211,18 @@ Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add("bootstrapCrossSigning", () => {
|
Cypress.Commands.add("bootstrapCrossSigning", (credentials: UserCredentials) => {
|
||||||
cy.window({ log: false }).then((win) => {
|
cy.window({ log: false }).then((win) => {
|
||||||
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
|
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
|
||||||
authUploadDeviceSigningKeys: async (func) => {
|
authUploadDeviceSigningKeys: async (func) => {
|
||||||
await func({});
|
await func({
|
||||||
|
type: "m.login.password",
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: credentials.userId,
|
||||||
|
},
|
||||||
|
password: credentials.password,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -76,6 +76,7 @@ export interface Credentials {
|
||||||
userId: string;
|
userId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
homeServer: string;
|
homeServer: string;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerUser(
|
function registerUser(
|
||||||
|
@ -120,6 +121,7 @@ function registerUser(
|
||||||
accessToken: response.body.access_token,
|
accessToken: response.body.access_token,
|
||||||
userId: response.body.user_id,
|
userId: response.body.user_id,
|
||||||
deviceId: response.body.device_id,
|
deviceId: response.body.device_id,
|
||||||
|
password: password,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.64.0",
|
"version": "3.64.1",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/* 1rem :: 10px */
|
/* 1rem :: 10px */
|
||||||
|
|
||||||
|
$spacing-2: 2px;
|
||||||
$spacing-4: 4px;
|
$spacing-4: 4px;
|
||||||
$spacing-8: 8px;
|
$spacing-8: 8px;
|
||||||
$spacing-12: 12px;
|
$spacing-12: 12px;
|
||||||
|
|
|
@ -548,7 +548,19 @@ $left-gutter: 64px;
|
||||||
pre,
|
pre,
|
||||||
code {
|
code {
|
||||||
font-family: $monospace-font-family !important;
|
font-family: $monospace-font-family !important;
|
||||||
background-color: $codeblock-background-color;
|
background-color: $system;
|
||||||
|
}
|
||||||
|
|
||||||
|
code:not(pre *) {
|
||||||
|
background-color: $inlinecode-background-color;
|
||||||
|
border: 1px solid $inlinecode-border-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
// The horizontal padding is added by gfm.css .markdown-body
|
||||||
|
padding: $spacing-2 0;
|
||||||
|
// Avoid inline code blocks to be sticked when on multiple lines
|
||||||
|
line-height: $font-22px;
|
||||||
|
// Avoid the border to be glued to the other words
|
||||||
|
margin-right: $spacing-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
@ -566,6 +578,8 @@ $left-gutter: 64px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
border: 1px solid $quinary-content;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
white-space: pre; /* we want code blocks to be scrollable and not wrap */
|
white-space: pre; /* we want code blocks to be scrollable and not wrap */
|
||||||
|
|
||||||
|
@ -744,6 +758,8 @@ $left-gutter: 64px;
|
||||||
|
|
||||||
.mx_EventTile_collapsedCodeBlock {
|
.mx_EventTile_collapsedCodeBlock {
|
||||||
max-height: 30vh;
|
max-height: 30vh;
|
||||||
|
padding-top: $spacing-12;
|
||||||
|
padding-bottom: $spacing-12;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inserted adjacent to <pre> blocks, (See TextualBody) */
|
/* Inserted adjacent to <pre> blocks, (See TextualBody) */
|
||||||
|
|
|
@ -46,9 +46,30 @@ limitations under the License.
|
||||||
|
|
||||||
// model output always includes a linebreak but we do not want the user
|
// model output always includes a linebreak but we do not want the user
|
||||||
// to see it when writing input in lists
|
// to see it when writing input in lists
|
||||||
:is(ol, ul) + br:last-of-type {
|
:is(ol, ul, pre) + br:last-of-type {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> pre {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: $spacing-8 $spacing-12;
|
||||||
|
|
||||||
|
background-color: $inlinecode-background-color;
|
||||||
|
border: 1px solid $inlinecode-border-color;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: $monospace-font-family !important;
|
||||||
|
background-color: $inlinecode-background-color;
|
||||||
|
border: 1px solid $inlinecode-border-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: $spacing-2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_WysiwygComposer_Editor_content_placeholder::before {
|
.mx_WysiwygComposer_Editor_content_placeholder::before {
|
||||||
|
|
|
@ -50,6 +50,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_FormattingButtons_disabled {
|
||||||
|
.mx_FormattingButtons_Icon {
|
||||||
|
color: $quinary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_FormattingButtons_Icon {
|
.mx_FormattingButtons_Icon {
|
||||||
--size: 16px;
|
--size: 16px;
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.14288 6.99997L5.47622 5.66663C5.5905 5.55235 5.64765 5.41266 5.64765 5.24758C5.64765 5.0825 5.5905 4.94282 5.47622 4.82854C5.36193 4.71425 5.22225 4.65711 5.05717 4.65711C4.89209 4.65711 4.75241 4.71425 4.63812 4.82854L2.86669 6.59997C2.8032 6.66346 2.75876 6.72695 2.73336 6.79044C2.70796 6.85393 2.69526 6.92377 2.69526 6.99997C2.69526 7.07616 2.70796 7.146 2.73336 7.20949C2.75876 7.27298 2.8032 7.33647 2.86669 7.39996L4.65717 9.19044C4.77145 9.30473 4.91114 9.36187 5.07622 9.36187C5.2413 9.36187 5.38098 9.30473 5.49526 9.19044C5.60955 9.07616 5.66669 8.93647 5.66669 8.77139C5.66669 8.60631 5.60955 8.46663 5.49526 8.35235L4.14288 6.99997ZM9.85717 6.99997L8.50479 8.35235C8.3905 8.46663 8.33336 8.60631 8.33336 8.77139C8.33336 8.93647 8.3905 9.07616 8.50479 9.19044C8.61907 9.30473 8.75876 9.36187 8.92384 9.36187C9.08891 9.36187 9.2286 9.30473 9.34288 9.19044L11.1334 7.39996C11.1969 7.33647 11.2413 7.27298 11.2667 7.20949C11.2921 7.146 11.3048 7.07616 11.3048 6.99997C11.3048 6.92377 11.2921 6.85393 11.2667 6.79044C11.2413 6.72695 11.1969 6.66346 11.1334 6.59997L9.34288 4.80949C9.29209 4.746 9.2286 4.70155 9.15241 4.67616C9.07622 4.65076 9.00003 4.63806 8.92384 4.63806C8.84765 4.63806 8.77463 4.65076 8.70479 4.67616C8.63495 4.70155 8.56828 4.746 8.50479 4.80949C8.3905 4.92377 8.33336 5.06346 8.33336 5.22854C8.33336 5.39362 8.3905 5.5333 8.50479 5.64758L9.85717 6.99997ZM1.28574 13.8571C0.980979 13.8571 0.714312 13.7428 0.48574 13.5143C0.257169 13.2857 0.142883 13.019 0.142883 12.7143V1.28568C0.142883 0.980918 0.257169 0.714251 0.48574 0.485679C0.714312 0.257108 0.980979 0.142822 1.28574 0.142822H12.7143C13.0191 0.142822 13.2857 0.257108 13.5143 0.485679C13.7429 0.714251 13.8572 0.980918 13.8572 1.28568V12.7143C13.8572 13.019 13.7429 13.2857 13.5143 13.5143C13.2857 13.7428 13.0191 13.8571 12.7143 13.8571H1.28574ZM1.28574 12.7143H12.7143V1.28568H1.28574V12.7143Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -224,6 +224,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||||
$breadcrumb-placeholder-bg-color: #272c35;
|
$breadcrumb-placeholder-bg-color: #272c35;
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
$resend-button-divider-color: rgba($header-panel-text-primary-color, 0.74);
|
$resend-button-divider-color: rgba($header-panel-text-primary-color, 0.74);
|
||||||
|
$inlinecode-border-color: $quinary-content;
|
||||||
|
$inlinecode-background-color: $system;
|
||||||
$codeblock-background-color: #2a3039;
|
$codeblock-background-color: #2a3039;
|
||||||
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
||||||
$selected-color: $room-highlight-color;
|
$selected-color: $room-highlight-color;
|
||||||
|
|
|
@ -190,6 +190,8 @@ $appearance-tab-border-color: $room-highlight-color;
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
$codeblock-background-color: #2a3039;
|
$codeblock-background-color: #2a3039;
|
||||||
|
$inlinecode-border-color: #2a3039;
|
||||||
|
$inlinecode-background-color: #2a3039;
|
||||||
|
|
||||||
/* Bubble tiles */
|
/* Bubble tiles */
|
||||||
$eventbubble-self-bg: #14322e;
|
$eventbubble-self-bg: #14322e;
|
||||||
|
|
|
@ -290,6 +290,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
$codeblock-background-color: $header-panel-bg-color;
|
$codeblock-background-color: $header-panel-bg-color;
|
||||||
|
$inlinecode-border-color: $header-panel-bg-color;
|
||||||
|
$inlinecode-background-color: $header-panel-bg-color;
|
||||||
|
|
||||||
/* Bubble tiles */
|
/* Bubble tiles */
|
||||||
$eventbubble-self-bg: #f0fbf8;
|
$eventbubble-self-bg: #f0fbf8;
|
||||||
|
|
|
@ -295,6 +295,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
$theme-button-bg-color: $quinary-content;
|
$theme-button-bg-color: $quinary-content;
|
||||||
$resend-button-divider-color: $input-darker-bg-color;
|
$resend-button-divider-color: $input-darker-bg-color;
|
||||||
|
$inlinecode-border-color: $quinary-content;
|
||||||
|
$inlinecode-background-color: $system;
|
||||||
$codeblock-background-color: $header-panel-bg-color;
|
$codeblock-background-color: $header-panel-bg-color;
|
||||||
$scrollbar-thumb-color: rgba(0, 0, 0, 0.2);
|
$scrollbar-thumb-color: rgba(0, 0, 0, 0.2);
|
||||||
$selected-color: $secondary-accent-color;
|
$selected-color: $secondary-accent-color;
|
||||||
|
|
|
@ -85,7 +85,7 @@ export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread):
|
||||||
// https://github.com/vector-im/element-web/issues/2427
|
// https://github.com/vector-im/element-web/issues/2427
|
||||||
// ...and possibly some of the others at
|
// ...and possibly some of the others at
|
||||||
// https://github.com/vector-im/element-web/issues/3363
|
// https://github.com/vector-im/element-web/issues/3363
|
||||||
if (roomOrThread.timeline.at(-1)?.getSender() === myUserId) {
|
if (roomOrThread.timeline[roomOrThread.timeline.length - 1]?.getSender() === myUserId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -262,6 +262,8 @@ export default class ForgotPassword extends React.Component<Props, State> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.reset.setNewPassword(this.state.password);
|
await this.reset.setNewPassword(this.state.password);
|
||||||
|
this.setState({ phase: Phase.Done });
|
||||||
|
return;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.httpStatus !== 401) {
|
if (err.httpStatus !== 401) {
|
||||||
// 401 = waiting for email verification, else unknown error
|
// 401 = waiting for email verification, else unknown error
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { MouseEventHandler, ReactNode } from "react";
|
import React, { MouseEventHandler, ReactNode } from "react";
|
||||||
import { FormattingFunctions, AllActionStates } from "@matrix-org/matrix-wysiwyg";
|
import { FormattingFunctions, AllActionStates, ActionState } from "@matrix-org/matrix-wysiwyg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg";
|
import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg";
|
||||||
|
@ -26,6 +26,7 @@ import { Icon as InlineCodeIcon } from "../../../../../../res/img/element-icons/
|
||||||
import { Icon as LinkIcon } from "../../../../../../res/img/element-icons/room/composer/link.svg";
|
import { Icon as LinkIcon } from "../../../../../../res/img/element-icons/room/composer/link.svg";
|
||||||
import { Icon as BulletedListIcon } from "../../../../../../res/img/element-icons/room/composer/bulleted_list.svg";
|
import { Icon as BulletedListIcon } from "../../../../../../res/img/element-icons/room/composer/bulleted_list.svg";
|
||||||
import { Icon as NumberedListIcon } from "../../../../../../res/img/element-icons/room/composer/numbered_list.svg";
|
import { Icon as NumberedListIcon } from "../../../../../../res/img/element-icons/room/composer/numbered_list.svg";
|
||||||
|
import { Icon as CodeBlockIcon } from "../../../../../../res/img/element-icons/room/composer/code_block.svg";
|
||||||
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
||||||
import { Alignment } from "../../../elements/Tooltip";
|
import { Alignment } from "../../../elements/Tooltip";
|
||||||
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
||||||
|
@ -53,21 +54,23 @@ function Tooltip({ label, keyCombo }: TooltipProps): JSX.Element {
|
||||||
|
|
||||||
interface ButtonProps extends TooltipProps {
|
interface ButtonProps extends TooltipProps {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
isActive: boolean;
|
actionState: ActionState;
|
||||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Button({ label, keyCombo, onClick, isActive, icon }: ButtonProps): JSX.Element {
|
function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
element="button"
|
element="button"
|
||||||
onClick={onClick as (e: ButtonEvent) => void}
|
onClick={onClick as (e: ButtonEvent) => void}
|
||||||
title={label}
|
title={label}
|
||||||
className={classNames("mx_FormattingButtons_Button", {
|
className={classNames("mx_FormattingButtons_Button", {
|
||||||
mx_FormattingButtons_active: isActive,
|
mx_FormattingButtons_active: actionState === "reversed",
|
||||||
mx_FormattingButtons_Button_hover: !isActive,
|
mx_FormattingButtons_Button_hover: actionState === "enabled",
|
||||||
|
mx_FormattingButtons_disabled: actionState === "disabled",
|
||||||
})}
|
})}
|
||||||
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
|
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
|
||||||
|
forceHide={actionState === "disabled"}
|
||||||
alignment={Alignment.Top}
|
alignment={Alignment.Top}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
@ -85,53 +88,59 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
|
||||||
return (
|
return (
|
||||||
<div className="mx_FormattingButtons">
|
<div className="mx_FormattingButtons">
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.bold === "reversed"}
|
actionState={actionStates.bold}
|
||||||
label={_td("Bold")}
|
label={_td("Bold")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
|
||||||
onClick={() => composer.bold()}
|
onClick={() => composer.bold()}
|
||||||
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
|
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.italic === "reversed"}
|
actionState={actionStates.italic}
|
||||||
label={_td("Italic")}
|
label={_td("Italic")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
|
||||||
onClick={() => composer.italic()}
|
onClick={() => composer.italic()}
|
||||||
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
|
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.underline === "reversed"}
|
actionState={actionStates.underline}
|
||||||
label={_td("Underline")}
|
label={_td("Underline")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
|
||||||
onClick={() => composer.underline()}
|
onClick={() => composer.underline()}
|
||||||
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
|
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.strikeThrough === "reversed"}
|
actionState={actionStates.strikeThrough}
|
||||||
label={_td("Strikethrough")}
|
label={_td("Strikethrough")}
|
||||||
onClick={() => composer.strikeThrough()}
|
onClick={() => composer.strikeThrough()}
|
||||||
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
|
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.unorderedList === "reversed"}
|
actionState={actionStates.unorderedList}
|
||||||
label={_td("Bulleted list")}
|
label={_td("Bulleted list")}
|
||||||
onClick={() => composer.unorderedList()}
|
onClick={() => composer.unorderedList()}
|
||||||
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
|
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.orderedList === "reversed"}
|
actionState={actionStates.orderedList}
|
||||||
label={_td("Numbered list")}
|
label={_td("Numbered list")}
|
||||||
onClick={() => composer.orderedList()}
|
onClick={() => composer.orderedList()}
|
||||||
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
|
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.inlineCode === "reversed"}
|
actionState={actionStates.inlineCode}
|
||||||
label={_td("Code")}
|
label={_td("Code")}
|
||||||
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
|
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
|
||||||
onClick={() => composer.inlineCode()}
|
onClick={() => composer.inlineCode()}
|
||||||
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.link === "reversed"}
|
actionState={actionStates.codeBlock}
|
||||||
|
label={_td("Code block")}
|
||||||
|
onClick={() => composer.codeBlock()}
|
||||||
|
icon={<CodeBlockIcon className="mx_FormattingButtons_Icon" />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
actionState={actionStates.link}
|
||||||
label={_td("Link")}
|
label={_td("Link")}
|
||||||
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
||||||
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
||||||
|
|
|
@ -19,6 +19,12 @@ import { useCallback } from "react";
|
||||||
|
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
|
||||||
|
function isEnterPressed(event: KeyboardEvent): boolean {
|
||||||
|
// Ugly but here we need to send the message only if Enter is pressed
|
||||||
|
// And we need to stop the event propagation on enter to avoid the composer to grow
|
||||||
|
return event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
|
||||||
|
}
|
||||||
|
|
||||||
export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent) => WysiwygEvent | null {
|
export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent) => WysiwygEvent | null {
|
||||||
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
return useCallback(
|
return useCallback(
|
||||||
|
@ -28,12 +34,12 @@ export function useInputEventProcessor(onSend: () => void): (event: WysiwygEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
const isKeyboardEvent = event instanceof KeyboardEvent;
|
const isKeyboardEvent = event instanceof KeyboardEvent;
|
||||||
const isEnterPress =
|
const isEnterPress = !isCtrlEnter && isKeyboardEvent && isEnterPressed(event);
|
||||||
!isCtrlEnter && (isKeyboardEvent ? event.key === "Enter" : event.inputType === "insertParagraph");
|
const isInsertParagraph = !isCtrlEnter && !isKeyboardEvent && event.inputType === "insertParagraph";
|
||||||
// sendMessage is sent when ctrl+enter is pressed
|
// sendMessage is sent when cmd+enter is pressed
|
||||||
const isSendMessage = !isKeyboardEvent && event.inputType === "sendMessage";
|
const isSendMessage = isCtrlEnter && !isKeyboardEvent && event.inputType === "sendMessage";
|
||||||
|
|
||||||
if (isEnterPress || isSendMessage) {
|
if (isEnterPress || isInsertParagraph || isSendMessage) {
|
||||||
event.stopPropagation?.();
|
event.stopPropagation?.();
|
||||||
event.preventDefault?.();
|
event.preventDefault?.();
|
||||||
onSend();
|
onSend();
|
||||||
|
|
|
@ -288,6 +288,37 @@ describe("<ForgotPassword>", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and confirm the email link and submitting the new password", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// fake link confirmed by resolving client.setPassword instead of raising an error
|
||||||
|
mocked(client.setPassword).mockResolvedValue({});
|
||||||
|
await click(screen.getByText("Reset password"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should send the new password (once)", () => {
|
||||||
|
expect(client.setPassword).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
type: "m.login.email.identity",
|
||||||
|
threepid_creds: {
|
||||||
|
client_secret: expect.any(String),
|
||||||
|
sid: testSid,
|
||||||
|
},
|
||||||
|
threepidCreds: {
|
||||||
|
client_secret: expect.any(String),
|
||||||
|
sid: testSid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testPassword,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// be sure that the next attempt to set the password would have been sent
|
||||||
|
jest.advanceTimersByTime(3000);
|
||||||
|
// it should not retry to set the password
|
||||||
|
expect(client.setPassword).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("and submitting it", () => {
|
describe("and submitting it", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await click(screen.getByText("Reset password"));
|
await click(screen.getByText("Reset password"));
|
||||||
|
|
|
@ -28,6 +28,7 @@ const mockWysiwyg = {
|
||||||
underline: jest.fn(),
|
underline: jest.fn(),
|
||||||
strikeThrough: jest.fn(),
|
strikeThrough: jest.fn(),
|
||||||
inlineCode: jest.fn(),
|
inlineCode: jest.fn(),
|
||||||
|
codeBlock: jest.fn(),
|
||||||
link: jest.fn(),
|
link: jest.fn(),
|
||||||
orderedList: jest.fn(),
|
orderedList: jest.fn(),
|
||||||
unorderedList: jest.fn(),
|
unorderedList: jest.fn(),
|
||||||
|
@ -36,7 +37,7 @@ const mockWysiwyg = {
|
||||||
const openLinkModalSpy = jest.spyOn(LinkModal, "openLinkModal");
|
const openLinkModalSpy = jest.spyOn(LinkModal, "openLinkModal");
|
||||||
|
|
||||||
const testCases: Record<
|
const testCases: Record<
|
||||||
Exclude<ActionTypes, "undo" | "redo" | "clear" | "codeBlock">,
|
Exclude<ActionTypes, "undo" | "redo" | "clear">,
|
||||||
{ label: string; mockFormatFn: jest.Func | jest.SpyInstance }
|
{ label: string; mockFormatFn: jest.Func | jest.SpyInstance }
|
||||||
> = {
|
> = {
|
||||||
bold: { label: "Bold", mockFormatFn: mockWysiwyg.bold },
|
bold: { label: "Bold", mockFormatFn: mockWysiwyg.bold },
|
||||||
|
@ -44,6 +45,7 @@ const testCases: Record<
|
||||||
underline: { label: "Underline", mockFormatFn: mockWysiwyg.underline },
|
underline: { label: "Underline", mockFormatFn: mockWysiwyg.underline },
|
||||||
strikeThrough: { label: "Strikethrough", mockFormatFn: mockWysiwyg.strikeThrough },
|
strikeThrough: { label: "Strikethrough", mockFormatFn: mockWysiwyg.strikeThrough },
|
||||||
inlineCode: { label: "Code", mockFormatFn: mockWysiwyg.inlineCode },
|
inlineCode: { label: "Code", mockFormatFn: mockWysiwyg.inlineCode },
|
||||||
|
codeBlock: { label: "Code block", mockFormatFn: mockWysiwyg.inlineCode },
|
||||||
link: { label: "Link", mockFormatFn: openLinkModalSpy },
|
link: { label: "Link", mockFormatFn: openLinkModalSpy },
|
||||||
orderedList: { label: "Numbered list", mockFormatFn: mockWysiwyg.orderedList },
|
orderedList: { label: "Numbered list", mockFormatFn: mockWysiwyg.orderedList },
|
||||||
unorderedList: { label: "Bulleted list", mockFormatFn: mockWysiwyg.unorderedList },
|
unorderedList: { label: "Bulleted list", mockFormatFn: mockWysiwyg.unorderedList },
|
||||||
|
@ -62,6 +64,7 @@ const renderComponent = (props = {}) => {
|
||||||
const classes = {
|
const classes = {
|
||||||
active: "mx_FormattingButtons_active",
|
active: "mx_FormattingButtons_active",
|
||||||
hover: "mx_FormattingButtons_Button_hover",
|
hover: "mx_FormattingButtons_Button_hover",
|
||||||
|
disabled: "mx_FormattingButtons_disabled",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("FormattingButtons", () => {
|
describe("FormattingButtons", () => {
|
||||||
|
@ -87,6 +90,16 @@ describe("FormattingButtons", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Each button should have disabled class when disabled", () => {
|
||||||
|
const disabledActionStates = createActionStates("disabled");
|
||||||
|
renderComponent({ actionStates: disabledActionStates });
|
||||||
|
|
||||||
|
Object.values(testCases).forEach((testCase) => {
|
||||||
|
const { label } = testCase;
|
||||||
|
expect(screen.getByLabelText(label)).toHaveClass(classes.disabled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("Should call wysiwyg function on button click", async () => {
|
it("Should call wysiwyg function on button click", async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
|
@ -98,14 +111,26 @@ describe("FormattingButtons", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Each button should display the tooltip on mouse over", async () => {
|
it("Each button should display the tooltip on mouse over when not disabled", async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
for (const testCase of Object.values(testCases)) {
|
for (const testCase of Object.values(testCases)) {
|
||||||
const { label } = testCase;
|
const { label } = testCase;
|
||||||
|
|
||||||
await userEvent.hover(screen.getByLabelText(label));
|
await userEvent.hover(screen.getByLabelText(label));
|
||||||
expect(await screen.findByText(label)).toBeTruthy();
|
expect(screen.getByText(label)).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Each button should not display the tooltip on mouse over when disabled", async () => {
|
||||||
|
const disabledActionStates = createActionStates("disabled");
|
||||||
|
renderComponent({ actionStates: disabledActionStates });
|
||||||
|
|
||||||
|
for (const testCase of Object.values(testCases)) {
|
||||||
|
const { label } = testCase;
|
||||||
|
|
||||||
|
await userEvent.hover(screen.getByLabelText(label));
|
||||||
|
expect(screen.queryByText(label)).not.toBeInTheDocument();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
|
@ -87,6 +88,45 @@ describe("WysiwygComposer", () => {
|
||||||
// Then it sends a message
|
// Then it sends a message
|
||||||
await waitFor(() => expect(onSend).toBeCalledTimes(1));
|
await waitFor(() => expect(onSend).toBeCalledTimes(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should not call onSend when Shift+Enter is pressed ", async () => {
|
||||||
|
//When
|
||||||
|
await userEvent.type(screen.getByRole("textbox"), "{shift>}{enter}");
|
||||||
|
|
||||||
|
// Then it sends a message
|
||||||
|
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not call onSend when ctrl+Enter is pressed ", async () => {
|
||||||
|
//When
|
||||||
|
// Using userEvent.type or .keyboard wasn't working as expected in the case of ctrl+enter
|
||||||
|
fireEvent(
|
||||||
|
screen.getByRole("textbox"),
|
||||||
|
new KeyboardEvent("keydown", {
|
||||||
|
ctrlKey: true,
|
||||||
|
code: "Enter",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then it sends a message
|
||||||
|
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not call onSend when alt+Enter is pressed ", async () => {
|
||||||
|
//When
|
||||||
|
await userEvent.type(screen.getByRole("textbox"), "{alt>}{enter}");
|
||||||
|
|
||||||
|
// Then it sends a message
|
||||||
|
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not call onSend when meta+Enter is pressed ", async () => {
|
||||||
|
//When
|
||||||
|
await userEvent.type(screen.getByRole("textbox"), "{meta>}{enter}");
|
||||||
|
|
||||||
|
// Then it sends a message
|
||||||
|
await waitFor(() => expect(onSend).toBeCalledTimes(0));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("When settings require Ctrl+Enter to send", () => {
|
describe("When settings require Ctrl+Enter to send", () => {
|
||||||
|
|
|
@ -6493,7 +6493,7 @@ matrix-events-sdk@0.0.1:
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "23.1.0"
|
version "23.1.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2fcc4811dd913bb774dd1c7f67cb693c4456d71e"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/83563c7a01bbeaf7f83f4b7feccc03647b536e7c"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2"
|
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2"
|
||||||
|
|
Loading…
Reference in New Issue