Merge branch 'develop' into t3chguy/safari-compat

This commit is contained in:
Michael Telatynski
2025-01-20 15:02:59 +00:00
committed by GitHub
70 changed files with 431 additions and 344 deletions

View File

@@ -1,5 +1,5 @@
module.exports = { module.exports = {
plugins: ["matrix-org"], plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"], extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: { parserOptions: {
project: ["./tsconfig.json"], project: ["./tsconfig.json"],
@@ -170,6 +170,8 @@ module.exports = {
"jsx-a11y/role-supports-aria-props": "off", "jsx-a11y/role-supports-aria-props": "off",
"matrix-org/require-copyright-header": "error", "matrix-org/require-copyright-header": "error",
"react-compiler/react-compiler": "error",
}, },
overrides: [ overrides: [
{ {
@@ -262,6 +264,7 @@ module.exports = {
// These are fine in tests // These are fine in tests
"no-restricted-globals": "off", "no-restricted-globals": "off",
"react-compiler/react-compiler": "off",
}, },
}, },
{ {

View File

@@ -17,6 +17,7 @@ class MockMap extends EventEmitter {
setCenter = jest.fn(); setCenter = jest.fn();
setStyle = jest.fn(); setStyle = jest.fn();
fitBounds = jest.fn(); fitBounds = jest.fn();
remove = jest.fn();
} }
const MockMapInstance = new MockMap(); const MockMapInstance = new MockMap();

View File

@@ -237,6 +237,7 @@
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "^2.0.2", "eslint-plugin-matrix-org": "^2.0.2",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-unicorn": "^56.0.0", "eslint-plugin-unicorn": "^56.0.0",
"express": "^4.18.2", "express": "^4.18.2",

View File

@@ -13,6 +13,14 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout"; import { Layout } from "../../../src/settings/enums/Layout";
import { ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
// Find and click "Reply" button
const clickButtonReply = async (tile: Locator) => {
await expect(async () => {
await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click();
}).toPass();
};
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.use({ test.use({
displayName: "Hanako", displayName: "Hanako",
@@ -222,8 +230,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Find and click "Reply" button on MessageActionBar // Find and click "Reply" button on MessageActionBar
const tile = page.locator(".mx_EventTile_last"); const tile = page.locator(".mx_EventTile_last");
await tile.hover(); await clickButtonReply(tile);
await tile.getByRole("button", { name: "Reply", exact: true }).click();
// Reply to the player with another audio file // Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/1sec.ogg"); await uploadFile(page, "playwright/sample-files/1sec.ogg");
@@ -251,18 +258,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const tile = page.locator(".mx_EventTile_last"); const tile = page.locator(".mx_EventTile_last");
// Find and click "Reply" button
const clickButtonReply = async () => {
await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click();
};
await uploadFile(page, "playwright/sample-files/upload-first.ogg"); await uploadFile(page, "playwright/sample-files/upload-first.ogg");
// Assert that the audio player is rendered // Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
await clickButtonReply(); await clickButtonReply(tile);
// Reply to the player with another audio file // Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/upload-second.ogg"); await uploadFile(page, "playwright/sample-files/upload-second.ogg");
@@ -270,7 +271,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Assert that the audio player is rendered // Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
await clickButtonReply(); await clickButtonReply(tile);
// Reply to the player with yet another audio file to create a reply chain // Reply to the player with yet another audio file to create a reply chain
await uploadFile(page, "playwright/sample-files/upload-third.ogg"); await uploadFile(page, "playwright/sample-files/upload-third.ogg");

View File

@@ -66,6 +66,9 @@ test.describe("Cryptography", function () {
// Bob has a second, not cross-signed, device // Bob has a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
// Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list
await page.getByRole("button", { name: "Not now" }).click();
await bob.sendEvent(testRoomId, null, "m.room.encrypted", { await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
algorithm: "m.megolm.v1.aes-sha2", algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "the bird is in the hand", ciphertext: "the bird is in the hand",

View File

@@ -413,3 +413,25 @@ export async function createSecondBotDevice(page: Page, homeserver: HomeserverIn
await bobSecondDevice.prepareClient(); await bobSecondDevice.prepareClient();
return bobSecondDevice; return bobSecondDevice;
} }
/**
* Remove the cached secrets from the indexedDB
* This is a workaround to simulate the case where the secrets are not cached.
*/
export async function deleteCachedSecrets(page: Page) {
await page.evaluate(async () => {
const removeCachedSecrets = new Promise((resolve) => {
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
request.onsuccess = (event: Event & { target: { result: IDBDatabase } }) => {
const db = event.target.result;
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
request.onsuccess = () => {
db.close();
resolve(undefined);
};
};
});
await removeCachedSecrets;
});
await page.reload();
}

View File

@@ -81,6 +81,7 @@ test.describe("Create Knock Room", () => {
const spotlightDialog = await app.openSpotlight(); const spotlightDialog = await app.openSpotlight();
await spotlightDialog.filter(Filter.PublicRooms); await spotlightDialog.filter(Filter.PublicRooms);
await spotlightDialog.search("Cyber");
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity"); await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
}); });
}); });

View File

@@ -284,6 +284,7 @@ test.describe("Knock Into Room", () => {
const spotlightDialog = await app.openSpotlight(); const spotlightDialog = await app.openSpotlight();
await spotlightDialog.filter(Filter.PublicRooms); await spotlightDialog.filter(Filter.PublicRooms);
await spotlightDialog.search("Cyber");
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity"); await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
await spotlightDialog.results.nth(0).click(); await spotlightDialog.results.nth(0).click();

View File

@@ -58,6 +58,16 @@ async function editMessage(page: Page, message: Locator, newMsg: string): Promis
await editComposer.press("Enter"); await editComposer.press("Enter");
} }
const screenshotOptions = (page?: Page) => ({
mask: page ? [page.locator(".mx_MessageTimestamp")] : undefined,
// Hide the jump to bottom button in the timeline to avoid flakiness
css: `
.mx_JumpToBottomButton {
display: none !important;
}
`,
});
test.describe("Message rendering", () => { test.describe("Message rendering", () => {
[ [
{ direction: "ltr", displayName: "Quentin" }, { direction: "ltr", displayName: "Quentin" },
@@ -79,9 +89,10 @@ test.describe("Message rendering", () => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "Hello, world!"); const msgTile = await sendMessage(page, "Hello, world!");
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], `basic-message-ltr-${direction}displayname.png`,
}); screenshotOptions(page),
);
}, },
); );
@@ -89,14 +100,17 @@ test.describe("Message rendering", () => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "/me lays an egg"); const msgTile = await sendMessage(page, "/me lays an egg");
await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`); await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`, screenshotOptions());
}); });
test("should render an LTR rich text emote", async ({ page, user, app, room }) => { test("should render an LTR rich text emote", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "/me lays a *free range* egg"); const msgTile = await sendMessage(page, "/me lays a *free range* egg");
await expect(msgTile).toMatchScreenshot(`emote-rich-ltr-${direction}displayname.png`); await expect(msgTile).toMatchScreenshot(
`emote-rich-ltr-${direction}displayname.png`,
screenshotOptions(),
);
}); });
test("should render an edited LTR message", async ({ page, user, app, room }) => { test("should render an edited LTR message", async ({ page, user, app, room }) => {
@@ -106,9 +120,10 @@ test.describe("Message rendering", () => {
await editMessage(page, msgTile, "Hello, universe!"); await editMessage(page, msgTile, "Hello, universe!");
await expect(msgTile).toMatchScreenshot(`edited-message-ltr-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], `edited-message-ltr-${direction}displayname.png`,
}); screenshotOptions(page),
);
}); });
test("should render a reply of a LTR message", async ({ page, user, app, room }) => { test("should render a reply of a LTR message", async ({ page, user, app, room }) => {
@@ -122,32 +137,37 @@ test.describe("Message rendering", () => {
]); ]);
await replyMessage(page, msgTile, "response to multiline message"); await replyMessage(page, msgTile, "response to multiline message");
await expect(msgTile).toMatchScreenshot(`reply-message-ltr-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], `reply-message-ltr-${direction}displayname.png`,
}); screenshotOptions(page),
);
}); });
test("should render a basic RTL text message", async ({ page, user, app, room }) => { test("should render a basic RTL text message", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "مرحبا بالعالم!"); const msgTile = await sendMessage(page, "مرحبا بالعالم!");
await expect(msgTile).toMatchScreenshot(`basic-message-rtl-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], `basic-message-rtl-${direction}displayname.png`,
}); screenshotOptions(page),
);
}); });
test("should render an RTL emote", async ({ page, user, app, room }) => { test("should render an RTL emote", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "/me يضع بيضة"); const msgTile = await sendMessage(page, "/me يضع بيضة");
await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`); await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`, screenshotOptions());
}); });
test("should render a richtext RTL emote", async ({ page, user, app, room }) => { test("should render a richtext RTL emote", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "/me أضع بيضة *حرة النطاق*"); const msgTile = await sendMessage(page, "/me أضع بيضة *حرة النطاق*");
await expect(msgTile).toMatchScreenshot(`emote-rich-rtl-${direction}displayname.png`); await expect(msgTile).toMatchScreenshot(
`emote-rich-rtl-${direction}displayname.png`,
screenshotOptions(),
);
}); });
test("should render an edited RTL message", async ({ page, user, app, room }) => { test("should render an edited RTL message", async ({ page, user, app, room }) => {
@@ -157,9 +177,10 @@ test.describe("Message rendering", () => {
await editMessage(page, msgTile, "مرحبا بالكون!"); await editMessage(page, msgTile, "مرحبا بالكون!");
await expect(msgTile).toMatchScreenshot(`edited-message-rtl-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], `edited-message-rtl-${direction}displayname.png`,
}); screenshotOptions(page),
);
}); });
test("should render a reply of a RTL message", async ({ page, user, app, room }) => { test("should render a reply of a RTL message", async ({ page, user, app, room }) => {
@@ -173,9 +194,10 @@ test.describe("Message rendering", () => {
]); ]);
await replyMessage(page, msgTile, "مرحبا بالعالم!"); await replyMessage(page, msgTile, "مرحبا بالعالم!");
await expect(msgTile).toMatchScreenshot(`reply-message-trl-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], `reply-message-trl-${direction}displayname.png`,
}); screenshotOptions(page),
);
}); });
}); });
}); });

View File

@@ -35,10 +35,10 @@ test.describe("Pinned messages", () => {
mask: [tile.locator(".mx_MessageTimestamp")], mask: [tile.locator(".mx_MessageTimestamp")],
// Hide the jump to bottom button in the timeline to avoid flakiness // Hide the jump to bottom button in the timeline to avoid flakiness
css: ` css: `
.mx_JumpToBottomButton { .mx_JumpToBottomButton {
display: none !important; display: none !important;
} }
`, `,
}); });
}, },
); );

View File

@@ -111,6 +111,10 @@ test.describe("Room Header", () => {
async ({ page, app, user }) => { async ({ page, app, user }) => {
await createVideoRoom(page, app); await createVideoRoom(page, app);
// Dismiss a toast that is otherwise in the way (it's the other
// side but there's no need to have it in the screenshot)
await page.getByRole("button", { name: "Later" }).click();
const header = page.locator(".mx_RoomHeader"); const header = page.locator(".mx_RoomHeader");
// There's two room info button - the header itself and the i button // There's two room info button - the header itself and the i button

View File

@@ -89,8 +89,7 @@ class Helpers {
await expect(dialog.getByText(title, { exact: true })).toBeVisible(); await expect(dialog.getByText(title, { exact: true })).toBeVisible();
await expect(dialog).toMatchScreenshot(screenshot); await expect(dialog).toMatchScreenshot(screenshot);
const handle = await this.page.evaluateHandle(() => navigator.clipboard.readText()); const clipboardContent = await this.app.getClipboard();
const clipboardContent = await handle.jsonValue();
await dialog.getByRole("textbox").fill(clipboardContent); await dialog.getByRole("textbox").fill(clipboardContent);
await dialog.getByRole("button", { name: confirmButtonLabel }).click(); await dialog.getByRole("button", { name: confirmButtonLabel }).click();
await expect(dialog).toMatchScreenshot("default-recovery.png"); await expect(dialog).toMatchScreenshot("default-recovery.png");

View File

@@ -6,13 +6,13 @@
*/ */
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { Page } from "@playwright/test";
import { test, expect } from "."; import { test, expect } from ".";
import { import {
checkDeviceIsConnectedKeyBackup, checkDeviceIsConnectedKeyBackup,
checkDeviceIsCrossSigned, checkDeviceIsCrossSigned,
createBot, createBot,
deleteCachedSecrets,
verifySession, verifySession,
} from "../../crypto/utils"; } from "../../crypto/utils";
@@ -53,7 +53,7 @@ test.describe("Recovery section in Encryption tab", () => {
test( test(
"should change the recovery key", "should change the recovery key",
{ tag: "@screenshot" }, { tag: ["@screenshot", "@no-webkit"] },
async ({ page, app, homeserver, credentials, util, context }) => { async ({ page, app, homeserver, credentials, util, context }) => {
await verifySession(app, "new passphrase"); await verifySession(app, "new passphrase");
const dialog = await util.openEncryptionTab(); const dialog = await util.openEncryptionTab();
@@ -81,7 +81,7 @@ test.describe("Recovery section in Encryption tab", () => {
}, },
); );
test("should setup the recovery key", { tag: "@screenshot" }, async ({ page, app, util }) => { test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => {
await verifySession(app, "new passphrase"); await verifySession(app, "new passphrase");
await util.removeSecretStorageDefaultKeyId(); await util.removeSecretStorageDefaultKeyId();
@@ -154,25 +154,3 @@ test.describe("Recovery section in Encryption tab", () => {
}, },
); );
}); });
/**
* Remove the cached secrets from the indexedDB
* This is a workaround to simulate the case where the secrets are not cached.
*/
async function deleteCachedSecrets(page: Page) {
await page.evaluate(async () => {
const removeCachedSecrets = new Promise((resolve) => {
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
request.onsuccess = async (event: Event & { target: { result: IDBDatabase } }) => {
const db = event.target.result;
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
request.onsuccess = () => {
db.close();
resolve(undefined);
};
};
});
await removeCachedSecrets;
});
await page.reload();
}

View File

@@ -84,7 +84,7 @@ test.describe("Spaces", () => {
// Copy matrix.to link // Copy matrix.to link
await page.getByRole("button", { name: "Share invite link" }).click(); await page.getByRole("button", { name: "Share invite link" }).click();
expect(await app.getClipboardText()).toEqual(`https://matrix.to/#/#lets-have-a-riot:${user.homeServer}`); expect(await app.getClipboard()).toEqual(`https://matrix.to/#/#lets-have-a-riot:${user.homeServer}`);
// Go to space home // Go to space home
await page.getByRole("button", { name: "Go to my first room" }).click(); await page.getByRole("button", { name: "Go to my first room" }).click();
@@ -177,7 +177,7 @@ test.describe("Spaces", () => {
const shareDialog = page.locator(".mx_SpacePublicShare"); const shareDialog = page.locator(".mx_SpacePublicShare");
// Copy link first // Copy link first
await shareDialog.getByRole("button", { name: "Share invite link" }).click(); await shareDialog.getByRole("button", { name: "Share invite link" }).click();
expect(await app.getClipboardText()).toEqual(`https://matrix.to/#/#space:${user.homeServer}`); expect(await app.getClipboard()).toEqual(`https://matrix.to/#/#space:${user.homeServer}`);
// Start Matrix invite flow // Start Matrix invite flow
await shareDialog.getByRole("button", { name: "Invite people" }).click(); await shareDialog.getByRole("button", { name: "Invite people" }).click();

View File

@@ -38,11 +38,13 @@ export const test = base.extend<{
room1Name: "Room 1", room1Name: "Room 1",
room1: async ({ room1Name: name, app, user, bot }, use) => { room1: async ({ room1Name: name, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] }); const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
await bot.awaitRoomMembership(roomId);
await use({ name, roomId }); await use({ name, roomId });
}, },
room2Name: "Room 2", room2Name: "Room 2",
room2: async ({ room2Name: name, app, user, bot }, use) => { room2: async ({ room2Name: name, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] }); const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
await bot.awaitRoomMembership(roomId);
await use({ name, roomId }); await use({ name, roomId });
}, },
msg: async ({ page, app, util }, use) => { msg: async ({ page, app, util }, use) => {

View File

@@ -1195,6 +1195,7 @@ test.describe("Timeline", () => {
}); });
await sendImage(app.client, room.roomId, NEW_AVATAR); await sendImage(app.client, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
await expect(page.locator(".mx_MImageBody").first()).toBeVisible(); await expect(page.locator(".mx_MImageBody").first()).toBeVisible();
// Exclude timestamp and read marker from snapshot // Exclude timestamp and read marker from snapshot

View File

@@ -0,0 +1,31 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("PSTN", () => {
test.beforeEach(async ({ page }) => {
// Mock the third party protocols endpoint to look like the HS has PSTN support
await page.route("**/_matrix/client/v3/thirdparty/protocols", async (route) => {
await route.fulfill({
status: 200,
json: {
"im.vector.protocol.pstn": {},
},
});
});
});
test("should render dialpad as expected", { tag: "@screenshot" }, async ({ page, user, toasts }) => {
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
await expect(page.locator(".mx_LeftPanel_filterContainer")).toMatchScreenshot("dialpad-trigger.png");
await page.getByLabel("Open dial pad").click();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("dialpad.png");
});
});

View File

@@ -24,18 +24,40 @@ type PaginationLinks = {
first?: string; first?: string;
}; };
// We see quite a few test flakes which are caused by the app exploding
// so we have some magic strings we check the logs for to better track the flake with its cause
const SPECIAL_CASES = {
"ChunkLoadError": "ChunkLoadError",
"Unreachable code should not be executed": "Rust crypto panic",
"Out of bounds memory access": "Rust crypto memory error",
};
class FlakyReporter implements Reporter { class FlakyReporter implements Reporter {
private flakes = new Map<string, TestCase[]>(); private flakes = new Map<string, TestCase[]>();
public onTestEnd(test: TestCase): void { public onTestEnd(test: TestCase): void {
// Ignores flakes on Dendrite and Pinecone as they have their own flakes we do not track // Ignores flakes on Dendrite and Pinecone as they have their own flakes we do not track
if (["Dendrite", "Pinecone"].includes(test.parent.project()?.name)) return; if (["Dendrite", "Pinecone"].includes(test.parent.project()?.name)) return;
const title = `${test.location.file.split("playwright/e2e/")[1]}: ${test.title}`; let failures = [`${test.location.file.split("playwright/e2e/")[1]}: ${test.title}`];
if (test.outcome() === "flaky") { if (test.outcome() === "flaky") {
if (!this.flakes.has(title)) { const timedOutRuns = test.results.filter((result) => result.status === "timedOut");
this.flakes.set(title, []); const pageLogs = timedOutRuns.flatMap((result) =>
result.attachments.filter((attachment) => attachment.name.startsWith("page-")),
);
// If a test failed due to a systemic fault then the test is not flaky, the app is, record it as such.
const specialCases = Object.keys(SPECIAL_CASES).filter((log) =>
pageLogs.some((attachment) => attachment.name.startsWith("page-") && attachment.body.includes(log)),
);
if (specialCases.length > 0) {
failures = specialCases.map((specialCase) => SPECIAL_CASES[specialCase]);
}
for (const title of failures) {
if (!this.flakes.has(title)) {
this.flakes.set(title, []);
}
this.flakes.get(title).push(test);
} }
this.flakes.get(title).push(test);
} }
} }

View File

@@ -158,10 +158,6 @@ export class ElementAppPage {
return button.click(); return button.click();
} }
public async getClipboardText(): Promise<string> {
return this.page.evaluate("navigator.clipboard.readText()");
}
public async openSpotlight(): Promise<Spotlight> { public async openSpotlight(): Promise<Spotlight> {
const spotlight = new Spotlight(this.page); const spotlight = new Spotlight(this.page);
await spotlight.open(); await spotlight.open();

View File

@@ -15,7 +15,6 @@ import type {
ICreateRoomOpts, ICreateRoomOpts,
ISendEventResponse, ISendEventResponse,
MatrixClient, MatrixClient,
Room,
MatrixEvent, MatrixEvent,
ReceiptType, ReceiptType,
IRoomDirectoryOptions, IRoomDirectoryOptions,
@@ -178,21 +177,12 @@ export class Client {
*/ */
public async createRoom(options: ICreateRoomOpts): Promise<string> { public async createRoom(options: ICreateRoomOpts): Promise<string> {
const client = await this.prepareClient(); const client = await this.prepareClient();
return await client.evaluate(async (cli, options) => { const roomId = await client.evaluate(async (cli, options) => {
const { room_id: roomId } = await cli.createRoom(options); const { room_id: roomId } = await cli.createRoom(options);
if (!cli.getRoom(roomId)) {
await new Promise<void>((resolve) => {
const onRoom = (room: Room) => {
if (room.roomId === roomId) {
cli.off(window.matrixcs.ClientEvent.Room, onRoom);
resolve();
}
};
cli.on(window.matrixcs.ClientEvent.Room, onRoom);
});
}
return roomId; return roomId;
}, options); }, options);
await this.awaitRoomMembership(roomId);
return roomId;
} }
/** /**

View File

@@ -155,9 +155,13 @@ export const test = base.extend<TestFixtures, Services & Options>({
{ scope: "worker" }, { scope: "worker" },
], ],
context: async ({ homeserverType, synapseConfig, logger, context, request, homeserver }, use, testInfo) => { context: async (
{ homeserverType, synapseConfig, logger, context, request, _homeserver, homeserver },
use,
testInfo,
) => {
testInfo.skip( testInfo.skip(
!(homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0, !(_homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0,
`Test specifies Synapse config options so is unsupported with ${homeserverType}`, `Test specifies Synapse config options so is unsupported with ${homeserverType}`,
); );
homeserver.setRequest(request); homeserver.setRequest(request);

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -19,7 +19,7 @@ import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverCon
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts"; import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts";
const TAG = "develop@sha256:436278578c6b396d3a581f6af020edaff37dd7c3d26d20362de9e05e4a70cee8"; const TAG = "develop@sha256:e48308d68dec00af6ce43a05785d475de21a37bc2afaabb440d3a575bcc3d57d";
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
server_name: "localhost", server_name: "localhost",

View File

@@ -34,11 +34,9 @@ import {
hideToast as hideUnverifiedSessionsToast, hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast, showToast as showUnverifiedSessionsToast,
} from "./toasts/UnverifiedSessionToast"; } from "./toasts/UnverifiedSessionToast";
import { accessSecretStorage, isSecretStorageBeingAccessed } from "./SecurityManager"; import { isSecretStorageBeingAccessed } from "./SecurityManager";
import { isSecureBackupRequired } from "./utils/WellKnownUtils";
import { ActionPayload } from "./dispatcher/payloads"; import { ActionPayload } from "./dispatcher/payloads";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import { isLoggedIn } from "./utils/login";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import PlatformPeg from "./PlatformPeg"; import PlatformPeg from "./PlatformPeg";
import { recordClientInformation, removeClientInformation } from "./utils/device/clientInformation"; import { recordClientInformation, removeClientInformation } from "./utils/device/clientInformation";
@@ -283,7 +281,21 @@ export default class DeviceListener {
const crossSigningReady = await crypto.isCrossSigningReady(); const crossSigningReady = await crypto.isCrossSigningReady();
const secretStorageReady = await crypto.isSecretStorageReady(); const secretStorageReady = await crypto.isSecretStorageReady();
const allSystemsReady = crossSigningReady && secretStorageReady; const crossSigningStatus = await crypto.getCrossSigningStatus();
const allCrossSigningSecretsCached =
crossSigningStatus.privateKeysCachedLocally.masterKey &&
crossSigningStatus.privateKeysCachedLocally.selfSigningKey &&
crossSigningStatus.privateKeysCachedLocally.userSigningKey;
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();
const isCurrentDeviceTrusted =
crossSigningReady &&
Boolean(
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
);
const allSystemsReady = crossSigningReady && secretStorageReady && allCrossSigningSecretsCached;
await this.reportCryptoSessionStateToAnalytics(cli); await this.reportCryptoSessionStateToAnalytics(cli);
if (this.dismissedThisDeviceToast || allSystemsReady) { if (this.dismissedThisDeviceToast || allSystemsReady) {
@@ -294,31 +306,31 @@ export default class DeviceListener {
// make sure our keys are finished downloading // make sure our keys are finished downloading
await crypto.getUserDeviceInfo([cli.getSafeUserId()]); await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
// cross signing isn't enabled - nag to enable it if (!crossSigningReady) {
// There are 3 different toasts for: // This account is legacy and doesn't have cross-signing set up at all.
if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) { // Prompt the user to set it up.
// Toast 1. Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
} else if (!isCurrentDeviceTrusted) {
// cross signing is ready but the current device is not trusted: prompt the user to verify
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
this.checkKeyBackupStatus(); } else if (!allCrossSigningSecretsCached) {
// cross signing ready & device trusted, but we are missing secrets from our local cache.
// prompt the user to enter their recovery key.
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
} else if (defaultKeyId === null) {
// the user just hasn't set up 4S yet: prompt them to do so
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
} else { } else {
const backupInfo = await this.getKeyBackupInfo(); // some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did
if (backupInfo) { // in 'other' situations. Possibly we should consider prompting for a full reset in this case?
// Toast 2: Key backup is enabled but recovery (4S) is not set up: prompt user to set up recovery. logger.warn("Couldn't match encryption state to a known case: showing 'setup encryption' prompt", {
// Since we now enable key backup at registration time, this will be the common case for crossSigningReady,
// new users. secretStorageReady,
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); allCrossSigningSecretsCached,
} else { isCurrentDeviceTrusted,
// Toast 3: No cross-signing or key backup on account (set up encryption) defaultKeyId,
await cli.waitForClientWellKnown(); });
if (isSecureBackupRequired(cli) && isLoggedIn()) { showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
// If we're meant to set up, and Secure Backup is required,
// trigger the flow directly without a toast once logged in.
hideSetupEncryptionToast();
accessSecretStorage();
} else {
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
}
}
} }
} }
@@ -334,12 +346,6 @@ export default class DeviceListener {
// Unverified devices that have appeared since then // Unverified devices that have appeared since then
const newUnverifiedDeviceIds = new Set<string>(); const newUnverifiedDeviceIds = new Set<string>();
const isCurrentDeviceTrusted =
crossSigningReady &&
Boolean(
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
);
// as long as cross-signing isn't ready, // as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts // you can't see or dismiss any device toasts
if (crossSigningReady) { if (crossSigningReady) {

View File

@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { MatrixError, RuleId, TweakName, SyncState } from "matrix-js-sdk/src/matrix"; import { MatrixError, RuleId, TweakName, SyncState, TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import { import {
CallError, CallError,
CallErrorCode, CallErrorCode,
@@ -22,7 +22,6 @@ import {
MatrixCall, MatrixCall,
} from "matrix-js-sdk/src/webrtc/call"; } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import EventEmitter from "events";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
@@ -137,14 +136,23 @@ export enum LegacyCallHandlerEvent {
CallChangeRoom = "call_change_room", CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed", SilencedCallsChanged = "silenced_calls_changed",
CallState = "call_state", CallState = "call_state",
ProtocolSupport = "protocol_support",
} }
type EventEmitterMap = {
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
};
/** /**
* LegacyCallHandler manages all currently active calls. It should be used for * LegacyCallHandler manages all currently active calls. It should be used for
* placing, answering, rejecting and hanging up calls. It also handles ringing, * placing, answering, rejecting and hanging up calls. It also handles ringing,
* PSTN support and other things. * PSTN support and other things.
*/ */
export default class LegacyCallHandler extends EventEmitter { export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandlerEvent, EventEmitterMap> {
private calls = new Map<string, MatrixCall>(); // roomId -> call private calls = new Map<string, MatrixCall>(); // roomId -> call
// Calls started as an attended transfer, ie. with the intention of transferring another // Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one. // call with a different party to this one.
@@ -271,15 +279,13 @@ export default class LegacyCallHandler extends EventEmitter {
this.supportsPstnProtocol = null; this.supportsPstnProtocol = null;
} }
dis.dispatch({ action: Action.PstnSupportUpdated });
if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) { if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
this.supportsSipNativeVirtual = Boolean( this.supportsSipNativeVirtual = Boolean(
protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL], protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL],
); );
} }
dis.dispatch({ action: Action.VirtualRoomSupportUpdated }); this.emit(LegacyCallHandlerEvent.ProtocolSupport);
} catch (e) { } catch (e) {
if (maxTries === 1) { if (maxTries === 1) {
logger.log("Failed to check for protocol support and no retries remain: assuming no support", e); logger.log("Failed to check for protocol support and no retries remain: assuming no support", e);
@@ -296,8 +302,8 @@ export default class LegacyCallHandler extends EventEmitter {
return !!SdkConfig.getObject("voip")?.get("obey_asserted_identity"); return !!SdkConfig.getObject("voip")?.get("obey_asserted_identity");
} }
public getSupportsPstnProtocol(): boolean | null { public getSupportsPstnProtocol(): boolean {
return this.supportsPstnProtocol; return this.supportsPstnProtocol ?? false;
} }
public getSupportsVirtualRooms(): boolean | null { public getSupportsVirtualRooms(): boolean | null {
@@ -568,6 +574,7 @@ export default class LegacyCallHandler extends EventEmitter {
if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return;
this.setCallState(call, newState); this.setCallState(call, newState);
// XXX: this is used by the IPC into Electron to keep device awake
dis.dispatch({ dis.dispatch({
action: "call_state", action: "call_state",
room_id: mappedRoomId, room_id: mappedRoomId,

View File

@@ -392,6 +392,7 @@ export const useRovingTabIndex = <T extends HTMLElement>(
}); });
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-compiler/react-compiler
const isActive = context.state.activeNode === nodeRef.current; const isActive = context.state.activeNode === nodeRef.current;
return [onFocus, isActive, ref, nodeRef]; return [onFocus, isActive, ref, nodeRef];
}; };

View File

@@ -142,6 +142,7 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
{isFocused && suggestions.length ? ( {isFocused && suggestions.length ? (
<div <div
className="mx_AutocompleteInput_matches" className="mx_AutocompleteInput_matches"
// eslint-disable-next-line react-compiler/react-compiler
style={{ top: editorContainerRef.current?.clientHeight }} style={{ top: editorContainerRef.current?.clientHeight }}
data-testid="autocomplete-matches" data-testid="autocomplete-matches"
> >

View File

@@ -607,6 +607,7 @@ export const useContextMenu = <T extends any = HTMLElement>(inputRef?: RefObject
setIsOpen(false); setIsOpen(false);
}; };
// eslint-disable-next-line react-compiler/react-compiler
return [button.current ? isOpen : false, button, open, close, setIsOpen]; return [button.current ? isOpen : false, button, open, close, setIsOpen];
}; };

View File

@@ -286,9 +286,7 @@ class FilePanel extends React.Component<IProps, IState> {
ref={this.card} ref={this.card}
header={_t("right_panel|files_button")} header={_t("right_panel|files_button")}
> >
{this.card.current && ( <Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />
)}
<SearchWarning isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} /> <SearchWarning isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
<TimelinePanel <TimelinePanel
manageReadReceipts={false} manageReadReceipts={false}

View File

@@ -13,7 +13,7 @@ import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList from "../views/rooms/RoomList"; import RoomList from "../views/rooms/RoomList";
import LegacyCallHandler from "../../LegacyCallHandler"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist"; import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import RoomSearch from "./RoomSearch"; import RoomSearch from "./RoomSearch";
@@ -51,6 +51,7 @@ enum BreadcrumbsMode {
interface IState { interface IState {
showBreadcrumbs: BreadcrumbsMode; showBreadcrumbs: BreadcrumbsMode;
activeSpace: SpaceKey; activeSpace: SpaceKey;
supportsPstnProtocol: boolean;
} }
export default class LeftPanel extends React.Component<IProps, IState> { export default class LeftPanel extends React.Component<IProps, IState> {
@@ -65,6 +66,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.state = { this.state = {
activeSpace: SpaceStore.instance.activeSpace, activeSpace: SpaceStore.instance.activeSpace,
showBreadcrumbs: LeftPanel.breadcrumbsMode, showBreadcrumbs: LeftPanel.breadcrumbsMode,
supportsPstnProtocol: LegacyCallHandler.instance.getSupportsPstnProtocol(),
}; };
} }
@@ -76,6 +78,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
if (this.listContainerRef.current) { if (this.listContainerRef.current) {
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
@@ -90,6 +93,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.stopTrackingElementDimensions("ListContainer");
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
this.listContainerRef.current?.removeEventListener("scroll", this.onScroll); this.listContainerRef.current?.removeEventListener("scroll", this.onScroll);
@@ -101,6 +105,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
} }
} }
private updateProtocolSupport = (): void => {
this.setState({ supportsPstnProtocol: LegacyCallHandler.instance.getSupportsPstnProtocol() });
};
private updateActiveSpace = (activeSpace: SpaceKey): void => { private updateActiveSpace = (activeSpace: SpaceKey): void => {
this.setState({ activeSpace }); this.setState({ activeSpace });
}; };
@@ -330,9 +338,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private renderSearchDialExplore(): React.ReactNode { private renderSearchDialExplore(): React.ReactNode {
let dialPadButton: JSX.Element | undefined; let dialPadButton: JSX.Element | undefined;
// If we have dialer support, show a button to bring up the dial pad // If we have dialer support, show a button to bring up the dial pad to start a new call
// to start a new call if (this.state.supportsPstnProtocol) {
if (LegacyCallHandler.instance.getSupportsPstnProtocol()) {
dialPadButton = ( dialPadButton = (
<AccessibleButton <AccessibleButton
className={classNames("mx_LeftPanel_dialPadButton", {})} className={classNames("mx_LeftPanel_dialPadButton", {})}

View File

@@ -95,7 +95,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
onClose={this.props.onClose} onClose={this.props.onClose}
withoutScrollContainer={true} withoutScrollContainer={true}
> >
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} <Measured sensor={this.card} onMeasurement={this.onMeasurement} />
{content} {content}
</BaseCard> </BaseCard>
</ScopedRoomContextProvider> </ScopedRoomContextProvider>

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react"; import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { import {
ISearchResults, ISearchResults,
IThreadBundledRelationship, IThreadBundledRelationship,
@@ -58,7 +58,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
const [results, setResults] = useState<ISearchResults | null>(null); const [results, setResults] = useState<ISearchResults | null>(null);
const aborted = useRef(false); const aborted = useRef(false);
// A map from room ID to permalink creator // A map from room ID to permalink creator
const permalinkCreators = useRef(new Map<string, RoomPermalinkCreator>()).current; const permalinkCreators = useMemo(() => new Map<string, RoomPermalinkCreator>(), []);
const innerRef = useRef<ScrollPanel | null>(); const innerRef = useRef<ScrollPanel | null>();
useEffect(() => { useEffect(() => {

View File

@@ -273,6 +273,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
} }
const onRetryClicked = (): void => { const onRetryClicked = (): void => {
// eslint-disable-next-line react-compiler/react-compiler
room.state = LocalRoomState.NEW; room.state = LocalRoomState.NEW;
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "local_room_event", action: "local_room_event",
@@ -1082,7 +1083,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
}; };
private onCallState = (roomId: string): void => { private onCallState = (roomId: string | null): void => {
// don't filter out payloads for room IDs other than props.room because // don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room // we may be interested in the conf 1:1 room
@@ -2514,9 +2515,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mainSplitContentClassName = "mx_MainSplit_timeline"; mainSplitContentClassName = "mx_MainSplit_timeline";
mainSplitBody = ( mainSplitBody = (
<> <>
{this.roomViewBody.current && ( <Measured sensor={this.roomViewBody} onMeasurement={this.onMeasurement} />
<Measured sensor={this.roomViewBody.current} onMeasurement={this.onMeasurement} />
)}
{auxPanel} {auxPanel}
{pinnedMessageBanner} {pinnedMessageBanner}
<main className={timelineClasses}> <main className={timelineClasses}>

View File

@@ -204,7 +204,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
ref={card} ref={card}
closeButtonRef={closeButonRef} closeButtonRef={closeButonRef}
> >
{card.current && <Measured sensor={card.current} onMeasurement={setNarrow} />} <Measured sensor={card} onMeasurement={setNarrow} />
{timelineSet ? ( {timelineSet ? (
<TimelinePanel <TimelinePanel
key={filterOption + ":" + (timelineSet.getFilter()?.filterId ?? roomId)} key={filterOption + ":" + (timelineSet.getFilter()?.filterId ?? roomId)}

View File

@@ -443,7 +443,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev); PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev);
}} }}
> >
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} <Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<div className="mx_ThreadView_timelinePanelWrapper">{timeline}</div> <div className="mx_ThreadView_timelinePanelWrapper">{timeline}</div>
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && ( {ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import classNames from "classnames"; import classNames from "classnames";
import React, { useEffect, useRef } from "react"; import React, { useMemo } from "react";
type FlexProps = { type FlexProps = {
/** /**
@@ -40,25 +40,6 @@ type FlexProps = {
grow?: string | null; grow?: string | null;
}; };
/**
* Set or remove a CSS property
* @param ref the reference
* @param name the CSS property name
* @param value the CSS property value
*/
function addOrRemoveProperty(
ref: React.MutableRefObject<HTMLElement | undefined>,
name: string,
value?: string | null,
): void {
const style = ref.current!.style;
if (value) {
style.setProperty(name, value);
} else {
style.removeProperty(name);
}
}
/** /**
* A flex child helper * A flex child helper
*/ */
@@ -71,12 +52,12 @@ export function Box({
children, children,
...props ...props
}: React.PropsWithChildren<FlexProps>): JSX.Element { }: React.PropsWithChildren<FlexProps>): JSX.Element {
const ref = useRef<HTMLElement>(); const style = useMemo(() => {
const style: Record<string, any> = {};
useEffect(() => { if (flex) style["--mx-box-flex"] = flex;
addOrRemoveProperty(ref, `--mx-box-flex`, flex); if (shrink) style["--mx-box-shrink"] = shrink;
addOrRemoveProperty(ref, `--mx-box-shrink`, shrink); if (grow) style["--mx-box-grow"] = grow;
addOrRemoveProperty(ref, `--mx-box-grow`, grow); return style;
}, [flex, grow, shrink]); }, [flex, grow, shrink]);
return React.createElement( return React.createElement(
@@ -88,7 +69,7 @@ export function Box({
"mx_Box--shrink": !!shrink, "mx_Box--shrink": !!shrink,
"mx_Box--grow": !!grow, "mx_Box--grow": !!grow,
}), }),
ref, style,
}, },
children, children,
); );

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import classNames from "classnames"; import classNames from "classnames";
import React, { useEffect, useRef } from "react"; import React, { useMemo } from "react";
type FlexProps = { type FlexProps = {
/** /**
@@ -64,15 +64,16 @@ export function Flex({
children, children,
...props ...props
}: React.PropsWithChildren<FlexProps>): JSX.Element { }: React.PropsWithChildren<FlexProps>): JSX.Element {
const ref = useRef<HTMLElement>(); const style = useMemo(
() => ({
"--mx-flex-display": display,
"--mx-flex-direction": direction,
"--mx-flex-align": align,
"--mx-flex-justify": justify,
"--mx-flex-gap": gap,
}),
[align, direction, display, gap, justify],
);
useEffect(() => { return React.createElement(as, { ...props, className: classNames("mx_Flex", className), style }, children);
ref.current!.style.setProperty(`--mx-flex-display`, display);
ref.current!.style.setProperty(`--mx-flex-direction`, direction);
ref.current!.style.setProperty(`--mx-flex-align`, align);
ref.current!.style.setProperty(`--mx-flex-justify`, justify);
ref.current!.style.setProperty(`--mx-flex-gap`, gap);
}, [align, direction, display, gap, justify]);
return React.createElement(as, { ...props, className: classNames("mx_Flex", className), ref }, children);
} }

View File

@@ -19,7 +19,7 @@ import {
UserEvent, UserEvent,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { throttle } from "lodash"; import { throttle } from "lodash";
import { RoomMember } from "../../../models/rooms/RoomMember"; import { RoomMember } from "../../../models/rooms/RoomMember";
@@ -120,19 +120,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
const sdkContext = useContext(SDKContext); const sdkContext = useContext(SDKContext);
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map()); const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
// This is the last known total number of members in this room. // This is the last known total number of members in this room.
const totalMemberCount = useRef<number>(0); const [totalMemberCount, setTotalMemberCount] = useState(0);
const searchQuery = useRef("");
const loadMembers = useMemo( const loadMembers = useMemo(
() => () =>
throttle( throttle(
async (): Promise<void> => { async (searchQuery?: string): Promise<void> => {
const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList( const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList(
roomId, roomId,
searchQuery.current, searchQuery,
); );
const newMemberMap = new Map<string, Member>(); const newMemberMap = new Map<string, Member>();
// First add the invited room members // First add the invited room members
@@ -141,7 +138,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
newMemberMap.set(member.userId, roomMember); newMemberMap.set(member.userId, roomMember);
} }
// Then add the third party invites // Then add the third party invites
const threePidInvited = getPending3PidInvites(room, searchQuery.current); const threePidInvited = getPending3PidInvites(room, searchQuery);
for (const invited of threePidInvited) { for (const invited of threePidInvited) {
const key = invited.threePidInvite!.event.getContent().display_name; const key = invited.threePidInvite!.event.getContent().display_name;
newMemberMap.set(key, invited); newMemberMap.set(key, invited);
@@ -152,26 +149,18 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
newMemberMap.set(member.userId, roomMember); newMemberMap.set(member.userId, roomMember);
} }
setMemberMap(newMemberMap); setMemberMap(newMemberMap);
if (!searchQuery.current) { if (!searchQuery) {
/** /**
* Since searching for members only gives you the relevant * Since searching for members only gives you the relevant
* members matching the query, do not update the totalMemberCount! * members matching the query, do not update the totalMemberCount!
**/ **/
totalMemberCount.current = newMemberMap.size; setTotalMemberCount(newMemberMap.size);
} }
}, },
500, 500,
{ leading: true, trailing: true }, { leading: true, trailing: true },
), ),
[roomId, sdkContext.memberListStore, room], [sdkContext.memberListStore, roomId, room],
);
const search = useCallback(
(query: string) => {
searchQuery.current = query;
loadMembers();
},
[loadMembers],
); );
const isPresenceEnabled = useMemo( const isPresenceEnabled = useMemo(
@@ -252,12 +241,12 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
return { return {
members: Array.from(memberMap.values()), members: Array.from(memberMap.values()),
search, search: loadMembers,
shouldShowInvite, shouldShowInvite,
isPresenceEnabled, isPresenceEnabled,
isLoading, isLoading,
onInviteButtonClick, onInviteButtonClick,
shouldShowSearch: totalMemberCount.current >= 20, shouldShowSearch: totalMemberCount >= 20,
canInvite, canInvite,
}; };
} }

View File

@@ -58,11 +58,10 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
if (canvas) canvas.height = UIStore.instance.windowHeight; if (canvas) canvas.height = UIStore.instance.windowHeight;
UIStore.instance.on(UI_EVENTS.Resize, resize); UIStore.instance.on(UI_EVENTS.Resize, resize);
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
return () => { return () => {
dis.unregister(dispatcherRef); dis.unregister(dispatcherRef);
UIStore.instance.off(UI_EVENTS.Resize, resize); UIStore.instance.off(UI_EVENTS.Resize, resize);
// eslint-disable-next-line react-hooks/exhaustive-deps
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
for (const effect in currentEffects) { for (const effect in currentEffects) {
const effectModule: ICanvasEffect = currentEffects.get(effect)!; const effectModule: ICanvasEffect = currentEffects.get(effect)!;
if (effectModule && effectModule.isRunning) { if (effectModule && effectModule.isRunning) {

View File

@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React, { RefObject } from "react";
import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
interface IProps { interface IProps {
sensor: Element; sensor: RefObject<Element>;
breakpoint: number; breakpoint: number;
onMeasurement(narrow: boolean): void; onMeasurement(narrow: boolean): void;
} }
@@ -35,14 +35,14 @@ export default class Measured extends React.PureComponent<IProps> {
} }
public componentDidUpdate(prevProps: Readonly<IProps>): void { public componentDidUpdate(prevProps: Readonly<IProps>): void {
const previous = prevProps.sensor; const previous = prevProps.sensor.current;
const current = this.props.sensor; const current = this.props.sensor.current;
if (previous === current) return; if (previous === current) return;
if (previous) { if (previous) {
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`); UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
} }
if (current) { if (current) {
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor); UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor.current);
} }
} }

View File

@@ -213,7 +213,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
header={_t("right_panel|video_room_chat|title")} header={_t("right_panel|video_room_chat|title")}
ref={this.card} ref={this.card}
> >
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} <Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<div className="mx_TimelineCard_timeline"> <div className="mx_TimelineCard_timeline">
{jumpToBottom} {jumpToBottom}
<TimelinePanel <TimelinePanel

View File

@@ -109,6 +109,7 @@ export function ReadReceiptGroup({
readReceiptPosition = readReceiptMap[userId]; readReceiptPosition = readReceiptMap[userId];
if (!readReceiptPosition) { if (!readReceiptPosition) {
readReceiptPosition = {}; readReceiptPosition = {};
// eslint-disable-next-line react-compiler/react-compiler
readReceiptMap[userId] = readReceiptPosition; readReceiptMap[userId] = readReceiptPosition;
} }
} }

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { EventType, RoomType, Room } from "matrix-js-sdk/src/matrix"; import { EventType, Room, RoomType } from "matrix-js-sdk/src/matrix";
import React, { ComponentType, createRef, ReactComponentElement, SyntheticEvent } from "react"; import React, { ComponentType, createRef, ReactComponentElement, SyntheticEvent } from "react";
import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
@@ -56,6 +56,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation"; import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler.tsx";
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void; onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
@@ -440,6 +441,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate); SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms); SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
this.updateLists(); // trigger the first update this.updateLists(); // trigger the first update
} }
@@ -448,8 +450,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate); SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport);
} }
private updateProtocolSupport = (): void => {
this.updateLists();
};
private onRoomViewStoreUpdate = (): void => { private onRoomViewStoreUpdate = (): void => {
this.setState({ this.setState({
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined, currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined,
@@ -471,8 +478,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
metricsViaKeyboard: true, metricsViaKeyboard: true,
}); });
} }
} else if (payload.action === Action.PstnSupportUpdated) {
this.updateLists();
} }
}; };

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { useCallback, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { EventType, KnownMembership, MatrixEvent, Room, RoomStateEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { EventType, KnownMembership, MatrixEvent, Room, RoomStateEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import { CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -213,9 +213,11 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
initialisedRef.current = InitialisationStatus.Completed; initialisedRef.current = InitialisationStatus.Completed;
}, [crypto, room, addMembersWhoNeedApproval, updateCurrentPrompt]); }, [crypto, room, addMembersWhoNeedApproval, updateCurrentPrompt]);
loadMembers().catch((e) => { useEffect(() => {
logger.error("Error initialising UserIdentityWarning:", e); loadMembers().catch((e) => {
}); logger.error("Error initialising UserIdentityWarning:", e);
});
}, [loadMembers]);
// When a user's verification status changes, we check if they need to be // When a user's verification status changes, we check if they need to be
// added/removed from the set of members needing approval. // added/removed from the set of members needing approval.

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react"; import React, { ForwardedRef, forwardRef, MutableRefObject, useMemo } from "react";
import classNames from "classnames"; import classNames from "classnames";
import EditorStateTransfer from "../../../../utils/EditorStateTransfer"; import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
@@ -44,7 +44,7 @@ export default function EditWysiwygComposer({
className, className,
...props ...props
}: EditWysiwygComposerProps): JSX.Element { }: EditWysiwygComposerProps): JSX.Element {
const defaultContextValue = useRef(getDefaultContextValue({ editorStateTransfer })); const defaultContextValue = useMemo(() => getDefaultContextValue({ editorStateTransfer }), [editorStateTransfer]);
const initialContent = useInitialContent(editorStateTransfer); const initialContent = useInitialContent(editorStateTransfer);
const isReady = !editorStateTransfer || initialContent !== undefined; const isReady = !editorStateTransfer || initialContent !== undefined;
@@ -55,7 +55,7 @@ export default function EditWysiwygComposer({
} }
return ( return (
<ComposerContext.Provider value={defaultContextValue.current}> <ComposerContext.Provider value={defaultContextValue}>
<WysiwygComposer <WysiwygComposer
className={classNames("mx_EditWysiwygComposer", className)} className={classNames("mx_EditWysiwygComposer", className)}
initialContent={initialContent} initialContent={initialContent}

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react"; import React, { ForwardedRef, forwardRef, MutableRefObject, useMemo } from "react";
import { IEventRelation } from "matrix-js-sdk/src/matrix"; import { IEventRelation } from "matrix-js-sdk/src/matrix";
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler"; import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
@@ -52,10 +52,13 @@ export default function SendWysiwygComposer({
...props ...props
}: SendWysiwygComposerProps): JSX.Element { }: SendWysiwygComposerProps): JSX.Element {
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer; const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
const defaultContextValue = useRef(getDefaultContextValue({ eventRelation: props.eventRelation })); const defaultContextValue = useMemo(
() => getDefaultContextValue({ eventRelation: props.eventRelation }),
[props.eventRelation],
);
return ( return (
<ComposerContext.Provider value={defaultContextValue.current}> <ComposerContext.Provider value={defaultContextValue}>
<Composer <Composer
className="mx_SendWysiwygComposer" className="mx_SendWysiwygComposer"
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />} leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}

View File

@@ -21,6 +21,7 @@ export function useComposerFunctions(
() => ({ () => ({
clear: () => { clear: () => {
if (ref.current) { if (ref.current) {
// eslint-disable-next-line react-compiler/react-compiler
ref.current.innerHTML = ""; ref.current.innerHTML = "";
} }
}, },

View File

@@ -12,6 +12,7 @@ export function usePlainTextInitialization(initialContent = "", ref: RefObject<H
useEffect(() => { useEffect(() => {
// always read and write the ref.current using .innerHTML for consistency in linebreak and HTML entity handling // always read and write the ref.current using .innerHTML for consistency in linebreak and HTML entity handling
if (ref.current) { if (ref.current) {
// eslint-disable-next-line react-compiler/react-compiler
ref.current.innerHTML = initialContent; ref.current.innerHTML = initialContent;
} }
}, [ref, initialContent]); }, [ref, initialContent]);

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
import React, { ChangeEvent, JSX, useCallback, useMemo, useRef, useState } from "react"; import React, { ChangeEvent, JSX, useCallback, useMemo, useState } from "react";
import { import {
InlineField, InlineField,
ToggleControl, ToggleControl,
@@ -39,12 +39,12 @@ import { useSettingValue } from "../../../hooks/useSettings";
*/ */
export function ThemeChoicePanel(): JSX.Element { export function ThemeChoicePanel(): JSX.Element {
const themeState = useTheme(); const themeState = useTheme();
const themeWatcher = useRef(new ThemeWatcher()); const themeWatcher = useMemo(() => new ThemeWatcher(), []);
const customThemeEnabled = useSettingValue("feature_custom_themes"); const customThemeEnabled = useSettingValue("feature_custom_themes");
return ( return (
<SettingsSubsection heading={_t("common|theme")} legacy={false} data-testid="themePanel"> <SettingsSubsection heading={_t("common|theme")} legacy={false} data-testid="themePanel">
{themeWatcher.current.isSystemThemeSupported() && ( {themeWatcher.isSystemThemeSupported() && (
<SystemTheme systemThemeActivated={themeState.systemThemeActivated} /> <SystemTheme systemThemeActivated={themeState.systemThemeActivated} />
)} )}
<ThemeSelectors theme={themeState.theme} disabled={themeState.systemThemeActivated} /> <ThemeSelectors theme={themeState.theme} disabled={themeState.systemThemeActivated} />

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import { JoinRule, EventType, RoomState, Room } from "matrix-js-sdk/src/matrix"; import { JoinRule, EventType, RoomState, Room } from "matrix-js-sdk/src/matrix";
import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
@@ -24,48 +25,49 @@ interface ElementCallSwitchProps {
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => { const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => {
const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]); const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]);
const [content, events, maySend] = useRoomState( const [content, maySend] = useRoomState(
room, room,
useCallback( useCallback(
(state: RoomState) => { (state: RoomState) => {
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); const content = state
?.getStateEvents(EventType.RoomPowerLevels, "")
?.getContent<RoomPowerLevelsEventContent>();
return [ return [
content ?? {}, content ?? {},
content?.["events"] ?? {},
state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()), state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()),
]; ] as const;
}, },
[room.client], [room.client],
), ),
); );
const [elementCallEnabled, setElementCallEnabled] = useState<boolean>(() => { const [elementCallEnabled, setElementCallEnabled] = useState<boolean>(() => {
return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0; return content.events?.[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
}); });
const onChange = useCallback( const onChange = useCallback(
(enabled: boolean): void => { (enabled: boolean): void => {
setElementCallEnabled(enabled); setElementCallEnabled(enabled);
// Take a copy to avoid mutating the original
const newContent = { events: {}, ...content };
if (enabled) { if (enabled) {
const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0; const userLevel = newContent.events[EventType.RoomMessage] ?? content.users_default ?? 0;
const moderatorLevel = content.kick ?? 50; const moderatorLevel = content.kick ?? 50;
events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; newContent.events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
} else { } else {
const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100; const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; newContent.events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
} }
room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, { room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent);
events: events,
...content,
});
}, },
[room.client, room.roomId, content, events, isPublic], [room.client, room.roomId, content, isPublic],
); );
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand; const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;

View File

@@ -27,7 +27,7 @@ type Props = {
const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID"; const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID";
const QuickThemeSwitcher: React.FC<Props> = ({ requestClose }) => { const QuickThemeSwitcher: React.FC<Props> = ({ requestClose }) => {
const orderedThemes = useMemo(getOrderedThemes, []); const orderedThemes = useMemo(() => getOrderedThemes(), []);
const themeState = useTheme(); const themeState = useTheme();
const nonHighContrast = findNonHighContrastTheme(themeState.theme); const nonHighContrast = findNonHighContrastTheme(themeState.theme);

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { ClientEvent, MatrixClient, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix"; import { ClientEvent, MatrixClient, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix";
import { throttle } from "lodash"; import { throttle } from "lodash";
@@ -42,14 +42,12 @@ export function useUnreadThreadRooms(forceComputation: boolean): Result {
setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs)); setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs));
}, [mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs]); }, [mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs]);
// The exhautive deps lint rule can't compute dependencies here since it's not a plain inline func. const scheduleUpdate = useMemo(
// We make this as simple as possible so its only dep is doUpdate itself. () =>
// eslint-disable-next-line react-hooks/exhaustive-deps throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, {
const scheduleUpdate = useCallback( leading: false,
throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, { trailing: true,
leading: false, }),
trailing: true,
}),
[doUpdate], [doUpdate],
); );

View File

@@ -25,6 +25,9 @@ interface IPropsExtended extends IProps {
SecondaryIcon?: ComponentType<React.SVGAttributes<SVGElement>>; SecondaryIcon?: ComponentType<React.SVGAttributes<SVGElement>>;
destructive?: "primary" | "secondary"; destructive?: "primary" | "secondary";
onSecondaryClick(): void; onSecondaryClick(): void;
// If set, this will override the max-width (of the description) making the toast wider or narrower than standard
overrideWidth?: string;
} }
const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
@@ -37,12 +40,13 @@ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
destructive, destructive,
onPrimaryClick, onPrimaryClick,
onSecondaryClick, onSecondaryClick,
overrideWidth,
}) => { }) => {
const detailContent = detail ? <div className="mx_Toast_detail">{detail}</div> : null; const detailContent = detail ? <div className="mx_Toast_detail">{detail}</div> : null;
return ( return (
<div> <div>
<div className="mx_Toast_description"> <div className="mx_Toast_description" style={{ maxWidth: overrideWidth }}>
{description} {description}
{detailContent} {detailContent}
</div> </div>

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useRef, useState } from "react"; import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useMemo, useState } from "react";
import { objectKeyChanges } from "../utils/objects.ts"; import { objectKeyChanges } from "../utils/objects.ts";
import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts"; import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts";
@@ -48,15 +48,16 @@ const ScopedRoomContext = createContext<EfficientContext<ContextValue> | undefin
// Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare) // Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare)
export const ScopedRoomContextProvider = memo( export const ScopedRoomContextProvider = memo(
({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => { ({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => {
const contextRef = useRef(new EfficientContext<ContextValue>(state)); // eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps
const context = useMemo(() => new EfficientContext<ContextValue>(state), []);
useEffect(() => { useEffect(() => {
contextRef.current.setState(state); context.setState(state);
}, [state]); }, [context, state]);
// Includes the legacy RoomContext provider for backwards compatibility with class components // Includes the legacy RoomContext provider for backwards compatibility with class components
return ( return (
<RoomContext.Provider value={state}> <RoomContext.Provider value={state}>
<ScopedRoomContext.Provider value={contextRef.current}>{children}</ScopedRoomContext.Provider> <ScopedRoomContext.Provider value={context}>{children}</ScopedRoomContext.Provider>
</RoomContext.Provider> </RoomContext.Provider>
); );
}, },

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import { ReactNode, createContext, useCallback, useContext, useEffect, useState, useMemo } from "react";
/** /**
* A ToastContext helps components display any kind of toast message and can be provided * A ToastContext helps components display any kind of toast message and can be provided
@@ -33,19 +33,19 @@ export function useToastContext(): ToastRack {
* the ToastRack object that should be provided to the context * the ToastRack object that should be provided to the context
*/ */
export function useActiveToast(): [ReactNode | undefined, ToastRack] { export function useActiveToast(): [ReactNode | undefined, ToastRack] {
const toastRack = useRef(new ToastRack()); const toastRack = useMemo(() => new ToastRack(), []);
const [activeToast, setActiveToast] = useState<ReactNode | undefined>(toastRack.current.getActiveToast()); const [activeToast, setActiveToast] = useState<ReactNode | undefined>(toastRack.getActiveToast());
const updateCallback = useCallback(() => { const updateCallback = useCallback(() => {
setActiveToast(toastRack.current.getActiveToast()); setActiveToast(toastRack.getActiveToast());
}, [setActiveToast, toastRack]); }, [setActiveToast, toastRack]);
useEffect(() => { useEffect(() => {
toastRack.current.setCallback(updateCallback); toastRack.setCallback(updateCallback);
}, [toastRack, updateCallback]); }, [toastRack, updateCallback]);
return [activeToast, toastRack.current]; return [activeToast, toastRack];
} }
interface DisplayedToast { interface DisplayedToast {

View File

@@ -135,20 +135,6 @@ export enum Action {
*/ */
OpenDialPad = "open_dial_pad", OpenDialPad = "open_dial_pad",
/**
* Fired when CallHandler has checked for PSTN protocol support
* payload: none
* XXX: Is an action the right thing for this?
*/
PstnSupportUpdated = "pstn_support_updated",
/**
* Similar to PstnSupportUpdated, fired when CallHandler has checked for virtual room support
* payload: none
* XXX: Ditto
*/
VirtualRoomSupportUpdated = "virtual_room_support_updated",
/** /**
* Fired when an upload has started. Should be used with UploadStartedPayload. * Fired when an upload has started. Should be used with UploadStartedPayload.
*/ */

View File

@@ -34,7 +34,7 @@ export function useAsyncRefreshMemo<T>(fn: Fn<T>, deps: DependencyList, initialV
return () => { return () => {
discard = true; discard = true;
}; };
}, deps); // eslint-disable-line react-hooks/exhaustive-deps }, deps); // eslint-disable-line react-hooks/exhaustive-deps,react-compiler/react-compiler
useEffect(refresh, [refresh]); useEffect(refresh, [refresh]);
return [value, refresh]; return [value, refresh];
} }

View File

@@ -25,6 +25,7 @@ export const useNotificationState = (room: Room): [RoomNotifState | undefined, (
setNotificationState(echoChamber.notificationVolume); setNotificationState(echoChamber.notificationVolume);
} }
}); });
// eslint-disable-next-line react-compiler/react-compiler
const setter = useCallback((state: RoomNotifState) => (echoChamber.notificationVolume = state), [echoChamber]); const setter = useCallback((state: RoomNotifState) => (echoChamber.notificationVolume = state), [echoChamber]);
return [notificationState, setter]; return [notificationState, setter];
}; };

View File

@@ -22,6 +22,6 @@ export const useTransition = <D extends DependencyList>(callback: (...params: D)
useEffect(() => { useEffect(() => {
if (args.current !== null) func.current(...args.current); if (args.current !== null) func.current(...args.current);
args.current = deps; args.current = deps;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps
}, deps); }, deps);
}; };

View File

@@ -2379,7 +2379,7 @@
"all_rooms_home_description": "Alle Räume, denen du beigetreten bist, werden auf der Startseite erscheinen.", "all_rooms_home_description": "Alle Räume, denen du beigetreten bist, werden auf der Startseite erscheinen.",
"always_show_message_timestamps": "Nachrichtenzeitstempel immer anzeigen", "always_show_message_timestamps": "Nachrichtenzeitstempel immer anzeigen",
"appearance": { "appearance": {
"bundled_emoji_font": "Verwenden Sie den mitgelieferten Emoji Font.", "bundled_emoji_font": "Mitgelieferte Emoji-Schriftart verwenden",
"compact_layout": "Kompakten Text und Nachrichten anzeigen", "compact_layout": "Kompakten Text und Nachrichten anzeigen",
"compact_layout_description": "Um diese Funktion nutzen zu können, muss das moderne Nachrichtenlayout ausgewählt sein.", "compact_layout_description": "Um diese Funktion nutzen zu können, muss das moderne Nachrichtenlayout ausgewählt sein.",
"custom_font": "Systemschriftart verwenden", "custom_font": "Systemschriftart verwenden",

View File

@@ -879,14 +879,18 @@
"title": "Destroy cross-signing keys?", "title": "Destroy cross-signing keys?",
"warning": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from." "warning": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from."
}, },
"enter_recovery_key": "Enter recovery key",
"event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.", "event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.",
"event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session", "event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session",
"event_shield_reason_unknown_device": "Encrypted by an unknown or deleted device.", "event_shield_reason_unknown_device": "Encrypted by an unknown or deleted device.",
"event_shield_reason_unsigned_device": "Encrypted by a device not verified by its owner.", "event_shield_reason_unsigned_device": "Encrypted by a device not verified by its owner.",
"event_shield_reason_unverified_identity": "Encrypted by an unverified user.", "event_shield_reason_unverified_identity": "Encrypted by an unverified user.",
"export_unsupported": "Your browser does not support the required cryptography extensions", "export_unsupported": "Your browser does not support the required cryptography extensions",
"forgot_recovery_key": "Forgot recovery key?",
"import_invalid_keyfile": "Not a valid %(brand)s keyfile", "import_invalid_keyfile": "Not a valid %(brand)s keyfile",
"import_invalid_passphrase": "Authentication check failed: incorrect password?", "import_invalid_passphrase": "Authentication check failed: incorrect password?",
"key_storage_out_of_sync": "Your key storage is out of sync.",
"key_storage_out_of_sync_description": "Confirm your recovery key to maintain access to your key storage and message history.",
"messages_not_secure": { "messages_not_secure": {
"cause_1": "Your homeserver", "cause_1": "Your homeserver",
"cause_2": "The homeserver the user you're verifying is connected to", "cause_2": "The homeserver the user you're verifying is connected to",

View File

@@ -27,6 +27,8 @@ const getTitle = (kind: Kind): string => {
return _t("encryption|set_up_recovery"); return _t("encryption|set_up_recovery");
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return _t("encryption|verify_toast_title"); return _t("encryption|verify_toast_title");
case Kind.KEY_STORAGE_OUT_OF_SYNC:
return _t("encryption|key_storage_out_of_sync");
} }
}; };
@@ -37,6 +39,7 @@ const getIcon = (kind: Kind): string | undefined => {
case Kind.SET_UP_RECOVERY: case Kind.SET_UP_RECOVERY:
return undefined; return undefined;
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
case Kind.KEY_STORAGE_OUT_OF_SYNC:
return "verification_warning"; return "verification_warning";
} }
}; };
@@ -49,6 +52,8 @@ const getSetupCaption = (kind: Kind): string => {
return _t("action|continue"); return _t("action|continue");
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return _t("action|verify"); return _t("action|verify");
case Kind.KEY_STORAGE_OUT_OF_SYNC:
return _t("encryption|enter_recovery_key");
} }
}; };
@@ -59,6 +64,8 @@ const getSecondaryButtonLabel = (kind: Kind): string => {
case Kind.SET_UP_ENCRYPTION: case Kind.SET_UP_ENCRYPTION:
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return _t("encryption|verification|unverified_sessions_toast_reject"); return _t("encryption|verification|unverified_sessions_toast_reject");
case Kind.KEY_STORAGE_OUT_OF_SYNC:
return _t("encryption|forgot_recovery_key");
} }
}; };
@@ -70,6 +77,8 @@ const getDescription = (kind: Kind): string => {
return _t("encryption|set_up_recovery_toast_description"); return _t("encryption|set_up_recovery_toast_description");
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return _t("encryption|verify_toast_description"); return _t("encryption|verify_toast_description");
case Kind.KEY_STORAGE_OUT_OF_SYNC:
return _t("encryption|key_storage_out_of_sync_description");
} }
}; };
@@ -89,6 +98,10 @@ export enum Kind {
* Prompt the user to verify this session * Prompt the user to verify this session
*/ */
VERIFY_THIS_SESSION = "verify_this_session", VERIFY_THIS_SESSION = "verify_this_session",
/**
* Prompt the user to enter their recovery key
*/
KEY_STORAGE_OUT_OF_SYNC = "key_storage_out_of_sync",
} }
const onReject = (): void => { const onReject = (): void => {
@@ -139,6 +152,7 @@ export const showToast = (kind: Kind): void => {
onPrimaryClick: onAccept, onPrimaryClick: onAccept,
secondaryLabel: getSecondaryButtonLabel(kind), secondaryLabel: getSecondaryButtonLabel(kind),
onSecondaryClick: onReject, onSecondaryClick: onReject,
overrideWidth: kind === Kind.KEY_STORAGE_OUT_OF_SYNC ? "366px" : undefined,
}, },
component: GenericToast, component: GenericToast,
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40, priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,

View File

@@ -30,8 +30,10 @@ export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreM
useEffect( useEffect(
() => { () => {
let map: MapLibreMap | undefined;
try { try {
setMap(createMap(cli, !!interactive, bodyId, onError)); map = createMap(cli, !!interactive, bodyId, onError);
setMap(map);
} catch (error) { } catch (error) {
console.error("Error encountered in useMap", error); console.error("Error encountered in useMap", error);
if (error instanceof Error) { if (error instanceof Error) {
@@ -46,8 +48,7 @@ export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreM
}; };
}, },
// map is excluded as a dependency // map is excluded as a dependency
// eslint-disable-next-line react-hooks/exhaustive-deps [cli, interactive, bodyId, onError],
[interactive, bodyId, onError],
); );
return map; return map;

View File

@@ -1,17 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import Views from "../Views";
export function isLoggedIn(): boolean {
// JRS: Maybe we should move the step that writes this to the window out of
// `element-web` and into this file? Better yet, we should probably create a
// store to hold this state.
// See also https://github.com/vector-im/element-web/issues/15034.
return window.matrixChat?.state.view === Views.LOGGED_IN;
}

View File

@@ -329,7 +329,7 @@ describe("DeviceListener", () => {
}); });
it("shows verify session toast when account has cross signing", async () => { it("shows verify session toast when account has cross signing", async () => {
mockCrypto!.userHasCrossSigningKeys.mockResolvedValue(true); mockCrypto!.isCrossSigningReady.mockResolvedValue(true);
await createAndStart(); await createAndStart();
expect(mockCrypto!.getUserDeviceInfo).toHaveBeenCalled(); expect(mockCrypto!.getUserDeviceInfo).toHaveBeenCalled();
@@ -337,24 +337,25 @@ describe("DeviceListener", () => {
SetupEncryptionToast.Kind.VERIFY_THIS_SESSION, SetupEncryptionToast.Kind.VERIFY_THIS_SESSION,
); );
}); });
it("checks key backup status when when account has cross signing", async () => {
mockCrypto!.getCrossSigningKeyId.mockResolvedValue(null);
mockCrypto!.userHasCrossSigningKeys.mockResolvedValue(true);
await createAndStart();
expect(mockCrypto!.getActiveSessionBackupVersion).toHaveBeenCalled();
});
}); });
describe("when user does have a cross signing id on this device", () => { describe("when user does have a cross signing id on this device", () => {
beforeEach(() => { beforeEach(() => {
mockCrypto!.isCrossSigningReady.mockResolvedValue(true);
mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc"); mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc");
mockCrypto!.getDeviceVerificationStatus.mockResolvedValue(
new DeviceVerificationStatus({
trustCrossSignedDevices: true,
crossSigningVerified: true,
}),
);
}); });
it("shows set up recovery toast when user has a key backup available", async () => { it("shows set up recovery toast when user has a key backup available", async () => {
// non falsy response // non falsy response
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
mockClient.secretStorage.getDefaultKeyId.mockResolvedValue(null);
await createAndStart(); await createAndStart();
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(

View File

@@ -16,9 +16,15 @@ describe("SetupEncryptionToast", () => {
render(<ToastContainer />); render(<ToastContainer />);
}); });
it("should render the se up recovery toast", async () => { it("should render the 'set up recovery' toast", async () => {
showToast(Kind.SET_UP_RECOVERY); showToast(Kind.SET_UP_RECOVERY);
await expect(screen.findByText("Set up recovery")).resolves.toBeInTheDocument(); await expect(screen.findByText("Set up recovery")).resolves.toBeInTheDocument();
}); });
it("should render the 'key storage out of sync' toast", async () => {
showToast(Kind.KEY_STORAGE_OUT_OF_SYNC);
await expect(screen.findByText("Your key storage is out of sync.")).resolves.toBeInTheDocument();
});
}); });

View File

@@ -1,22 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import MatrixChat from "../../../src/components/structures/MatrixChat.tsx";
import { isLoggedIn } from "../../../src/utils/login.ts";
import Views from "../../../src/Views.ts";
describe("isLoggedIn", () => {
it("should return true if MatrixChat state view is LOGGED_IN", () => {
window.matrixChat = {
state: {
view: Views.LOGGED_IN,
},
} as unknown as MatrixChat;
expect(isLoggedIn()).toBe(true);
});
});

View File

@@ -61,7 +61,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02"
integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==
"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9": "@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9", "@babel/core@^7.24.4":
version "7.26.0" version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40"
integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
@@ -303,7 +303,7 @@
dependencies: dependencies:
"@babel/types" "^7.25.8" "@babel/types" "^7.25.8"
"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": "@babel/parser@^7.24.4", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3":
version "7.26.3" version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234"
integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==
@@ -3529,9 +3529,8 @@
ts-xor "^1.3.0" ts-xor "^1.3.0"
vaul "^1.0.0" vaul "^1.0.0"
"@vector-im/matrix-wysiwyg-wasm@link:../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.0-af862ffd231dc0a6b8d6f2cb3601e68456c0ff24-integrity/node_modules/bindings/wysiwyg-wasm": "@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.0-af862ffd231dc0a6b8d6f2cb3601e68456c0ff24-integrity/node_modules/bindings/wysiwyg-wasm":
version "0.0.0" version "0.0.0"
uid ""
"@vector-im/matrix-wysiwyg@2.38.0": "@vector-im/matrix-wysiwyg@2.38.0":
version "2.38.0" version "2.38.0"
@@ -5977,6 +5976,18 @@ eslint-plugin-matrix-org@^2.0.2:
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-2.0.2.tgz#95b86b0f16704ab19740f7c3c62eae69e20365e6" resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-2.0.2.tgz#95b86b0f16704ab19740f7c3c62eae69e20365e6"
integrity sha512-cQy5Rjeq6uyu1mLXlPZwEJdyM0NmclrnEz68y792FSuuxzMyJNNYLGDQ5CkYW8H+PrD825HUFZ34pNXnjMOzOw== integrity sha512-cQy5Rjeq6uyu1mLXlPZwEJdyM0NmclrnEz68y792FSuuxzMyJNNYLGDQ5CkYW8H+PrD825HUFZ34pNXnjMOzOw==
eslint-plugin-react-compiler@^19.0.0-beta-df7b47d-20241124:
version "19.0.0-beta-df7b47d-20241124"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-df7b47d-20241124.tgz#468751d3a8a6781189405ee56b39b80545306df8"
integrity sha512-82PfnllC8jP/68KdLAbpWuYTcfmtGLzkqy2IW85WopKMTr+4rdQpp+lfliQ/QE79wWrv/dRoADrk3Pdhq25nTw==
dependencies:
"@babel/core" "^7.24.4"
"@babel/parser" "^7.24.4"
"@babel/plugin-transform-private-methods" "^7.25.9"
hermes-parser "^0.25.1"
zod "^3.22.4"
zod-validation-error "^3.0.3"
eslint-plugin-react-hooks@^5.0.0: eslint-plugin-react-hooks@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101"
@@ -6936,6 +6947,18 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hermes-estree@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
hermes-parser@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
dependencies:
hermes-estree "0.25.1"
highlight.js@^11.3.1: highlight.js@^11.3.1:
version "11.10.0" version "11.10.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"
@@ -8216,9 +8239,9 @@ jwt-decode@4.0.0, jwt-decode@^4.0.0:
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
katex@^0.16.0: katex@^0.16.0:
version "0.16.11" version "0.16.21"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.11.tgz#4bc84d5584f996abece5f01c6ad11304276a33f5" resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.21.tgz#8f63c659e931b210139691f2cc7bb35166b792a3"
integrity sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ== integrity sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==
dependencies: dependencies:
commander "^8.3.0" commander "^8.3.0"