diff --git a/CHANGELOG.md b/CHANGELOG.md
index cebdf6b3f42..afeec56d197 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,41 @@
+Changes in [3.75.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.75.0) (2023-07-04)
+=====================================================================================================
+
+## 🦖 Deprecations
+ * Remove `feature_favourite_messages` as it is has been abandoned for now ([\#11097](https://github.com/matrix-org/matrix-react-sdk/pull/11097)). Fixes vector-im/element-web#25555.
+
+## ✨ Features
+ * Don't setup keys on login when encryption is force disabled ([\#11125](https://github.com/matrix-org/matrix-react-sdk/pull/11125)). Contributed by @kerryarchibald.
+ * OIDC: attempt dynamic client registration ([\#11074](https://github.com/matrix-org/matrix-react-sdk/pull/11074)). Fixes vector-im/element-web#25468 and vector-im/element-web#25467. Contributed by @kerryarchibald.
+ * OIDC: Check static client registration and add login flow ([\#11088](https://github.com/matrix-org/matrix-react-sdk/pull/11088)). Fixes vector-im/element-web#25467. Contributed by @kerryarchibald.
+ * Improve message body output from plain text editor ([\#11124](https://github.com/matrix-org/matrix-react-sdk/pull/11124)). Contributed by @alunturner.
+ * Disable encryption toggle in room settings when force disabled ([\#11122](https://github.com/matrix-org/matrix-react-sdk/pull/11122)). Contributed by @kerryarchibald.
+ * Add .well-known config option to force disable encryption on room creation ([\#11120](https://github.com/matrix-org/matrix-react-sdk/pull/11120)). Contributed by @kerryarchibald.
+ * Handle permalinks in room topic ([\#11115](https://github.com/matrix-org/matrix-react-sdk/pull/11115)). Fixes vector-im/element-web#23395.
+ * Add at room avatar for RTE ([\#11106](https://github.com/matrix-org/matrix-react-sdk/pull/11106)). Contributed by @alunturner.
+ * Remove new room breadcrumbs ([\#11104](https://github.com/matrix-org/matrix-react-sdk/pull/11104)).
+ * Update rich text editor dependency and associated changes ([\#11098](https://github.com/matrix-org/matrix-react-sdk/pull/11098)). Contributed by @alunturner.
+ * Implement new model, hooks and reconcilation code for new GYU notification settings ([\#11089](https://github.com/matrix-org/matrix-react-sdk/pull/11089)). Contributed by @justjanne.
+ * Allow maintaining a different right panel width for thread panels ([\#11064](https://github.com/matrix-org/matrix-react-sdk/pull/11064)). Fixes vector-im/element-web#25487.
+ * Make AppPermission pane scrollable ([\#10954](https://github.com/matrix-org/matrix-react-sdk/pull/10954)). Fixes vector-im/element-web#25438 and vector-im/element-web#25511. Contributed by @luixxiul.
+ * Integrate compound design tokens ([\#11091](https://github.com/matrix-org/matrix-react-sdk/pull/11091)). Fixes vector-im/internal-planning#450.
+ * Don't warn about the effects of redacting state events when redacting non-state-events ([\#11071](https://github.com/matrix-org/matrix-react-sdk/pull/11071)). Fixes vector-im/element-web#8478.
+ * Allow specifying help URLs in config.json ([\#11070](https://github.com/matrix-org/matrix-react-sdk/pull/11070)). Fixes vector-im/element-web#15268.
+
+## 🐛 Bug Fixes
+ * Fix spurious notifications on non-live events ([\#11133](https://github.com/matrix-org/matrix-react-sdk/pull/11133)). Fixes vector-im/element-web#24336.
+ * Prevent auto-translation within composer ([\#11114](https://github.com/matrix-org/matrix-react-sdk/pull/11114)). Fixes vector-im/element-web#25624.
+ * Fix caret jump when backspacing into empty line at beginning of editor ([\#11128](https://github.com/matrix-org/matrix-react-sdk/pull/11128)). Fixes vector-im/element-web#22335.
+ * Fix server picker not allowing you to switch from custom to default ([\#11127](https://github.com/matrix-org/matrix-react-sdk/pull/11127)). Fixes vector-im/element-web#25650.
+ * Consider the unthreaded read receipt for Unread dot state ([\#11117](https://github.com/matrix-org/matrix-react-sdk/pull/11117)). Fixes vector-im/element-web#24229.
+ * Increase RTE resilience ([\#11111](https://github.com/matrix-org/matrix-react-sdk/pull/11111)). Fixes vector-im/element-web#25277. Contributed by @alunturner.
+ * Fix RoomView ignoring alias lookup errors due to them not knowing the roomId ([\#11099](https://github.com/matrix-org/matrix-react-sdk/pull/11099)). Fixes vector-im/element-web#24783 and vector-im/element-web#25562.
+ * Fix style inconsistencies on SecureBackupPanel ([\#11102](https://github.com/matrix-org/matrix-react-sdk/pull/11102)). Fixes vector-im/element-web#25615. Contributed by @luixxiul.
+ * Remove unknown MXIDs from invite suggestions ([\#11055](https://github.com/matrix-org/matrix-react-sdk/pull/11055)). Fixes vector-im/element-web#25446.
+ * Reduce volume of ring sounds to normalised levels ([\#9143](https://github.com/matrix-org/matrix-react-sdk/pull/9143)). Contributed by @JMoVS.
+ * Fix slash commands not being enabled in certain cases ([\#11090](https://github.com/matrix-org/matrix-react-sdk/pull/11090)). Fixes vector-im/element-web#25572.
+ * Prevent escape in threads from sending focus to main timeline composer ([\#11061](https://github.com/matrix-org/matrix-react-sdk/pull/11061)). Fixes vector-im/element-web#23397.
+
Changes in [3.74.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.74.0) (2023-06-20)
=====================================================================================================
diff --git a/cypress/e2e/crypto/complete-security.spec.ts b/cypress/e2e/crypto/complete-security.spec.ts
index b598829b86a..eab7fe26e24 100644
--- a/cypress/e2e/crypto/complete-security.spec.ts
+++ b/cypress/e2e/crypto/complete-security.spec.ts
@@ -14,11 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils";
-import { CypressBot } from "../../support/bot";
-import { skipIfRustCrypto } from "../../support/util";
+import { logIntoElement } from "./utils";
describe("Complete security", () => {
let homeserver: HomeserverInstance;
@@ -46,39 +43,5 @@ describe("Complete security", () => {
cy.findByText("Welcome Jeff");
});
- it("should walk through device verification if we have a signed device", () => {
- skipIfRustCrypto();
-
- // create a new user, and have it bootstrap cross-signing
- let botClient: CypressBot;
- cy.getBot(homeserver, { displayName: "Jeff" })
- .then(async (bot) => {
- botClient = bot;
- await bot.bootstrapCrossSigning({});
- })
- .then(() => {
- // now log in, in Element. We go in through the login page because otherwise the device setup flow
- // doesn't get triggered
- console.log("%cAccount set up; logging in user", "font-weight: bold; font-size:x-large");
- logIntoElement(homeserver.baseUrl, botClient.getSafeUserId(), botClient.__cypress_password);
-
- // we should see a prompt for a device verification
- cy.findByRole("heading", { name: "Verify this device" });
- const botVerificationRequestPromise = waitForVerificationRequest(botClient);
- cy.findByRole("button", { name: "Verify with another device" }).click();
-
- // accept the verification request on the "bot" side
- cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
- await handleVerificationRequest(verificationRequest);
- });
-
- // confirm that the emojis match
- cy.findByRole("button", { name: "They match" }).click();
-
- // we should get the confirmation box
- cy.findByText(/You've successfully verified/);
-
- cy.findByRole("button", { name: "Got it" }).click();
- });
- });
+ // see also "Verify device during login with SAS" in `verifiction.spec.ts`.
});
diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts
index 0a332a376de..0d323dbcec8 100644
--- a/cypress/e2e/crypto/crypto.spec.ts
+++ b/cypress/e2e/crypto/crypto.spec.ts
@@ -15,17 +15,11 @@ limitations under the License.
*/
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
-import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import type { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import type { CypressBot } from "../../support/bot";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login";
-import {
- checkDeviceIsCrossSigned,
- EmojiMapping,
- handleVerificationRequest,
- logIntoElement,
- waitForVerificationRequest,
-} from "./utils";
+import { doTwoWaySasVerification, waitForVerificationRequest } from "./utils";
import { skipIfRustCrypto } from "../../support/util";
interface CryptoTestContext extends Mocha.Context {
@@ -110,27 +104,6 @@ function autoJoin(client: MatrixClient) {
});
}
-/**
- * Given a VerificationRequest in a bot client, add cypress commands to:
- * - wait for the bot to receive a 'verify by emoji' notification
- * - check that the bot sees the same emoji as the application
- *
- * @param botVerificationRequest - a verification request in a bot client
- */
-function doTwoWaySasVerification(botVerificationRequest: VerificationRequest): void {
- // on the bot side, wait for the emojis, confirm they match, and return them
- const emojiPromise = handleVerificationRequest(botVerificationRequest);
-
- // then, check that our application shows an emoji panel with the same emojis.
- cy.wrap(emojiPromise).then((emojis: EmojiMapping[]) => {
- cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
- emojis.forEach((emoji: EmojiMapping, index: number) => {
- expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
- });
- });
- });
-}
-
const verify = function (this: CryptoTestContext) {
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
@@ -139,9 +112,14 @@ const verify = function (this: CryptoTestContext) {
cy.findByText("Bob").click();
cy.findByRole("button", { name: "Verify" }).click();
cy.findByRole("button", { name: "Start Verification" }).click();
- cy.findByRole("button", { name: "Verify by emoji" }).click();
+
+ // this requires creating a DM, so can take a while. Give it a longer timeout.
+ cy.findByRole("button", { name: "Verify by emoji", timeout: 30000 }).click();
+
cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => {
- doTwoWaySasVerification(request);
+ // the bot user races with the Element user to hit the "verify by emoji" button
+ const verifier = request.beginKeyVerification("m.sas.v1");
+ doTwoWaySasVerification(verifier);
});
cy.findByRole("button", { name: "They match" }).click();
cy.findByText("You've successfully verified Bob!").should("exist");
@@ -171,29 +149,110 @@ describe("Cryptography", function () {
cy.stopHomeserver(this.homeserver);
});
- it("setting up secure key backup should work", () => {
- skipIfRustCrypto();
- cy.openUserSettings("Security & Privacy");
- cy.findByRole("button", { name: "Set up Secure Backup" }).click();
- cy.get(".mx_Dialog").within(() => {
- cy.findByRole("button", { name: "Continue" }).click();
- cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey");
- // Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851
- cy.findByRole("button", { name: "Download" }).click();
- cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
- cy.get(".mx_InteractiveAuthDialog").within(() => {
- cy.get(".mx_Dialog_title").within(() => {
- cy.findByText("Setting up keys").should("exist");
- cy.findByText("Setting up keys").should("not.exist");
+ describe.each([{ isDeviceVerified: true }, { isDeviceVerified: false }])(
+ "setting up secure key backup should work %j",
+ ({ isDeviceVerified }) => {
+ /**
+ * Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
+ * @param keyType
+ */
+ function verifyKey(keyType: string) {
+ return cy
+ .getClient()
+ .then((cli) => cy.wrap(cli.getAccountDataFromServer(`m.cross_signing.${keyType}`)))
+ .then((accountData: { encrypted: Record> }) => {
+ expect(accountData.encrypted).to.exist;
+ const keys = Object.keys(accountData.encrypted);
+ const key = accountData.encrypted[keys[0]];
+ expect(key.ciphertext).to.exist;
+ expect(key.iv).to.exist;
+ expect(key.mac).to.exist;
+ });
+ }
+
+ /**
+ * Click on download button and continue
+ */
+ function downloadKey() {
+ // Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851
+ cy.findByRole("button", { name: "Download" }).click();
+ cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
+ }
+
+ it("by recovery code", () => {
+ skipIfRustCrypto();
+
+ // Verified the device
+ if (isDeviceVerified) {
+ cy.bootstrapCrossSigning(aliceCredentials);
+ }
+
+ cy.openUserSettings("Security & Privacy");
+ cy.findByRole("button", { name: "Set up Secure Backup" }).click();
+ cy.get(".mx_Dialog").within(() => {
+ // Recovery key is selected by default
+ cy.findByRole("button", { name: "Continue" }).click();
+ cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey");
+
+ downloadKey();
+
+ // When the device is verified, the `Setting up keys` step is skipped
+ if (!isDeviceVerified) {
+ cy.get(".mx_InteractiveAuthDialog").within(() => {
+ cy.get(".mx_Dialog_title").within(() => {
+ cy.findByText("Setting up keys").should("exist");
+ cy.findByText("Setting up keys").should("not.exist");
+ });
+ });
+ }
+
+ cy.findByText("Secure Backup successful").should("exist");
+ cy.findByRole("button", { name: "Done" }).click();
+ cy.findByText("Secure Backup successful").should("not.exist");
});
+
+ // Verify that the SSSS keys are in the account data stored in the server
+ verifyKey("master");
+ verifyKey("self_signing");
+ verifyKey("user_signing");
});
- cy.findByText("Secure Backup successful").should("exist");
- cy.findByRole("button", { name: "Done" }).click();
- cy.findByText("Secure Backup successful").should("not.exist");
- });
- return;
- });
+ it("by passphrase", () => {
+ skipIfRustCrypto();
+
+ // Verified the device
+ if (isDeviceVerified) {
+ cy.bootstrapCrossSigning(aliceCredentials);
+ }
+
+ cy.openUserSettings("Security & Privacy");
+ cy.findByRole("button", { name: "Set up Secure Backup" }).click();
+ cy.get(".mx_Dialog").within(() => {
+ // Select passphrase option
+ cy.findByText("Enter a Security Phrase").click();
+ cy.findByRole("button", { name: "Continue" }).click();
+
+ // Fill passphrase input
+ cy.get("input").type("new passphrase for setting up a secure key backup");
+ cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
+ // Confirm passphrase
+ cy.get("input").type("new passphrase for setting up a secure key backup");
+ cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
+
+ downloadKey();
+
+ cy.findByText("Secure Backup successful").should("exist");
+ cy.findByRole("button", { name: "Done" }).click();
+ cy.findByText("Secure Backup successful").should("not.exist");
+ });
+
+ // Verify that the SSSS keys are in the account data stored in the server
+ verifyKey("master");
+ verifyKey("self_signing");
+ verifyKey("user_signing");
+ });
+ },
+ );
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
skipIfRustCrypto();
@@ -324,68 +383,3 @@ describe("Cryptography", function () {
});
});
});
-
-describe("Verify own device", () => {
- let aliceBotClient: CypressBot;
- let homeserver: HomeserverInstance;
-
- beforeEach(() => {
- skipIfRustCrypto();
- cy.startHomeserver("default").then((data: HomeserverInstance) => {
- homeserver = data;
-
- // Visit the login page of the app, to load the matrix sdk
- cy.visit("/#/login");
-
- // wait for the page to load
- cy.window({ log: false }).should("have.property", "matrixcs");
-
- // Create a new device for alice
- cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => {
- aliceBotClient = bot;
- });
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- /* Click the "Verify with another device" button, and have the bot client auto-accept it.
- *
- * Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`.
- */
- function initiateAliceVerificationRequest() {
- // alice bot waits for verification request
- const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
-
- // Click on "Verify with another device"
- cy.get(".mx_AuthPage").within(() => {
- cy.findByRole("button", { name: "Verify with another device" }).click();
- });
-
- // alice bot responds yes to verification request from alice
- cy.wrap(promiseVerificationRequest).as("verificationRequest");
- }
-
- it("with SAS", function (this: CryptoTestContext) {
- logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
-
- // Launch the verification request between alice and the bot
- initiateAliceVerificationRequest();
-
- // Handle emoji SAS verification
- cy.get(".mx_InfoDialog").within(() => {
- cy.get("@verificationRequest").then((request: VerificationRequest) => {
- // Handle emoji request and check that emojis are matching
- doTwoWaySasVerification(request);
- });
-
- cy.findByRole("button", { name: "They match" }).click();
- cy.findByRole("button", { name: "Got it" }).click();
- });
-
- // Check that our device is now cross-signed
- checkDeviceIsCrossSigned();
- });
-});
diff --git a/cypress/e2e/crypto/utils.ts b/cypress/e2e/crypto/utils.ts
index 3e91d1e93db..3d63fcb124d 100644
--- a/cypress/e2e/crypto/utils.ts
+++ b/cypress/e2e/crypto/utils.ts
@@ -16,7 +16,7 @@ limitations under the License.
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
-import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api";
export type EmojiMapping = [emoji: string, name: string];
@@ -39,15 +39,15 @@ export function waitForVerificationRequest(cli: MatrixClient): Promise {
+export function handleSasVerification(verifier: Verifier): Promise {
return new Promise((resolve) => {
const onShowSas = (event: ISasEvent) => {
// @ts-ignore VerifierEvent is a pain to get at here as we don't have a reference to matrixcs;
@@ -57,7 +57,6 @@ export function handleVerificationRequest(request: VerificationRequest): Promise
resolve(event.sas.emoji);
};
- const verifier = request.beginKeyVerification("m.sas.v1");
// @ts-ignore as above, avoiding reference to VerifierEvent
verifier.on("show_sas", onShowSas);
verifier.verify();
@@ -119,3 +118,24 @@ export function logIntoElement(homeserverUrl: string, username: string, password
cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click();
}
+
+/**
+ * Given a SAS verifier for a bot client, add cypress commands to:
+ * - wait for the bot to receive the emojis
+ * - check that the bot sees the same emoji as the application
+ *
+ * @param botVerificationRequest - a verification request in a bot client
+ */
+export function doTwoWaySasVerification(verifier: Verifier): void {
+ // on the bot side, wait for the emojis, confirm they match, and return them
+ const emojiPromise = handleSasVerification(verifier);
+
+ // then, check that our application shows an emoji panel with the same emojis.
+ cy.wrap(emojiPromise).then((emojis: EmojiMapping[]) => {
+ cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
+ emojis.forEach((emoji: EmojiMapping, index: number) => {
+ expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
+ });
+ });
+ });
+}
diff --git a/cypress/e2e/crypto/verification.spec.ts b/cypress/e2e/crypto/verification.spec.ts
new file mode 100644
index 00000000000..cf07159cb50
--- /dev/null
+++ b/cypress/e2e/crypto/verification.spec.ts
@@ -0,0 +1,159 @@
+/*
+Copyright 2023 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 type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api/verification";
+import { CypressBot } from "../../support/bot";
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { emitPromise, skipIfRustCrypto } from "../../support/util";
+import { checkDeviceIsCrossSigned, doTwoWaySasVerification, logIntoElement, waitForVerificationRequest } from "./utils";
+import { getToast } from "../../support/toasts";
+
+describe("Device verification", () => {
+ let aliceBotClient: CypressBot;
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ skipIfRustCrypto();
+ cy.startHomeserver("default").then((data: HomeserverInstance) => {
+ homeserver = data;
+
+ // Visit the login page of the app, to load the matrix sdk
+ cy.visit("/#/login");
+
+ // wait for the page to load
+ cy.window({ log: false }).should("have.property", "matrixcs");
+
+ // Create a new device for alice
+ cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => {
+ aliceBotClient = bot;
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ /* Click the "Verify with another device" button, and have the bot client auto-accept it.
+ *
+ * Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`.
+ */
+ function initiateAliceVerificationRequest() {
+ // alice bot waits for verification request
+ const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
+
+ // Click on "Verify with another device"
+ cy.get(".mx_AuthPage").within(() => {
+ cy.findByRole("button", { name: "Verify with another device" }).click();
+ });
+
+ // alice bot responds yes to verification request from alice
+ cy.wrap(promiseVerificationRequest).as("verificationRequest");
+ }
+
+ it("Verify device during login with SAS", () => {
+ logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
+
+ // Launch the verification request between alice and the bot
+ initiateAliceVerificationRequest();
+
+ // Handle emoji SAS verification
+ cy.get(".mx_InfoDialog").within(() => {
+ cy.get("@verificationRequest").then((request: VerificationRequest) => {
+ // the bot chooses to do an emoji verification
+ const verifier = request.beginKeyVerification("m.sas.v1");
+
+ // Handle emoji request and check that emojis are matching
+ doTwoWaySasVerification(verifier);
+ });
+
+ cy.findByRole("button", { name: "They match" }).click();
+ cy.findByRole("button", { name: "Got it" }).click();
+ });
+
+ // Check that our device is now cross-signed
+ checkDeviceIsCrossSigned();
+ });
+
+ it("Handle incoming verification request with SAS", () => {
+ logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
+
+ /* Dismiss "Verify this device" */
+ cy.get(".mx_AuthPage").within(() => {
+ cy.findByRole("button", { name: "Skip verification for now" }).click();
+ cy.findByRole("button", { name: "I'll verify later" }).click();
+ });
+
+ /* figure out the device id of the Element client */
+ let elementDeviceId: string;
+ cy.window({ log: false }).then((win) => {
+ const cli = win.mxMatrixClientPeg.safeGet();
+ elementDeviceId = cli.getDeviceId();
+ expect(elementDeviceId).to.exist;
+ cy.log(`Got element device id: ${elementDeviceId}`);
+ });
+
+ /* Now initiate a verification request from the *bot* device. */
+ let botVerificationRequest: VerificationRequest;
+ cy.then(() => {
+ async function initVerification() {
+ botVerificationRequest = await aliceBotClient
+ .getCrypto()!
+ .requestDeviceVerification(aliceBotClient.getUserId(), elementDeviceId);
+ }
+
+ cy.wrap(initVerification(), { log: false });
+ }).then(() => {
+ cy.log("Initiated verification request");
+ });
+
+ /* Check the toast for the incoming request */
+ getToast("Verification requested").within(() => {
+ // it should contain the device ID of the requesting device
+ cy.contains(`${aliceBotClient.getDeviceId()} from `);
+
+ // Accept
+ cy.findByRole("button", { name: "Verify Session" }).click();
+ });
+
+ /* Click 'Start' to start SAS verification */
+ cy.findByRole("button", { name: "Start" }).click();
+
+ /* on the bot side, wait for the verifier to exist ... */
+ async function awaitVerifier() {
+ // wait for the verifier to exist
+ while (!botVerificationRequest.verifier) {
+ await emitPromise(botVerificationRequest, "change");
+ }
+ return botVerificationRequest.verifier;
+ }
+
+ cy.then(() => cy.wrap(awaitVerifier())).then((verifier: Verifier) => {
+ // ... confirm ...
+ botVerificationRequest.verifier.verify();
+
+ // ... and then check the emoji match
+ doTwoWaySasVerification(verifier);
+ });
+
+ /* And we're all done! */
+ cy.get(".mx_InfoDialog").within(() => {
+ cy.findByRole("button", { name: "They match" }).click();
+ cy.findByText(`You've successfully verified (${aliceBotClient.getDeviceId()})!`).should("exist");
+ cy.findByRole("button", { name: "Got it" }).click();
+ });
+ });
+});
diff --git a/cypress/e2e/settings/security-user-settings-tab.spec.ts b/cypress/e2e/settings/security-user-settings-tab.spec.ts
index 341624dee30..aed3eeb6893 100644
--- a/cypress/e2e/settings/security-user-settings-tab.spec.ts
+++ b/cypress/e2e/settings/security-user-settings-tab.spec.ts
@@ -25,7 +25,7 @@ describe("Security user settings tab", () => {
cy.stopHomeserver(homeserver);
});
- describe("with posthog enabled", () => {
+ describe.skip("with posthog enabled", () => {
beforeEach(() => {
// Enable posthog
cy.intercept("/config.json?cachebuster=*", (req) => {
diff --git a/cypress/e2e/toasts/analytics-toast.spec.ts b/cypress/e2e/toasts/analytics-toast.spec.ts
index 4cc8baa838e..c1c6edc9020 100644
--- a/cypress/e2e/toasts/analytics-toast.spec.ts
+++ b/cypress/e2e/toasts/analytics-toast.spec.ts
@@ -39,7 +39,7 @@ function rejectToast(expectedTitle: string): void {
});
}
-describe("Analytics Toast", () => {
+describe.skip("Analytics Toast", () => {
let homeserver: HomeserverInstance;
afterEach(() => {
diff --git a/cypress/e2e/widgets/layout.spec.ts b/cypress/e2e/widgets/layout.spec.ts
index 0f18ce85c22..16470fd5a0b 100644
--- a/cypress/e2e/widgets/layout.spec.ts
+++ b/cypress/e2e/widgets/layout.spec.ts
@@ -102,7 +102,7 @@ describe("Widget Layout", () => {
it("manually resize the height of the top container layout", () => {
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
- cy.get(".mx_AppsContainer_resizerHandle")
+ cy.get(".mx_AppsDrawer_resizer_container_handle")
.trigger("mousedown")
.trigger("mousemove", { clientX: 0, clientY: 550, force: true })
.trigger("mouseup", { clientX: 0, clientY: 550, force: true });
diff --git a/cypress/support/toasts.ts b/cypress/support/toasts.ts
new file mode 100644
index 00000000000..43059bfdfa3
--- /dev/null
+++ b/cypress/support/toasts.ts
@@ -0,0 +1,27 @@
+/*
+Copyright 2023 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.
+*/
+
+///
+
+/**
+ * Assert that a toast with the given title exists, and return it
+ *
+ * @param expectedTitle - Expected title of the test
+ * @returns a Chainable for the DOM element of the toast
+ */
+export function getToast(expectedTitle: string): Cypress.Chainable {
+ return cy.contains(".mx_Toast_toast h2", expectedTitle).should("exist").closest(".mx_Toast_toast");
+}
diff --git a/cypress/support/util.ts b/cypress/support/util.ts
index c61a8c755b9..637a61b3800 100644
--- a/cypress/support/util.ts
+++ b/cypress/support/util.ts
@@ -16,6 +16,9 @@ limitations under the License.
///
+import "cypress-each";
+import EventEmitter from "events";
+
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
@@ -73,3 +76,10 @@ export function skipIfRustCrypto() {
export function isRustCryptoEnabled(): boolean {
return !!Cypress.env("RUST_CRYPTO");
}
+
+/**
+ * Returns a Promise which will resolve when the given event emitter emits a given event
+ */
+export function emitPromise(e: EventEmitter, k: string | symbol) {
+ return new Promise((r) => e.once(k, r));
+}
diff --git a/package.json b/package.json
index 0e80ef43a41..c0ea05466e8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.74.0",
+ "version": "3.75.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -61,12 +61,12 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.5.0",
- "@matrix-org/matrix-wysiwyg": "^2.2.2",
+ "@matrix-org/matrix-wysiwyg": "^2.3.0",
"@matrix-org/react-sdk-module-api": "^0.0.5",
"@sentry/browser": "^7.0.0",
"@sentry/tracing": "^7.0.0",
"@testing-library/react-hooks": "^8.0.1",
- "@types/cheerio": "^0.22.31",
+ "@vector-im/compound-design-tokens": "^0.0.3",
"await-lock": "^2.1.0",
"blurhash": "^1.1.3",
"classnames": "^2.2.6",
@@ -74,9 +74,9 @@
"counterpart": "^0.18.6",
"diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5",
- "emojibase": "6.1.0",
- "emojibase-data": "7.0.1",
- "emojibase-regex": "6.0.1",
+ "emojibase": "15.0.0",
+ "emojibase-data": "15.0.0",
+ "emojibase-regex": "15.0.0",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "10.0.7",
@@ -97,7 +97,7 @@
"maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
- "matrix-js-sdk": "26.1.0",
+ "matrix-js-sdk": "26.2.0",
"matrix-widget-api": "^1.4.0",
"memoize-one": "^6.0.0",
"minimist": "^1.2.5",
@@ -182,6 +182,7 @@
"chokidar": "^3.5.1",
"cypress": "^12.0.0",
"cypress-axe": "^1.0.0",
+ "cypress-each": "^1.13.3",
"cypress-multi-reporters": "^1.6.1",
"cypress-real-events": "^1.7.1",
"eslint": "8.42.0",
diff --git a/res/css/_common.pcss b/res/css/_common.pcss
index dda2ee0a261..1785667eccd 100644
--- a/res/css/_common.pcss
+++ b/res/css/_common.pcss
@@ -17,6 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css");
@import "./_font-sizes.pcss";
@import "./_font-weights.pcss";
@import "./_border-radii.pcss";
@@ -727,6 +728,15 @@ legend {
color: $accent;
}
+.mx_AppWarning,
+.mx_AppPermission {
+ text-align: center;
+ display: flex;
+ height: 100%;
+ flex-direction: column;
+ align-items: center;
+}
+
@define-mixin ProgressBarColour $colour {
color: $colour;
&::-moz-progress-bar {
diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 9b6a5053266..145714fdd85 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -21,6 +21,7 @@
@import "./components/views/dialogs/polls/_PollListItem.pcss";
@import "./components/views/dialogs/polls/_PollListItemEnded.pcss";
@import "./components/views/elements/_AppPermission.pcss";
+@import "./components/views/elements/_AppWarning.pcss";
@import "./components/views/elements/_FilterDropdown.pcss";
@import "./components/views/elements/_FilterTabGroup.pcss";
@import "./components/views/elements/_LearnMore.pcss";
@@ -287,7 +288,6 @@
@import "./views/rooms/_PinnedEventTile.pcss";
@import "./views/rooms/_PresenceLabel.pcss";
@import "./views/rooms/_ReadReceiptGroup.pcss";
-@import "./views/rooms/_RecentlyViewedButton.pcss";
@import "./views/rooms/_ReplyPreview.pcss";
@import "./views/rooms/_ReplyTile.pcss";
@import "./views/rooms/_RoomBreadcrumbs.pcss";
diff --git a/res/css/components/views/elements/_AppPermission.pcss b/res/css/components/views/elements/_AppPermission.pcss
index be78efa43b4..71f282ebeee 100644
--- a/res/css/components/views/elements/_AppPermission.pcss
+++ b/res/css/components/views/elements/_AppPermission.pcss
@@ -16,41 +16,29 @@ limitations under the License.
*/
.mx_AppPermission {
- > div {
- margin-bottom: 12px;
- }
-
- h4 {
- margin: 0;
- padding: 0;
- }
+ font-size: $font-12px;
+ width: 100%; /* make mx_AppPermission fill width of mx_AppTileBody so that scroll bar appears on the edge */
+ overflow-y: scroll;
- .mx_AppPermission_smallText {
- font-size: $font-12px;
- }
+ .mx_AppPermission_content {
+ margin-block: auto; /* place at the center */
- .mx_AppPermission_bolder {
- font-weight: var(--font-semi-bold);
- }
+ > div {
+ margin-block: 12px;
+ }
- .mx_AppPermission_helpIcon {
- margin-top: 1px;
- margin-right: 2px;
- width: 10px;
- height: 10px;
- display: inline-block;
+ .mx_AppPermission_content_bolder {
+ font-weight: var(--font-semi-bold);
+ }
- &::before {
+ .mx_TextWithTooltip_target--helpIcon {
display: inline-block;
- background-color: $accent;
- mask-repeat: no-repeat;
- mask-size: 12px;
- width: 12px;
- height: 12px;
- mask-position: center;
- content: "";
+ height: $font-14px; /* align with characters on the same line */
vertical-align: middle;
- mask-image: url("$(res)/img/feather-customised/help-circle.svg");
+
+ .mx_Icon {
+ color: $accent;
+ }
}
}
}
diff --git a/res/css/components/views/elements/_AppWarning.pcss b/res/css/components/views/elements/_AppWarning.pcss
new file mode 100644
index 00000000000..8d859d12a86
--- /dev/null
+++ b/res/css/components/views/elements/_AppWarning.pcss
@@ -0,0 +1,25 @@
+/*
+Copyright 2023 Suguru Hirahara
+
+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.
+*/
+
+.mx_AppWarning {
+ font-size: $font-16px;
+ justify-content: center;
+
+ h4 {
+ margin: 0;
+ padding: 0;
+ }
+}
diff --git a/res/css/views/messages/_MessageActionBar.pcss b/res/css/views/messages/_MessageActionBar.pcss
index 8dd22b84bc8..9d3b78ad133 100644
--- a/res/css/views/messages/_MessageActionBar.pcss
+++ b/res/css/views/messages/_MessageActionBar.pcss
@@ -21,7 +21,6 @@ limitations under the License.
--MessageActionBar-item-hover-background: $panel-actions;
--MessageActionBar-item-hover-borderRadius: $border-radius-6px;
--MessageActionBar-item-hover-zIndex: 1;
- --MessageActionBar-star-button-color: #ffa534;
position: absolute;
visibility: hidden;
@@ -118,10 +117,6 @@ limitations under the License.
color: $primary-content;
}
- &.mx_MessageActionBar_favouriteButton_fillstar {
- color: var(--MessageActionBar-star-button-color);
- }
-
&.mx_MessageActionBar_downloadButton {
--MessageActionBar-icon-size: 14px;
diff --git a/res/css/views/rooms/_AppsDrawer.pcss b/res/css/views/rooms/_AppsDrawer.pcss
index ddf38354998..7d2b44482e7 100644
--- a/res/css/views/rooms/_AppsDrawer.pcss
+++ b/res/css/views/rooms/_AppsDrawer.pcss
@@ -32,43 +32,47 @@ limitations under the License.
overflow: hidden;
flex-grow: 1;
- .mx_AppsContainer_resizerHandleContainer {
+ .mx_AppsDrawer_resizer {
+ margin-bottom: var(--container-gap-width);
+ }
+
+ .mx_AppsDrawer_resizer_container {
width: 100%;
height: 10px;
display: block;
position: relative;
- }
- .mx_AppsContainer_resizerHandle {
- cursor: ns-resize;
+ .mx_AppsDrawer_resizer_container_handle {
+ cursor: ns-resize;
- /* Override styles from library, making the whole area the target area */
- width: 100% !important;
- height: 100% !important;
+ /* Override styles from library, making the whole area the target area */
+ width: 100% !important;
+ height: 100% !important;
- /* This is positioned directly below frame */
- position: absolute;
- bottom: 50% !important; /* override from library */
-
- /* We then render the pill handle in an ::after to keep it in the handle's */
- /* area without being a massive line across the screen */
- &::after {
- content: "";
+ /* This is positioned directly below frame */
position: absolute;
- border-radius: $border-radius-3px;
+ bottom: 50% !important; /* override from library */
+
+ /* We then render the pill handle in an ::after to keep it in the handle's */
+ /* area without being a massive line across the screen */
+ &::after {
+ content: "";
+ position: absolute;
+ border-radius: 3px;
- height: 4px;
- bottom: 0;
+ height: 4px;
+ bottom: 0;
- /* Together, these make the bar 64px wide */
- /* These are also overridden from the library */
- left: calc(50% - 32px);
- right: calc(50% - 32px);
+ /* Together, these make the bar 64px wide */
+ /* These are also overridden from the library */
+ left: calc(50% - 32px);
+ right: calc(50% - 32px);
+ }
}
}
&:hover {
- .mx_AppsContainer_resizerHandle::after {
+ .mx_AppsDrawer_resizer_container_handle::after {
opacity: 0.8;
background: $primary-content;
}
@@ -123,10 +127,6 @@ limitations under the License.
}
}
-.mx_AppsContainer_resizer {
- margin-bottom: var(--container-gap-width);
-}
-
.mx_AppsContainer {
display: flex;
flex-direction: row;
@@ -254,20 +254,13 @@ limitations under the License.
}
}
-.mx_AppTileBody,
-.mx_AppTileBody_mini {
- width: 100%;
- overflow: hidden;
+/* Rules added to this selector style appTileBody generally */
+.mx_AppTileBody {
+ /* Apply to every variant of appTileBody */
border-radius: $border-radius-8px;
- height: var(--AppTileBody-height);
-
- iframe {
- border: none;
- width: 100%;
- height: 100%;
- }
/* const loadingElement */
+ /* Note the loading spinner and the message next to it are not always included in mx_AppTileBody--loading */
.mx_AppTileBody_fadeInSpinner {
/* place spinner and the message at the center of mx_AppTileBody */
height: 100%;
@@ -279,66 +272,63 @@ limitations under the License.
animation-delay: 500ms;
animation-name: mx_AppTileBody_fadeInSpinnerAnimation;
}
-}
-
-.mx_AppTileBody {
- --AppTileBody-height: 100%;
-
- background-color: $widget-body-bg-color;
- iframe {
+ &.mx_AppTileBody--large,
+ &.mx_AppTileBody--mini {
+ width: 100%;
overflow: hidden;
- padding: 0;
- margin: 0;
- display: block;
+ height: var(--AppTileBody-height);
+
+ iframe {
+ border: none;
+ width: 100%;
+ height: 100%;
+ }
}
-}
-.mx_AppTileBody_mini {
- --AppTileBody-height: var(--AppTile_mini-height);
-}
+ &.mx_AppTileBody--large {
+ --AppTileBody-height: 100%;
-.mx_AppTile .mx_AppTileBody,
-.mx_AppTileFullWidth .mx_AppTileBody,
-.mx_AppTile_mini .mx_AppTileBody_mini {
- height: inherit;
- flex: 1;
-}
+ background-color: $widget-body-bg-color;
-.mx_AppWarning,
-.mx_AppPermission {
- text-align: center;
- display: flex;
- height: 100%;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- font-size: $font-16px;
+ iframe {
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+ display: block;
+ }
+ }
- h4 {
- margin: 0;
- padding: 0;
+ &.mx_AppTileBody--mini {
+ --AppTileBody-height: var(--AppTile_mini-height);
}
-}
-.mx_AppTile_loading {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- font-weight: bold;
- position: relative;
- height: 100%;
+ &.mx_AppTileBody--loading {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ height: 100%;
- /* match bg of border so that the cut corners have the right fill */
- background-color: $widget-body-bg-color !important;
- border-radius: $border-radius-8px;
+ /* match bg of border so that the cut corners have the right fill */
+ background-color: $widget-body-bg-color !important;
- iframe {
- display: none;
+ iframe {
+ display: none;
+ }
}
}
+/* appTileBody is embedded to PersistedElement outside of mx_AppTile,
+ so rules to style appTileBody generally should not be included here. */
+.mx_AppTile .mx_AppTileBody--large,
+.mx_AppTileFullWidth .mx_AppTileBody--large,
+.mx_AppTile_mini .mx_AppTileBody--mini {
+ height: inherit;
+ flex: 1;
+}
+
@keyframes mx_AppTileBody_fadeInSpinnerAnimation {
from {
opacity: 0;
diff --git a/res/css/views/rooms/_RecentlyViewedButton.pcss b/res/css/views/rooms/_RecentlyViewedButton.pcss
deleted file mode 100644
index f4e9206c67f..00000000000
--- a/res/css/views/rooms/_RecentlyViewedButton.pcss
+++ /dev/null
@@ -1,77 +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.
-*/
-
-.mx_RecentlyViewedButton_ContextMenu {
- padding: 16px 8px 16px 16px;
- width: max-content;
- max-width: 240px;
- max-height: 400px;
- border: 1px solid rgba($primary-content, 0.1);
- border-radius: 8px;
- box-shadow: 0 8px 4px rgba(0, 0, 0, 0.08);
- display: flex;
- flex-direction: column;
-
- > h4 {
- margin: 0 0 12px 0;
- }
-
- > div {
- overflow-y: auto;
-
- * {
- margin-right: 4px;
- }
- }
-
- .mx_AccessibleButton {
- margin-top: 2px;
- padding: 4px;
- display: flex;
- align-items: center;
- border-radius: 8px;
- min-height: 34px;
-
- &:hover {
- background-color: $panel-actions;
- }
-
- .mx_DecoratedRoomAvatar {
- margin-right: 8px;
- width: 24px;
-
- .mx_BaseAvatar {
- width: inherit;
- }
- }
-
- .mx_RecentlyViewedButton_entry_label {
- display: grid;
-
- > div {
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- }
-
- .mx_RecentlyViewedButton_entry_spaces {
- font-size: $font-12px;
- line-height: $font-15px;
- color: $secondary-content;
- }
- }
-}
diff --git a/res/img/feather-customised/help-circle.svg b/res/img/feather-customised/help-circle.svg
index dde6ec394f7..61b853aae86 100644
--- a/res/img/feather-customised/help-circle.svg
+++ b/res/img/feather-customised/help-circle.svg
@@ -1,5 +1,5 @@
);
- let content;
- if (MatrixClientPeg.get().getKeyBackupEnabled()) {
+ let content: JSX.Element | undefined;
+ if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) {
content = (
{newMethodDetected}
diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts
index 6ae899704bf..c9b61cc2f6d 100644
--- a/src/audio/PlaybackQueue.ts
+++ b/src/audio/PlaybackQueue.ts
@@ -64,7 +64,7 @@ export class PlaybackQueue {
}
public static forRoom(roomId: string): PlaybackQueue {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(roomId);
if (!room) throw new Error("Unknown room");
if (PlaybackQueue.queues.has(room.roomId)) {
diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx
index 995760f4b33..2cac8177628 100644
--- a/src/autocomplete/CommandProvider.tsx
+++ b/src/autocomplete/CommandProvider.tsx
@@ -27,6 +27,7 @@ import { TextualCompletion } from "./Components";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import { Command, Commands, CommandMap } from "../SlashCommands";
import { TimelineRenderingType } from "../contexts/RoomContext";
+import { MatrixClientPeg } from "../MatrixClientPeg";
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
@@ -51,12 +52,14 @@ export default class CommandProvider extends AutocompleteProvider {
const { command, range } = this.getCurrentCommand(query, selection);
if (!command) return [];
+ const cli = MatrixClientPeg.get();
+
let matches: Command[] = [];
// check if the full match differs from the first word (i.e. returns false if the command has args)
if (command[0] !== command[1]) {
// The input looks like a command with arguments, perform exact match
const name = command[1].slice(1); // strip leading `/`
- if (CommandMap.has(name) && CommandMap.get(name)!.isEnabled()) {
+ if (CommandMap.has(name) && CommandMap.get(name)!.isEnabled(cli)) {
// some commands, namely `me` don't suit having the usage shown whilst typing their arguments
if (CommandMap.get(name)!.hideCompletionAfterSpace) return [];
matches = [CommandMap.get(name)!];
@@ -75,7 +78,7 @@ export default class CommandProvider extends AutocompleteProvider {
return matches
.filter((cmd) => {
const display = !cmd.renderingTypes || cmd.renderingTypes.includes(this.renderingType);
- return cmd.isEnabled() && display;
+ return cmd.isEnabled(cli) && display;
})
.map((result) => {
let completion = result.getCommand() + " ";
diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx
index d4a4793ab51..51373a74030 100644
--- a/src/autocomplete/NotifProvider.tsx
+++ b/src/autocomplete/NotifProvider.tsx
@@ -38,9 +38,9 @@ export default class NotifProvider extends AutocompleteProvider {
force = false,
limit = -1,
): Promise
{
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
- if (!this.room.currentState.mayTriggerNotifOfType("room", client.credentials.userId!)) return [];
+ if (!this.room.currentState.mayTriggerNotifOfType("room", client.getSafeUserId())) return [];
const { command, range } = this.getCurrentCommand(query, selection, force);
if (
diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx
index 48f1582be01..a28777e65d6 100644
--- a/src/autocomplete/RoomProvider.tsx
+++ b/src/autocomplete/RoomProvider.tsx
@@ -65,7 +65,7 @@ export default class RoomProvider extends AutocompleteProvider {
}
protected getRooms(): Room[] {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
// filter out spaces here as they get their own autocomplete provider
return cli
diff --git a/src/autocomplete/SpaceProvider.tsx b/src/autocomplete/SpaceProvider.tsx
index 3f6bd30e3e6..4c6b8a32590 100644
--- a/src/autocomplete/SpaceProvider.tsx
+++ b/src/autocomplete/SpaceProvider.tsx
@@ -24,7 +24,7 @@ import RoomProvider from "./RoomProvider";
export default class SpaceProvider extends RoomProvider {
protected getRooms(): Room[] {
- return MatrixClientPeg.get()
+ return MatrixClientPeg.safeGet()
.getVisibleRooms(SettingsStore.getValue("feature_dynamic_room_predecessors"))
.filter((r) => r.isSpaceRoom());
}
diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx
index 3d4c542c48c..51a98064083 100644
--- a/src/autocomplete/UserProvider.tsx
+++ b/src/autocomplete/UserProvider.tsx
@@ -60,15 +60,13 @@ export default class UserProvider extends AutocompleteProvider {
shouldMatchWordsOnly: false,
});
- MatrixClientPeg.get().on(RoomEvent.Timeline, this.onRoomTimeline);
- MatrixClientPeg.get().on(RoomStateEvent.Update, this.onRoomStateUpdate);
+ MatrixClientPeg.safeGet().on(RoomEvent.Timeline, this.onRoomTimeline);
+ MatrixClientPeg.safeGet().on(RoomStateEvent.Update, this.onRoomStateUpdate);
}
public destroy(): void {
- if (MatrixClientPeg.get()) {
- MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.onRoomTimeline);
- MatrixClientPeg.get().removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
- }
+ MatrixClientPeg.get()?.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
+ MatrixClientPeg.get()?.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
}
private onRoomTimeline = (
@@ -155,7 +153,7 @@ export default class UserProvider extends AutocompleteProvider {
lastSpoken[event.getSender()!] = event.getTs();
}
- const currentUserId = MatrixClientPeg.get().credentials.userId;
+ const currentUserId = MatrixClientPeg.safeGet().credentials.userId;
this.users = this.room.getJoinedMembers().filter(({ userId }) => userId !== currentUserId);
this.users = this.users.concat(this.room.getMembersWithMembership("invite"));
@@ -167,7 +165,7 @@ export default class UserProvider extends AutocompleteProvider {
public onUserSpoke(user: RoomMember | null): void {
if (!this.users) return;
if (!user) return;
- if (user.userId === MatrixClientPeg.get().credentials.userId) return;
+ if (user.userId === MatrixClientPeg.safeGet().getSafeUserId()) return;
// Move the user that spoke to the front of the array
this.users.splice(
diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx
index 182f8038e22..f7353e5d24f 100644
--- a/src/components/structures/FilePanel.tsx
+++ b/src/components/structures/FilePanel.tsx
@@ -76,7 +76,7 @@ class FilePanel extends React.Component {
if (room?.roomId !== this.props.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
client.decryptEventIfNeeded(ev);
if (ev.isBeingDecrypted()) {
@@ -111,11 +111,11 @@ class FilePanel extends React.Component {
}
public async componentDidMount(): Promise {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
await this.updateTimelineSet(this.props.roomId);
- if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
+ if (!client.isRoomEncrypted(this.props.roomId)) return;
// The timelineSets filter makes sure that encrypted events that contain
// URLs never get added to the timeline, even if they are live events.
@@ -135,7 +135,7 @@ class FilePanel extends React.Component {
const client = MatrixClientPeg.get();
if (client === null) return;
- if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
+ if (!client.isRoomEncrypted(this.props.roomId)) return;
if (EventIndexPeg.get() !== null) {
client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
@@ -144,9 +144,9 @@ class FilePanel extends React.Component {
}
public async fetchFileEventsServer(room: Room): Promise {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
- const filter = new Filter(client.credentials.userId);
+ const filter = new Filter(client.getSafeUserId());
filter.setDefinition({
room: {
timeline: {
@@ -165,7 +165,7 @@ class FilePanel extends React.Component {
direction: Direction,
limit: number,
): Promise => {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId;
@@ -187,7 +187,7 @@ class FilePanel extends React.Component {
};
public async updateTimelineSet(roomId: string): Promise {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const room = client.getRoom(roomId);
const eventIndex = EventIndexPeg.get();
@@ -223,7 +223,7 @@ class FilePanel extends React.Component {
}
public render(): React.ReactNode {
- if (MatrixClientPeg.get().isGuest()) {
+ if (MatrixClientPeg.safeGet().isGuest()) {
return (
@@ -258,7 +258,7 @@ class FilePanel extends React.Component {
);
- const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
+ const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId);
if (this.state.timelineSet) {
return (
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index b64856476aa..a0729ecd3fc 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -33,13 +33,11 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import RoomListHeader from "../views/rooms/RoomListHeader";
-import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import IndicatorScrollbar from "./IndicatorScrollbar";
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
-import SettingsStore from "../../settings/SettingsStore";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
import { UIComponent } from "../../settings/UIFeature";
@@ -57,7 +55,6 @@ interface IProps {
enum BreadcrumbsMode {
Disabled,
Legacy,
- Labs,
}
interface IState {
@@ -85,8 +82,7 @@ export default class LeftPanel extends React.Component {
}
private static get breadcrumbsMode(): BreadcrumbsMode {
- if (!BreadcrumbsStore.instance.visible) return BreadcrumbsMode.Disabled;
- return SettingsStore.getValue("feature_breadcrumbs_v2") ? BreadcrumbsMode.Labs : BreadcrumbsMode.Legacy;
+ return !BreadcrumbsStore.instance.visible ? BreadcrumbsMode.Disabled : BreadcrumbsMode.Legacy;
}
public componentDidMount(): void {
@@ -344,9 +340,7 @@ export default class LeftPanel extends React.Component {
}
let rightButton: JSX.Element | undefined;
- if (this.state.showBreadcrumbs === BreadcrumbsMode.Labs) {
- rightButton = ;
- } else if (
+ if (
// SC: Always show explore button in any space
// eslint-disable-next-line no-constant-condition
true ||
diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts
index c87f08bfffc..467b579410f 100644
--- a/src/components/structures/LegacyCallEventGrouper.ts
+++ b/src/components/structures/LegacyCallEventGrouper.ts
@@ -129,7 +129,7 @@ export default class LegacyCallEventGrouper extends EventEmitter {
public get callWasMissed(): boolean {
return (
this.state === CallState.Ended &&
- ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId())
+ ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.safeGet().getUserId())
);
}
diff --git a/src/components/structures/MainSplit.tsx b/src/components/structures/MainSplit.tsx
index ab29cd2b397..64b2e5c04fd 100644
--- a/src/components/structures/MainSplit.tsx
+++ b/src/components/structures/MainSplit.tsx
@@ -26,9 +26,24 @@ interface IProps {
collapsedRhs?: boolean;
panel?: JSX.Element;
children: ReactNode;
+ /**
+ * A unique identifier for this panel split.
+ *
+ * This is appended to the key used to store the panel size in localStorage, allowing the widths of different
+ * panels to be stored.
+ */
+ sizeKey?: string;
+ /**
+ * The size to use for the panel component if one isn't persisted in storage. Defaults to 350.
+ */
+ defaultSize: number;
}
export default class MainSplit extends React.Component {
+ public static defaultProps = {
+ defaultSize: 350,
+ };
+
private onResizeStart = (): void => {
this.props.resizeNotifier.startResizing();
};
@@ -37,6 +52,14 @@ export default class MainSplit extends React.Component {
this.props.resizeNotifier.notifyRightHandleResized();
};
+ private get sizeSettingStorageKey(): string {
+ let key = "mx_rhs_size";
+ if (!!this.props.sizeKey) {
+ key += `_${this.props.sizeKey}`;
+ }
+ return key;
+ }
+
private onResizeStop = (
event: MouseEvent | TouchEvent,
direction: Direction,
@@ -44,14 +67,17 @@ export default class MainSplit extends React.Component {
delta: NumberSize,
): void => {
this.props.resizeNotifier.stopResizing();
- window.localStorage.setItem("mx_rhs_size", (this.loadSidePanelSize().width + delta.width).toString());
+ window.localStorage.setItem(
+ this.sizeSettingStorageKey,
+ (this.loadSidePanelSize().width + delta.width).toString(),
+ );
};
private loadSidePanelSize(): { height: string | number; width: number } {
- let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size")!, 10);
+ let rhsSize = parseInt(window.localStorage.getItem(this.sizeSettingStorageKey)!, 10);
if (isNaN(rhsSize)) {
- rhsSize = 350;
+ rhsSize = this.props.defaultSize;
}
return {
@@ -70,6 +96,7 @@ export default class MainSplit extends React.Component {
if (hasResizer) {
children = (
{
}
private async postLoginSetup(): Promise {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const cryptoEnabled = cli.isCryptoEnabled();
if (!cryptoEnabled) {
this.onLoggedIn();
@@ -394,7 +396,10 @@ export default class MatrixChat extends React.PureComponent {
} else {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
}
- } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
+ } else if (
+ (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) &&
+ !shouldSkipSetupEncryption(cli)
+ ) {
// if cross-signing is not yet set up, do so now if possible.
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {
@@ -575,11 +580,11 @@ export default class MatrixChat extends React.PureComponent {
if (payload.event_type === "m.identity_server") {
const fullUrl = payload.event_content ? payload.event_content["base_url"] : null;
if (!fullUrl) {
- MatrixClientPeg.get().setIdentityServerUrl(undefined);
+ MatrixClientPeg.safeGet().setIdentityServerUrl(undefined);
localStorage.removeItem("mx_is_access_token");
localStorage.removeItem("mx_is_url");
} else {
- MatrixClientPeg.get().setIdentityServerUrl(fullUrl);
+ MatrixClientPeg.safeGet().setIdentityServerUrl(fullUrl);
localStorage.removeItem("mx_is_access_token"); // clear token
localStorage.setItem("mx_is_url", fullUrl); // XXX: Do we still need this?
}
@@ -626,7 +631,7 @@ export default class MatrixChat extends React.PureComponent {
this.notifyNewScreen("forgot_password");
break;
case "start_chat":
- createRoom(MatrixClientPeg.get(), {
+ createRoom(MatrixClientPeg.safeGet(), {
dmUserId: payload.user_id,
});
break;
@@ -648,7 +653,7 @@ export default class MatrixChat extends React.PureComponent {
// FIXME: controller shouldn't be loading a view :(
const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner");
- MatrixClientPeg.get()
+ MatrixClientPeg.safeGet()
.leave(payload.room_id)
.then(
() => {
@@ -756,7 +761,7 @@ export default class MatrixChat extends React.PureComponent {
this.viewSomethingBehindModal();
break;
case "view_invite": {
- const room = MatrixClientPeg.get().getRoom(payload.roomId);
+ const room = MatrixClientPeg.safeGet().getRoom(payload.roomId);
if (room?.isSpaceRoom()) {
showSpaceInvite(room);
} else {
@@ -795,7 +800,7 @@ export default class MatrixChat extends React.PureComponent {
Modal.createDialog(DialPadModal, {}, "mx_Dialog_dialPadWrapper");
break;
case Action.OnLoggedIn:
- this.stores.client = MatrixClientPeg.get();
+ this.stores.client = MatrixClientPeg.safeGet();
if (
// Skip this handling for token login as that always calls onLoggedIn itself
!this.tokenLogin &&
@@ -936,7 +941,7 @@ export default class MatrixChat extends React.PureComponent {
}
let presentedId = roomInfo.room_alias || roomInfo.room_id!;
- const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
+ const room = MatrixClientPeg.safeGet().getRoom(roomInfo.room_id);
if (room) {
// Not all timeline events are decrypted ahead of time anymore
// Only the critical ones for a typical UI are
@@ -1063,14 +1068,14 @@ export default class MatrixChat extends React.PureComponent {
const [shouldCreate, opts] = await modal.finished;
if (shouldCreate) {
- createRoom(MatrixClientPeg.get(), opts!);
+ createRoom(MatrixClientPeg.safeGet(), opts!);
}
}
private chatCreateOrReuse(userId: string): void {
const snakedConfig = new SnakedObject(this.props.config);
// Use a deferred action to reshow the dialog once the user has registered
- if (MatrixClientPeg.get().isGuest()) {
+ if (MatrixClientPeg.safeGet().isGuest()) {
// No point in making 2 DMs with welcome bot. This assumes view_set_mxid will
// result in a new DM with the welcome user.
if (userId !== snakedConfig.get("welcome_user_id")) {
@@ -1099,7 +1104,7 @@ export default class MatrixChat extends React.PureComponent {
// TODO: Immutable DMs replaces this
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const dmRoom = findDMForUser(client, userId);
if (dmRoom) {
@@ -1117,7 +1122,7 @@ export default class MatrixChat extends React.PureComponent {
}
private leaveRoomWarnings(roomId: string): JSX.Element[] {
- const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
+ const roomToLeave = MatrixClientPeg.safeGet().getRoom(roomId);
const isSpace = roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications.
const warnings: JSX.Element[] = [];
@@ -1155,7 +1160,7 @@ export default class MatrixChat extends React.PureComponent {
}
private leaveRoom(roomId: string): void {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const roomToLeave = cli.getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId);
@@ -1189,8 +1194,8 @@ export default class MatrixChat extends React.PureComponent {
}
private forgetRoom(roomId: string): void {
- const room = MatrixClientPeg.get().getRoom(roomId);
- MatrixClientPeg.get()
+ const room = MatrixClientPeg.safeGet().getRoom(roomId);
+ MatrixClientPeg.safeGet()
.forget(roomId)
.then(() => {
// Switch to home page if we're currently viewing the forgotten room
@@ -1213,7 +1218,7 @@ export default class MatrixChat extends React.PureComponent {
}
private async copyRoom(roomId: string): Promise {
- const roomLink = makeRoomPermalink(MatrixClientPeg.get(), roomId);
+ const roomLink = makeRoomPermalink(MatrixClientPeg.safeGet(), roomId);
const success = await copyPlaintext(roomLink);
if (!success) {
Modal.createDialog(ErrorDialog, {
@@ -1247,7 +1252,7 @@ export default class MatrixChat extends React.PureComponent {
const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(welcomeUserId);
if (welcomeUserRooms.length === 0) {
- const roomId = await createRoom(MatrixClientPeg.get(), {
+ const roomId = await createRoom(MatrixClientPeg.safeGet(), {
dmUserId: snakedConfig.get("welcome_user_id"),
// Only view the welcome user if we're NOT looking at a room
andView: !this.state.currentRoomId,
@@ -1263,11 +1268,11 @@ export default class MatrixChat extends React.PureComponent {
// the saved sync to be loaded).
const saveWelcomeUser = (ev: MatrixEvent): void => {
if (ev.getType() === EventType.Direct && ev.getContent()[welcomeUserId]) {
- MatrixClientPeg.get().store.save(true);
- MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser);
+ MatrixClientPeg.safeGet().store.save(true);
+ MatrixClientPeg.safeGet().removeListener(ClientEvent.AccountData, saveWelcomeUser);
}
};
- MatrixClientPeg.get().on(ClientEvent.AccountData, saveWelcomeUser);
+ MatrixClientPeg.safeGet().on(ClientEvent.AccountData, saveWelcomeUser);
return roomId;
}
@@ -1429,7 +1434,7 @@ export default class MatrixChat extends React.PureComponent {
// Before defaulting to directory, show the last viewed room
this.viewLastRoom();
} else {
- if (MatrixClientPeg.get().isGuest()) {
+ if (MatrixClientPeg.safeGet().isGuest()) {
dis.dispatch({ action: "view_welcome_page" });
} else {
dis.dispatch({ action: Action.ViewHomePage });
@@ -1492,7 +1497,7 @@ export default class MatrixChat extends React.PureComponent {
// to do the first sync
this.firstSyncComplete = false;
this.firstSyncPromise = defer();
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
// Allow the JS SDK to reap timeline events. This reduces the amount of
// memory consumed as the JS SDK stores multiple distinct copies of room
@@ -1612,7 +1617,7 @@ export default class MatrixChat extends React.PureComponent {
cli.on(MatrixEventEvent.Decrypted, (e, err) => dft.eventDecrypted(e, err as DecryptionError));
cli.on(ClientEvent.Room, (room) => {
- if (MatrixClientPeg.get().isCryptoEnabled()) {
+ if (cli.isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
SettingLevel.ROOM_DEVICE,
"blacklistUnverifiedDevices",
@@ -1642,15 +1647,15 @@ export default class MatrixChat extends React.PureComponent {
}
});
cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise => {
- let haveNewVersion;
- let newVersionInfo;
+ let haveNewVersion: boolean | undefined;
+ let newVersionInfo: IKeyBackupInfo | null = null;
// if key backup is still enabled, there must be a new backup in place
- if (MatrixClientPeg.get().getKeyBackupEnabled()) {
+ if (cli.getKeyBackupEnabled()) {
haveNewVersion = true;
} else {
// otherwise check the server to see if there's a new one
try {
- newVersionInfo = await MatrixClientPeg.get().getKeyBackupVersion();
+ newVersionInfo = await cli.getKeyBackupVersion();
if (newVersionInfo !== null) haveNewVersion = true;
} catch (e) {
logger.error("Saw key backup error but failed to check backup version!", e);
@@ -1663,7 +1668,7 @@ export default class MatrixChat extends React.PureComponent {
import(
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
) as unknown as Promise,
- { newVersionInfo },
+ { newVersionInfo: newVersionInfo! },
);
} else {
Modal.createDialogAsync(
@@ -1710,7 +1715,7 @@ export default class MatrixChat extends React.PureComponent {
* @private
*/
private onClientStarted(): void {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
if (cli.isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, "blacklistUnverifiedDevices");
@@ -1758,7 +1763,7 @@ export default class MatrixChat extends React.PureComponent {
params: params,
});
} else if (screen === "soft_logout") {
- if (cli.getUserId() && !Lifecycle.isSoftLogout()) {
+ if (!!cli?.getUserId() && !Lifecycle.isSoftLogout()) {
// Logged in - visit a room
this.viewLastRoom();
} else {
@@ -2081,7 +2086,7 @@ export default class MatrixChat extends React.PureComponent {
{...this.props}
{...this.state}
ref={this.loggedInView}
- matrixClient={MatrixClientPeg.get()}
+ matrixClient={MatrixClientPeg.safeGet()}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
/>
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 8e26a0b422c..5be9d8ec9b8 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -25,6 +25,7 @@ import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
import { Optional } from "matrix-events-sdk";
+import { MatrixClient } from "matrix-js-sdk/src/matrix";
import shouldHideEvent from "../../shouldHideEvent";
import { wantsDateSeparator } from "../../DateUtils";
@@ -74,6 +75,7 @@ const groupedStateEvents = [
export function shouldFormContinuation(
prevEvent: MatrixEvent | null,
mxEvent: MatrixEvent,
+ matrixClient: MatrixClient,
showHiddenEvents: boolean,
timelineRenderingType?: TimelineRenderingType,
): boolean {
@@ -111,7 +113,7 @@ export function shouldFormContinuation(
}
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
- if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false;
+ if (!haveRendererForEvent(prevEvent, matrixClient, showHiddenEvents)) return false;
return true;
}
@@ -483,7 +485,7 @@ export default class MessagePanel extends React.Component {
}
}
- if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender()!)) {
+ if (MatrixClientPeg.safeGet().isUserIgnored(mxEv.getSender()!)) {
return false; // ignored = no show (only happens if the ignore happens after an event was received)
}
@@ -491,7 +493,7 @@ export default class MessagePanel extends React.Component {
return true;
}
- if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) {
+ if (!haveRendererForEvent(mxEv, MatrixClientPeg.safeGet(), this.showHiddenEvents)) {
return false; // no tile = no show
}
@@ -746,6 +748,7 @@ export default class MessagePanel extends React.Component {
ret.push(dateSeparator);
}
+ const cli = MatrixClientPeg.safeGet();
let lastInSection = true;
if (nextEventWithTile) {
const nextEv = nextEventWithTile;
@@ -753,14 +756,14 @@ export default class MessagePanel extends React.Component {
lastInSection =
willWantDateSeparator ||
mxEv.getSender() !== nextEv.getSender() ||
- getEventDisplayInfo(MatrixClientPeg.get(), nextEv, this.showHiddenEvents).isInfoMessage ||
- !shouldFormContinuation(mxEv, nextEv, this.showHiddenEvents, this.context.timelineRenderingType);
+ getEventDisplayInfo(cli, nextEv, this.showHiddenEvents).isInfoMessage ||
+ !shouldFormContinuation(mxEv, nextEv, cli, this.showHiddenEvents, this.context.timelineRenderingType);
}
// is this a continuation of the previous message?
const continuation =
!wantsDateSeparator &&
- shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents, this.context.timelineRenderingType);
+ shouldFormContinuation(prevEvent, mxEv, cli, this.showHiddenEvents, this.context.timelineRenderingType);
const eventId = mxEv.getId()!;
const highlight = eventId === this.props.highlightedEventId;
@@ -789,7 +792,7 @@ export default class MessagePanel extends React.Component {
// We only want to consider "last successful" if the event is sent by us, otherwise of course
// it's successful: we received it.
- isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
+ isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.safeGet().getUserId();
const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id);
// use txnId as key if available so that we don't remount during sending
@@ -846,7 +849,7 @@ export default class MessagePanel extends React.Component {
// Get a list of read receipts that should be shown next to this event
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] | null {
- const myUserId = MatrixClientPeg.get().credentials.userId;
+ const myUserId = MatrixClientPeg.safeGet().credentials.userId;
// get list of read receipts, sorted most recent first
const { room } = this.props;
@@ -869,7 +872,7 @@ export default class MessagePanel extends React.Component {
if (!r.userId || !isSupportedReceiptType(r.type) || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
}
- if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
+ if (MatrixClientPeg.safeGet().isUserIgnored(r.userId)) {
return; // ignore ignored users
}
const member = room.getMember(r.userId);
@@ -1314,7 +1317,7 @@ class MainGrouper extends BaseGrouper {
public add({ event: ev, shouldShow }: EventAndShouldShow): void {
if (ev.getType() === EventType.RoomMember) {
// We can ignore any events that don't actually have a message to display
- if (!hasText(ev, MatrixClientPeg.get(), this.panel.showHiddenEvents)) return;
+ if (!hasText(ev, MatrixClientPeg.safeGet(), this.panel.showHiddenEvents)) return;
}
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId()!, ev === this.lastShownEvent);
if (!this.panel.showHiddenEvents && !shouldShow) {
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx
index 0ecf4b5116e..0def24799cd 100644
--- a/src/components/structures/NotificationPanel.tsx
+++ b/src/components/structures/NotificationPanel.tsx
@@ -65,8 +65,8 @@ export default class NotificationPanel extends React.PureComponent
);
- let content;
- const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
+ let content: JSX.Element;
+ const timelineSet = MatrixClientPeg.safeGet().getNotifTimelineSet();
if (timelineSet) {
// wrap a TimelinePanel with the jump-to-event bits turned off.
content = (
diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx
index 6d761f9c793..6f249fd6779 100644
--- a/src/components/structures/PipContainer.tsx
+++ b/src/components/structures/PipContainer.tsx
@@ -139,8 +139,8 @@ class PipContainerInner extends React.Component {
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
- MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
- const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
+ MatrixClientPeg.safeGet().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
+ const room = MatrixClientPeg.safeGet().getRoom(this.state.viewedRoomId);
if (room) {
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
}
@@ -239,7 +239,7 @@ class PipContainerInner extends React.Component {
let notDocked = false;
// Sanity check the room - the widget may have been destroyed between render cycles, and
// thus no room is associated anymore.
- if (persistentWidgetId && persistentRoomId && MatrixClientPeg.get().getRoom(persistentRoomId)) {
+ if (persistentWidgetId && persistentRoomId && MatrixClientPeg.safeGet().getRoom(persistentRoomId)) {
notDocked = !ActiveWidgetStore.instance.isDocked(persistentWidgetId, persistentRoomId);
fromAnotherRoom = this.state.viewedRoomId !== persistentRoomId;
}
@@ -318,7 +318,7 @@ class PipContainerInner extends React.Component {
pipContent.push(({ onStartMoving }) => (
(
continue;
}
- if (!haveRendererForEvent(mxEv, roomContext.showHiddenEvents)) {
+ if (!haveRendererForEvent(mxEv, client, roomContext.showHiddenEvents)) {
// XXX: can this ever happen? It will make the result count
// not match the displayed count.
continue;
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index dc160534b69..7f11483daea 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -181,6 +181,11 @@ export interface IRoomState {
showApps: boolean;
isPeeking: boolean;
showRightPanel: boolean;
+ /**
+ * Whether the right panel shown is either of ThreadPanel or ThreadView.
+ * Always false when `showRightPanel` is false.
+ */
+ threadRightPanel: boolean;
// error object, as from the matrix client/server API
// If we failed to load information about the room,
// store the error here.
@@ -221,7 +226,6 @@ export interface IRoomState {
showUrlPreview?: boolean;
e2eStatus?: E2EStatus;
rejecting?: boolean;
- rejectError?: Error;
hasPinnedWidgets?: boolean;
mainSplitContentType?: MainSplitContentType;
// whether or not a spaces context switch brought us here,
@@ -229,7 +233,6 @@ export interface IRoomState {
wasContextSwitch?: boolean;
editState?: EditorStateTransfer;
timelineRenderingType: TimelineRenderingType;
- threadId?: string;
liveTimeline?: EventTimeline;
narrow: boolean;
msc3946ProcessDynamicPredecessor: boolean;
@@ -408,6 +411,7 @@ export class RoomView extends React.Component {
showApps: false,
isPeeking: false,
showRightPanel: false,
+ threadRightPanel: false,
joining: false,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
@@ -546,7 +550,7 @@ export class RoomView extends React.Component {
/**
* Removes the Jitsi widget from the current user if
* - Multiple Jitsi widgets have been added within {@link PREVENT_MULTIPLE_JITSI_WITHIN}
- * - The last (server timestamp) of these widgets is from the currrent user
+ * - The last (server timestamp) of these widgets is from the current user
* This solves the issue if some people decide to start a conference and click the call button at the same time.
*/
private doMaybeRemoveOwnJitsiWidget(): void {
@@ -619,7 +623,8 @@ export class RoomView extends React.Component {
return;
}
- if (!initial && this.state.roomId !== this.context.roomViewStore.getRoomId()) {
+ const roomLoadError = this.context.roomViewStore.getRoomLoadError() ?? undefined;
+ if (!initial && !roomLoadError && this.state.roomId !== this.context.roomViewStore.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
@@ -641,7 +646,7 @@ export class RoomView extends React.Component {
roomId: roomId ?? undefined,
roomAlias: this.context.roomViewStore.getRoomAlias() ?? undefined,
roomLoading: this.context.roomViewStore.isRoomLoading(),
- roomLoadError: this.context.roomViewStore.getRoomLoadError() ?? undefined,
+ roomLoadError,
joining: this.context.roomViewStore.isJoining(),
replyToEvent: this.context.roomViewStore.getQuotingEvent() ?? undefined,
// we should only peek once we have a ready client
@@ -655,6 +660,11 @@ export class RoomView extends React.Component {
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
+ threadRightPanel: roomId
+ ? [RightPanelPhases.ThreadView, RightPanelPhases.ThreadPanel].includes(
+ this.context.rightPanelStore.currentCardForRoom(roomId).phase!,
+ )
+ : false,
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
};
@@ -1058,8 +1068,14 @@ export class RoomView extends React.Component {
}
private onRightPanelStoreUpdate = (): void => {
+ const { roomId } = this.state;
this.setState({
- showRightPanel: this.state.roomId ? this.context.rightPanelStore.isOpenForRoom(this.state.roomId) : false,
+ showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
+ threadRightPanel: roomId
+ ? [RightPanelPhases.ThreadView, RightPanelPhases.ThreadPanel].includes(
+ this.context.rightPanelStore.currentCardForRoom(roomId).phase!,
+ )
+ : false,
});
};
@@ -1752,7 +1768,6 @@ export class RoomView extends React.Component {
this.setState({
rejecting: false,
- rejectError: error,
});
},
);
@@ -1786,7 +1801,6 @@ export class RoomView extends React.Component {
this.setState({
rejecting: false,
- rejectError: error,
});
}
};
@@ -2559,7 +2573,14 @@ export class RoomView extends React.Component {
viewingCall={viewingCall}
activeCall={this.state.activeCall}
/>
-
+
{
// This would cause jumping to happen on Chrome/macOS.
return new Promise((resolve) => window.setTimeout(resolve, 1))
.then(() => {
- return this.props.onFillRequest(backwards);
+ return this.props.onFillRequest?.(backwards);
})
.finally(() => {
this.pendingFillRequests[dir] = false;
diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx
index 118c09bda90..cad79096c20 100644
--- a/src/components/structures/ThreadView.tsx
+++ b/src/components/structures/ThreadView.tsx
@@ -329,7 +329,7 @@ export default class ThreadView extends React.Component
{
Array.from(dataTransfer.files),
roomId,
this.threadRelation,
- MatrixClientPeg.get(),
+ MatrixClientPeg.safeGet(),
TimelineRenderingType.Thread,
);
} else {
diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx
index e89e938ff99..79f906ef964 100644
--- a/src/components/structures/TimelinePanel.tsx
+++ b/src/components/structures/TimelinePanel.tsx
@@ -302,7 +302,7 @@ class TimelinePanel extends React.Component {
readMarkerEventId: this.initialReadMarkerId,
backPaginating: false,
forwardPaginating: false,
- clientSyncState: MatrixClientPeg.get().getSyncState(),
+ clientSyncState: MatrixClientPeg.safeGet().getSyncState(),
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
@@ -310,7 +310,7 @@ class TimelinePanel extends React.Component {
};
this.dispatcherRef = dis.register(this.onAction);
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
cli.on(RoomEvent.Timeline, this.onRoomTimeline);
cli.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
cli.on(RoomEvent.Redaction, this.onRoomRedaction);
@@ -799,7 +799,7 @@ class TimelinePanel extends React.Component {
// read-marker when a remote echo of an event we have just sent takes
// more than the timeout on userActiveRecently.
//
- const myUserId = MatrixClientPeg.get().credentials.userId;
+ const myUserId = MatrixClientPeg.safeGet().credentials.userId;
callRMUpdated = false;
if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
updatedState.readMarkerVisible = true;
@@ -892,7 +892,7 @@ class TimelinePanel extends React.Component {
if (!this.hasTimelineSetFor(member.roomId)) return;
// ignore events for other users
- if (member.userId != MatrixClientPeg.get().credentials?.userId) return;
+ if (member.userId != MatrixClientPeg.safeGet().credentials?.userId) return;
// We could skip an update if the power level change didn't cross the
// threshold for `VISIBILITY_CHANGE_TYPE`.
@@ -1301,7 +1301,7 @@ class TimelinePanel extends React.Component {
}
// now think about advancing it
- const myUserId = MatrixClientPeg.get().credentials.userId;
+ const myUserId = MatrixClientPeg.safeGet().credentials.userId;
for (i++; i < events.length; i++) {
const ev = events[i];
if (ev.getSender() !== myUserId) {
@@ -1582,7 +1582,7 @@ class TimelinePanel extends React.Component {
* @param {boolean?} scrollIntoView whether to scroll the event into view.
*/
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap });
this.overlayTimelineWindow = this.props.overlayTimelineSet
? new TimelineWindow(cli, this.props.overlayTimelineSet, { windowLimit: this.props.timelineCap })
@@ -1651,12 +1651,12 @@ class TimelinePanel extends React.Component {
// dialog, let's jump to the end of the timeline. If we weren't,
// something has gone badly wrong and rather than causing a loop of
// undismissable dialogs, let's just give up.
- if (eventId) {
+ if (eventId && this.props.timelineSet.room) {
onFinished = () => {
// go via the dispatcher so that the URL is updated
dis.dispatch({
action: Action.ViewRoom,
- room_id: this.props.timelineSet.room.roomId,
+ room_id: this.props.timelineSet.room!.roomId,
metricsTrigger: undefined, // room doesn't change
});
};
@@ -1776,7 +1776,7 @@ class TimelinePanel extends React.Component {
arrayFastClone(events)
.reverse()
.forEach((event) => {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
client.decryptEventIfNeeded(event);
});
@@ -1824,7 +1824,7 @@ class TimelinePanel extends React.Component {
* such events were found, then it returns 0.
*/
private checkForPreJoinUISI(events: MatrixEvent[]): number {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const room = this.props.timelineSet.room;
const isThreadTimeline = [TimelineRenderingType.Thread, TimelineRenderingType.ThreadsList].includes(
@@ -1949,7 +1949,7 @@ class TimelinePanel extends React.Component {
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
- const myUserId = MatrixClientPeg.get().credentials.userId;
+ const myUserId = MatrixClientPeg.safeGet().credentials.userId;
const isNodeInView = (node?: HTMLElement): boolean => {
if (node) {
@@ -1993,7 +1993,8 @@ class TimelinePanel extends React.Component {
!!ev.status || // local echo
(ignoreOwn && ev.getSender() === myUserId); // own message
const isWithoutTile =
- !haveRendererForEvent(ev, this.context?.showHiddenEvents) || shouldHideEvent(ev, this.context);
+ !haveRendererForEvent(ev, MatrixClientPeg.safeGet(), this.context?.showHiddenEvents) ||
+ shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) {
// don't start counting if the event should be ignored,
@@ -2125,7 +2126,7 @@ class TimelinePanel extends React.Component {
// forwards, otherwise if somebody hits the bottom of the loaded
// events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room.
- const stickyBottom = !this.timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ const stickyBottom = !this.timelineWindow?.canPaginate(EventTimeline.FORWARDS);
// If the state is PREPARED or CATCHUP, we're still waiting for the js-sdk to sync with
// the HS and fetch the latest events, so we are effectively forward paginating.
@@ -2149,7 +2150,7 @@ class TimelinePanel extends React.Component {
canBackPaginate={this.state.canBackPaginate && this.state.firstVisibleEventIndex === 0}
showUrlPreview={this.props.showUrlPreview}
showReadReceipts={this.props.showReadReceipts}
- ourUserId={MatrixClientPeg.get().getSafeUserId()}
+ ourUserId={MatrixClientPeg.safeGet().getSafeUserId()}
stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll}
onFillRequest={this.onMessageListFillRequest}
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 76686e5ef1d..3ec63f142f3 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -252,7 +252,7 @@ export default class UserMenu extends React.Component {
if (!this.state.contextMenuPosition) return null;
let topSection: JSX.Element | undefined;
- if (MatrixClientPeg.get().isGuest()) {
+ if (MatrixClientPeg.safeGet().isGuest()) {
topSection = (
{_t(
@@ -333,7 +333,7 @@ export default class UserMenu extends React.Component
{
);
- if (MatrixClientPeg.get().isGuest()) {
+ if (MatrixClientPeg.safeGet().isGuest()) {
primaryOptionList = (
{homeButton}
@@ -360,7 +360,7 @@ export default class UserMenu extends React.Component {
{UserIdentifierCustomisations.getDisplayUserIdentifier(
- MatrixClientPeg.get().getSafeUserId(),
+ MatrixClientPeg.safeGet().getSafeUserId(),
{
withDisplayName: true,
},
@@ -393,7 +393,7 @@ export default class UserMenu extends React.Component {
public render(): React.ReactNode {
const avatarSize = 32; // should match border-radius of the avatar
- const userId = MatrixClientPeg.get().getSafeUserId();
+ const userId = MatrixClientPeg.safeGet().getSafeUserId();
const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx
index 4cff508dfba..84e5552e602 100644
--- a/src/components/structures/UserView.tsx
+++ b/src/components/structures/UserView.tsx
@@ -20,7 +20,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
-import { MatrixClientPeg } from "../../MatrixClientPeg";
import Modal from "../../Modal";
import { _t } from "../../languageHandler";
import ErrorDialog from "../views/dialogs/ErrorDialog";
@@ -30,6 +29,7 @@ import Spinner from "../views/elements/Spinner";
import ResizeNotifier from "../../utils/ResizeNotifier";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
+import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
userId: string;
@@ -42,6 +42,9 @@ interface IState {
}
export default class UserView extends React.Component {
+ public static contextType = MatrixClientContext;
+ public context!: React.ContextType;
+
public constructor(props: IProps) {
super(props);
this.state = {
@@ -65,15 +68,14 @@ export default class UserView extends React.Component {
}
private async loadProfileInfo(): Promise {
- const cli = MatrixClientPeg.get();
this.setState({ loading: true });
let profileInfo: Awaited>;
try {
- profileInfo = await cli.getProfileInfo(this.props.userId);
+ profileInfo = await this.context.getProfileInfo(this.props.userId);
} catch (err) {
Modal.createDialog(ErrorDialog, {
title: _t("Could not load user profile"),
- description: err && err.message ? err.message : _t("Operation failed"),
+ description: err instanceof Error ? err.message : _t("Operation failed"),
});
this.setState({ loading: false });
return;
diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx
index a4e10e12e07..d1e60ad12f6 100644
--- a/src/components/structures/ViewSource.tsx
+++ b/src/components/structures/ViewSource.tsx
@@ -142,7 +142,7 @@ export default class ViewSource extends React.Component {
}
private canSendStateEvent(mxEvent: MatrixEvent): boolean {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(mxEvent.getRoomId());
return !!room?.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
}
@@ -155,7 +155,7 @@ export default class ViewSource extends React.Component {
const eventId = mxEvent.getId()!;
const canEdit = mxEvent.isState()
? this.canSendStateEvent(mxEvent)
- : canEditContent(MatrixClientPeg.get(), this.props.mxEvent);
+ : canEditContent(MatrixClientPeg.safeGet(), this.props.mxEvent);
return (
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 3299cae3fc3..b69c7f3e09f 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -17,10 +17,10 @@ limitations under the License.
import React, { ReactNode } from "react";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
-import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
+import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
import { _t, _td, UserFriendlyError } from "../../../languageHandler";
-import Login from "../../../Login";
+import Login, { ClientLoginFlow } from "../../../Login";
import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import AuthPage from "../../views/auth/AuthPage";
@@ -38,6 +38,7 @@ import AuthHeader from "../../views/auth/AuthHeader";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { filterBoolean } from "../../../utils/arrays";
+import { Features } from "../../../settings/Settings";
// These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n.
@@ -49,7 +50,6 @@ _td("Invalid identity server discovery response");
_td("Invalid base_url for m.identity_server");
_td("Identity server URL does not appear to be a valid identity server");
_td("General failure");
-
interface IProps {
serverConfig: ValidatedServerConfig;
// If true, the component will consider itself busy.
@@ -84,7 +84,7 @@ interface IState {
// can we attempt to log in or are there validation errors?
canTryLogin: boolean;
- flows?: LoginFlow[];
+ flows?: ClientLoginFlow[];
// used for preserving form values when changing homeserver
username: string;
@@ -110,6 +110,7 @@ type OnPasswordLogin = {
*/
export default class LoginComponent extends React.PureComponent
{
private unmounted = false;
+ private oidcNativeFlowEnabled = false;
private loginLogic!: Login;
private readonly stepRendererMap: Record ReactNode>;
@@ -117,6 +118,9 @@ export default class LoginComponent extends React.PureComponent
public constructor(props: IProps) {
super(props);
+ // only set on a config level, so we don't need to watch
+ this.oidcNativeFlowEnabled = SettingsStore.getValue(Features.OidcNativeFlow);
+
this.state = {
busy: false,
errorText: null,
@@ -156,7 +160,10 @@ export default class LoginComponent extends React.PureComponent
public componentDidUpdate(prevProps: IProps): void {
if (
prevProps.serverConfig.hsUrl !== this.props.serverConfig.hsUrl ||
- prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl
+ prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl ||
+ // delegatedAuthentication is only set by buildValidatedConfigFromDiscovery and won't be modified
+ // so shallow comparison is fine
+ prevProps.serverConfig.delegatedAuthentication !== this.props.serverConfig.delegatedAuthentication
) {
// Ensure that we end up actually logging in to the right place
this.initLoginLogic(this.props.serverConfig);
@@ -322,28 +329,10 @@ export default class LoginComponent extends React.PureComponent
}
};
- private async initLoginLogic({ hsUrl, isUrl }: ValidatedServerConfig): Promise {
- let isDefaultServer = false;
- if (
- this.props.serverConfig.isDefault &&
- hsUrl === this.props.serverConfig.hsUrl &&
- isUrl === this.props.serverConfig.isUrl
- ) {
- isDefaultServer = true;
- }
-
- const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl! : null;
-
- const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
- defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
- });
- this.loginLogic = loginLogic;
-
- this.setState({
- busy: true,
- loginIncorrect: false,
- });
-
+ private async checkServerLiveliness({
+ hsUrl,
+ isUrl,
+ }: Pick): Promise {
// Do a quick liveliness check on the URLs
try {
const { warning } = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
@@ -361,9 +350,38 @@ export default class LoginComponent extends React.PureComponent
} catch (e) {
this.setState({
busy: false,
- ...AutoDiscoveryUtils.authComponentStateForError(e),
+ ...AutoDiscoveryUtils.authComponentStateForError(e as Error),
});
}
+ }
+
+ private async initLoginLogic({ hsUrl, isUrl }: ValidatedServerConfig): Promise {
+ let isDefaultServer = false;
+ if (
+ this.props.serverConfig.isDefault &&
+ hsUrl === this.props.serverConfig.hsUrl &&
+ isUrl === this.props.serverConfig.isUrl
+ ) {
+ isDefaultServer = true;
+ }
+
+ const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl! : null;
+
+ this.setState({
+ busy: true,
+ loginIncorrect: false,
+ });
+
+ await this.checkServerLiveliness({ hsUrl, isUrl });
+
+ const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
+ defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
+ // if native OIDC is enabled in the client pass the server's delegated auth settings
+ delegatedAuthentication: this.oidcNativeFlowEnabled
+ ? this.props.serverConfig.delegatedAuthentication
+ : undefined,
+ });
+ this.loginLogic = loginLogic;
loginLogic
.getFlows()
@@ -401,7 +419,7 @@ export default class LoginComponent extends React.PureComponent
});
}
- private isSupportedFlow = (flow: LoginFlow): boolean => {
+ private isSupportedFlow = (flow: ClientLoginFlow): boolean => {
// technically the flow can have multiple steps, but no one does this
// for login and loginLogic doesn't support it so we can ignore it.
if (!this.stepRendererMap[flow.type]) {
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index d8fd848320b..75269301526 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -420,7 +420,7 @@ export default class Registration extends React.Component {
if (!this.props.brand) {
return Promise.resolve();
}
- const matrixClient = MatrixClientPeg.get();
+ const matrixClient = MatrixClientPeg.safeGet();
return matrixClient.getPushers().then(
(resp) => {
const pushers = resp.pushers;
diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx
index 4be5efa5c92..1de7cc7d83f 100644
--- a/src/components/structures/auth/SetupEncryptionBody.tsx
+++ b/src/components/structures/auth/SetupEncryptionBody.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React from "react";
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
-import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
@@ -87,9 +87,9 @@ export default class SetupEncryptionBody extends React.Component
};
private onVerifyClick = (): void => {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const userId = cli.getSafeUserId();
- const requestPromise = cli.requestVerification(userId);
+ const requestPromise = cli.getCrypto()!.requestOwnUserVerification();
// We need to call onFinished now to close this dialog, and
// again later to signal that the verification is complete.
@@ -142,7 +142,7 @@ export default class SetupEncryptionBody extends React.Component
};
public render(): React.ReactNode {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const { phase, lostKeys } = this.state;
if (this.state.verificationRequest && cli.getUser(this.state.verificationRequest.otherUserId)) {
diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx
index f8a058fc7b4..932cdef475c 100644
--- a/src/components/structures/auth/SoftLogout.tsx
+++ b/src/components/structures/auth/SoftLogout.tsx
@@ -94,7 +94,7 @@ export default class SoftLogout extends React.Component {
this.initLogin();
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
if (cli.isCryptoEnabled()) {
cli.countSessionsNeedingBackup().then((remaining) => {
this.setState({ keyBackupNeeded: remaining > 0 });
@@ -124,7 +124,7 @@ export default class SoftLogout extends React.Component {
// Note: we don't use the existing Login class because it is heavily flow-based. We don't
// care about login flows here, unless it is the single flow we support.
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const flows = (await client.loginFlows()).flows;
const loginViews = flows.map((f) => STATIC_FLOWS_TO_VIEWS[f.type]);
@@ -148,16 +148,17 @@ export default class SoftLogout extends React.Component {
this.setState({ busy: true });
- const hsUrl = MatrixClientPeg.get().getHomeserverUrl();
- const isUrl = MatrixClientPeg.get().getIdentityServerUrl();
+ const cli = MatrixClientPeg.safeGet();
+ const hsUrl = cli.getHomeserverUrl();
+ const isUrl = cli.getIdentityServerUrl();
const loginType = "m.login.password";
const loginParams = {
identifier: {
type: "m.id.user",
- user: MatrixClientPeg.get().getUserId(),
+ user: cli.getUserId(),
},
password: this.state.password,
- device_id: MatrixClientPeg.get().getDeviceId() ?? undefined,
+ device_id: cli.getDeviceId() ?? undefined,
};
let credentials: IMatrixClientCreds;
@@ -196,11 +197,11 @@ export default class SoftLogout extends React.Component {
return;
}
- const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
+ const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.safeGet().getIdentityServerUrl();
const loginType = "m.login.token";
const loginParams = {
token: this.props.realQueryParams["loginToken"],
- device_id: MatrixClientPeg.get().getDeviceId() ?? undefined,
+ device_id: MatrixClientPeg.safeGet().getDeviceId() ?? undefined,
};
let credentials: IMatrixClientCreds;
@@ -262,7 +263,7 @@ export default class SoftLogout extends React.Component {
{introText ?
{introText}
: null}
{
}
public componentDidMount(): void {
- MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
+ MatrixClientPeg.safeGet().on(RoomStateEvent.Events, this.onRoomStateEvents);
}
public componentWillUnmount(): void {
diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx
index 9fc7ca81106..ea5170c38a2 100644
--- a/src/components/views/context_menus/MessageContextMenu.tsx
+++ b/src/components/views/context_menus/MessageContextMenu.tsx
@@ -144,7 +144,7 @@ export default class MessageContextMenu extends React.Component
}
public componentDidMount(): void {
- MatrixClientPeg.get().on(RoomMemberEvent.PowerLevel, this.checkPermissions);
+ MatrixClientPeg.safeGet().on(RoomMemberEvent.PowerLevel, this.checkPermissions);
// re-check the permissions on send progress (`maySendRedactionForEvent` only returns true for events that have
// been fully sent and echoed back, and we want to ensure the "Remove" option is added once that happens.)
@@ -162,7 +162,7 @@ export default class MessageContextMenu extends React.Component
}
private checkPermissions = (): void => {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
// We explicitly decline to show the redact option on ACL events as it has a potential
@@ -184,7 +184,7 @@ export default class MessageContextMenu extends React.Component
};
private isPinned(): boolean {
- const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
+ const room = MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room?.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
if (!pinnedEvent) return false;
const content = pinnedEvent.getContent();
@@ -195,13 +195,13 @@ export default class MessageContextMenu extends React.Component
return (
M_POLL_START.matches(mxEvent.getType()) &&
this.state.canRedact &&
- !isPollEnded(mxEvent, MatrixClientPeg.get())
+ !isPollEnded(mxEvent, MatrixClientPeg.safeGet())
);
}
private onResendReactionsClick = (): void => {
for (const reaction of this.getUnsentReactions()) {
- Resend.resend(MatrixClientPeg.get(), reaction);
+ Resend.resend(MatrixClientPeg.safeGet(), reaction);
}
this.closeMenu();
};
@@ -253,7 +253,7 @@ export default class MessageContextMenu extends React.Component
};
private onPinClick = (): void => {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
if (!room) return;
const eventId = this.props.mxEvent.getId();
@@ -318,7 +318,7 @@ export default class MessageContextMenu extends React.Component
private onEditClick = (): void => {
editEvent(
- MatrixClientPeg.get(),
+ MatrixClientPeg.safeGet(),
this.props.mxEvent,
this.context.timelineRenderingType,
this.props.getRelationsForEvent,
@@ -345,7 +345,7 @@ export default class MessageContextMenu extends React.Component
};
private onEndPollClick = (): void => {
- const matrixClient = MatrixClientPeg.get();
+ const matrixClient = MatrixClientPeg.safeGet();
Modal.createDialog(
EndPollDialog,
{
@@ -359,7 +359,7 @@ export default class MessageContextMenu extends React.Component
};
private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId();
return (
@@ -386,7 +386,7 @@ export default class MessageContextMenu extends React.Component
};
public render(): React.ReactNode {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const me = cli.getUserId();
const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain, ...other } = this.props;
delete other.getRelationsForEvent;
diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx
index 543f2b9d969..c4464a6edd0 100644
--- a/src/components/views/context_menus/ThreadListContextMenu.tsx
+++ b/src/components/views/context_menus/ThreadListContextMenu.tsx
@@ -84,7 +84,7 @@ const ThreadListContextMenu: React.FC = ({
onMenuToggle?.(menuDisplayed);
}, [menuDisplayed, onMenuToggle]);
- const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
+ const room = MatrixClientPeg.safeGet().getRoom(mxEvent.getRoomId());
const isMainSplitTimelineShown = !!room && !WidgetLayoutStore.instance.hasMaximisedWidget(room);
return (
diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx
index 8dc6ca62eb0..6482b817734 100644
--- a/src/components/views/context_menus/WidgetContextMenu.tsx
+++ b/src/components/views/context_menus/WidgetContextMenu.tsx
@@ -71,7 +71,7 @@ export const WidgetContextMenu: React.FC = ({
} catch (err) {
logger.error("Failed to start livestream", err);
// XXX: won't i18n well, but looks like widget api only support 'message'?
- const message = err.message || _t("Unable to start audio streaming.");
+ const message = err instanceof Error ? err.message : _t("Unable to start audio streaming.");
Modal.createDialog(ErrorDialog, {
title: _t("Failed to start livestream"),
description: message,
diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx
index 49a2a633ab9..2642fb6f2fb 100644
--- a/src/components/views/dialogs/BugReportDialog.tsx
+++ b/src/components/views/dialogs/BugReportDialog.tsx
@@ -160,7 +160,7 @@ export default class BugReportDialog extends React.Component {
if (!this.unmounted) {
this.setState({
downloadBusy: false,
- downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
+ downloadProgress: _t("Failed to send logs: ") + `${err instanceof Error ? err.message : ""}`,
});
}
}
diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx
index a792ccbd634..40395b97871 100644
--- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx
+++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx
@@ -15,6 +15,8 @@ limitations under the License.
*/
import React from "react";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { HTTPError, MatrixError } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import ConfirmRedactDialog from "./ConfirmRedactDialog";
@@ -23,6 +25,7 @@ import BaseDialog from "./BaseDialog";
import Spinner from "../elements/Spinner";
interface IProps {
+ event: MatrixEvent;
redact: () => Promise;
onFinished: (success?: boolean) => void;
}
@@ -60,7 +63,13 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent;
+ return ;
}
}
}
diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx
index f51b89131c8..0cf77871c0a 100644
--- a/src/components/views/dialogs/ConfirmRedactDialog.tsx
+++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx
@@ -26,6 +26,7 @@ import ErrorDialog from "./ErrorDialog";
import TextInputDialog from "./TextInputDialog";
interface IProps {
+ event: MatrixEvent;
onFinished(success?: false, reason?: void): void;
onFinished(success: true, reason?: string): void;
}
@@ -35,14 +36,16 @@ interface IProps {
*/
export default class ConfirmRedactDialog extends React.Component {
public render(): React.ReactNode {
+ let description = _t("Are you sure you wish to remove (delete) this event?");
+ if (this.props.event.isState()) {
+ description += " " + _t("Note that removing room changes like this could undo the change.");
+ }
+
return (
=> {
if (!proceed) return;
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const withRelTypes: Pick = {};
// redact related events if this is a voice broadcast started event and
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index 2484d5fd7e6..50482af550f 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -24,7 +24,7 @@ import SdkConfig from "../../../SdkConfig";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import { IOpts } from "../../../createRoom";
+import { checkUserIsAllowedToChangeEncryption, IOpts } from "../../../createRoom";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
@@ -75,7 +75,7 @@ export default class CreateRoomDialog extends React.Component {
joinRule = JoinRule.Restricted;
}
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
this.state = {
isPublic: this.props.defaultPublic || false,
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
@@ -86,11 +86,15 @@ export default class CreateRoomDialog extends React.Component {
detailsOpen: false,
noFederate: SdkConfig.get().default_federate === false,
nameIsValid: false,
- canChangeEncryption: true,
+ canChangeEncryption: false,
};
- cli.doesServerForceEncryptionForPreset(Preset.PrivateChat).then((isForced) =>
- this.setState({ canChangeEncryption: !isForced }),
+ checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) =>
+ this.setState((state) => ({
+ canChangeEncryption: allowChange,
+ // override with forcedValue if it is set
+ isEncrypted: forcedValue ?? state.isEncrypted,
+ })),
);
}
@@ -107,8 +111,7 @@ export default class CreateRoomDialog extends React.Component {
const { alias } = this.state;
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
} else {
- // If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
- opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;
+ opts.encryption = this.state.isEncrypted;
}
if (this.state.topic) {
@@ -222,7 +225,7 @@ export default class CreateRoomDialog extends React.Component {
let aliasField: JSX.Element | undefined;
if (this.state.joinRule === JoinRule.Public) {
- const domain = MatrixClientPeg.get().getDomain()!;
+ const domain = MatrixClientPeg.safeGet().getDomain()!;
aliasField = (
{
let e2eeSection: JSX.Element | undefined;
if (this.state.joinRule !== JoinRule.Public) {
let microcopy: string;
- if (privateShouldBeEncrypted(MatrixClientPeg.get())) {
+ if (privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
if (this.state.canChangeEncryption) {
microcopy = isVideoRoom
? _t("You can't disable this later. The room will be encrypted but the embedded call will not.")
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx
index 8e8f0be7703..0c3a9c85657 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.tsx
+++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx
@@ -125,7 +125,7 @@ export default class DeactivateAccountDialog extends React.Component {
// Deactivation worked - logout & close this dialog
@@ -158,7 +158,7 @@ export default class DeactivateAccountDialog extends React.Component {
// If we got here, oops. The server didn't require any auth.
@@ -190,7 +190,7 @@ export default class DeactivateAccountDialog extends React.Component
{this.state.bodyText}
{
private async fetchOpponentProfile(): Promise {
try {
- const prof = await MatrixClientPeg.get().getProfileInfo(this.props.verifier.userId);
+ const prof = await MatrixClientPeg.safeGet().getProfileInfo(this.props.verifier.userId);
this.setState({
opponentProfile: prof,
});
@@ -143,7 +143,7 @@ export default class IncomingSasDialog extends React.Component {
};
private renderPhaseStart(): ReactNode {
- const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
+ const isSelf = this.props.verifier.userId === MatrixClientPeg.safeGet().getUserId();
let profile;
const oppProfile = this.state.opponentProfile;
@@ -233,7 +233,7 @@ export default class IncomingSasDialog extends React.Component {
sas={this.showSasEvent.sas}
onCancel={this.onCancelClick}
onDone={this.onSasMatchesClick}
- isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
+ isSelf={this.props.verifier.userId === MatrixClientPeg.safeGet().getUserId()}
inDialog={true}
/>
);
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 30dc15e5cc8..68a31a8bdb5 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -26,12 +26,7 @@ import { Icon as InfoIcon } from "../../../../res/img/element-icons/info.svg";
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
import { _t, _td } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import {
- getHostnameFromMatrixServerName,
- getServerName,
- makeRoomPermalink,
- makeUserPermalink,
-} from "../../../utils/permalinks/Permalinks";
+import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import DMRoomMap from "../../../utils/DMRoomMap";
import SdkConfig from "../../../SdkConfig";
import * as Email from "../../../email";
@@ -373,12 +368,12 @@ export default class InviteDialog extends React.PureComponent alreadyInvited.add(m.userId));
room.getMembersWithMembership("join").forEach((m) => alreadyInvited.add(m.userId));
@@ -395,7 +390,7 @@ export default class InviteDialog extends React.PureComponent u.userId !== myUserId);
for (const member of otherMembers) {
@@ -491,7 +486,7 @@ export default class InviteDialog extends React.PureComponent): { userId: string; user: Member }[] {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const activityScores = buildActivityScores(cli);
const memberScores = buildMemberScores(cli);
@@ -560,7 +555,7 @@ export default class InviteDialog extends React.PureComponent t.userId);
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(this.props.roomId);
if (!room) {
logger.error("Failed to find the room to invite users to");
@@ -694,7 +689,7 @@ export default class InviteDialog extends React.PureComponent => {
- MatrixClientPeg.get()
+ MatrixClientPeg.safeGet()
.searchUserDirectory({ term })
.then(async (r): Promise => {
if (term !== this.state.filterText) {
@@ -724,18 +719,6 @@ export default class InviteDialog extends React.PureComponent 0 || (this.state.filterText && this.state.filterText.includes("@"));
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const userId = cli.getUserId()!;
if (this.props.kind === InviteKind.Dm) {
title = _t("Direct Messages");
@@ -1332,11 +1315,11 @@ export default class InviteDialog extends React.PureComponent{_t("If you can't see who you're looking for, send them your invite link below.")}
);
- const link = makeUserPermalink(MatrixClientPeg.get().getUserId()!);
+ const link = makeUserPermalink(MatrixClientPeg.safeGet().getSafeUserId());
footer = (
{_t("Or send invite link")}
-
makeUserPermalink(MatrixClientPeg.get().getUserId()!)}>
+ makeUserPermalink(MatrixClientPeg.safeGet().getSafeUserId())}>
{link}
diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx
index daf81ced65d..bf92719c195 100644
--- a/src/components/views/dialogs/LogoutDialog.tsx
+++ b/src/components/views/dialogs/LogoutDialog.tsx
@@ -50,7 +50,7 @@ export default class LogoutDialog extends React.Component {
public constructor(props: IProps) {
super(props);
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
this.state = {
@@ -66,7 +66,7 @@ export default class LogoutDialog extends React.Component {
private async loadBackupStatus(): Promise {
try {
- const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
+ const backupInfo = await MatrixClientPeg.safeGet().getKeyBackupVersion();
this.setState({
loading: false,
backupInfo,
@@ -86,7 +86,7 @@ export default class LogoutDialog extends React.Component {
typeof ExportE2eKeysDialog
>,
{
- matrixClient: MatrixClientPeg.get(),
+ matrixClient: MatrixClientPeg.safeGet(),
},
);
};
diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
index d81d6e76ca4..0946f2dbb90 100644
--- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
+++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
@@ -37,7 +37,7 @@ export function ManualDeviceKeyVerificationDialog({
device,
onFinished,
}: IManualDeviceKeyVerificationDialogProps): JSX.Element {
- const mxClient = MatrixClientPeg.get();
+ const mxClient = MatrixClientPeg.safeGet();
const onLegacyFinished = useCallback(
(confirm: boolean) => {
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
index a8b446df09a..58e7ba9b188 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
@@ -67,7 +67,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent();
let result: Awaited>;
@@ -102,7 +102,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent b.id);
@@ -130,7 +130,7 @@ export default class ModalWidgetDialog extends React.PureComponent {
// Does the room support it, too?
// Extract state events to determine whether we should display
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const room = client.getRoom(props.mxEvent.getRoomId());
for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
@@ -237,7 +237,7 @@ export default class ReportEventDialog extends React.Component {
});
try {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const ev = this.props.mxEvent;
if (this.moderation && this.state.nature !== NonStandardValue.Admin) {
const nature = this.state.nature;
@@ -312,7 +312,7 @@ export default class ReportEventDialog extends React.Component {
if (this.moderation) {
// Display report-to-moderator dialog.
// We let the user pick a nature.
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const homeServerName = SdkConfig.get("validated_server_config")!.hsName;
let subtitle: string;
switch (this.state.nature) {
diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx
index 55e49843b2a..86d748730b6 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.tsx
+++ b/src/components/views/dialogs/RoomSettingsDialog.tsx
@@ -73,7 +73,7 @@ class RoomSettingsDialog extends React.Component {
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
- MatrixClientPeg.get().on(RoomEvent.Name, this.onRoomName);
+ MatrixClientPeg.safeGet().on(RoomEvent.Name, this.onRoomName);
this.onRoomName();
}
@@ -89,7 +89,7 @@ class RoomSettingsDialog extends React.Component {
dis.unregister(this.dispatcherRef);
}
- MatrixClientPeg.get().removeListener(RoomEvent.Name, this.onRoomName);
+ MatrixClientPeg.get()?.removeListener(RoomEvent.Name, this.onRoomName);
}
/**
@@ -98,7 +98,7 @@ class RoomSettingsDialog extends React.Component {
* @throws when room is not found
*/
private getRoom(): Room {
- const room = MatrixClientPeg.get().getRoom(this.props.roomId)!;
+ const room = MatrixClientPeg.safeGet().getRoom(this.props.roomId)!;
// something is really wrong if we encounter this
if (!room) {
diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx
index 4ae49a327fa..be59a3e0117 100644
--- a/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx
+++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx
@@ -60,7 +60,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
});
return;
}
- this.addThreepid = new AddThreepid(MatrixClientPeg.get());
+ this.addThreepid = new AddThreepid(MatrixClientPeg.safeGet());
this.addThreepid.addEmailAddress(emailAddress).then(
() => {
Modal.createDialog(QuestionDialog, {
diff --git a/src/components/views/dialogs/SlashCommandHelpDialog.tsx b/src/components/views/dialogs/SlashCommandHelpDialog.tsx
index c650e78f41f..e59c3178a53 100644
--- a/src/components/views/dialogs/SlashCommandHelpDialog.tsx
+++ b/src/components/views/dialogs/SlashCommandHelpDialog.tsx
@@ -19,6 +19,7 @@ import React from "react";
import { _t } from "../../../languageHandler";
import { Command, CommandCategories, Commands } from "../../../SlashCommands";
import InfoDialog from "./InfoDialog";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
interface IProps {
onFinished(): void;
@@ -27,7 +28,7 @@ interface IProps {
const SlashCommandHelpDialog: React.FC = ({ onFinished }) => {
const categories: Record = {};
Commands.forEach((cmd) => {
- if (!cmd.isEnabled()) return;
+ if (!cmd.isEnabled(MatrixClientPeg.get())) return;
if (!categories[cmd.category]) {
categories[cmd.category] = [];
}
diff --git a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx
index 9ef3e83edea..37457000911 100644
--- a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx
+++ b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx
@@ -63,7 +63,7 @@ async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise
}
export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean): void }> = ({ onFinished }) => {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const currentProxy = SettingsStore.getValue("feature_sliding_sync_proxy_url");
const hasNativeSupport = useAsyncMemo(
() =>
@@ -87,7 +87,7 @@ export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean):
const validProxy = withValidation({
async deriveData({ value }): Promise<{ error?: Error }> {
try {
- await proxyHealthCheck(value!, MatrixClientPeg.get().baseUrl);
+ await proxyHealthCheck(value!, MatrixClientPeg.safeGet().baseUrl);
return {};
} catch (error) {
return { error };
diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
index 393590ff9d9..296a8f06102 100644
--- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx
+++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
@@ -34,7 +34,7 @@ const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) =
let askToVerifyText: string;
let newSessionText: string;
- if (MatrixClientPeg.get().getUserId() === user.userId) {
+ if (MatrixClientPeg.safeGet().getUserId() === user.userId) {
newSessionText = _t("You signed in to a new session without verifying it:");
askToVerifyText = _t("Verify your other session using one of the options below.");
} else {
diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx
index 540cb3190eb..f087969c1b9 100644
--- a/src/components/views/dialogs/VerificationRequestDialog.tsx
+++ b/src/components/views/dialogs/VerificationRequestDialog.tsx
@@ -15,7 +15,7 @@ limitations under the License.
*/
import React from "react";
-import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import { User } from "matrix-js-sdk/src/models/user";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@@ -48,7 +48,7 @@ export default class VerificationRequestDialog extends React.Component => {
// Now reset cross-signing so everything Just Works™ again.
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest): Promise => {
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
index 2bac1f028c9..7a6412c8ed3 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
@@ -73,7 +73,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
try {
- await MatrixClientPeg.get().uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
+ await MatrixClientPeg.safeGet().uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
@@ -98,11 +98,11 @@ export default class CreateCrossSigningDialog extends React.PureComponent): void => {
this.setState({
recoveryKey: e.target.value,
- recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
+ recoveryKeyValid: MatrixClientPeg.safeGet().isValidRecoveryKey(e.target.value),
});
};
@@ -145,7 +145,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent => {
if (!this.state.backupInfo) return;
- await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
+ await MatrixClientPeg.safeGet().restoreKeyBackupWithSecretStorage(
this.state.backupInfo,
undefined,
undefined,
@@ -252,7 +252,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
if (!backupInfo) return false;
try {
- const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache(
+ const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithCache(
undefined /* targetRoomId */,
undefined /* targetSessionId */,
backupInfo,
@@ -274,7 +274,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
- const myUserId = MatrixClientPeg.get().getUserId();
+ const myUserId = MatrixClientPeg.safeGet().getUserId();
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (otherUserId) {
@@ -281,7 +281,7 @@ interface IDirectoryOpts {
const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = null, onFinished }) => {
const inputRef = useRef(null);
const scrollContainerRef = useRef(null);
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const rovingContext = useContext(RovingTabIndexContext);
const [query, _setQuery] = useState(initialText);
const [recentSearches, clearRecentSearches] = useRecentSearches();
diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx
index b4ed2e10aea..081d7e2a7c8 100644
--- a/src/components/views/directory/NetworkDropdown.tsx
+++ b/src/components/views/directory/NetworkDropdown.tsx
@@ -42,7 +42,7 @@ const validServer = withValidation({
deriveData: async ({ value }): Promise<{ error?: MatrixError }> => {
try {
// check if we can successfully load this server's room directory
- await MatrixClientPeg.get().publicRooms({
+ await MatrixClientPeg.safeGet().publicRooms({
limit: 1,
server: value ?? undefined,
});
diff --git a/src/components/views/elements/AppPermission.tsx b/src/components/views/elements/AppPermission.tsx
index e73c8ddf8a1..463db9f2e3b 100644
--- a/src/components/views/elements/AppPermission.tsx
+++ b/src/components/views/elements/AppPermission.tsx
@@ -25,9 +25,11 @@ import WidgetUtils from "../../../utils/WidgetUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MemberAvatar from "../avatars/MemberAvatar";
import BaseAvatar from "../avatars/BaseAvatar";
+import Heading from "../typography/Heading";
import AccessibleButton from "./AccessibleButton";
import TextWithTooltip from "./TextWithTooltip";
import { parseUrl } from "../../../utils/UrlUtils";
+import { Icon as HelpIcon } from "../../../../res/img/feather-customised/help-circle.svg";
interface IProps {
url: string;
@@ -55,7 +57,7 @@ export default class AppPermission extends React.Component {
const urlInfo = this.parseWidgetUrl();
// The second step is to find the user's profile so we can show it on the prompt
- const room = MatrixClientPeg.get().getRoom(this.props.roomId);
+ const room = MatrixClientPeg.safeGet().getRoom(this.props.roomId);
let roomMember: RoomMember | null = null;
if (room) roomMember = room.getMember(this.props.creatorUserId);
@@ -117,8 +119,9 @@ export default class AppPermission extends React.Component {
-
+
);
@@ -139,20 +142,22 @@ export default class AppPermission extends React.Component {
return (
-
{_t("Widget added by")}
-
- {avatar}
-
{displayName}
-
{userId}
-
-
{warning}
-
- {_t("This widget may use cookies.")} {encryptionWarning}
-
-
-
- {_t("Continue")}
-
+
+
{_t("Widget added by")}
+
+ {avatar}
+
{displayName}
+
{userId}
+
+
{warning}
+
+ {_t("This widget may use cookies.")} {encryptionWarning}
+
+
);
diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx
index 10cd8c40aa2..0d48113b7b4 100644
--- a/src/components/views/elements/AppTile.tsx
+++ b/src/components/views/elements/AppTile.tsx
@@ -596,9 +596,10 @@ export default class AppTile extends React.Component
{
"microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; " + "clipboard-read;";
const appTileBodyClass = classNames({
- mx_AppTileBody: !this.props.miniMode,
- mx_AppTileBody_mini: this.props.miniMode,
- mx_AppTile_loading: this.state.loading,
+ "mx_AppTileBody": true,
+ "mx_AppTileBody--large": !this.props.miniMode,
+ "mx_AppTileBody--mini": this.props.miniMode,
+ "mx_AppTileBody--loading": this.state.loading,
});
const appTileBodyStyles: CSSProperties = {};
if (this.props.pointerEvents) {
diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx
index 08f0f7ef2e1..be472baa9ad 100644
--- a/src/components/views/elements/ErrorBoundary.tsx
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -60,8 +60,8 @@ export default class ErrorBoundary extends React.PureComponent {
private onClearCacheAndReload = (): void => {
if (!PlatformPeg.get()) return;
- MatrixClientPeg.get().stopClient();
- MatrixClientPeg.get()
+ MatrixClientPeg.safeGet().stopClient();
+ MatrixClientPeg.safeGet()
.store.deleteAllData()
.then(() => {
PlatformPeg.get()?.reload();
diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx
index b1d53247db7..2d4c763eb20 100644
--- a/src/components/views/elements/PersistedElement.tsx
+++ b/src/components/views/elements/PersistedElement.tsx
@@ -162,7 +162,7 @@ export default class PersistedElement extends React.Component {
private renderApp(): void {
const content = (
-
+
{this.props.children}
diff --git a/src/components/views/elements/Pill.tsx b/src/components/views/elements/Pill.tsx
index 49c6c1bfd2d..e5e9d383eaf 100644
--- a/src/components/views/elements/Pill.tsx
+++ b/src/components/views/elements/Pill.tsx
@@ -106,7 +106,7 @@ export const Pill: React.FC = ({ type: propType, url, inMessage, room
mx_RoomPill: type === PillType.RoomMention,
mx_SpacePill: type === "space",
mx_UserPill: type === PillType.UserMention,
- mx_UserPill_me: resourceId === MatrixClientPeg.get().getUserId(),
+ mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
});
@@ -158,7 +158,7 @@ export const Pill: React.FC = ({ type: propType, url, inMessage, room
return (
-
+
{inMessage && url ? (
{
}
private get matrixClient(): MatrixClient {
- return MatrixClientPeg.get();
+ return MatrixClientPeg.safeGet();
}
public componentDidMount(): void {
diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx
index 02a9bfe2d77..2cfaa3dfdc5 100644
--- a/src/components/views/elements/RoomTopic.tsx
+++ b/src/components/views/elements/RoomTopic.tsx
@@ -31,6 +31,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
import TooltipTarget from "./TooltipTarget";
import { Linkify, topicToHtml } from "../../../HtmlUtils";
+import { tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
interface IProps extends React.HTMLProps {
room: Room;
@@ -46,12 +47,22 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
const onClick = useCallback(
(e: React.MouseEvent) => {
props.onClick?.(e);
+
const target = e.target as HTMLElement;
- if (target.tagName.toUpperCase() === "A") {
+
+ if (target.tagName.toUpperCase() !== "A") {
+ dis.fire(Action.ShowRoomTopic);
return;
}
- dis.fire(Action.ShowRoomTopic);
+ const anchor = e.target as HTMLLinkElement;
+ const localHref = tryTransformPermalinkToLocalHref(anchor.href);
+
+ if (localHref !== anchor.href) {
+ // it could be converted to a localHref -> therefore handle locally
+ e.preventDefault();
+ window.location.hash = localHref;
+ }
},
[props],
);
diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx
index de5fc07d889..c95a7f819c1 100644
--- a/src/components/views/emojipicker/ReactionPicker.tsx
+++ b/src/components/views/emojipicker/ReactionPicker.tsx
@@ -78,7 +78,7 @@ class ReactionPicker extends React.Component {
if (!this.props.reactions) {
return {};
}
- const userId = MatrixClientPeg.get().getUserId()!;
+ const userId = MatrixClientPeg.safeGet().getSafeUserId();
const myAnnotations = this.props.reactions.getAnnotationsBySender()?.[userId] ?? new Set();
return Object.fromEntries(
[...myAnnotations]
@@ -101,7 +101,7 @@ class ReactionPicker extends React.Component {
if (myReactions.hasOwnProperty(reaction)) {
if (this.props.mxEvent.isRedacted() || !this.context.canSelfRedact) return false;
- MatrixClientPeg.get().redactEvent(this.props.mxEvent.getRoomId()!, myReactions[reaction]);
+ MatrixClientPeg.safeGet().redactEvent(this.props.mxEvent.getRoomId()!, myReactions[reaction]);
dis.dispatch({
action: Action.FocusAComposer,
context: this.context.timelineRenderingType,
@@ -109,7 +109,7 @@ class ReactionPicker extends React.Component {
// Tell the emoji picker not to bump this in the more frequently used list.
return false;
} else {
- MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId()!, EventType.Reaction, {
+ MatrixClientPeg.safeGet().sendEvent(this.props.mxEvent.getRoomId()!, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: this.props.mxEvent.getId(),
diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx
index 4eed475d5cc..87969ac4b7f 100644
--- a/src/components/views/messages/DateSeparator.tsx
+++ b/src/components/views/messages/DateSeparator.tsx
@@ -125,7 +125,7 @@ export default class DateSeparator extends React.Component {
const roomIdForJumpRequest = this.props.roomId;
try {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent(
roomIdForJumpRequest,
unixTimestamp,
diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx
index 3531e6e5fa5..6c06321e69a 100644
--- a/src/components/views/messages/EditHistoryMessage.tsx
+++ b/src/components/views/messages/EditHistoryMessage.tsx
@@ -25,13 +25,13 @@ import { formatTime } from "../../../DateUtils";
import { pillifyLinks, unmountPills } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify";
import { _t } from "../../../languageHandler";
-import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import RedactedBody from "./RedactedBody";
import AccessibleButton from "../elements/AccessibleButton";
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource";
import SettingsStore from "../../../settings/SettingsStore";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
function getReplacedContent(event: MatrixEvent): IContent {
const originalContent = event.getOriginalContent();
@@ -52,14 +52,18 @@ interface IState {
}
export default class EditHistoryMessage extends React.PureComponent {
+ public static contextType = MatrixClientContext;
+ public context!: React.ContextType;
+
private content = createRef();
private pills: Element[] = [];
private tooltips: Element[] = [];
- public constructor(props: IProps) {
+ public constructor(props: IProps, context: React.ContextType) {
super(props);
+ this.context = context;
- const cli = MatrixClientPeg.get();
+ const cli = this.context;
const userId = cli.getSafeUserId();
const event = this.props.mxEvent;
const room = cli.getRoom(event.getRoomId());
@@ -74,11 +78,12 @@ export default class EditHistoryMessage extends React.PureComponent => {
const event = this.props.mxEvent;
- const cli = MatrixClientPeg.get();
+ const cli = this.context;
Modal.createDialog(
ConfirmAndWaitRedactDialog,
{
+ event,
redact: async () => {
await cli.redactEvent(event.getRoomId()!, event.getId()!);
},
@@ -101,7 +106,7 @@ export default class EditHistoryMessage extends React.PureComponent(({ mxEvent, timestamp }, ref) => {
const cli = useContext(MatrixClientContext);
const roomId = mxEvent.getRoomId()!;
- const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
+ const isRoomEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);
const prevContent = mxEvent.getPrevContent() as IRoomEncryption;
const content = mxEvent.getContent();
diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx
index 5d8264bba08..684c162c066 100644
--- a/src/components/views/messages/MImageBody.tsx
+++ b/src/components/views/messages/MImageBody.tsx
@@ -160,7 +160,7 @@ export default class MImageBody extends React.Component {
};
private clearError = (): void => {
- MatrixClientPeg.get().off(ClientEvent.Sync, this.reconnectedListener);
+ MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener);
this.setState({ imgError: false });
};
@@ -177,7 +177,7 @@ export default class MImageBody extends React.Component {
this.setState({
imgError: true,
});
- MatrixClientPeg.get().on(ClientEvent.Sync, this.reconnectedListener);
+ MatrixClientPeg.safeGet().on(ClientEvent.Sync, this.reconnectedListener);
};
private onImageLoad = (): void => {
@@ -373,7 +373,7 @@ export default class MImageBody extends React.Component {
public componentWillUnmount(): void {
this.unmounted = true;
- MatrixClientPeg.get().off(ClientEvent.Sync, this.reconnectedListener);
+ MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener);
this.clearBlurhashTimeout();
if (this.sizeWatcher) SettingsStore.unwatchSetting(this.sizeWatcher);
if (this.state.isAnimated && this.state.thumbUrl) {
diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index b299d96f6dd..e71a4681437 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -37,7 +37,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent {
const url = this.props.mxEvent.getContent()["url"];
const prevUrl = this.props.mxEvent.getPrevContent()["url"];
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
- const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
+ const room = MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId());
if (!room) return null;
const widgetId = this.props.mxEvent.getStateKey();
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId);
diff --git a/src/components/views/messages/MKeyVerificationConclusion.tsx b/src/components/views/messages/MKeyVerificationConclusion.tsx
index 5c51d5c2ce8..4ec24231dcb 100644
--- a/src/components/views/messages/MKeyVerificationConclusion.tsx
+++ b/src/components/views/messages/MKeyVerificationConclusion.tsx
@@ -17,11 +17,7 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import {
- Phase as VerificationPhase,
- VerificationRequest,
- VerificationRequestEvent,
-} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { VerificationPhase, VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
@@ -46,7 +42,7 @@ export default class MKeyVerificationConclusion extends React.Component
if (request) {
request.on(VerificationRequestEvent.Change, this.onRequestChanged);
}
- MatrixClientPeg.get().on(CryptoEvent.UserTrustStatusChanged, this.onTrustChanged);
+ MatrixClientPeg.safeGet().on(CryptoEvent.UserTrustStatusChanged, this.onTrustChanged);
}
public componentWillUnmount(): void {
@@ -93,7 +89,7 @@ export default class MKeyVerificationConclusion extends React.Component
}
// User isn't actually verified
- if (!MatrixClientPeg.get().checkUserTrust(request.otherUserId).isCrossSigningVerified()) {
+ if (!MatrixClientPeg.safeGet().checkUserTrust(request.otherUserId).isCrossSigningVerified()) {
return false;
}
@@ -108,7 +104,7 @@ export default class MKeyVerificationConclusion extends React.Component
return null;
}
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const myUserId = client.getUserId();
let title: string | undefined;
diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx
index 60fc2963c96..c8a356d3239 100644
--- a/src/components/views/messages/MKeyVerificationRequest.tsx
+++ b/src/components/views/messages/MKeyVerificationRequest.tsx
@@ -18,9 +18,10 @@ import React from "react";
import { MatrixEvent, User } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import {
- Phase as VerificationPhase,
+ canAcceptVerificationRequest,
+ VerificationPhase,
VerificationRequestEvent,
-} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+} from "matrix-js-sdk/src/crypto-api";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
@@ -54,7 +55,7 @@ export default class MKeyVerificationRequest extends React.Component {
let member: User | undefined;
const { verificationRequest } = this.props.mxEvent;
if (verificationRequest) {
- member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId) ?? undefined;
+ member = MatrixClientPeg.safeGet().getUser(verificationRequest.otherUserId) ?? undefined;
}
RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.RoomSummary },
@@ -74,7 +75,7 @@ export default class MKeyVerificationRequest extends React.Component {
this.openRequest();
await request.accept();
} catch (err) {
- logger.error(err.message);
+ logger.error(err);
}
}
};
@@ -85,13 +86,13 @@ export default class MKeyVerificationRequest extends React.Component {
try {
await request.cancel();
} catch (err) {
- logger.error(err.message);
+ logger.error(err);
}
}
};
private acceptedLabel(userId: string): string {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const myUserId = client.getUserId();
if (userId === myUserId) {
return _t("You accepted");
@@ -103,7 +104,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
private cancelledLabel(userId: string): string {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const myUserId = client.getUserId();
const cancellationCode = this.props.mxEvent.verificationRequest?.cancellationCode;
const declined = cancellationCode === "m.user";
@@ -127,7 +128,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
public render(): React.ReactNode {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const { mxEvent } = this.props;
const request = mxEvent.verificationRequest;
@@ -139,7 +140,7 @@ export default class MKeyVerificationRequest extends React.Component {
let subtitle: string;
let stateNode: JSX.Element | undefined;
- if (!request.canAccept) {
+ if (!canAcceptVerificationRequest(request)) {
let stateLabel;
const accepted =
request.phase === VerificationPhase.Ready ||
@@ -165,7 +166,7 @@ export default class MKeyVerificationRequest extends React.Component {
const name = getNameForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!);
title = _t("%(name)s wants to verify", { name });
subtitle = userLabelForEventRoom(client, request.otherUserId, mxEvent.getRoomId()!);
- if (request.canAccept) {
+ if (canAcceptVerificationRequest(request)) {
stateNode = (
diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx
index ad3c11eb7c3..b1d5fdca46e 100644
--- a/src/components/views/messages/MPollBody.tsx
+++ b/src/components/views/messages/MPollBody.tsx
@@ -118,7 +118,7 @@ export function pollAlreadyHasVotes(mxEvent: MatrixEvent, getRelationsForEvent?:
}
export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: GetRelationsForEvent): void {
- const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
+ const room = MatrixClientPeg.safeGet().getRoom(mxEvent.getRoomId());
if (pollAlreadyHasVotes(mxEvent, getRelationsForEvent)) {
Modal.createDialog(ErrorDialog, {
title: _t("Can't edit poll"),
diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx
index 22f13178b3f..e792a47221b 100644
--- a/src/components/views/messages/MessageActionBar.tsx
+++ b/src/components/views/messages/MessageActionBar.tsx
@@ -28,7 +28,6 @@ import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/messag
import { Icon as ResendIcon } from "../../../../res/img/element-icons/retry.svg";
import { Icon as ThreadIcon } from "../../../../res/img/element-icons/message/thread.svg";
import { Icon as TrashcanIcon } from "../../../../res/img/element-icons/trashcan.svg";
-import { Icon as StarIcon } from "../../../../res/img/element-icons/room/message-bar/star.svg";
import { Icon as ReplyIcon } from "../../../../res/img/element-icons/room/message-bar/reply.svg";
import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg";
import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg";
@@ -45,7 +44,6 @@ import Resend from "../../../Resend";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import DownloadActionButton from "./DownloadActionButton";
-import SettingsStore from "../../../settings/SettingsStore";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import ReplyChain from "../elements/ReplyChain";
import ReactionPicker from "../emojipicker/ReactionPicker";
@@ -55,7 +53,6 @@ import { Key } from "../../../Keyboard";
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { Action } from "../../../dispatcher/actions";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
-import useFavouriteMessages from "../../../hooks/useFavouriteMessages";
import { GetRelationsForEvent } from "../rooms/EventTile";
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
import { ButtonEvent } from "../elements/AccessibleButton";
@@ -254,42 +251,6 @@ const ReplyInThreadButton: React.FC = ({ mxEvent }) => {
);
};
-interface IFavouriteButtonProp {
- mxEvent: MatrixEvent;
-}
-
-const FavouriteButton: React.FC = ({ mxEvent }) => {
- const { isFavourite, toggleFavourite } = useFavouriteMessages();
-
- const eventId = mxEvent.getId()!;
- const classes = classNames("mx_MessageActionBar_iconButton mx_MessageActionBar_favouriteButton", {
- mx_MessageActionBar_favouriteButton_fillstar: isFavourite(eventId),
- });
-
- const onClick = useCallback(
- (e: ButtonEvent) => {
- // Don't open the regular browser or our context menu on right-click
- e.preventDefault();
- e.stopPropagation();
-
- toggleFavourite(eventId);
- },
- [toggleFavourite, eventId],
- );
-
- return (
-
-
-
- );
-};
-
interface IMessageActionBarProps {
mxEvent: MatrixEvent;
reactions?: Relations | null | undefined;
@@ -311,7 +272,7 @@ export default class MessageActionBar extends React.PureComponent Resend.resend(MatrixClientPeg.get(), tarEv));
+ this.runActionOnFailedEv((tarEv) => Resend.resend(MatrixClientPeg.safeGet(), tarEv));
};
private onCancelClick = (ev: ButtonEvent): void => {
this.runActionOnFailedEv(
- (tarEv) => Resend.removeFromQueue(MatrixClientPeg.get(), tarEv),
+ (tarEv) => Resend.removeFromQueue(MatrixClientPeg.safeGet(), tarEv),
(testEv) => canCancel(testEv.status),
);
};
public render(): React.ReactNode {
const toolbarOpts: JSX.Element[] = [];
- if (canEditContent(MatrixClientPeg.get(), this.props.mxEvent)) {
+ if (canEditContent(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
toolbarOpts.push(
,
);
}
- if (SettingsStore.getValue("feature_favourite_messages")) {
- toolbarOpts.splice(-1, 0, );
- }
// XXX: Assuming that the underlying tile will be a media event if it is eligible media.
if (MediaEventHelper.isEligible(this.props.mxEvent)) {
diff --git a/src/components/views/messages/RoomAvatarEvent.tsx b/src/components/views/messages/RoomAvatarEvent.tsx
index 15e535210ae..14bebf563b1 100644
--- a/src/components/views/messages/RoomAvatarEvent.tsx
+++ b/src/components/views/messages/RoomAvatarEvent.tsx
@@ -33,7 +33,7 @@ interface IProps {
export default class RoomAvatarEvent extends React.Component {
private onAvatarClick = (): void => {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
const ev = this.props.mxEvent;
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
if (!httpUrl) return;
@@ -63,7 +63,7 @@ export default class RoomAvatarEvent extends React.Component {
);
}
- const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
+ const room = MatrixClientPeg.safeGet().getRoom(ev.getRoomId());
// Provide all arguments to RoomAvatar via oobData because the avatar is historic
const oobData = {
avatarUrl: ev.getContent().url,
diff --git a/src/components/views/messages/RoomPredecessorTile.tsx b/src/components/views/messages/RoomPredecessorTile.tsx
index bee0849fa41..67be715634c 100644
--- a/src/components/views/messages/RoomPredecessorTile.tsx
+++ b/src/components/views/messages/RoomPredecessorTile.tsx
@@ -87,7 +87,7 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) =>
return ;
}
- const prevRoom = MatrixClientPeg.get().getRoom(predecessor.roomId);
+ const prevRoom = MatrixClientPeg.safeGet().getRoom(predecessor.roomId);
// We need either the previous room, or some servers to find it with.
// Otherwise, we must bail out here
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index e488b5791ce..10afa84afeb 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -93,7 +93,7 @@ export default class TextualBody extends React.Component {
this.activateSpoilers([content]);
HtmlUtils.linkifyElement(content);
- pillifyLinks(MatrixClientPeg.get(), [content], this.props.mxEvent, this.pills);
+ pillifyLinks(MatrixClientPeg.safeGet(), [content], this.props.mxEvent, this.pills);
this.calculateUrlPreview();
diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx
index 93501bac671..fa82c3bf314 100644
--- a/src/components/views/messages/TextualEvent.tsx
+++ b/src/components/views/messages/TextualEvent.tsx
@@ -31,7 +31,7 @@ export default class TextualEvent extends React.Component {
public render(): React.ReactNode {
const text = TextForEvent.textForEvent(
this.props.mxEvent,
- MatrixClientPeg.get(),
+ MatrixClientPeg.safeGet(),
true,
this.context?.showHiddenEvents,
);
diff --git a/src/components/views/messages/ViewSourceEvent.tsx b/src/components/views/messages/ViewSourceEvent.tsx
index 1d33074a7a0..5bd85507961 100644
--- a/src/components/views/messages/ViewSourceEvent.tsx
+++ b/src/components/views/messages/ViewSourceEvent.tsx
@@ -42,7 +42,7 @@ export default class ViewSourceEvent extends React.PureComponent
public componentDidMount(): void {
const { mxEvent } = this.props;
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
client.decryptEventIfNeeded(mxEvent);
if (mxEvent.isBeingDecrypted()) {
diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx
index 6ba9eb9e2c7..ede0e96c99b 100644
--- a/src/components/views/right_panel/EncryptionPanel.tsx
+++ b/src/components/views/right_panel/EncryptionPanel.tsx
@@ -15,19 +15,12 @@ limitations under the License.
*/
import React, { useCallback, useEffect, useState } from "react";
-import {
- PHASE_REQUESTED,
- PHASE_UNSENT,
- Phase as VerificationPhase,
- VerificationRequest,
- VerificationRequestEvent,
-} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { VerificationPhase, VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { User } from "matrix-js-sdk/src/models/user";
import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel";
-import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { ensureDMExists } from "../../../createRoom";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import Modal from "../../../Modal";
@@ -35,6 +28,7 @@ import { _t } from "../../../languageHandler";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import ErrorDialog from "../dialogs/ErrorDialog";
+import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
@@ -49,6 +43,7 @@ interface IProps {
}
const EncryptionPanel: React.FC = (props: IProps) => {
+ const cli = useMatrixClientContext();
const { verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted } = props;
const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification",
@@ -77,7 +72,11 @@ const EncryptionPanel: React.FC = (props: IProps) => {
}, [verificationRequestPromise]);
const changeHandler = useCallback(() => {
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
- if (request && request.phase === VerificationPhase.Cancelled && MISMATCHES.includes(request.cancellationCode)) {
+ if (
+ request &&
+ request.phase === VerificationPhase.Cancelled &&
+ MISMATCHES.includes(request.cancellationCode ?? "")
+ ) {
Modal.createDialog(ErrorDialog, {
headerImage: require("../../../../res/img/e2e/warning-deprecated.svg").default,
title: _t("Your messages are not secure"),
@@ -106,7 +105,6 @@ const EncryptionPanel: React.FC = (props: IProps) => {
const onStartVerification = useCallback(async (): Promise => {
setRequesting(true);
- const cli = MatrixClientPeg.get();
let verificationRequest_: VerificationRequest;
try {
const roomId = await ensureDMExists(cli, member.userId);
@@ -135,14 +133,13 @@ const EncryptionPanel: React.FC = (props: IProps) => {
});
}
if (!RightPanelStore.instance.isOpen) RightPanelStore.instance.togglePanel(null);
- }, [member]);
+ }, [cli, member]);
const requested: boolean =
(!request && isRequesting) ||
- (!!request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
- const isSelfVerification = request
- ? request.isSelfVerification
- : member.userId === MatrixClientPeg.get().getUserId();
+ (!!request &&
+ (phase === VerificationPhase.Requested || phase === VerificationPhase.Unsent || phase === undefined));
+ const isSelfVerification = request ? request.isSelfVerification : member.userId === cli.getUserId();
if (!request || requested) {
const initiatedByMe = (!request && isRequesting) || (!!request && request.initiatedByMe);
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index caa1692b206..aa06ef3cc70 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -24,7 +24,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { User } from "matrix-js-sdk/src/models/user";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index d594f7c75cd..29af0672fbf 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -18,15 +18,15 @@ import React from "react";
import { verificationMethods } from "matrix-js-sdk/src/crypto";
import { SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
import {
- Phase,
+ VerificationPhase as Phase,
VerificationRequest,
VerificationRequestEvent,
-} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+} from "matrix-js-sdk/src/crypto-api";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { User } from "matrix-js-sdk/src/models/user";
import { logger } from "matrix-js-sdk/src/logger";
-import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { ShowQrCodeCallbacks, ShowSasCallbacks, VerifierEvent } from "matrix-js-sdk/src/crypto-api/verification";
+import { Device } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
@@ -52,11 +52,21 @@ interface IState {
emojiButtonClicked?: boolean;
reciprocateButtonClicked?: boolean;
reciprocateQREvent: ShowQrCodeCallbacks | null;
+
+ /**
+ * Details of the other device involved in the transaction.
+ *
+ * `undefined` if there is not (yet) another device in the transaction, or if we do not know about it.
+ */
+ otherDeviceDetails?: Device;
}
export default class VerificationPanel extends React.PureComponent {
private hasVerifier: boolean;
+ /** have we yet tried to check the other device's info */
+ private haveCheckedDevice = false;
+
public constructor(props: IProps) {
super(props);
this.state = { sasEvent: null, reciprocateQREvent: null };
@@ -201,14 +211,25 @@ export default class VerificationPanel extends React.PureComponent {
+ if (this.haveCheckedDevice) return;
+
+ const client = MatrixClientPeg.safeGet();
+ const deviceId = this.props.request?.otherDeviceId;
+ const userId = client.getUserId();
+ if (!deviceId || !userId) {
+ return;
}
+ this.haveCheckedDevice = true;
+
+ const deviceMap = await client.getCrypto()?.getUserDeviceInfo([userId]);
+ if (!deviceMap) return;
+ const userDevices = deviceMap.get(userId);
+ if (!userDevices) return;
+ this.setState({ otherDeviceDetails: userDevices.get(deviceId) });
}
private renderQRReciprocatePhase(): JSX.Element {
@@ -272,7 +293,7 @@ export default class VerificationPanel extends React.PureComponent => {
const { request } = this.props;
+
+ // if we have a device ID and did not have one before, fetch the device's details
+ this.maybeGetOtherDevice();
+
const hadVerifier = this.hasVerifier;
this.hasVerifier = !!request.verifier;
if (!hadVerifier && this.hasVerifier) {
diff --git a/src/components/views/room_settings/RoomProfileSettings.tsx b/src/components/views/room_settings/RoomProfileSettings.tsx
index 7117608f396..21768cd16ae 100644
--- a/src/components/views/room_settings/RoomProfileSettings.tsx
+++ b/src/components/views/room_settings/RoomProfileSettings.tsx
@@ -52,7 +52,7 @@ export default class RoomProfileSettings extends React.Component
public constructor(props: IProps) {
super(props);
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const room = client.getRoom(props.roomId);
if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`);
@@ -124,7 +124,7 @@ export default class RoomProfileSettings extends React.Component
if (!this.isSaveEnabled()) return;
this.setState({ profileFieldsTouched: {} });
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const newState: Partial = {};
// TODO: What do we do about errors?
diff --git a/src/components/views/room_settings/RoomPublishSetting.tsx b/src/components/views/room_settings/RoomPublishSetting.tsx
index 869ced94c2e..c79b960976f 100644
--- a/src/components/views/room_settings/RoomPublishSetting.tsx
+++ b/src/components/views/room_settings/RoomPublishSetting.tsx
@@ -45,7 +45,7 @@ export default class RoomPublishSetting extends React.PureComponent {
this.setState({ isRoomPublished: result.visibility === "public" });
});
}
public render(): React.ReactNode {
- const client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.safeGet();
const room = client.getRoom(this.props.roomId);
const isRoomPublishable = room && room.getJoinRule() !== JoinRule.Invite;
diff --git a/src/components/views/room_settings/UrlPreviewSettings.tsx b/src/components/views/room_settings/UrlPreviewSettings.tsx
index e859f3adc4c..35e0a370610 100644
--- a/src/components/views/room_settings/UrlPreviewSettings.tsx
+++ b/src/components/views/room_settings/UrlPreviewSettings.tsx
@@ -28,14 +28,14 @@ import { Action } from "../../../dispatcher/actions";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsFieldset from "../settings/SettingsFieldset";
-import AccessibleButton from "../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
interface IProps {
room: Room;
}
export default class UrlPreviewSettings extends React.Component {
- private onClickUserSettings = (e: React.MouseEvent): void => {
+ private onClickUserSettings = (e: ButtonEvent): void => {
e.preventDefault();
e.stopPropagation();
dis.fire(Action.ViewUserSettings);
@@ -43,7 +43,7 @@ export default class UrlPreviewSettings extends React.Component {
public render(): ReactNode {
const roomId = this.props.room.roomId;
- const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
+ const isEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);
let previewsForAccount: ReactNode | undefined;
let previewsForRoom: ReactNode | undefined;
diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx
index 378538af182..d68a606c9d5 100644
--- a/src/components/views/rooms/AppsDrawer.tsx
+++ b/src/components/views/rooms/AppsDrawer.tsx
@@ -283,9 +283,9 @@ export default class AppsDrawer extends React.Component {
room={this.props.room}
minHeight={100}
maxHeight={this.props.maxHeight - 50}
- handleClass="mx_AppsContainer_resizerHandle"
- handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
- className="mx_AppsContainer_resizer"
+ className="mx_AppsDrawer_resizer"
+ handleWrapperClass="mx_AppsDrawer_resizer_container"
+ handleClass="mx_AppsDrawer_resizer_container_handle"
resizeNotifier={this.props.resizeNotifier}
>
{appContainers}
@@ -358,9 +358,9 @@ const PersistentVResizer: React.FC = ({
resizeNotifier.stopResizing();
}}
+ className={className}
handleWrapperClass={handleWrapperClass}
handleClasses={{ bottom: handleClass }}
- className={className}
enable={{ bottom: true }}
>
{children}
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 4656ba950c5..68bbf75fbd6 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -70,7 +70,7 @@ export default class AuxPanel extends React.Component {
}
public componentDidMount(): void {
- const cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.safeGet();
if (SettingsStore.getValue("feature_state_counters")) {
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
}
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index ec9fcf5c329..10bf15c29da 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -52,6 +52,7 @@ import { _t } from "../../../languageHandler";
import { linkify } from "../../../linkify-matrix";
import { ICustomEmoji } from "../../../emojipicker/customemoji";
import { SdkContextClass } from "../../../contexts/SDKContext";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")\\s|:^$");
@@ -132,7 +133,7 @@ export default class BasicMessageEditor extends React.Component
private hasTextSelected = false;
private _isCaretAtEnd = false;
- private lastCaret: DocumentOffset;
+ private lastCaret!: DocumentOffset;
private lastSelection: ReturnType | null = null;
private readonly useMarkdownHandle: string;
@@ -269,7 +270,7 @@ export default class BasicMessageEditor extends React.Component