Switch to rendering svg icons rather than masking them (#31557)

* Switch to rendering svg icons rather than masking them in SpacePanel

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix badly rendered icon in JoinRuleDropdown

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix badly rendered icon in RoomPreviewCard

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix badly rendered icon in Space menus

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix badly rendered icon in ThreadPanel

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshot

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unused icon underfill

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshot

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add missing snapshot

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unused AccessibleButton kinds

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in EventTileBubble

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in ReactionsRow

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in MFileBody

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in BaseCard

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in RedactedBody

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in E2ePadlock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy up E2EIcon

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in DialPad

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in AccessSecretStorageDialog

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering svg icons rather than masking them in HiddenBody

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix overflow in base card size

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add missing screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-01-05 17:14:51 +00:00
committed by GitHub
parent be7be39d0f
commit 914c959e31
63 changed files with 594 additions and 500 deletions
@@ -351,7 +351,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const composer = thread.locator(".mx_MessageComposer--compact"); const composer = thread.locator(".mx_MessageComposer--compact");
// Assert that the reply preview contains audio ReplyTile the file info button // Assert that the reply preview contains audio ReplyTile the file info button
await expect( await expect(
composer.locator(".mx_ReplyPreview .mx_ReplyTile_audio .mx_MFileBody_info[role='button']"), composer.locator(".mx_ReplyPreview .mx_ReplyTile .mx_MFileBody_info[role='button']"),
).toBeVisible(); ).toBeVisible();
// Select :smile: emoji and send it // Select :smile: emoji and send it
@@ -360,6 +360,6 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await composer.getByTestId("basicmessagecomposer").press("Enter"); await composer.getByTestId("basicmessagecomposer").press("Enter");
// Assert that the file name is rendered on the file button // Assert that the file name is rendered on the file button
await expect(threadTile.locator(".mx_ReplyTile_audio .mx_MFileBody_info[role='button']")).toBeVisible(); await expect(threadTile.locator(".mx_ReplyTile .mx_MFileBody_info[role='button']")).toBeVisible();
}); });
}); });
+2 -6
View File
@@ -31,15 +31,11 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => { const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => {
// check the invite message // check the invite message
await expect( await expect(page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon_warning"),
).not.toBeVisible();
// Bob sends a response // Bob sends a response
await bob.sendMessage(bobRoomId, "Hoo!"); await bob.sendMessage(bobRoomId, "Hoo!");
await expect( await expect(page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"),
).not.toBeVisible();
}; };
const bobJoin = async (page: Page, bob: Bot) => { const bobJoin = async (page: Page, bob: Bot) => {
@@ -30,69 +30,80 @@ test.describe("Cryptography", function () {
test.describe("decryption failure messages", () => { test.describe("decryption failure messages", () => {
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
test("should handle device-relative historical messages", async ({ test(
homeserver, "should handle device-relative historical messages",
page, { tag: "@screenshot" },
app, async ({ homeserver, page, app, credentials, user }) => {
credentials, test.setTimeout(60000);
user,
}) => {
test.setTimeout(60000);
// Start with a logged-in session, without key backup, and send a message. // Start with a logged-in session, without key backup, and send a message.
await createRoom(page, "Test room", true); await createRoom(page, "Test room", true);
await sendMessageInCurrentRoom(page, "test test"); await sendMessageInCurrentRoom(page, "test test");
// Log out, discarding the key for the sent message. // Log out, discarding the key for the sent message.
await logOutOfElement(page, true); await logOutOfElement(page, true);
// Log in again, and see how the message looks. // Log in again, and see how the message looks.
await logIntoElement(page, credentials); await logIntoElement(page, credentials);
await app.viewRoomByName("Test room"); await app.viewRoomByName("Test room");
const lastTile = page.locator(".mx_EventTile").last(); const lastTile = page.locator(".mx_EventTile").last();
await expect(lastTile).toContainText("Historical messages are not available on this device"); await expect(lastTile).toContainText("Historical messages are not available on this device");
await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); await expect(lastTile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
"This message could not be decrypted",
);
await expect(lastTile).toMatchScreenshot("history-not-available.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
// Now, we set up key backup, and then send another message. // Now, we set up key backup, and then send another message.
const secretStorageKey = await enableKeyBackup(app); const secretStorageKey = await enableKeyBackup(app);
await app.viewRoomByName("Test room"); await app.viewRoomByName("Test room");
await sendMessageInCurrentRoom(page, "test2 test2"); await sendMessageInCurrentRoom(page, "test2 test2");
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
// the key to be backed up. // the key to be backed up.
await page.waitForTimeout(10000); await page.waitForTimeout(10000);
// Finally, log out again, and back in, skipping verification for now, and see what we see. // Finally, log out again, and back in, skipping verification for now, and see what we see.
await logOutOfElement(page); await logOutOfElement(page);
await logIntoElement(page, credentials); await logIntoElement(page, credentials);
await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
await app.viewRoomByName("Test room"); await app.viewRoomByName("Test room");
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve // In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
// There should be two historical events in the timeline // There should be two historical events in the timeline
const tiles = await page.locator(".mx_EventTile").all(); const tiles = await page.locator(".mx_EventTile").all();
expect(tiles.length).toBeGreaterThanOrEqual(2); expect(tiles.length).toBeGreaterThanOrEqual(2);
// look at the last two tiles only // look at the last two tiles only
for (const tile of tiles.slice(-2)) { for (const tile of tiles.slice(-2)) {
await expect(tile).toContainText("You need to verify this device for access to historical messages"); await expect(tile).toContainText(
await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); "You need to verify this device for access to historical messages",
} );
await expect(tile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
"This message could not be decrypted",
);
}
// Now verify our device (setting up key backup), and check what happens // Now verify our device (setting up key backup), and check what happens
await verifySession(app, secretStorageKey); await verifySession(app, secretStorageKey);
const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2); const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2);
// The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though. // The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though.
await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message"); await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message");
await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
"This message could not be decrypted",
);
// The second message should now be decrypted, with a grey shield // The second message should now be decrypted, with a grey shield
await expect(tilesAfterVerify[1]).toContainText("test2 test2"); await expect(tilesAfterVerify[1]).toContainText("test2 test2");
await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible(); await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
}); "The authenticity of this encrypted message can't be guaranteed on this device.",
);
},
);
test.describe("non-joined historical messages", () => { test.describe("non-joined historical messages", () => {
test.skip(isDendrite, "does not yet support membership on events"); test.skip(isDendrite, "does not yet support membership on events");
@@ -186,7 +197,9 @@ test.describe("Cryptography", function () {
// The first message from Bob was sent before Alice was in the room, so should // The first message from Bob was sent before Alice was in the room, so should
// be different from the standard UTD message // be different from the standard UTD message
await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message"); await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message");
await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
"This message could not be decrypted",
);
// The second message from Bob should be decryptable // The second message from Bob should be decryptable
await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable"); await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable");
@@ -196,7 +209,9 @@ test.describe("Cryptography", function () {
// in the room and is expected to be decryptable, so this should have the // in the room and is expected to be decryptable, so this should have the
// standard UTD message // standard UTD message
await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message"); await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message");
await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
"This message could not be decrypted",
);
}); });
test("should be able to jump to a message sent before our last join event", async ({ test("should be able to jump to a message sent before our last join event", async ({
@@ -68,7 +68,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await doTwoWaySasVerification(page, verifier); await doTwoWaySasVerification(page, verifier);
await infoDialog.getByRole("button", { name: "They match" }).click(); await infoDialog.getByRole("button", { name: "They match" }).click();
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png"); await expect(page.locator(".mx_E2EIcon")).toMatchScreenshot("device-verified-e2eIcon.png");
await infoDialog.getByRole("button", { name: "Got it" }).click(); await infoDialog.getByRole("button", { name: "Got it" }).click();
// Check that our device is now cross-signed // Check that our device is now cross-signed
+102 -126
View File
@@ -77,11 +77,8 @@ test.describe("Cryptography", function () {
const last = page.locator(".mx_EventTile_last"); const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("Unable to decrypt message"); await expect(last).toContainText("Unable to decrypt message");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/); await expect(lastE2eIcon).toHaveAccessibleName("This message could not be decrypted");
await lastE2eIcon.focus(); await expect(lastE2eIcon).toMatchScreenshot("event-shield-utd.png");
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"This message could not be decrypted",
);
/* Should show a red padlock for an unencrypted message in an e2e room */ /* Should show a red padlock for an unencrypted message in an e2e room */
await bob.evaluate( await bob.evaluate(
@@ -99,10 +96,8 @@ test.describe("Cryptography", function () {
); );
await expect(last).toContainText("test unencrypted"); await expect(last).toContainText("test unencrypted");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await expect(lastE2eIcon).toHaveAccessibleName("Not encrypted");
await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png"); await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png");
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
/* Should show no padlock for an unverified user */ /* Should show no padlock for an unverified user */
// bob sends a valid event // bob sends a valid event
@@ -133,11 +128,8 @@ test.describe("Cryptography", function () {
/* should show red padlock for a message from an unverified device */ /* should show red padlock for a message from an unverified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
await expect(lastTile).toContainText("test encrypted from unverified"); await expect(lastTile).toContainText("test encrypted from unverified");
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await expect(lastTileE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
await lastTileE2eIcon.focus(); await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
"Encrypted by a device not verified by its owner.",
);
/* Should show a red padlock for a message from an unverified device. /* Should show a red padlock for a message from an unverified device.
* Rust crypto remembers the verification state of the sending device, so it will know that the device was * Rust crypto remembers the verification state of the sending device, so it will know that the device was
@@ -153,64 +145,58 @@ test.describe("Cryptography", function () {
await app.viewRoomByName("TestRoom"); await app.viewRoomByName("TestRoom");
await expect(last).toContainText("test encrypted from unverified"); await expect(last).toContainText("test encrypted from unverified");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
await lastE2eIcon.focus(); await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Encrypted by a device not verified by its owner.",
);
}, },
); );
test("Should show a grey padlock for a key restored from backup", async ({ test(
page, "Should show a grey padlock for a key restored from backup",
app, { tag: "@screenshot" },
bot: bob, async ({ page, app, bot: bob, homeserver, user: aliceCredentials }) => {
homeserver, test.slow();
user: aliceCredentials, const securityKey = await enableKeyBackup(app);
}) => {
test.slow();
const securityKey = await enableKeyBackup(app);
// bob sends a valid event // bob sends a valid event
await bob.sendMessage(testRoomId, "test encrypted 1"); await bob.sendMessage(testRoomId, "test encrypted 1");
const lastTile = page.locator(".mx_EventTile_last"); const lastTile = page.locator(".mx_EventTile_last");
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon"); const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
await expect(lastTile).toContainText("test encrypted 1"); await expect(lastTile).toContainText("test encrypted 1");
// no e2e icon // no e2e icon
await expect(lastTileE2eIcon).not.toBeVisible(); await expect(lastTileE2eIcon).not.toBeVisible();
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
// the key to be backed up. // the key to be backed up.
await page.waitForTimeout(10000); await page.waitForTimeout(10000);
/* log out, and back in */ /* log out, and back in */
await logOutOfElement(page); await logOutOfElement(page);
// Reload to work around a Rust crypto bug where it can hold onto the indexeddb even after logout // Reload to work around a Rust crypto bug where it can hold onto the indexeddb even after logout
// https://github.com/element-hq/element-web/issues/25779 // https://github.com/element-hq/element-web/issues/25779
await page.addInitScript(() => { await page.addInitScript(() => {
// When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures // When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures
// will re-inject the original credentials into localStorage, which we don't want. // will re-inject the original credentials into localStorage, which we don't want.
// To work around, we add a second initScript which will clear localStorage again. // To work around, we add a second initScript which will clear localStorage again.
window.localStorage.clear(); window.localStorage.clear();
}); });
await page.reload(); await page.reload();
await logIntoElementAndVerify(page, aliceCredentials, securityKey); await logIntoElementAndVerify(page, aliceCredentials, securityKey);
/* go back to the test room and find Bob's message again */ /* go back to the test room and find Bob's message again */
await app.viewRoomById(testRoomId); await app.viewRoomById(testRoomId);
await expect(lastTile).toContainText("test encrypted 1"); await expect(lastTile).toContainText("test encrypted 1");
// The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning. // The gray shield would be a Compound info icon. The red shield would be a Compound error solid icon.
// No shield would have no div mx_EventTile_e2eIcon at all. // No shield would have no div mx_EventTile_e2eIcon at all.
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); // The key is coming from backup, so it is not anymore possible to establish if the claimed device
await lastTileE2eIcon.hover(); // creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device."
// The key is coming from backup, so it is not anymore possible to establish if the claimed device // It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted.
// creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device." await expect(lastTileE2eIcon).toHaveAccessibleName(
// It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted. "The authenticity of this encrypted message can't be guaranteed on this device.",
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText( );
"The authenticity of this encrypted message can't be guaranteed on this device.", await expect(lastTileE2eIcon).toMatchScreenshot("event-shield-authenticity.png");
); },
}); );
test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => { test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => {
// bob has a second, not cross-signed, device // bob has a second, not cross-signed, device
@@ -224,7 +210,7 @@ test.describe("Cryptography", function () {
// the message should appear, decrypted, with no warning // the message should appear, decrypted, with no warning
await expect( await expect(
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"), page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon"),
).not.toBeVisible(); ).not.toBeVisible();
// bob sends an edit to the first message with his unverified device // bob sends an edit to the first message with his unverified device
@@ -241,7 +227,7 @@ test.describe("Cryptography", function () {
// the edit should have a warning // the edit should have a warning
await expect( await expect(
page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon_warning"), page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon"),
).toBeVisible(); ).toBeVisible();
// a second edit from the verified device should be ok // a second edit from the verified device should be ok
@@ -257,77 +243,69 @@ test.describe("Cryptography", function () {
}); });
await expect( await expect(
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"), page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon"),
).not.toBeVisible(); ).not.toBeVisible();
}); });
test("should show correct shields on events sent by devices which have since been deleted", async ({ test(
page, "should show correct shields on events sent by devices which have since been deleted",
app, { tag: "@screenshot" },
bot: bob, async ({ page, app, bot: bob, homeserver }) => {
homeserver, // Workaround for https://github.com/element-hq/element-web/issues/28640:
}) => { // make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
// Workaround for https://github.com/element-hq/element-web/issues/28640: // his user info.
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening await waitForDevices(app, bob.credentials.userId, 1);
// his user info.
await waitForDevices(app, bob.credentials.userId, 1);
// Our app is blocked from syncing while Bob sends his messages. // Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline(); await app.client.network.goOffline();
// Bob sends a message from his verified device // Bob sends a message from his verified device
await bob.sendMessage(testRoomId, "test encrypted from verified"); await bob.sendMessage(testRoomId, "test encrypted from verified");
// And one from a second, not cross-signed, device // And one from a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
// ... and then logs out both devices. // ... and then logs out both devices.
await bob.evaluate((cli) => cli.logout(true)); await bob.evaluate((cli) => cli.logout(true));
await bobSecondDevice.evaluate((cli) => cli.logout(true)); await bobSecondDevice.evaluate((cli) => cli.logout(true));
// Let our app start syncing again // Let our app start syncing again
await app.client.network.goOnline(); await app.client.network.goOnline();
// Wait for the messages to arrive. It can take quite a while for the sync to wake up. // Wait for the messages to arrive. It can take quite a while for the sync to wake up.
const last = page.locator(".mx_EventTile_last"); const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 }); await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 });
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
await lastE2eIcon.focus(); await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Encrypted by a device not verified by its owner.",
);
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await assertNoE2EIcon(penultimate, app); await assertNoE2EIcon(penultimate, app);
}); },
);
test("should show correct shields on events sent by users with changed identity", async ({ test(
page, "should show correct shields on events sent by users with changed identity",
app, { tag: "@screenshot" },
bot: bob, async ({ page, app, bot: bob, homeserver }) => {
homeserver, // Verify Bob
}) => { await verify(app, bob);
// Verify Bob
await verify(app, bob);
// Bob logs in a new device and resets cross-signing // Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true); await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
/* should show an error for a message from a previously verified device */ /* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified"); await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const last = page.locator(".mx_EventTile_last"); const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from user that was previously verified"); await expect(last).toContainText("test encrypted from user that was previously verified");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await expect(lastE2eIcon).toHaveAccessibleName("Sender's verified identity was reset");
await lastE2eIcon.focus(); await expect(lastE2eIcon).toMatchScreenshot("event-shield-identity-reset.png");
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( },
"Sender's verified identity was reset", );
);
});
}); });
}); });
@@ -343,8 +321,6 @@ async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) {
const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon"); const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon");
if ((await e2eIcon.count()) > 0) { if ((await e2eIcon.count()) > 0) {
// uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error. // uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error.
await e2eIcon.focus(); await expect(e2eIcon).toHaveAccessibleName("None");
const tooltip = await app.getTooltipForElement(e2eIcon);
throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`);
} }
} }
-18
View File
@@ -244,24 +244,6 @@ export class ElementAppPage {
await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click(); await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click();
} }
/**
* Get a locator for the tooltip associated with an element
* @param e The element with the tooltip
* @returns Locator to the tooltip
*/
public async getTooltipForElement(e: Locator): Promise<Locator> {
const [labelledById, describedById] = await Promise.all([
e.getAttribute("aria-labelledby"),
e.getAttribute("aria-describedby"),
]);
if (!labelledById && !describedById) {
throw new Error(
"Element has no aria-labelledby or aria-describedy attributes! The tooltip should have added either one of these.",
);
}
return this.page.locator(`id=${labelledById ?? describedById}`);
}
/** /**
* Close the notification toast * Close the notification toast
*/ */
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

+1 -2
View File
@@ -40,8 +40,7 @@ Please see LICENSE files in the repository root for full details.
/* that our preview is unencrypted, which doesn't actually matter */ /* that our preview is unencrypted, which doesn't actually matter */
/* We also hide download links to not encourage users to try interacting */ /* We also hide download links to not encourage users to try interacting */
.mx_EventTile_msgOption, .mx_EventTile_msgOption,
.mx_EventTile_e2eIcon_unencrypted, .mx_EventTile_e2eIcon,
.mx_EventTile_e2eIcon_warning,
.mx_MFileBody_download { .mx_MFileBody_download {
display: none; display: none;
} }
@@ -33,14 +33,11 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_AccessSecretStorageDialog_recoveryKeyFeedback { .mx_AccessSecretStorageDialog_recoveryKeyFeedback {
&::before { svg {
content: "";
display: inline-block; display: inline-block;
vertical-align: bottom; vertical-align: bottom;
width: 20px; width: 20px;
height: 20px; height: 20px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 20px; mask-size: 20px;
margin-inline-end: 5px; margin-inline-end: 5px;
} }
@@ -48,9 +45,8 @@ Please see LICENSE files in the repository root for full details.
&.mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid { &.mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid {
color: $alert; color: $alert;
&::before { svg {
mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); color: $alert;
background-color: $alert;
} }
} }
} }
@@ -63,22 +63,6 @@ Please see LICENSE files in the repository root for full details.
font-weight: var(--cpd-font-weight-semibold); font-weight: var(--cpd-font-weight-semibold);
} }
&.mx_AccessibleButton_kind_confirm_sm {
background-color: var(--cpd-color-bg-action-primary-rest);
&::before {
mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
}
}
&.mx_AccessibleButton_kind_cancel_sm {
background-color: var(--cpd-color-bg-critical-primary);
&::before {
mask-image: url("@vector-im/compound-design-tokens/icons/close.svg");
}
}
&.mx_AccessibleButton_kind_icon, &.mx_AccessibleButton_kind_icon,
&.mx_AccessibleButton_kind_icon_primary, &.mx_AccessibleButton_kind_icon_primary,
&.mx_AccessibleButton_kind_icon_primary_outline { &.mx_AccessibleButton_kind_icon_primary_outline {
@@ -114,10 +98,6 @@ Please see LICENSE files in the repository root for full details.
text-decoration: underline; text-decoration: underline;
} }
&.mx_AccessibleButton_kind_secondary_content {
color: $secondary-content;
}
&.mx_AccessibleButton_kind_danger { &.mx_AccessibleButton_kind_danger {
color: var(--cpd-color-text-on-solid-primary); color: var(--cpd-color-text-on-solid-primary);
background-color: var(--cpd-color-bg-critical-primary); background-color: var(--cpd-color-bg-critical-primary);
@@ -175,25 +155,4 @@ Please see LICENSE files in the repository root for full details.
&.mx_AccessibleButton_kind_content_inline { &.mx_AccessibleButton_kind_content_inline {
display: inline; display: inline;
} }
&.mx_AccessibleButton_kind_confirm_sm,
&.mx_AccessibleButton_kind_cancel_sm {
padding: 0px;
width: 16px;
height: 16px;
border-radius: 100%;
position: relative;
display: block;
&::before {
content: "";
display: block;
position: absolute;
inset: 0;
background-color: #ffffff;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 80%;
}
}
} }
+2 -3
View File
@@ -9,8 +9,7 @@ Please see LICENSE files in the repository root for full details.
.mx_EventTileBubble.mx_CreateEvent { .mx_EventTileBubble.mx_CreateEvent {
margin: var(--EventTileBubble_margin-block) auto; margin: var(--EventTileBubble_margin-block) auto;
&::before { svg {
background-color: $header-panel-text-primary-color; color: $header-panel-text-primary-color;
mask-image: url("@vector-im/compound-design-tokens/icons/chat-solid.svg");
} }
} }
+1 -2
View File
@@ -18,8 +18,7 @@ Please see LICENSE files in the repository root for full details.
display: grid; display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content min-content; grid-template-columns: 24px minmax(0, 1fr) min-content min-content;
&::before, svg {
&::after {
position: relative; position: relative;
grid-column: 1; grid-column: 1;
grid-row: 1 / 3; grid-row: 1 / 3;
+5 -14
View File
@@ -11,21 +11,12 @@ Please see LICENSE files in the repository root for full details.
color: $muted-fg-color; color: $muted-fg-color;
vertical-align: middle; vertical-align: middle;
padding-left: 20px; svg {
position: relative;
&::before {
height: 14px; height: 14px;
width: 14px; width: 14px;
background-color: $muted-fg-color; display: inline-block;
mask-image: url("@vector-im/compound-design-tokens/icons/visibility-off.svg"); margin-right: var(--cpd-space-1-5x);
color: $muted-fg-color;
mask-repeat: no-repeat; vertical-align: -2px;
mask-position: center;
mask-size: contain;
content: "";
position: absolute;
top: 1px;
left: 0;
} }
} }
+8 -16
View File
@@ -34,25 +34,17 @@ Please see LICENSE files in the repository root for full details.
background-color: $system; background-color: $system;
border-radius: 20px; border-radius: 20px;
display: inline-block; display: inline-block;
width: 32px; width: 16px;
height: 32px; height: 16px;
position: relative; padding: var(--cpd-space-2x);
vertical-align: middle; vertical-align: middle;
margin-right: 12px; margin-right: 12px;
&::before { svg {
content: ""; color: $secondary-content;
mask-repeat: no-repeat; width: inherit;
mask-position: center; height: inherit;
mask-size: cover; display: block;
mask-image: url("@vector-im/compound-design-tokens/icons/attachment.svg");
background-color: $secondary-content;
width: 16px;
height: 16px;
position: absolute;
top: 8px;
left: 8px;
} }
} }
@@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
.mx_EventTileBubble.mx_MJitsiWidgetEvent { .mx_EventTileBubble.mx_MJitsiWidgetEvent {
&::before { svg {
background-color: $header-panel-text-primary-color; /* XXX: Variable abuse */ color: $header-panel-text-primary-color; /* XXX: Variable abuse */
mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg");
} }
} }
+9 -15
View File
@@ -9,25 +9,19 @@ Please see LICENSE files in the repository root for full details.
color: var(--cpd-color-text-primary); color: var(--cpd-color-text-primary);
.mx_ReactionsRow_addReactionButton { .mx_ReactionsRow_addReactionButton {
position: relative;
display: inline-block; display: inline-block;
visibility: hidden; /* show on hover of the .mx_EventTile */ visibility: hidden; /* show on hover of the .mx_EventTile */
width: 24px; width: 16px;
height: 24px; height: 16px;
padding: var(--cpd-space-1x);
vertical-align: middle; vertical-align: middle;
margin-left: 4px; margin-left: 4px;
margin-right: 4px; margin-right: 4px;
&::before { svg {
content: ""; height: inherit;
position: absolute; width: inherit;
height: 100%; color: $tertiary-content;
width: 100%;
mask-size: 16px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $tertiary-content;
mask-image: url("@vector-im/compound-design-tokens/icons/reaction-add.svg");
} }
&.mx_ReactionsRow_addReactionButton_active { &.mx_ReactionsRow_addReactionButton_active {
@@ -36,8 +30,8 @@ Please see LICENSE files in the repository root for full details.
&:hover, &:hover,
&.mx_ReactionsRow_addReactionButton_active { &.mx_ReactionsRow_addReactionButton_active {
&::before { svg {
background-color: $primary-content; color: $primary-content;
} }
} }
} }
+4 -13
View File
@@ -11,20 +11,11 @@ Please see LICENSE files in the repository root for full details.
color: $secondary-content; color: $secondary-content;
vertical-align: middle; vertical-align: middle;
padding-left: 20px; svg {
position: relative; margin-right: 6px;
&::before {
height: 14px; height: 14px;
width: 14px; width: 14px;
background-color: $icon-button-color; color: $icon-button-color;
mask-image: url("@vector-im/compound-design-tokens/icons/delete.svg"); vertical-align: -2px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
content: "";
position: absolute;
top: 1px;
left: 0;
} }
} }
@@ -9,28 +9,8 @@ Please see LICENSE files in the repository root for full details.
.mx_EventTileBubble.mx_cryptoEvent { .mx_EventTileBubble.mx_cryptoEvent {
margin: var(--EventTileBubble_margin-block) auto; margin: var(--EventTileBubble_margin-block) auto;
/* white infill for the transparency */ &.mx_cryptoEvent_icon svg {
&.mx_cryptoEvent_icon::before { color: $header-panel-text-primary-color;
background-color: #ffffff;
mask-image: url("@vector-im/compound-design-tokens/icons/lock-solid.svg");
mask-repeat: no-repeat;
mask-position: center;
mask-size: 80%;
}
&.mx_cryptoEvent_icon::after {
mask-image: url("@vector-im/compound-design-tokens/icons/lock-solid.svg");
background-color: $header-panel-text-primary-color;
}
&.mx_cryptoEvent_icon_verified::after {
mask-image: url("@vector-im/compound-design-tokens/icons/shield.svg");
background-color: $accent;
}
&.mx_cryptoEvent_icon_warning::after {
mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
background-color: $e2e-warning-color;
} }
.mx_cryptoEvent_state, .mx_cryptoEvent_state,
+10 -15
View File
@@ -61,24 +61,19 @@ Please see LICENSE files in the repository root for full details.
.mx_BaseCard_header_title_button--option { .mx_BaseCard_header_title_button--option {
position: relative; position: relative;
width: var(--BaseCard_header-button-size); width: calc(var(--BaseCard_header-button-size) - 4px);
height: var(--BaseCard_header-button-size); height: calc(var(--BaseCard_header-button-size) - 4px);
padding: 2px;
&::after { svg {
content: ""; width: inherit;
position: absolute; height: inherit;
inset-block-start: 0; display: block;
inset-inline-start: 0; color: $secondary-content;
height: 100%;
width: 100%;
mask-repeat: no-repeat;
mask-position: center;
mask-image: url("@vector-im/compound-design-tokens/icons/overflow-horizontal.svg");
background-color: $secondary-content;
} }
&:hover::after { &:hover svg {
background-color: $primary-content; color: $primary-content;
} }
} }
} }
-19
View File
@@ -21,22 +21,3 @@ Please see LICENSE files in the repository root for full details.
.mx_E2EIcon.mx_E2EIcon_inline { .mx_E2EIcon.mx_E2EIcon_inline {
display: inline-block; display: inline-block;
} }
.mx_E2EIcon_warning {
color: $e2e-warning-color;
}
.mx_E2EIcon_normal {
color: var(--cpd-color-icon-tertiary);
}
.mx_E2EIcon_verified,
.mx_E2EIcon_warning {
.mx_E2EIcon_normal::after {
background-color: white;
}
}
.mx_E2EIcon_verified {
color: $e2e-verified-color;
}
+3 -24
View File
@@ -836,32 +836,11 @@ $left-gutter: 64px;
width: 14px; width: 14px;
height: 14px; height: 14px;
display: block; display: block;
background-repeat: no-repeat;
background-size: contain;
&::after { svg {
content: ""; height: inherit;
width: inherit;
display: block; display: block;
position: absolute;
inset: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
&.mx_EventTile_e2eIcon_warning::after {
mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
background-color: $e2e-warning-color; /* red */
}
&.mx_EventTile_e2eIcon_normal::after {
mask-image: url("@vector-im/compound-design-tokens/icons/info.svg");
background-color: var(--cpd-color-icon-tertiary); /* grey */
}
&.mx_EventTile_e2eIcon_decryption_failure::after {
mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
background-color: var(--cpd-color-icon-tertiary);
} }
} }
+2 -3
View File
@@ -9,8 +9,7 @@ Please see LICENSE files in the repository root for full details.
.mx_EventTileBubble.mx_HistoryTile { .mx_EventTileBubble.mx_HistoryTile {
margin: var(--EventTileBubble_margin-block) auto; margin: var(--EventTileBubble_margin-block) auto;
&::before { svg {
background-color: $header-panel-text-primary-color; color: $header-panel-text-primary-color;
mask-image: url("@vector-im/compound-design-tokens/icons/visibility-off.svg");
} }
} }
-8
View File
@@ -11,14 +11,6 @@ Please see LICENSE files in the repository root for full details.
padding: 2px 0; padding: 2px 0;
font: var(--cpd-font-body-md-regular); font: var(--cpd-font-body-md-regular);
&.mx_ReplyTile_audio .mx_MFileBody_info_icon::before {
mask-image: url("@vector-im/compound-design-tokens/icons/volume-on-solid.svg");
}
&.mx_ReplyTile_video .mx_MFileBody_info_icon::before {
mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg");
}
> a { > a {
display: grid; display: grid;
grid-template: grid-template:
+5 -10
View File
@@ -44,16 +44,11 @@ Please see LICENSE files in the repository root for full details.
grid-column: 2; grid-column: 2;
background-color: $accent; background-color: $accent;
&::before { svg {
content: "";
display: inline-block; display: inline-block;
height: 40px; height: 20px;
width: 40px; width: 20px;
vertical-align: middle; padding: 10px;
mask-repeat: no-repeat; color: #fff; /* on all themes */
mask-size: 20px;
mask-position: center;
background-color: #fff; /* on all themes */
mask-image: url("@vector-im/compound-design-tokens/icons/voice-call-solid.svg");
} }
} }
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type RefObject } from "react"; import React, { type RefObject } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import type ResizeNotifier from "../../utils/ResizeNotifier"; import type ResizeNotifier from "../../utils/ResizeNotifier";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
@@ -43,6 +44,7 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
<div className="mx_RoomView_timeline"> <div className="mx_RoomView_timeline">
<ScrollPanel className="mx_RoomView_messagePanel"> <ScrollPanel className="mx_RoomView_messagePanel">
<EventTileBubble <EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon" className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("room|waiting_for_join_title", { brand })} title={_t("room|waiting_for_join_title", { brand })}
subtitle={_t("room|waiting_for_join_subtitle", { brand })} subtitle={_t("room|waiting_for_join_subtitle", { brand })}
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { LinkIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { LinkIcon, OverflowHorizontalIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { type ButtonEvent } from "../elements/AccessibleButton"; import { type ButtonEvent } from "../elements/AccessibleButton";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
@@ -90,7 +90,9 @@ const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
isExpanded={menuDisplayed} isExpanded={menuDisplayed}
ref={button} ref={button}
data-testid="threadlist-dropdown-button" data-testid="threadlist-dropdown-button"
/> >
<OverflowHorizontalIcon />
</ContextMenuTooltipButton>
{menuDisplayed && ( {menuDisplayed && (
<IconizedContextMenu <IconizedContextMenu
onFinished={closeThreadOptions} onFinished={closeThreadOptions}
@@ -10,9 +10,10 @@ import { Button, PasswordInput } from "@vector-im/compound-web";
import LockSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid"; import LockSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
import { debounce } from "lodash"; import { debounce } from "lodash";
import classNames from "classnames"; import classNames from "classnames";
import React, { type ChangeEvent, type FormEvent } from "react"; import React, { type ChangeEvent, type FormEvent, type ReactNode } from "react";
import { type SecretStorage } from "matrix-js-sdk/src/matrix"; import { type SecretStorage } from "matrix-js-sdk/src/matrix";
import { Flex } from "@element-hq/web-shared-components"; import { Flex } from "@element-hq/web-shared-components";
import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import { EncryptionCard } from "../../settings/encryption/EncryptionCard"; import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
@@ -142,27 +143,29 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
}; };
private getRecoveryKeyFeedback(): React.ReactNode | null { private getRecoveryKeyFeedback(): React.ReactNode | null {
let validationText: string; let content: ReactNode;
let classes: string | undefined; let classes: string | undefined;
if (this.state.recoveryKeyCorrect) { if (this.state.recoveryKeyCorrect) {
// The recovery key is good. Empty feedback. // The recovery key is good. Empty feedback.
validationText = "\xA0"; // &nbsp; content = "\xA0"; // &nbsp;
} else if (this.state.recoveryKeyCorrect === null) { } else if (this.state.recoveryKeyCorrect === null) {
// The input element is empty. Tell the user they can also use a passphrase. // The input element is empty. Tell the user they can also use a passphrase.
validationText = _t("encryption|access_secret_storage_dialog|alternatives"); content = _t("encryption|access_secret_storage_dialog|alternatives");
} else { } else {
// The entered key is not (yet) correct. Tell them so. // The entered key is not (yet) correct. Tell them so.
validationText = _t("encryption|access_secret_storage_dialog|key_validation_text|wrong_security_key"); content = (
classes = classNames({ <>
"mx_AccessSecretStorageDialog_recoveryKeyFeedback": true, <ErrorSolidIcon />
"mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid": true, {_t("encryption|access_secret_storage_dialog|key_validation_text|wrong_security_key")}
}); </>
);
classes = "mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid";
} }
return ( return (
<Flex align="center" className={classes}> <Flex align="center" className={classNames("mx_AccessSecretStorageDialog_recoveryKeyFeedback", classes)}>
{validationText} {content}
</Flex> </Flex>
); );
} }
@@ -30,7 +30,6 @@ export type AccessibleButtonKind =
| "primary_outline" | "primary_outline"
| "primary_sm" | "primary_sm"
| "secondary" | "secondary"
| "secondary_content"
| "content_inline" | "content_inline"
| "danger" | "danger"
| "danger_outline" | "danger_outline"
@@ -39,8 +38,6 @@ export type AccessibleButtonKind =
| "link" | "link"
| "link_inline" | "link_inline"
| "link_sm" | "link_sm"
| "confirm_sm"
| "cancel_sm"
| "icon" | "icon"
| "icon_primary" | "icon_primary"
| "icon_primary_outline"; | "icon_primary_outline";
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, type Ref, type ReactNode } from "react"; import React, { type JSX, type Ref, type ReactNode } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { ErrorSolidIcon, LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types"; import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -58,6 +59,7 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
return ( return (
<EventTileBubble <EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon" className="mx_cryptoEvent mx_cryptoEvent_icon"
title={stateEncrypted ? _t("common|state_encryption_enabled") : _t("common|encryption_enabled")} title={stateEncrypted ? _t("common|state_encryption_enabled") : _t("common|encryption_enabled")}
subtitle={subtitle} subtitle={subtitle}
@@ -69,6 +71,7 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
if (isRoomEncrypted) { if (isRoomEncrypted) {
return ( return (
<EventTileBubble <EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon" className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("common|encryption_enabled")} title={_t("common|encryption_enabled")}
subtitle={_t("timeline|m.room.encryption|disable_attempt")} subtitle={_t("timeline|m.room.encryption|disable_attempt")}
@@ -79,7 +82,8 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
return ( return (
<EventTileBubble <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon mx_cryptoEvent_icon_warning" icon={<ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />}
className="mx_cryptoEvent"
title={_t("timeline|m.room.encryption|disabled")} title={_t("timeline|m.room.encryption|disabled")}
subtitle={_t("timeline|m.room.encryption|unsupported")} subtitle={_t("timeline|m.room.encryption|unsupported")}
ref={ref} ref={ref}
@@ -11,6 +11,7 @@ import classNames from "classnames";
interface IProps { interface IProps {
className: string; className: string;
icon: JSX.Element;
title: string; title: string;
timestamp?: JSX.Element; timestamp?: JSX.Element;
subtitle?: ReactNode; subtitle?: ReactNode;
@@ -18,9 +19,10 @@ interface IProps {
ref?: Ref<HTMLDivElement>; ref?: Ref<HTMLDivElement>;
} }
const EventTileBubble = ({ className, title, timestamp, subtitle, children, ref }: IProps): JSX.Element => { const EventTileBubble = ({ className, icon, title, timestamp, subtitle, children, ref }: IProps): JSX.Element => {
return ( return (
<div className={classNames("mx_EventTileBubble", className)} ref={ref}> <div className={classNames("mx_EventTileBubble", className)} ref={ref}>
{icon}
<div className="mx_EventTileBubble_title">{title}</div> <div className="mx_EventTileBubble_title">{title}</div>
{subtitle && <div className="mx_EventTileBubble_subtitle">{subtitle}</div>} {subtitle && <div className="mx_EventTileBubble_subtitle">{subtitle}</div>}
{children} {children}
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { type JSX } from "react"; import React, { type JSX } from "react";
import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { type IBodyProps } from "./IBodyProps"; import { type IBodyProps } from "./IBodyProps";
@@ -34,6 +35,7 @@ const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
return ( return (
<span className="mx_HiddenBody" ref={ref}> <span className="mx_HiddenBody" ref={ref}>
<VisibilityOffIcon />
{text} {text}
</span> </span>
); );
+16 -2
View File
@@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details.
import React, { type AllHTMLAttributes, createRef } from "react"; import React, { type AllHTMLAttributes, createRef } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { type MediaEventContent } from "matrix-js-sdk/src/types"; import { type MediaEventContent } from "matrix-js-sdk/src/types";
import { MsgType } from "matrix-js-sdk/src/matrix";
import { Button } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";
import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import {
AttachmentIcon,
DownloadIcon,
VideoCallSolidIcon,
VolumeOnSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@@ -190,9 +196,17 @@ export default class MFileBody extends React.Component<IProps, IState> {
let placeholder: React.ReactNode = null; let placeholder: React.ReactNode = null;
if (showGenericPlaceholder) { if (showGenericPlaceholder) {
let icon = <AttachmentIcon />;
// MFileBody is not generally used for Audio/Video but can be as part of ReplyTile
if (this.content.msgtype === MsgType.Audio) {
icon = <VolumeOnSolidIcon />;
} else if (this.content.msgtype === MsgType.Video) {
icon = <VideoCallSolidIcon />;
}
placeholder = ( placeholder = (
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}> <AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
<span className="mx_MFileBody_info_icon" /> <span className="mx_MFileBody_info_icon">{icon}</span>
<TextWithTooltip tooltip={presentableTextForFile(this.content, _t("common|attachment"), true)}> <TextWithTooltip tooltip={presentableTextForFile(this.content, _t("common|attachment"), true)}>
<span className="mx_MFileBody_info_filename"> <span className="mx_MFileBody_info_filename">
{presentableTextForFile(this.content, _t("common|attachment"), true, true)} {presentableTextForFile(this.content, _t("common|attachment"), true, true)}
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react"; import React, { type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import WidgetStore from "../../../stores/WidgetStore"; import WidgetStore from "../../../stores/WidgetStore";
@@ -41,6 +42,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
// removed // removed
return ( return (
<EventTileBubble <EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent" className="mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_ended", { senderName })} title={_t("timeline|m.widget|jitsi_ended", { senderName })}
timestamp={this.props.timestamp} timestamp={this.props.timestamp}
@@ -50,6 +52,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
// modified // modified
return ( return (
<EventTileBubble <EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent" className="mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_updated", { senderName })} title={_t("timeline|m.widget|jitsi_updated", { senderName })}
subtitle={joinCopy} subtitle={joinCopy}
@@ -60,6 +63,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
// assume added // assume added
return ( return (
<EventTileBubble <EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent" className="mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_started", { senderName })} title={_t("timeline|m.widget|jitsi_started", { senderName })}
subtitle={joinCopy} subtitle={joinCopy}
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react"; import React, { type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver"; import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
@@ -73,6 +74,7 @@ const MKeyVerificationRequest: React.FC<Props> = ({ mxEvent, timestamp }) => {
return ( return (
<EventTileBubble <EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon" className="mx_cryptoEvent mx_cryptoEvent_icon"
title={title} title={title}
subtitle={subtitle} subtitle={subtitle}
@@ -11,6 +11,7 @@ import classNames from "classnames";
import { type MatrixEvent, MatrixEventEvent, type Relations, RelationsEvent } from "matrix-js-sdk/src/matrix"; import { type MatrixEvent, MatrixEventEvent, type Relations, RelationsEvent } from "matrix-js-sdk/src/matrix";
import { uniqBy } from "lodash"; import { uniqBy } from "lodash";
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
import { ReactionAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { isContentActionable } from "../../../utils/EventUtils"; import { isContentActionable } from "../../../utils/EventUtils";
@@ -54,7 +55,9 @@ const ReactButton: React.FC<IProps> = ({ mxEvent, reactions }) => {
}} }}
isExpanded={menuDisplayed} isExpanded={menuDisplayed}
ref={button} ref={button}
/> >
<ReactionAddIcon />
</ContextMenuTooltipButton>
{contextMenu} {contextMenu}
</React.Fragment> </React.Fragment>
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useContext, type JSX } from "react"; import React, { useContext, type JSX } from "react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -34,6 +35,7 @@ const RedactedBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
return ( return (
<span className="mx_RedactedBody" ref={ref} title={titleText}> <span className="mx_RedactedBody" ref={ref} title={titleText}>
<DeleteIcon />
{text} {text}
</span> </span>
); );
@@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, useCallback } from "react"; import React, { type JSX, useCallback } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { type MatrixEvent, type Room, type RoomState } from "matrix-js-sdk/src/matrix"; import { type MatrixEvent, type Room, type RoomState } from "matrix-js-sdk/src/matrix";
import { ChatSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
@@ -89,6 +90,7 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
return ( return (
<EventTileBubble <EventTileBubble
icon={<ChatSolidIcon />}
className="mx_CreateEvent" className="mx_CreateEvent"
title={_t("timeline|m.room.create|continuation")} title={_t("timeline|m.room.create|continuation")}
timestamp={timestamp} timestamp={timestamp}
@@ -128,6 +130,7 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
return ( return (
<EventTileBubble <EventTileBubble
icon={<ChatSolidIcon />}
className="mx_CreateEvent" className="mx_CreateEvent"
title={_t("timeline|m.room.create|continuation")} title={_t("timeline|m.room.create|continuation")}
subtitle={link} subtitle={link}
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, useContext, useEffect } from "react"; import React, { type JSX, useContext, useEffect } from "react";
import { type Room } from "matrix-js-sdk/src/matrix"; import { type Room } from "matrix-js-sdk/src/matrix";
import { OverflowHorizontalIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import BaseCard from "./BaseCard"; import BaseCard from "./BaseCard";
@@ -73,7 +74,9 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
onClick={openMenu} onClick={openMenu}
isExpanded={menuDisplayed} isExpanded={menuDisplayed}
label={_t("common|options")} label={_t("common|options")}
/> >
<OverflowHorizontalIcon />
</ContextMenuButton>
{contextMenu} {contextMenu}
</div> </div>
); );
+3 -3
View File
@@ -38,9 +38,9 @@ interface Props {
} }
const icons: Record<E2EStatus, JSX.Element> = { const icons: Record<E2EStatus, JSX.Element> = {
[E2EStatus.Warning]: <ErrorSolidIcon className="mx_E2EIcon_warning" />, [E2EStatus.Warning]: <ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />,
[E2EStatus.Normal]: <LockSolidIcon className="mx_E2EIcon_normal" />, [E2EStatus.Normal]: <LockSolidIcon color="var(--cpd-color-icon-tertiary)" />,
[E2EStatus.Verified]: <ShieldIcon className="mx_E2EIcon_verified" />, [E2EStatus.Verified]: <ShieldIcon color="var(--cpd-color-icon-success-primary)" />,
}; };
const E2EIcon: React.FC<Props> = ({ isUser, status, className, size, onClick, hideTooltip, tooltipPlacement }) => { const E2EIcon: React.FC<Props> = ({ isUser, status, className, size, onClick, hideTooltip, tooltipPlacement }) => {
+13 -5
View File
@@ -35,6 +35,7 @@ import {
} from "matrix-js-sdk/src/crypto-api"; } from "matrix-js-sdk/src/crypto-api";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import { uniqueId } from "lodash"; import { uniqueId } from "lodash";
import { ErrorSolidIcon, InfoIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import ReplyChain from "../elements/ReplyChain"; import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -1518,13 +1519,13 @@ function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "ic
} }
enum E2ePadlockIcon { enum E2ePadlockIcon {
/** grey shield */ /** Compound Info icon in grey */
Normal = "normal", Normal = "normal",
/** red shield with (!) */ /** Compound ErrorSolid icon in red */
Warning = "warning", Warning = "warning",
/** key in grey circle */ /** Compound ErrorSolid icon in grey */
DecryptionFailure = "decryption_failure", DecryptionFailure = "decryption_failure",
} }
@@ -1534,6 +1535,12 @@ interface IE2ePadlockProps {
} }
class E2ePadlock extends React.Component<IE2ePadlockProps> { class E2ePadlock extends React.Component<IE2ePadlockProps> {
private static icons: Record<E2ePadlockIcon, JSX.Element> = {
[E2ePadlockIcon.Normal]: <InfoIcon color="var(--cpd-color-icon-tertiary)" />,
[E2ePadlockIcon.Warning]: <ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />,
[E2ePadlockIcon.DecryptionFailure]: <ErrorSolidIcon color="var(--cpd-color-icon-tertiary)" />,
};
public constructor(props: IE2ePadlockProps) { public constructor(props: IE2ePadlockProps) {
super(props); super(props);
@@ -1543,12 +1550,13 @@ class E2ePadlock extends React.Component<IE2ePadlockProps> {
} }
public render(): ReactNode { public render(): ReactNode {
const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
// We specify isTriggerInteractive=true and make the div interactive manually as a workaround for // We specify isTriggerInteractive=true and make the div interactive manually as a workaround for
// https://github.com/element-hq/compound/issues/294 // https://github.com/element-hq/compound/issues/294
return ( return (
<Tooltip label={this.props.title} isTriggerInteractive={true}> <Tooltip label={this.props.title} isTriggerInteractive={true}>
<div className={classes} tabIndex={0} aria-label={_t("timeline|e2e_state")} /> <div className="mx_EventTile_e2eIcon" tabIndex={0} aria-label={_t("timeline|e2e_state")}>
{E2ePadlock.icons[this.props.icon]}
</div>
</Tooltip> </Tooltip>
); );
} }
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react"; import React from "react";
import { EventTimeline } from "matrix-js-sdk/src/matrix"; import { EventTimeline } from "matrix-js-sdk/src/matrix";
import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import EventTileBubble from "../messages/EventTileBubble"; import EventTileBubble from "../messages/EventTileBubble";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -28,6 +29,7 @@ const HistoryTile: React.FC = () => {
return ( return (
<EventTileBubble <EventTileBubble
icon={<VisibilityOffIcon />}
className="mx_HistoryTile" className="mx_HistoryTile"
title={_t("timeline|historical_messages_unavailable")} title={_t("timeline|historical_messages_unavailable")}
subtitle={subtitle} subtitle={subtitle}
+3 -1
View File
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, useContext } from "react"; import React, { type JSX, useContext } from "react";
import { EventType, type Room, type User, type MatrixClient } from "matrix-js-sdk/src/matrix"; import { EventType, type Room, type User, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
@@ -291,7 +292,8 @@ const NewRoomIntro: React.FC = () => {
<li className="mx_NewRoomIntro"> <li className="mx_NewRoomIntro">
{!hasExpectedEncryptionSettings(cli, room) && ( {!hasExpectedEncryptionSettings(cli, room) && (
<EventTileBubble <EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon_warning" icon={<ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />}
className="mx_cryptoEvent"
title={_t("room|intro|unencrypted_warning")} title={_t("room|intro|unencrypted_warning")}
subtitle={subtitle} subtitle={subtitle}
/> />
-2
View File
@@ -112,8 +112,6 @@ export default class ReplyTile extends React.PureComponent<IProps> {
const classes = classNames("mx_ReplyTile", { const classes = classNames("mx_ReplyTile", {
mx_ReplyTile_inline: msgType === MsgType.Emote, mx_ReplyTile_inline: msgType === MsgType.Emote,
mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(), mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
mx_ReplyTile_audio: msgType === MsgType.Audio,
mx_ReplyTile_video: msgType === MsgType.Video,
}); });
let permalink = "#"; let permalink = "#";
+4 -1
View File
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { type JSX } from "react"; import React, { type JSX } from "react";
import { VoiceCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@@ -59,7 +60,9 @@ class DialPadButton extends React.PureComponent<DigitButtonProps | DialButtonPro
className="mx_DialPad_button mx_DialPad_dialButton" className="mx_DialPad_button mx_DialPad_dialButton"
onClick={this.onClick} onClick={this.onClick}
aria-label={_t("voip|dial")} aria-label={_t("voip|dial")}
/> >
<VoiceCallSolidIcon />
</AccessibleButton>
); );
} }
} }
@@ -594,12 +594,14 @@ describe("RoomView", () => {
const { container } = await renderRoomView(); const { container } = await renderRoomView();
// We no longer show the grey shield for encrypted rooms, so it should not be there. // We no longer show the grey shield for encrypted rooms, so it should not be there.
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_normal")).not.toBeInTheDocument()); await waitFor(() => expect(container.querySelector(".mx_E2EIcon")).not.toBeInTheDocument());
const verificationStatus = new UserVerificationStatus(true, true, false); const verificationStatus = new UserVerificationStatus(true, true, false);
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(verificationStatus); jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(verificationStatus);
cli.emit(CryptoEvent.UserTrustStatusChanged, cli.getSafeUserId(), verificationStatus); cli.emit(CryptoEvent.UserTrustStatusChanged, cli.getSafeUserId(), verificationStatus);
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_verified")).toBeInTheDocument()); await waitFor(() =>
expect(container.querySelector(".mx_E2EIcon")).toHaveAccessibleName("Everyone in this room is verified"),
);
}); });
describe("video rooms", () => { describe("video rooms", () => {
@@ -63,6 +63,17 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
<div <div
class="mx_EventTileBubble mx_HistoryTile" class="mx_EventTileBubble mx_HistoryTile"
> >
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m16.1 13.3-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.424-.2.863-.3A4.2 4.2 0 0 1 12 7q1.875 0 3.188 1.312Q16.5 9.625 16.5 11.5q0 .5-.1.938t-.3.862m3.2 3.15-1.45-1.4a11 11 0 0 0 1.688-1.588A9 9 0 0 0 20.8 11.5q-1.25-2.524-3.588-4.013Q14.875 6 12 6q-.724 0-1.425.1a10 10 0 0 0-1.375.3L7.65 4.85A11.1 11.1 0 0 1 12 4q3.575 0 6.425 1.887T22.7 10.8a.8.8 0 0 1 .1.313q.025.188.025.387a2 2 0 0 1-.125.7 10.9 10.9 0 0 1-3.4 4.25m-.2 5.45-3.5-3.45q-.874.274-1.762.413Q12.95 19 12 19q-3.575 0-6.425-1.887T1.3 12.2a.8.8 0 0 1-.1-.312 3 3 0 0 1 0-.763.8.8 0 0 1 .1-.3Q1.825 9.7 2.55 8.75A13.3 13.3 0 0 1 4.15 7L2.075 4.9a.93.93 0 0 1-.275-.688q0-.412.3-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275l17 17q.275.275.288.688a.93.93 0 0 1-.288.712.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275M5.55 8.4q-.725.65-1.325 1.425A9 9 0 0 0 3.2 11.5q1.25 2.524 3.588 4.012T12 17q.5 0 .975-.062.475-.063.975-.138l-.9-.95q-.274.075-.525.113A3.5 3.5 0 0 1 12 16q-1.875 0-3.187-1.312Q7.5 13.375 7.5 11.5q0-.274.038-.525.037-.25.112-.525z"
/>
</svg>
<div <div
class="mx_EventTileBubble_title" class="mx_EventTileBubble_title"
> >
@@ -152,8 +152,20 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
class="mx_NewRoomIntro" class="mx_NewRoomIntro"
> >
<div <div
class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning" class="mx_EventTileBubble mx_cryptoEvent"
> >
<svg
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
<div <div
class="mx_EventTileBubble_title" class="mx_EventTileBubble_title"
> >
@@ -322,8 +334,20 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
class="mx_NewRoomIntro" class="mx_NewRoomIntro"
> >
<div <div
class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning" class="mx_EventTileBubble mx_cryptoEvent"
> >
<svg
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
<div <div
class="mx_EventTileBubble_title" class="mx_EventTileBubble_title"
> >
@@ -687,6 +711,17 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<div <div
class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon" class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
> >
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div <div
class="mx_EventTileBubble_title" class="mx_EventTileBubble_title"
> >
@@ -2985,7 +3020,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="width: 12px; height: 12px;" style="width: 12px; height: 12px;"
> >
<svg <svg
class="mx_E2EIcon_verified" color="var(--cpd-color-icon-success-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -27,7 +27,7 @@ exports[`<UntrustedDeviceDialog /> should display the dialog for the device of a
style="width: 24px; height: 24px;" style="width: 24px; height: 24px;"
> >
<svg <svg
class="mx_E2EIcon_warning" color="var(--cpd-color-icon-critical-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -116,7 +116,7 @@ exports[`<UntrustedDeviceDialog /> should display the dialog for the device of t
style="width: 24px; height: 24px;" style="width: 24px; height: 24px;"
> >
<svg <svg
class="mx_E2EIcon_warning" color="var(--cpd-color-icon-critical-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -37,7 +37,7 @@ exports[`VerificationRequestDialog After scanning QR, shows confirmation dialog
style="width: 128px; height: 128px;" style="width: 128px; height: 128px;"
> >
<svg <svg
class="mx_E2EIcon_verified" color="var(--cpd-color-icon-success-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -324,7 +324,7 @@ exports[`VerificationRequestDialog Shows a successful message if verification fi
style="width: 128px; height: 128px;" style="width: 128px; height: 128px;"
> >
<svg <svg
class="mx_E2EIcon_verified" color="var(--cpd-color-icon-success-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -73,7 +73,7 @@ exports[`<Users /> should render a single device - signed by owner 1`] = `
data-testid="e2e-icon" data-testid="e2e-icon"
> >
<svg <svg
class="mx_E2EIcon_normal" color="var(--cpd-color-icon-tertiary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -237,7 +237,7 @@ exports[`<Users /> should render a single device - unsigned 1`] = `
data-testid="e2e-icon" data-testid="e2e-icon"
> >
<svg <svg
class="mx_E2EIcon_warning" color="var(--cpd-color-icon-critical-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -401,7 +401,7 @@ exports[`<Users /> should render a single device - verified by cross-signing 1`]
data-testid="e2e-icon" data-testid="e2e-icon"
> >
<svg <svg
class="mx_E2EIcon_verified" color="var(--cpd-color-icon-success-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -555,7 +555,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon" data-testid="e2e-icon"
> >
<svg <svg
class="mx_E2EIcon_verified" color="var(--cpd-color-icon-success-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -587,7 +587,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon" data-testid="e2e-icon"
> >
<svg <svg
class="mx_E2EIcon_verified" color="var(--cpd-color-icon-success-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -613,7 +613,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon" data-testid="e2e-icon"
> >
<svg <svg
class="mx_E2EIcon_normal" color="var(--cpd-color-icon-tertiary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -637,7 +637,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon" data-testid="e2e-icon"
> >
<svg <svg
class="mx_E2EIcon_warning" color="var(--cpd-color-icon-critical-primary)"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -28,7 +28,19 @@ exports[`AppTile destroys non-persisted right panel widget on room change 1`] =
class="mx_AccessibleButton mx_BaseCard_header_title_button--option" class="mx_AccessibleButton mx_BaseCard_header_title_button--option"
role="button" role="button"
tabindex="0" tabindex="0"
/> >
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div> </div>
<button <button
aria-labelledby="_r_0_" aria-labelledby="_r_0_"
@@ -85,4 +85,31 @@ describe("<MFileBody/>", () => {
expect(getByRole("link", { name: "Download" })).toBeInTheDocument(); expect(getByRole("link", { name: "Download" })).toBeInTheDocument();
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it.each(["m.file", "m.audio", "m.video"])("should show %s generic placeholder", async (msgtype) => {
const mediaEvent = new MatrixEvent({
room_id: "!room:server",
sender: userId,
type: EventType.RoomMessage,
content: {
body: "alt",
msgtype,
url: "mxc://server/image",
},
});
const { container, getByRole } = render(
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.File } as any)}>
<MFileBody
{...props}
mxEvent={mediaEvent}
mediaEventHelper={new MediaEventHelper(mediaEvent)}
showGenericPlaceholder={true}
/>
</ScopedRoomContextProvider>,
);
expect(getByRole("button", { name: "alt" })).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
}); });
@@ -35,3 +35,126 @@ exports[`<MFileBody/> should show a download button in file rendering type 1`] =
</span> </span>
</div> </div>
`; `;
exports[`<MFileBody/> should show m.audio generic placeholder 1`] = `
<div>
<span
class="mx_MFileBody"
>
<div
class="mx_AccessibleButton mx_MediaBody mx_MFileBody_info"
role="button"
tabindex="0"
>
<span
class="mx_MFileBody_info_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 14v-4a2 2 0 0 1 2-2h2l3.293-3.293c.63-.63 1.707-.184 1.707.707v13.172c0 .89-1.077 1.337-1.707.707L7 16H5a2 2 0 0 1-2-2m11.122-5.536a1 1 0 0 1 1.414 0A5 5 0 0 1 17 12c0 1.38-.56 2.632-1.464 3.536a1 1 0 0 1-1.415-1.415 3 3 0 0 0 .88-2.121c0-.829-.335-1.577-.88-2.121a1 1 0 0 1 0-1.415"
/>
<path
d="M16.95 5.636a1 1 0 0 1 1.414 0A8.98 8.98 0 0 1 21 12a8.98 8.98 0 0 1-2.636 6.364 1 1 0 0 1-1.414-1.414A6.98 6.98 0 0 0 19 12a6.98 6.98 0 0 0-2.05-4.95 1 1 0 0 1 0-1.414"
/>
</svg>
</span>
<span
aria-labelledby="_r_6_"
tabindex="0"
>
<span
class="mx_MFileBody_info_filename"
>
alt
</span>
</span>
</div>
</span>
</div>
`;
exports[`<MFileBody/> should show m.file generic placeholder 1`] = `
<div>
<span
class="mx_MFileBody"
>
<div
class="mx_AccessibleButton mx_MediaBody mx_MFileBody_info"
role="button"
tabindex="0"
>
<span
class="mx_MFileBody_info_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</span>
<span
aria-labelledby="_r_0_"
tabindex="0"
>
<span
class="mx_MFileBody_info_filename"
>
alt
</span>
</span>
</div>
</span>
</div>
`;
exports[`<MFileBody/> should show m.video generic placeholder 1`] = `
<div>
<span
class="mx_MFileBody"
>
<div
class="mx_AccessibleButton mx_MediaBody mx_MFileBody_info"
role="button"
tabindex="0"
>
<span
class="mx_MFileBody_info_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
</span>
<span
aria-labelledby="_r_c_"
tabindex="0"
>
<span
class="mx_MFileBody_info_filename"
>
alt
</span>
</span>
</div>
</span>
</div>
`;
@@ -183,7 +183,19 @@ exports[`<MImageBody/> should render MFileBody for svg with no thumbnail 1`] = `
> >
<span <span
class="mx_MFileBody_info_icon" class="mx_MFileBody_info_icon"
/> >
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 22q-2.3 0-3.9-1.6T6 16.5V6q0-1.65 1.175-2.825T10 2t2.825 1.175T14 6v9.5q0 1.05-.725 1.775T11.5 18t-1.775-.725T9 15.5V6.75A.73.73 0 0 1 9.75 6a.73.73 0 0 1 .75.75v8.75q0 .424.287.712.288.288.713.288.424 0 .713-.288a.97.97 0 0 0 .287-.712V6q0-1.05-.725-1.775T10 3.5t-1.775.725T7.5 6v10.5q0 1.65 1.175 2.825T11.5 20.5t2.825-1.175T15.5 16.5V6.75a.73.73 0 0 1 .75-.75.73.73 0 0 1 .75.75v9.75q0 2.3-1.6 3.9T11.5 22"
/>
</svg>
</span>
<span <span
aria-labelledby="_r_0_" aria-labelledby="_r_0_"
tabindex="0" tabindex="0"
@@ -5,6 +5,17 @@ exports[`<RoomPredecessorTile /> Renders as expected 1`] = `
<div <div
class="mx_EventTileBubble mx_CreateEvent" class="mx_EventTileBubble mx_CreateEvent"
> >
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.95 16.3 1.5 21.25a.94.94 0 0 0 .25 1 .94.94 0 0 0 1 .25l4.95-1.45a10.2 10.2 0 0 0 2.1.712Q10.875 22 12 22a9.7 9.7 0 0 0 3.9-.788 10.1 10.1 0 0 0 3.175-2.137q1.35-1.35 2.137-3.175A9.7 9.7 0 0 0 22 12a9.7 9.7 0 0 0-.788-3.9 10.1 10.1 0 0 0-2.137-3.175q-1.35-1.35-3.175-2.137A9.7 9.7 0 0 0 12 2a9.7 9.7 0 0 0-3.9.788 10.1 10.1 0 0 0-3.175 2.137Q3.575 6.275 2.788 8.1A9.7 9.7 0 0 0 2 12q0 1.125.238 2.2.237 1.076.712 2.1"
/>
</svg>
<div <div
class="mx_EventTileBubble_title" class="mx_EventTileBubble_title"
> >
@@ -269,8 +269,8 @@ describe("EventTile", () => {
// there should be a warning shield // there should be a warning shield
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1); expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain( expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName(
"mx_EventTile_e2eIcon_warning", "Encrypted by a device not verified by its owner.",
); );
}); });
@@ -298,10 +298,13 @@ describe("EventTile", () => {
it.each([ it.each([
[EventShieldReason.UNKNOWN, "Unknown error"], [EventShieldReason.UNKNOWN, "Unknown error"],
[EventShieldReason.UNVERIFIED_IDENTITY, "unverified user"], [EventShieldReason.UNVERIFIED_IDENTITY, "Encrypted by an unverified user."],
[EventShieldReason.UNSIGNED_DEVICE, "device not verified by its owner"], [EventShieldReason.UNSIGNED_DEVICE, "Encrypted by a device not verified by its owner."],
[EventShieldReason.UNKNOWN_DEVICE, "unknown or deleted device"], [EventShieldReason.UNKNOWN_DEVICE, "Encrypted by an unknown or deleted device."],
[EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"], [
EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
"The authenticity of this encrypted message can't be guaranteed on this device.",
],
[EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"], [EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
[EventShieldReason.SENT_IN_CLEAR, "Not encrypted"], [EventShieldReason.SENT_IN_CLEAR, "Not encrypted"],
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity was reset"], [EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity was reset"],
@@ -326,12 +329,7 @@ describe("EventTile", () => {
const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon"); const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon");
expect(e2eIcons).toHaveLength(1); expect(e2eIcons).toHaveLength(1);
expect(e2eIcons[0].classList).toContain("mx_EventTile_e2eIcon_normal"); expect(e2eIcons[0]).toHaveAccessibleName(expectedText);
fireEvent.focus(e2eIcons[0]);
expect(e2eIcons[0].getAttribute("aria-labelledby")).toBeTruthy();
expect(document.getElementById(e2eIcons[0].getAttribute("aria-labelledby")!)).toHaveTextContent(
expectedText,
);
}); });
describe("undecryptable event", () => { describe("undecryptable event", () => {
@@ -360,8 +358,8 @@ describe("EventTile", () => {
expect(eventTiles).toHaveLength(1); expect(eventTiles).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1); expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain( expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName(
"mx_EventTile_e2eIcon_decryption_failure", "This message could not be decrypted",
); );
}); });
@@ -436,8 +434,8 @@ describe("EventTile", () => {
// check it was updated // check it was updated
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1); expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain( expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName(
"mx_EventTile_e2eIcon_warning", "Encrypted by a device not verified by its owner.",
); );
}); });
@@ -481,9 +479,7 @@ describe("EventTile", () => {
// check it was updated // check it was updated
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1); expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain( expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName("Not encrypted");
"mx_EventTile_e2eIcon_warning",
);
}); });
}); });
@@ -330,10 +330,10 @@ describe("SendWysiwygComposer", () => {
"Left icon when %s", "Left icon when %s",
({ isRichTextEnabled }) => { ({ isRichTextEnabled }) => {
it.each([ it.each([
[E2EStatus.Verified, "mx_E2EIcon_verified"], [E2EStatus.Verified, "Everyone in this room is verified"],
[E2EStatus.Warning, "mx_E2EIcon_warning"], [E2EStatus.Warning, "Someone is using an unknown session"],
[undefined, undefined], [undefined, undefined],
])("Should render left icon when e2eStatus is %s", async (e2eStatus, expectedClass) => { ])("Should render left icon when e2eStatus is %s", async (e2eStatus, expectedLabel) => {
// When // When
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, undefined, e2eStatus); customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, undefined, e2eStatus);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
@@ -341,9 +341,9 @@ describe("SendWysiwygComposer", () => {
// Then // Then
expect(leftIcon).toBeInTheDocument(); expect(leftIcon).toBeInTheDocument();
expect(leftIcon).toHaveClass("mx_E2EIcon"); expect(leftIcon).toHaveClass("mx_E2EIcon");
if (expectedClass) { if (expectedLabel) {
// eslint-disable-next-line jest/no-conditional-expect // eslint-disable-next-line jest/no-conditional-expect
expect(leftIcon.querySelector("svg")).toHaveClass(expectedClass); expect(leftIcon).toHaveAccessibleName(expectedLabel);
} else { } else {
// eslint-disable-next-line jest/no-conditional-expect // eslint-disable-next-line jest/no-conditional-expect
expect(leftIcon.querySelector("svg")).not.toBeInTheDocument(); expect(leftIcon.querySelector("svg")).not.toBeInTheDocument();