Compare commits

...

33 Commits

Author SHA1 Message Date
Michael Telatynski
20be92ab7e Merge branch 'develop' of ssh://github.com/element-hq/element-web into t3chguy/css-masks-again
# Conflicts:
#	res/css/views/rooms/_EventTile.pcss
2025-12-16 14:47:08 +00:00
Michael Telatynski
1afa202ef5 Add tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 14:02:16 +00:00
Michael Telatynski
9d3cba1621 Add missing screenshots
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 13:38:18 +00:00
Michael Telatynski
e6d572c413 Merge branch 't3chguy/a11y-icons-dec15' of ssh://github.com/element-hq/element-web into t3chguy/css-masks-again 2025-12-16 13:31:30 +00:00
Michael Telatynski
ced4456c16 Merge branch 'develop' into t3chguy/a11y-icons-dec15 2025-12-16 13:31:06 +00:00
Michael Telatynski
654a9eeb3a Merge branch 'develop' of ssh://github.com/element-hq/element-web into t3chguy/css-masks-again
# Conflicts:
#	res/css/views/rooms/_EventTile.pcss
2025-12-16 13:03:13 +00:00
Michael Telatynski
dc0763f58a Fix overflow in base card size
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 13:02:38 +00:00
Michael Telatynski
19d8798dc0 Update tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 13:02:30 +00:00
Michael Telatynski
d9f6355693 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:58:50 +00:00
Michael Telatynski
4613c44f47 Update tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:31:01 +00:00
Michael Telatynski
0961e6f6e4 Switch to rendering svg icons rather than masking them in HiddenBody
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:17:12 +00:00
Michael Telatynski
5c2d89a0d7 Switch to rendering svg icons rather than masking them in AccessSecretStorageDialog
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:16:01 +00:00
Michael Telatynski
40120cc226 Merge branch 't3chguy/a11y-icons-dec15' of ssh://github.com/element-hq/element-web into t3chguy/css-masks-again
# Conflicts:
#	res/css/views/rooms/_EventTile.pcss
2025-12-16 12:12:59 +00:00
Michael Telatynski
8fef5c5ec6 Switch to rendering svg icons rather than masking them in DialPad
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:08:35 +00:00
Michael Telatynski
9864294575 Tidy up E2EIcon
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:07:23 +00:00
Michael Telatynski
d1a0875d7f Switch to rendering svg icons rather than masking them in E2ePadlock
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:06:46 +00:00
Michael Telatynski
523fba271f Switch to rendering svg icons rather than masking them in RedactedBody
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:06:27 +00:00
Michael Telatynski
99690f6314 Switch to rendering svg icons rather than masking them in BaseCard
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:06:14 +00:00
Michael Telatynski
58b75e0fe3 Switch to rendering svg icons rather than masking them in MFileBody
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:05:54 +00:00
Michael Telatynski
ed59a53734 Switch to rendering svg icons rather than masking them in ReactionsRow
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:05:46 +00:00
Michael Telatynski
b243c94093 Switch to rendering svg icons rather than masking them in EventTileBubble
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:04:11 +00:00
Michael Telatynski
7cd9ec4fbd Remove unused AccessibleButton kinds
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 12:01:54 +00:00
Michael Telatynski
9fa23d3c19 Add missing snapshot
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 10:45:15 +00:00
Michael Telatynski
05c64bd6bc Add test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 10:34:04 +00:00
Michael Telatynski
c4d8c3e85f Update screenshot
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 09:44:26 +00:00
Michael Telatynski
82390d6a23 Remove unused icon underfill
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-16 09:42:54 +00:00
Michael Telatynski
2c2b607d3c Update screenshot
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 18:03:24 +00:00
Michael Telatynski
60e574f37d Update snapshots
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 17:55:37 +00:00
Michael Telatynski
d2ba05a3d8 Fix badly rendered icon in ThreadPanel
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 17:54:27 +00:00
Michael Telatynski
5c28ed6738 Fix badly rendered icon in Space menus
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 17:40:47 +00:00
Michael Telatynski
1808a16ed3 Fix badly rendered icon in RoomPreviewCard
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 17:17:58 +00:00
Michael Telatynski
bff234749f Fix badly rendered icon in JoinRuleDropdown
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 17:09:53 +00:00
Michael Telatynski
0733bebc38 Switch to rendering svg icons rather than masking them in SpacePanel
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-15 17:09:38 +00:00
63 changed files with 594 additions and 500 deletions

View File

@@ -351,7 +351,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const composer = thread.locator(".mx_MessageComposer--compact");
// Assert that the reply preview contains audio ReplyTile the file info button
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();
// 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");
// 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();
});
});

View File

@@ -31,15 +31,11 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => {
// check the invite message
await expect(
page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon_warning"),
).not.toBeVisible();
await expect(page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
// Bob sends a response
await bob.sendMessage(bobRoomId, "Hoo!");
await expect(
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"),
).not.toBeVisible();
await expect(page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
};
const bobJoin = async (page: Page, bob: Bot) => {

View File

@@ -30,69 +30,80 @@ test.describe("Cryptography", function () {
test.describe("decryption failure messages", () => {
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
test("should handle device-relative historical messages", async ({
homeserver,
page,
app,
credentials,
user,
}) => {
test.setTimeout(60000);
test(
"should handle device-relative historical messages",
{ tag: "@screenshot" },
async ({ homeserver, page, app, credentials, user }) => {
test.setTimeout(60000);
// Start with a logged-in session, without key backup, and send a message.
await createRoom(page, "Test room", true);
await sendMessageInCurrentRoom(page, "test test");
// Start with a logged-in session, without key backup, and send a message.
await createRoom(page, "Test room", true);
await sendMessageInCurrentRoom(page, "test test");
// Log out, discarding the key for the sent message.
await logOutOfElement(page, true);
// Log out, discarding the key for the sent message.
await logOutOfElement(page, true);
// Log in again, and see how the message looks.
await logIntoElement(page, credentials);
await app.viewRoomByName("Test room");
const lastTile = page.locator(".mx_EventTile").last();
await expect(lastTile).toContainText("Historical messages are not available on this device");
await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
// Log in again, and see how the message looks.
await logIntoElement(page, credentials);
await app.viewRoomByName("Test room");
const lastTile = page.locator(".mx_EventTile").last();
await expect(lastTile).toContainText("Historical messages are not available on this device");
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.
const secretStorageKey = await enableKeyBackup(app);
await app.viewRoomByName("Test room");
await sendMessageInCurrentRoom(page, "test2 test2");
// Now, we set up key backup, and then send another message.
const secretStorageKey = await enableKeyBackup(app);
await app.viewRoomByName("Test room");
await sendMessageInCurrentRoom(page, "test2 test2");
// 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.
await page.waitForTimeout(10000);
// 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.
await page.waitForTimeout(10000);
// Finally, log out again, and back in, skipping verification for now, and see what we see.
await logOutOfElement(page);
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: "I'll verify later" }).click();
await app.viewRoomByName("Test room");
// Finally, log out again, and back in, skipping verification for now, and see what we see.
await logOutOfElement(page);
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: "I'll verify later" }).click();
await app.viewRoomByName("Test room");
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
await page.waitForTimeout(1000);
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
await page.waitForTimeout(1000);
// There should be two historical events in the timeline
const tiles = await page.locator(".mx_EventTile").all();
expect(tiles.length).toBeGreaterThanOrEqual(2);
// look at the last two tiles only
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.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
}
// There should be two historical events in the timeline
const tiles = await page.locator(".mx_EventTile").all();
expect(tiles.length).toBeGreaterThanOrEqual(2);
// look at the last two tiles only
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.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
"This message could not be decrypted",
);
}
// Now verify our device (setting up key backup), and check what happens
await verifySession(app, secretStorageKey);
const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2);
// Now verify our device (setting up key backup), and check what happens
await verifySession(app, secretStorageKey);
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.
await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message");
await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
// 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].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
"This message could not be decrypted",
);
// The second message should now be decrypted, with a grey shield
await expect(tilesAfterVerify[1]).toContainText("test2 test2");
await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible();
});
// The second message should now be decrypted, with a grey shield
await expect(tilesAfterVerify[1]).toContainText("test2 test2");
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.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
// 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].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
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
// standard UTD 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 ({

View File

@@ -68,7 +68,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await doTwoWaySasVerification(page, verifier);
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();
// Check that our device is now cross-signed

View File

@@ -77,11 +77,8 @@ test.describe("Cryptography", function () {
const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("Unable to decrypt message");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"This message could not be decrypted",
);
await expect(lastE2eIcon).toHaveAccessibleName("This message could not be decrypted");
await expect(lastE2eIcon).toMatchScreenshot("event-shield-utd.png");
/* Should show a red padlock for an unencrypted message in an e2e room */
await bob.evaluate(
@@ -99,10 +96,8 @@ test.describe("Cryptography", function () {
);
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 lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
/* Should show no padlock for an unverified user */
// bob sends a valid event
@@ -133,11 +128,8 @@ test.describe("Cryptography", function () {
/* should show red padlock for a message from an unverified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
await expect(lastTile).toContainText("test encrypted from unverified");
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastTileE2eIcon.focus();
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
"Encrypted by a device not verified by its owner.",
);
await expect(lastTileE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
/* 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
@@ -153,64 +145,58 @@ test.describe("Cryptography", function () {
await app.viewRoomByName("TestRoom");
await expect(last).toContainText("test encrypted from unverified");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Encrypted by a device not verified by its owner.",
);
await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
},
);
test("Should show a grey padlock for a key restored from backup", async ({
page,
app,
bot: bob,
homeserver,
user: aliceCredentials,
}) => {
test.slow();
const securityKey = await enableKeyBackup(app);
test(
"Should show a grey padlock for a key restored from backup",
{ tag: "@screenshot" },
async ({ page, app, bot: bob, homeserver, user: aliceCredentials }) => {
test.slow();
const securityKey = await enableKeyBackup(app);
// bob sends a valid event
await bob.sendMessage(testRoomId, "test encrypted 1");
// bob sends a valid event
await bob.sendMessage(testRoomId, "test encrypted 1");
const lastTile = page.locator(".mx_EventTile_last");
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
await expect(lastTile).toContainText("test encrypted 1");
// no e2e icon
await expect(lastTileE2eIcon).not.toBeVisible();
const lastTile = page.locator(".mx_EventTile_last");
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
await expect(lastTile).toContainText("test encrypted 1");
// no e2e icon
await expect(lastTileE2eIcon).not.toBeVisible();
// 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.
await page.waitForTimeout(10000);
// 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.
await page.waitForTimeout(10000);
/* log out, and back in */
await logOutOfElement(page);
// 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
await page.addInitScript(() => {
// When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures
// 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.
window.localStorage.clear();
});
await page.reload();
await logIntoElementAndVerify(page, aliceCredentials, securityKey);
/* log out, and back in */
await logOutOfElement(page);
// 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
await page.addInitScript(() => {
// When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures
// 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.
window.localStorage.clear();
});
await page.reload();
await logIntoElementAndVerify(page, aliceCredentials, securityKey);
/* go back to the test room and find Bob's message again */
await app.viewRoomById(testRoomId);
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.
// No shield would have no div mx_EventTile_e2eIcon at all.
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/);
await lastTileE2eIcon.hover();
// The key is coming from backup, so it is not anymore possible to establish if the claimed device
// creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device."
// It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted.
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
"The authenticity of this encrypted message can't be guaranteed on this device.",
);
});
/* go back to the test room and find Bob's message again */
await app.viewRoomById(testRoomId);
await expect(lastTile).toContainText("test encrypted 1");
// 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.
// The key is coming from backup, so it is not anymore possible to establish if the claimed device
// creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device."
// It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted.
await expect(lastTileE2eIcon).toHaveAccessibleName(
"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 }) => {
// bob has a second, not cross-signed, device
@@ -224,7 +210,7 @@ test.describe("Cryptography", function () {
// the message should appear, decrypted, with no warning
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();
// 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
await expect(
page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon_warning"),
page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon"),
).toBeVisible();
// a second edit from the verified device should be ok
@@ -257,77 +243,69 @@ test.describe("Cryptography", function () {
});
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();
});
test("should show correct shields on events sent by devices which have since been deleted", async ({
page,
app,
bot: bob,
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
// his user info.
await waitForDevices(app, bob.credentials.userId, 1);
test(
"should show correct shields on events sent by devices which have since been deleted",
{ tag: "@screenshot" },
async ({ page, app, bot: bob, 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
// his user info.
await waitForDevices(app, bob.credentials.userId, 1);
// Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline();
// Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline();
// Bob sends a message from his verified device
await bob.sendMessage(testRoomId, "test encrypted from verified");
// Bob sends a message from his verified device
await bob.sendMessage(testRoomId, "test encrypted from verified");
// And one from a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
// And one from a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
// ... and then logs out both devices.
await bob.evaluate((cli) => cli.logout(true));
await bobSecondDevice.evaluate((cli) => cli.logout(true));
// ... and then logs out both devices.
await bob.evaluate((cli) => cli.logout(true));
await bobSecondDevice.evaluate((cli) => cli.logout(true));
// Let our app start syncing again
await app.client.network.goOnline();
// Let our app start syncing again
await app.client.network.goOnline();
// 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");
await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 });
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Encrypted by a device not verified by its owner.",
);
// 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");
await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 });
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await assertNoE2EIcon(penultimate, app);
});
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await assertNoE2EIcon(penultimate, app);
},
);
test("should show correct shields on events sent by users with changed identity", async ({
page,
app,
bot: bob,
homeserver,
}) => {
// Verify Bob
await verify(app, bob);
test(
"should show correct shields on events sent by users with changed identity",
{ tag: "@screenshot" },
async ({ page, app, bot: bob, homeserver }) => {
// Verify Bob
await verify(app, bob);
// Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
// Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from user that was previously verified");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Sender's verified identity was reset",
);
});
/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from user that was previously verified");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveAccessibleName("Sender's verified identity was reset");
await expect(lastE2eIcon).toMatchScreenshot("event-shield-identity-reset.png");
},
);
});
});
@@ -343,8 +321,6 @@ async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) {
const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon");
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.
await e2eIcon.focus();
const tooltip = await app.getTooltipForElement(e2eIcon);
throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`);
await expect(e2eIcon).toHaveAccessibleName("None");
}
}

View File

@@ -244,24 +244,6 @@ export class ElementAppPage {
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
*/

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

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 */
/* We also hide download links to not encourage users to try interacting */
.mx_EventTile_msgOption,
.mx_EventTile_e2eIcon_unencrypted,
.mx_EventTile_e2eIcon_warning,
.mx_EventTile_e2eIcon,
.mx_MFileBody_download {
display: none;
}

View File

@@ -33,14 +33,11 @@ Please see LICENSE files in the repository root for full details.
}
.mx_AccessSecretStorageDialog_recoveryKeyFeedback {
&::before {
content: "";
svg {
display: inline-block;
vertical-align: bottom;
width: 20px;
height: 20px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 20px;
margin-inline-end: 5px;
}
@@ -48,9 +45,8 @@ Please see LICENSE files in the repository root for full details.
&.mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid {
color: $alert;
&::before {
mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg");
background-color: $alert;
svg {
color: $alert;
}
}
}

View File

@@ -63,22 +63,6 @@ Please see LICENSE files in the repository root for full details.
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_primary,
&.mx_AccessibleButton_kind_icon_primary_outline {
@@ -114,10 +98,6 @@ Please see LICENSE files in the repository root for full details.
text-decoration: underline;
}
&.mx_AccessibleButton_kind_secondary_content {
color: $secondary-content;
}
&.mx_AccessibleButton_kind_danger {
color: var(--cpd-color-text-on-solid-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 {
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%;
}
}
}

View File

@@ -9,8 +9,7 @@ Please see LICENSE files in the repository root for full details.
.mx_EventTileBubble.mx_CreateEvent {
margin: var(--EventTileBubble_margin-block) auto;
&::before {
background-color: $header-panel-text-primary-color;
mask-image: url("@vector-im/compound-design-tokens/icons/chat-solid.svg");
svg {
color: $header-panel-text-primary-color;
}
}

View File

@@ -18,8 +18,7 @@ Please see LICENSE files in the repository root for full details.
display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content min-content;
&::before,
&::after {
svg {
position: relative;
grid-column: 1;
grid-row: 1 / 3;

View File

@@ -11,21 +11,12 @@ Please see LICENSE files in the repository root for full details.
color: $muted-fg-color;
vertical-align: middle;
padding-left: 20px;
position: relative;
&::before {
svg {
height: 14px;
width: 14px;
background-color: $muted-fg-color;
mask-image: url("@vector-im/compound-design-tokens/icons/visibility-off.svg");
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
content: "";
position: absolute;
top: 1px;
left: 0;
display: inline-block;
margin-right: var(--cpd-space-1-5x);
color: $muted-fg-color;
vertical-align: -2px;
}
}

View File

@@ -34,25 +34,17 @@ Please see LICENSE files in the repository root for full details.
background-color: $system;
border-radius: 20px;
display: inline-block;
width: 32px;
height: 32px;
position: relative;
width: 16px;
height: 16px;
padding: var(--cpd-space-2x);
vertical-align: middle;
margin-right: 12px;
&::before {
content: "";
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
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;
svg {
color: $secondary-content;
width: inherit;
height: inherit;
display: block;
}
}

View File

@@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
.mx_EventTileBubble.mx_MJitsiWidgetEvent {
&::before {
background-color: $header-panel-text-primary-color; /* XXX: Variable abuse */
mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg");
svg {
color: $header-panel-text-primary-color; /* XXX: Variable abuse */
}
}

View File

@@ -9,25 +9,19 @@ Please see LICENSE files in the repository root for full details.
color: var(--cpd-color-text-primary);
.mx_ReactionsRow_addReactionButton {
position: relative;
display: inline-block;
visibility: hidden; /* show on hover of the .mx_EventTile */
width: 24px;
height: 24px;
width: 16px;
height: 16px;
padding: var(--cpd-space-1x);
vertical-align: middle;
margin-left: 4px;
margin-right: 4px;
&::before {
content: "";
position: absolute;
height: 100%;
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");
svg {
height: inherit;
width: inherit;
color: $tertiary-content;
}
&.mx_ReactionsRow_addReactionButton_active {
@@ -36,8 +30,8 @@ Please see LICENSE files in the repository root for full details.
&:hover,
&.mx_ReactionsRow_addReactionButton_active {
&::before {
background-color: $primary-content;
svg {
color: $primary-content;
}
}
}

View File

@@ -11,20 +11,11 @@ Please see LICENSE files in the repository root for full details.
color: $secondary-content;
vertical-align: middle;
padding-left: 20px;
position: relative;
&::before {
svg {
margin-right: 6px;
height: 14px;
width: 14px;
background-color: $icon-button-color;
mask-image: url("@vector-im/compound-design-tokens/icons/delete.svg");
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
content: "";
position: absolute;
top: 1px;
left: 0;
color: $icon-button-color;
vertical-align: -2px;
}
}

View File

@@ -9,28 +9,8 @@ Please see LICENSE files in the repository root for full details.
.mx_EventTileBubble.mx_cryptoEvent {
margin: var(--EventTileBubble_margin-block) auto;
/* white infill for the transparency */
&.mx_cryptoEvent_icon::before {
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_icon svg {
color: $header-panel-text-primary-color;
}
.mx_cryptoEvent_state,

View File

@@ -61,24 +61,19 @@ Please see LICENSE files in the repository root for full details.
.mx_BaseCard_header_title_button--option {
position: relative;
width: var(--BaseCard_header-button-size);
height: var(--BaseCard_header-button-size);
width: calc(var(--BaseCard_header-button-size) - 4px);
height: calc(var(--BaseCard_header-button-size) - 4px);
padding: 2px;
&::after {
content: "";
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
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;
svg {
width: inherit;
height: inherit;
display: block;
color: $secondary-content;
}
&:hover::after {
background-color: $primary-content;
&:hover svg {
color: $primary-content;
}
}
}

View File

@@ -21,22 +21,3 @@ Please see LICENSE files in the repository root for full details.
.mx_E2EIcon.mx_E2EIcon_inline {
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;
}

View File

@@ -830,32 +830,11 @@ $left-gutter: 64px;
width: 14px;
height: 14px;
display: block;
background-repeat: no-repeat;
background-size: contain;
&::after {
content: "";
svg {
height: inherit;
width: inherit;
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);
}
}

View File

@@ -9,8 +9,7 @@ Please see LICENSE files in the repository root for full details.
.mx_EventTileBubble.mx_HistoryTile {
margin: var(--EventTileBubble_margin-block) auto;
&::before {
background-color: $header-panel-text-primary-color;
mask-image: url("@vector-im/compound-design-tokens/icons/visibility-off.svg");
svg {
color: $header-panel-text-primary-color;
}
}

View File

@@ -11,14 +11,6 @@ Please see LICENSE files in the repository root for full details.
padding: 2px 0;
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 {
display: grid;
grid-template:

View File

@@ -44,16 +44,11 @@ Please see LICENSE files in the repository root for full details.
grid-column: 2;
background-color: $accent;
&::before {
content: "";
svg {
display: inline-block;
height: 40px;
width: 40px;
vertical-align: middle;
mask-repeat: no-repeat;
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");
height: 20px;
width: 20px;
padding: 10px;
color: #fff; /* on all themes */
}
}

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type RefObject } from "react";
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 ErrorBoundary from "../views/elements/ErrorBoundary";
@@ -43,6 +44,7 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
<div className="mx_RoomView_timeline">
<ScrollPanel className="mx_RoomView_messagePanel">
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("room|waiting_for_join_title", { brand })}
subtitle={_t("room|waiting_for_join_subtitle", { brand })}

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useCallback, useEffect } from "react";
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 dis from "../../../dispatcher/dispatcher";
@@ -90,7 +90,9 @@ const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
isExpanded={menuDisplayed}
ref={button}
data-testid="threadlist-dropdown-button"
/>
>
<OverflowHorizontalIcon />
</ContextMenuTooltipButton>
{menuDisplayed && (
<IconizedContextMenu
onFinished={closeThreadOptions}

View File

@@ -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 { debounce } from "lodash";
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 { Flex } from "@element-hq/web-shared-components";
import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../../languageHandler";
import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
@@ -142,27 +143,29 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
};
private getRecoveryKeyFeedback(): React.ReactNode | null {
let validationText: string;
let content: ReactNode;
let classes: string | undefined;
if (this.state.recoveryKeyCorrect) {
// The recovery key is good. Empty feedback.
validationText = "\xA0"; // &nbsp;
content = "\xA0"; // &nbsp;
} else if (this.state.recoveryKeyCorrect === null) {
// 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 {
// The entered key is not (yet) correct. Tell them so.
validationText = _t("encryption|access_secret_storage_dialog|key_validation_text|wrong_security_key");
classes = classNames({
"mx_AccessSecretStorageDialog_recoveryKeyFeedback": true,
"mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid": true,
});
content = (
<>
<ErrorSolidIcon />
{_t("encryption|access_secret_storage_dialog|key_validation_text|wrong_security_key")}
</>
);
classes = "mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid";
}
return (
<Flex align="center" className={classes}>
{validationText}
<Flex align="center" className={classNames("mx_AccessSecretStorageDialog_recoveryKeyFeedback", classes)}>
{content}
</Flex>
);
}

View File

@@ -30,7 +30,6 @@ export type AccessibleButtonKind =
| "primary_outline"
| "primary_sm"
| "secondary"
| "secondary_content"
| "content_inline"
| "danger"
| "danger_outline"
@@ -39,8 +38,6 @@ export type AccessibleButtonKind =
| "link"
| "link_inline"
| "link_sm"
| "confirm_sm"
| "cancel_sm"
| "icon"
| "icon_primary"
| "icon_primary_outline";

View File

@@ -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 { 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 { _t } from "../../../languageHandler";
@@ -53,6 +54,7 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("common|encryption_enabled")}
subtitle={subtitle}
@@ -64,6 +66,7 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
if (isRoomEncrypted) {
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("common|encryption_enabled")}
subtitle={_t("timeline|m.room.encryption|disable_attempt")}
@@ -74,7 +77,8 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
return (
<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")}
subtitle={_t("timeline|m.room.encryption|unsupported")}
ref={ref}

View File

@@ -11,6 +11,7 @@ import classNames from "classnames";
interface IProps {
className: string;
icon: JSX.Element;
title: string;
timestamp?: JSX.Element;
subtitle?: ReactNode;
@@ -18,9 +19,10 @@ interface IProps {
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 (
<div className={classNames("mx_EventTileBubble", className)} ref={ref}>
{icon}
<div className="mx_EventTileBubble_title">{title}</div>
{subtitle && <div className="mx_EventTileBubble_subtitle">{subtitle}</div>}
{children}

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import { type IBodyProps } from "./IBodyProps";
@@ -34,6 +35,7 @@ const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
return (
<span className="mx_HiddenBody" ref={ref}>
<VisibilityOffIcon />
{text}
</span>
);

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 { logger } from "matrix-js-sdk/src/logger";
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 { 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 Modal from "../../../Modal";
@@ -190,9 +196,17 @@ export default class MFileBody extends React.Component<IProps, IState> {
let placeholder: React.ReactNode = null;
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 = (
<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)}>
<span className="mx_MFileBody_info_filename">
{presentableTextForFile(this.content, _t("common|attachment"), true, true)}

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
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 WidgetStore from "../../../stores/WidgetStore";
@@ -41,6 +42,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
// removed
return (
<EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_ended", { senderName })}
timestamp={this.props.timestamp}
@@ -50,6 +52,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
// modified
return (
<EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_updated", { senderName })}
subtitle={joinCopy}
@@ -60,6 +63,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
// assume added
return (
<EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_started", { senderName })}
subtitle={joinCopy}

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
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 { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
@@ -73,6 +74,7 @@ const MKeyVerificationRequest: React.FC<Props> = ({ mxEvent, timestamp }) => {
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
title={title}
subtitle={subtitle}

View File

@@ -11,6 +11,7 @@ import classNames from "classnames";
import { type MatrixEvent, MatrixEventEvent, type Relations, RelationsEvent } from "matrix-js-sdk/src/matrix";
import { uniqBy } from "lodash";
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
import { ReactionAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import { isContentActionable } from "../../../utils/EventUtils";
@@ -54,7 +55,9 @@ const ReactButton: React.FC<IProps> = ({ mxEvent, reactions }) => {
}}
isExpanded={menuDisplayed}
ref={button}
/>
>
<ReactionAddIcon />
</ContextMenuTooltipButton>
{contextMenu}
</React.Fragment>

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useContext, type JSX } from "react";
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 MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -34,6 +35,7 @@ const RedactedBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
return (
<span className="mx_RedactedBody" ref={ref} title={titleText}>
<DeleteIcon />
{text}
</span>
);

View File

@@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, useCallback } from "react";
import { logger } from "matrix-js-sdk/src/logger";
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 { Action } from "../../../dispatcher/actions";
@@ -89,6 +90,7 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
return (
<EventTileBubble
icon={<ChatSolidIcon />}
className="mx_CreateEvent"
title={_t("timeline|m.room.create|continuation")}
timestamp={timestamp}
@@ -128,6 +130,7 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
return (
<EventTileBubble
icon={<ChatSolidIcon />}
className="mx_CreateEvent"
title={_t("timeline|m.room.create|continuation")}
subtitle={link}

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, useContext, useEffect } from "react";
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 BaseCard from "./BaseCard";
@@ -73,7 +74,9 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
onClick={openMenu}
isExpanded={menuDisplayed}
label={_t("common|options")}
/>
>
<OverflowHorizontalIcon />
</ContextMenuButton>
{contextMenu}
</div>
);

View File

@@ -38,9 +38,9 @@ interface Props {
}
const icons: Record<E2EStatus, JSX.Element> = {
[E2EStatus.Warning]: <ErrorSolidIcon className="mx_E2EIcon_warning" />,
[E2EStatus.Normal]: <LockSolidIcon className="mx_E2EIcon_normal" />,
[E2EStatus.Verified]: <ShieldIcon className="mx_E2EIcon_verified" />,
[E2EStatus.Warning]: <ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />,
[E2EStatus.Normal]: <LockSolidIcon color="var(--cpd-color-icon-tertiary)" />,
[E2EStatus.Verified]: <ShieldIcon color="var(--cpd-color-icon-success-primary)" />,
};
const E2EIcon: React.FC<Props> = ({ isUser, status, className, size, onClick, hideTooltip, tooltipPlacement }) => {

View File

@@ -35,6 +35,7 @@ import {
} from "matrix-js-sdk/src/crypto-api";
import { Tooltip } from "@vector-im/compound-web";
import { uniqueId } from "lodash";
import { ErrorSolidIcon, InfoIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler";
@@ -1518,13 +1519,13 @@ function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "ic
}
enum E2ePadlockIcon {
/** grey shield */
/** Compound Info icon in grey */
Normal = "normal",
/** red shield with (!) */
/** Compound ErrorSolid icon in red */
Warning = "warning",
/** key in grey circle */
/** Compound ErrorSolid icon in grey */
DecryptionFailure = "decryption_failure",
}
@@ -1534,6 +1535,12 @@ interface 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) {
super(props);
@@ -1543,12 +1550,13 @@ class E2ePadlock extends React.Component<IE2ePadlockProps> {
}
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
// https://github.com/element-hq/compound/issues/294
return (
<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>
);
}

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
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 { _t } from "../../../languageHandler";
@@ -28,6 +29,7 @@ const HistoryTile: React.FC = () => {
return (
<EventTileBubble
icon={<VisibilityOffIcon />}
className="mx_HistoryTile"
title={_t("timeline|historical_messages_unavailable")}
subtitle={subtitle}

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 { EventType, type Room, type User, type MatrixClient } from "matrix-js-sdk/src/matrix";
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 DMRoomMap from "../../../utils/DMRoomMap";
@@ -291,7 +292,8 @@ const NewRoomIntro: React.FC = () => {
<li className="mx_NewRoomIntro">
{!hasExpectedEncryptionSettings(cli, room) && (
<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")}
subtitle={subtitle}
/>

View File

@@ -112,8 +112,6 @@ export default class ReplyTile extends React.PureComponent<IProps> {
const classes = classNames("mx_ReplyTile", {
mx_ReplyTile_inline: msgType === MsgType.Emote,
mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
mx_ReplyTile_audio: msgType === MsgType.Audio,
mx_ReplyTile_video: msgType === MsgType.Video,
});
let permalink = "#";

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
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 { _t } from "../../../languageHandler";
@@ -59,7 +60,9 @@ class DialPadButton extends React.PureComponent<DigitButtonProps | DialButtonPro
className="mx_DialPad_button mx_DialPad_dialButton"
onClick={this.onClick}
aria-label={_t("voip|dial")}
/>
>
<VoiceCallSolidIcon />
</AccessibleButton>
);
}
}

View File

@@ -594,12 +594,14 @@ describe("RoomView", () => {
const { container } = await renderRoomView();
// 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);
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(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", () => {

View File

@@ -63,6 +63,17 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
<div
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
class="mx_EventTileBubble_title"
>

View File

@@ -141,8 +141,20 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
class="mx_NewRoomIntro"
>
<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
class="mx_EventTileBubble_title"
>
@@ -311,8 +323,20 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
class="mx_NewRoomIntro"
>
<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
class="mx_EventTileBubble_title"
>
@@ -676,6 +700,17 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<div
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
class="mx_EventTileBubble_title"
>
@@ -2974,7 +3009,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="width: 12px; height: 12px;"
>
<svg
class="mx_E2EIcon_verified"
color="var(--cpd-color-icon-success-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"

View File

@@ -27,7 +27,7 @@ exports[`<UntrustedDeviceDialog /> should display the dialog for the device of a
style="width: 24px; height: 24px;"
>
<svg
class="mx_E2EIcon_warning"
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
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;"
>
<svg
class="mx_E2EIcon_warning"
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"

View File

@@ -37,7 +37,7 @@ exports[`VerificationRequestDialog After scanning QR, shows confirmation dialog
style="width: 128px; height: 128px;"
>
<svg
class="mx_E2EIcon_verified"
color="var(--cpd-color-icon-success-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -313,7 +313,7 @@ exports[`VerificationRequestDialog Shows a successful message if verification fi
style="width: 128px; height: 128px;"
>
<svg
class="mx_E2EIcon_verified"
color="var(--cpd-color-icon-success-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"

View File

@@ -73,7 +73,7 @@ exports[`<Users /> should render a single device - signed by owner 1`] = `
data-testid="e2e-icon"
>
<svg
class="mx_E2EIcon_normal"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -237,7 +237,7 @@ exports[`<Users /> should render a single device - unsigned 1`] = `
data-testid="e2e-icon"
>
<svg
class="mx_E2EIcon_warning"
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
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"
>
<svg
class="mx_E2EIcon_verified"
color="var(--cpd-color-icon-success-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -555,7 +555,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon"
>
<svg
class="mx_E2EIcon_verified"
color="var(--cpd-color-icon-success-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -587,7 +587,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon"
>
<svg
class="mx_E2EIcon_verified"
color="var(--cpd-color-icon-success-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -613,7 +613,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon"
>
<svg
class="mx_E2EIcon_normal"
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -637,7 +637,7 @@ exports[`<Users /> should render a single user 1`] = `
data-testid="e2e-icon"
>
<svg
class="mx_E2EIcon_warning"
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"

View File

@@ -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"
role="button"
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>
<button
aria-labelledby="_r_0_"

View File

@@ -85,4 +85,31 @@ describe("<MFileBody/>", () => {
expect(getByRole("link", { name: "Download" })).toBeInTheDocument();
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();
});
});

View File

@@ -35,3 +35,126 @@ exports[`<MFileBody/> should show a download button in file rendering type 1`] =
</span>
</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>
`;

View File

@@ -172,7 +172,19 @@ exports[`<MImageBody/> should render MFileBody for svg with no thumbnail 1`] = `
>
<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"

View File

@@ -5,6 +5,17 @@ exports[`<RoomPredecessorTile /> Renders as expected 1`] = `
<div
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
class="mx_EventTileBubble_title"
>

View File

@@ -269,8 +269,8 @@ describe("EventTile", () => {
// there should be a warning shield
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
"mx_EventTile_e2eIcon_warning",
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName(
"Encrypted by a device not verified by its owner.",
);
});
@@ -298,10 +298,13 @@ describe("EventTile", () => {
it.each([
[EventShieldReason.UNKNOWN, "Unknown error"],
[EventShieldReason.UNVERIFIED_IDENTITY, "unverified user"],
[EventShieldReason.UNSIGNED_DEVICE, "device not verified by its owner"],
[EventShieldReason.UNKNOWN_DEVICE, "unknown or deleted device"],
[EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"],
[EventShieldReason.UNVERIFIED_IDENTITY, "Encrypted by an unverified user."],
[EventShieldReason.UNSIGNED_DEVICE, "Encrypted by a device not verified by its owner."],
[EventShieldReason.UNKNOWN_DEVICE, "Encrypted by an unknown or deleted device."],
[
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.SENT_IN_CLEAR, "Not encrypted"],
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity was reset"],
@@ -326,12 +329,7 @@ describe("EventTile", () => {
const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon");
expect(e2eIcons).toHaveLength(1);
expect(e2eIcons[0].classList).toContain("mx_EventTile_e2eIcon_normal");
fireEvent.focus(e2eIcons[0]);
expect(e2eIcons[0].getAttribute("aria-labelledby")).toBeTruthy();
expect(document.getElementById(e2eIcons[0].getAttribute("aria-labelledby")!)).toHaveTextContent(
expectedText,
);
expect(e2eIcons[0]).toHaveAccessibleName(expectedText);
});
describe("undecryptable event", () => {
@@ -360,8 +358,8 @@ describe("EventTile", () => {
expect(eventTiles).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
"mx_EventTile_e2eIcon_decryption_failure",
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName(
"This message could not be decrypted",
);
});
@@ -436,8 +434,8 @@ describe("EventTile", () => {
// check it was updated
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
"mx_EventTile_e2eIcon_warning",
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName(
"Encrypted by a device not verified by its owner.",
);
});
@@ -481,9 +479,7 @@ describe("EventTile", () => {
// check it was updated
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
"mx_EventTile_e2eIcon_warning",
);
expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0]).toHaveAccessibleName("Not encrypted");
});
});

View File

@@ -330,10 +330,10 @@ describe("SendWysiwygComposer", () => {
"Left icon when %s",
({ isRichTextEnabled }) => {
it.each([
[E2EStatus.Verified, "mx_E2EIcon_verified"],
[E2EStatus.Warning, "mx_E2EIcon_warning"],
[E2EStatus.Verified, "Everyone in this room is verified"],
[E2EStatus.Warning, "Someone is using an unknown session"],
[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
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, undefined, e2eStatus);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
@@ -341,9 +341,9 @@ describe("SendWysiwygComposer", () => {
// Then
expect(leftIcon).toBeInTheDocument();
expect(leftIcon).toHaveClass("mx_E2EIcon");
if (expectedClass) {
if (expectedLabel) {
// eslint-disable-next-line jest/no-conditional-expect
expect(leftIcon.querySelector("svg")).toHaveClass(expectedClass);
expect(leftIcon).toHaveAccessibleName(expectedLabel);
} else {
// eslint-disable-next-line jest/no-conditional-expect
expect(leftIcon.querySelector("svg")).not.toBeInTheDocument();