Show a blue lock for unencrypted rooms and hide the grey shield for encrypted rooms (#30440)

* Show a blue lock instead of a grey shield for unencrypted rooms

* Update screenshots and snapshot

* Update snapshots and fix e2e test that used to expect the grey shield

* lint and add tests for shield

* Update more screen shots

* finish unit test for left icon

* Remove unneeded check

* Don't bother adding stray props to E2EIcon for data-testid

* Upate snapshots
This commit is contained in:
David Langley
2025-08-11 10:35:04 +01:00
committed by GitHub
parent 4da27eb199
commit 59531ea512
36 changed files with 128 additions and 19 deletions

View File

@@ -158,7 +158,8 @@ test.describe("Cryptography", function () {
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
await checkDMRoom(page); await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob); const bobRoomId = await bobJoin(page, bob);
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon-normal.png"); // We no longer show the grey badge in the composer, check that it is not there.
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toHaveCount(0);
await testMessages(page, bob, bobRoomId); await testMessages(page, bob, bobRoomId);
await verify(app, bob); await verify(app, bob);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -74,18 +74,18 @@ const E2EIcon: React.FC<Props> = ({
let content: JSX.Element; let content: JSX.Element;
if (onClick) { if (onClick) {
content = <AccessibleButton onClick={onClick} className={classes} style={style} />; content = <AccessibleButton onClick={onClick} className={classes} style={style} data-testid="e2e-icon" />;
} else { } else {
// Verified and warning icon have a transparent cutout, so add a white background. // Verified and warning icon have a transparent cutout, so add a white background.
// The normal icon already has the correct shape and size, so reuse that. // The normal icon already has the correct shape and size, so reuse that.
if (status === E2EStatus.Verified || status === E2EStatus.Warning) { if (status === E2EStatus.Verified || status === E2EStatus.Warning) {
content = ( content = (
<div className={classes} style={style}> <div className={classes} style={style} data-testid="e2e-icon">
<div className="mx_E2EIcon_normal" /> <div className="mx_E2EIcon_normal" />
</div> </div>
); );
} else { } else {
content = <div className={classes} style={style} />; content = <div className={classes} style={style} data-testid="e2e-icon" />;
} }
} }

View File

@@ -19,6 +19,7 @@ import {
import { type Optional } from "matrix-events-sdk"; import { type Optional } from "matrix-events-sdk";
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { LockOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
@@ -36,7 +37,7 @@ import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../audio/VoiceRecording"; import { RecordingState } from "../../../audio/VoiceRecording";
import type ResizeNotifier from "../../../utils/ResizeNotifier"; import type ResizeNotifier from "../../../utils/ResizeNotifier";
import { type E2EStatus } from "../../../utils/ShieldUtils"; import { E2EStatus } from "../../../utils/ShieldUtils";
import SendMessageComposer, { type SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer"; import SendMessageComposer, { type SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
@@ -525,12 +526,27 @@ export class MessageComposer extends React.Component<IProps, IState> {
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus); let leftIcon: false | JSX.Element = false;
const e2eIcon = hasE2EIcon && ( if (!this.state.isWysiwygLabEnabled) {
if (!this.props.e2eStatus) {
leftIcon = (
<div className="mx_MessageComposer_e2eIconWrapper"> <div className="mx_MessageComposer_e2eIconWrapper">
<E2EIcon key="e2eIcon" status={this.props.e2eStatus!} className="mx_MessageComposer_e2eIcon" /> <LockOffIcon
width={12}
height={12}
color="var(--cpd-color-icon-info-primary)"
className="mx_E2EIcon mx_MessageComposer_e2eIcon"
/>
</div> </div>
); );
} else if (this.props.e2eStatus !== E2EStatus.Normal) {
leftIcon = (
<div className="mx_MessageComposer_e2eIconWrapper">
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />
</div>
);
}
}
const controls: ReactNode[] = []; const controls: ReactNode[] = [];
const menuPosition = this.getMenuPosition(); const menuPosition = this.getMenuPosition();
@@ -640,7 +656,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
const classes = classNames({ const classes = classNames({
"mx_MessageComposer": true, "mx_MessageComposer": true,
"mx_MessageComposer--compact": this.props.compact, "mx_MessageComposer--compact": this.props.compact,
"mx_MessageComposer_e2eStatus": hasE2EIcon, "mx_MessageComposer_e2eStatus": leftIcon,
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled, "mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled,
}); });
@@ -654,7 +670,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
/> />
<div className="mx_MessageComposer_row"> <div className="mx_MessageComposer_row">
{e2eIcon} {leftIcon}
{composer} {composer}
<div className="mx_MessageComposer_actions"> <div className="mx_MessageComposer_actions">
{controls} {controls}

View File

@@ -8,12 +8,13 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react"; import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react";
import { type IEventRelation } from "matrix-js-sdk/src/matrix"; import { type IEventRelation } from "matrix-js-sdk/src/matrix";
import LockOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-off";
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler"; import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
import { WysiwygComposer } from "./components/WysiwygComposer"; import { WysiwygComposer } from "./components/WysiwygComposer";
import { PlainTextComposer } from "./components/PlainTextComposer"; import { PlainTextComposer } from "./components/PlainTextComposer";
import { type ComposerFunctions } from "./types"; import { type ComposerFunctions } from "./types";
import { type E2EStatus } from "../../../../utils/ShieldUtils"; import { E2EStatus } from "../../../../utils/ShieldUtils";
import E2EIcon from "../E2EIcon"; import E2EIcon from "../E2EIcon";
import { type MenuProps } from "../../../structures/ContextMenu"; import { type MenuProps } from "../../../structures/ContextMenu";
import { Emoji } from "./components/Emoji"; import { Emoji } from "./components/Emoji";
@@ -55,11 +56,25 @@ export default function SendWysiwygComposer({
[props.eventRelation], [props.eventRelation],
); );
let leftIcon: false | JSX.Element = false;
if (!e2eStatus) {
leftIcon = (
<LockOffIcon
data-testid="e2e-icon"
width={12}
height={12}
color="var(--cpd-color-icon-info-primary)"
className="mx_E2EIcon"
/>
);
} else if (e2eStatus !== E2EStatus.Normal) {
leftIcon = <E2EIcon status={e2eStatus} />;
}
return ( return (
<ComposerContext.Provider value={defaultContextValue}> <ComposerContext.Provider value={defaultContextValue}>
<Composer <Composer
className="mx_SendWysiwygComposer" className="mx_SendWysiwygComposer"
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />} leftComponent={leftIcon}
rightComponent={<Emoji menuPosition={menuPosition} />} rightComponent={<Emoji menuPosition={menuPosition} />}
{...props} {...props}
> >

View File

@@ -442,7 +442,8 @@ describe("RoomView", () => {
); );
const { container } = await renderRoomView(); const { container } = await renderRoomView();
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_normal")).toBeInTheDocument()); // 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());
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);

View File

@@ -787,7 +787,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</div> </div>
<div <div
aria-label="Message composer" aria-label="Message composer"
class="mx_MessageComposer" class="mx_MessageComposer mx_MessageComposer_e2eStatus"
role="region" role="region"
> >
<div <div
@@ -796,6 +796,23 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
<div <div
class="mx_MessageComposer_row" class="mx_MessageComposer_row"
> >
<div
class="mx_MessageComposer_e2eIconWrapper"
>
<svg
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
height="12"
viewBox="0 0 24 24"
width="12"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.825 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412a2 2 0 0 1 .702-.463L1.333 4.167a1 1 0 0 1 1.414-1.414L7 7.006v-.012l13 13v.012l1.247 1.247a1 1 0 1 1-1.414 1.414l-.896-.896A1.94 1.94 0 0 1 18 22zm14-4.834V10q0-.825-.587-1.412A1.93 1.93 0 0 0 18 8h-1V6q0-2.075-1.463-3.537Q14.075 1 12 1T8.463 2.463a4.9 4.9 0 0 0-1.22 1.946L9 6.166V6q0-1.25.875-2.125A2.9 2.9 0 0 1 12 3q1.25 0 2.125.875T15 6v2h-4.166z"
/>
</svg>
</div>
<div <div
class="mx_SendMessageComposer" class="mx_SendMessageComposer"
> >
@@ -1164,7 +1181,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</div> </div>
<div <div
aria-label="Message composer" aria-label="Message composer"
class="mx_MessageComposer" class="mx_MessageComposer mx_MessageComposer_e2eStatus"
role="region" role="region"
> >
<div <div
@@ -1173,6 +1190,23 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<div <div
class="mx_MessageComposer_row" class="mx_MessageComposer_row"
> >
<div
class="mx_MessageComposer_e2eIconWrapper"
>
<svg
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
height="12"
viewBox="0 0 24 24"
width="12"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.825 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412a2 2 0 0 1 .702-.463L1.333 4.167a1 1 0 0 1 1.414-1.414L7 7.006v-.012l13 13v.012l1.247 1.247a1 1 0 1 1-1.414 1.414l-.896-.896A1.94 1.94 0 0 1 18 22zm14-4.834V10q0-.825-.587-1.412A1.93 1.93 0 0 0 18 8h-1V6q0-2.075-1.463-3.537Q14.075 1 12 1T8.463 2.463a4.9 4.9 0 0 0-1.22 1.946L9 6.166V6q0-1.25.875-2.125A2.9 2.9 0 0 1 12 3q1.25 0 2.125.875T15 6v2h-4.166z"
/>
</svg>
</div>
<div <div
class="mx_SendMessageComposer" class="mx_SendMessageComposer"
> >
@@ -1809,6 +1843,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
<div <div
aria-labelledby="«r3e»" aria-labelledby="«r3e»"
class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon" class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon"
data-testid="e2e-icon"
> >
<div <div
class="mx_E2EIcon_normal" class="mx_E2EIcon_normal"
@@ -2221,7 +2256,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</div> </div>
<div <div
aria-label="Message composer" aria-label="Message composer"
class="mx_MessageComposer mx_MessageComposer--compact" class="mx_MessageComposer mx_MessageComposer--compact mx_MessageComposer_e2eStatus"
role="region" role="region"
> >
<div <div
@@ -2230,6 +2265,23 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
<div <div
class="mx_MessageComposer_row" class="mx_MessageComposer_row"
> >
<div
class="mx_MessageComposer_e2eIconWrapper"
>
<svg
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
height="12"
viewBox="0 0 24 24"
width="12"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.825 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412a2 2 0 0 1 .702-.463L1.333 4.167a1 1 0 0 1 1.414-1.414L7 7.006v-.012l13 13v.012l1.247 1.247a1 1 0 1 1-1.414 1.414l-.896-.896A1.94 1.94 0 0 1 18 22zm14-4.834V10q0-.825-.587-1.412A1.93 1.93 0 0 0 18 8h-1V6q0-2.075-1.463-3.537Q14.075 1 12 1T8.463 2.463a4.9 4.9 0 0 0-1.22 1.946L9 6.166V6q0-1.25.875-2.125A2.9 2.9 0 0 1 12 3q1.25 0 2.125.875T15 6v2h-4.166z"
/>
</svg>
</div>
<div <div
class="mx_SendMessageComposer" class="mx_SendMessageComposer"
> >

View File

@@ -22,6 +22,7 @@ exports[`<UntrustedDeviceDialog /> should display the dialog for the device of a
> >
<div <div
class="mx_E2EIcon mx_E2EIcon_warning" class="mx_E2EIcon mx_E2EIcon_warning"
data-testid="e2e-icon"
style="width: 24px; height: 24px;" style="width: 24px; height: 24px;"
> >
<div <div
@@ -100,6 +101,7 @@ exports[`<UntrustedDeviceDialog /> should display the dialog for the device of t
> >
<div <div
class="mx_E2EIcon mx_E2EIcon_warning" class="mx_E2EIcon mx_E2EIcon_warning"
data-testid="e2e-icon"
style="width: 24px; height: 24px;" style="width: 24px; height: 24px;"
> >
<div <div

View File

@@ -24,6 +24,7 @@ import {
import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
import { createMocks } from "./utils"; import { createMocks } from "./utils";
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx"; import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx";
import { E2EStatus } from "../../../../../../src/utils/ShieldUtils.ts";
jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({ jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({
EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => { EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => {
@@ -69,6 +70,7 @@ describe("SendWysiwygComposer", () => {
disabled = false, disabled = false,
isRichTextEnabled = true, isRichTextEnabled = true,
placeholder?: string, placeholder?: string,
e2eStatus?: E2EStatus,
) => { ) => {
return render( return render(
<MatrixClientContext.Provider value={mockClient}> <MatrixClientContext.Provider value={mockClient}>
@@ -80,6 +82,7 @@ describe("SendWysiwygComposer", () => {
isRichTextEnabled={isRichTextEnabled} isRichTextEnabled={isRichTextEnabled}
menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })} menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })}
placeholder={placeholder} placeholder={placeholder}
e2eStatus={e2eStatus}
/> />
</ScopedRoomContextProvider> </ScopedRoomContextProvider>
</MatrixClientContext.Provider>, </MatrixClientContext.Provider>,
@@ -322,4 +325,23 @@ describe("SendWysiwygComposer", () => {
}); });
}, },
); );
describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])(
"Left icon when %s",
({ isRichTextEnabled }) => {
it.each([
[E2EStatus.Verified, "mx_E2EIcon_verified"],
[E2EStatus.Warning, "mx_E2EIcon_warning"],
[undefined, undefined],
])("Should render left icon when e2eStatus is %s", async (e2eStatus, expectedClass) => {
// When
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled, undefined, e2eStatus);
await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true"));
const leftIcon = screen.getByTestId("e2e-icon");
// Then
expect(leftIcon).toBeInTheDocument();
expect(leftIcon).toHaveClass(expectedClass ? `mx_E2EIcon ${expectedClass}` : `mx_E2EIcon`);
});
},
);
}); });