diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 668fd20021..0df312f19c 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -154,8 +154,8 @@ test.describe("Cryptography", function () { await app.client.bootstrapCrossSigning(aliceCredentials); await startDMWithBob(page, bob); // send first message - await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!"); - await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); + await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); + await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); await checkDMRoom(page); const bobRoomId = await bobJoin(page, bob); // We no longer show the grey badge in the composer, check that it is not there. diff --git a/playwright/e2e/room/create-room.spec.ts b/playwright/e2e/room/create-room.spec.ts index b5a8ce3ccd..a5c1004c6d 100644 --- a/playwright/e2e/room/create-room.spec.ts +++ b/playwright/e2e/room/create-room.spec.ts @@ -44,6 +44,21 @@ test.describe("Create Room", () => { }, ); + test("should allow us to start a chat and show encryption state", async ({ page, user, app }) => { + await page.getByRole("button", { name: "Add", exact: true }).click(); + await page.getByText("Start new chat").click(); + + await page.getByTestId("invite-dialog-input").fill(user.userId); + + await page.getByRole("button", { name: "Go" }).click(); + + await expect(page.getByText("Encryption enabled")).toBeVisible(); + await expect(page.getByText("Send your first message to")).toBeVisible(); + + const composer = page.getByRole("region", { name: "Message composer" }); + await expect(composer.getByRole("textbox", { name: "Send a message…" })).toBeVisible(); + }); + test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => { await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index 542b537c13..7a5f7d4ea8 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise { // Send first message to actually start DM await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName); - const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" }); + const locator = page.getByRole("textbox", { name: "Send a message…" }); await locator.fill("Hey!"); await locator.press("Enter"); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 20951448af..df7ee8770f 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -133,6 +133,7 @@ import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext"; import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog"; import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts"; +import { isRoomEncrypted } from "../../hooks/useIsEncrypted"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -257,6 +258,7 @@ interface LocalRoomViewProps { roomView: RefObject; onFileDrop: (dataTransfer: DataTransfer) => Promise; mainSplitContentType: MainSplitContentType; + e2eStatus?: E2EStatus; } /** @@ -304,6 +306,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { } else { composer = ( { } private async getIsRoomEncrypted(roomId = this.state.roomId): Promise { - const crypto = this.context.client?.getCrypto(); - if (!crypto || !roomId) return false; + if (!roomId) return false; - return await crypto.isEncryptionEnabledInRoom(roomId); + const room = this.context.client?.getRoom(roomId); + const crypto = this.context.client?.getCrypto(); + if (!room || !crypto) return false; + + return isRoomEncrypted(room, crypto); } private async calculateRecommendedVersion(room: Room): Promise { @@ -2061,6 +2067,7 @@ export class RoomView extends React.Component { return ( { + if (room instanceof LocalRoom) { + // For local room check the state. + // The crypto check fails because the room ID is not valid (it is a local id) + return (room as LocalRoom).isEncryptionEnabled(); + } + + return await cryptoApi.isEncryptionEnabledInRoom(room.roomId); +} // Hook to simplify watching whether a Matrix room is encrypted, returns null if room is undefined or the state is loading export function useIsEncrypted(cli: MatrixClient, room?: Room): boolean | null { @@ -22,7 +42,7 @@ export function useIsEncrypted(cli: MatrixClient, room?: Room): boolean | null { const crypto = cli.getCrypto(); if (!room || !crypto) return null; - return crypto.isEncryptionEnabledInRoom(room.roomId); + return isRoomEncrypted(room, crypto); }, [room, encryptionStateEvent], null, diff --git a/src/models/LocalRoom.ts b/src/models/LocalRoom.ts index a8e2c255b2..960c7f634c 100644 --- a/src/models/LocalRoom.ts +++ b/src/models/LocalRoom.ts @@ -6,7 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type MatrixClient, Room, PendingEventOrdering } from "matrix-js-sdk/src/matrix"; +import { + type MatrixClient, + Room, + PendingEventOrdering, + MatrixEvent, + Direction, + EventType, +} from "matrix-js-sdk/src/matrix"; import { type Member } from "../utils/direct-messages"; @@ -50,4 +57,20 @@ export class LocalRoom extends Room { public get isError(): boolean { return this.state === LocalRoomState.ERROR; } + + /** + * Check if encryption is enabled in this room. + * True if the room has any encryption state event + */ + public isEncryptionEnabled(): boolean { + const roomState = this.getLiveTimeline().getState(Direction.Forward); + if (!roomState) return false; + + const stateEvents = roomState.getStateEvents(EventType.RoomEncryption); + if (stateEvents.length === 0) return false; + + // if there is an encryption state event, it is encrypted. + // Regardless of the content/algorithm, we assume it is encrypted. + return stateEvents[0] instanceof MatrixEvent; + } } diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 4cc720951b..401355f5ea 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1183,7 +1183,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
-
- - - -
@@ -1269,14 +1250,14 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t aria-autocomplete="list" aria-disabled="false" aria-haspopup="listbox" - aria-label="Send an unencrypted message…" + aria-label="Send a message…" aria-multiline="true" class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" contenteditable="true" data-testid="basicmessagecomposer" dir="auto" role="textbox" - style="--placeholder: 'Send\\ an\\ unencrypted\\ message…';" + style="--placeholder: 'Send\\ a\\ message…';" tabindex="0" translate="no" > diff --git a/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx b/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx index c91a274c70..150b1156d3 100644 --- a/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx +++ b/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx @@ -112,16 +112,19 @@ describe("EncryptionEvent", () => { }); describe("for an encrypted local room", () => { + let localRoom: LocalRoom; + beforeEach(() => { event.event.content!.algorithm = algorithm; - jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); - const localRoom = new LocalRoom(roomId, client, client.getUserId()!); + // jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + localRoom = new LocalRoom(roomId, client, client.getUserId()!); + jest.spyOn(localRoom, "isEncryptionEnabled").mockReturnValue(true); mocked(client.getRoom).mockReturnValue(localRoom); renderEncryptionEvent(client, event); }); it("should show the expected texts", async () => { - expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId); + expect(localRoom.isEncryptionEnabled).toHaveBeenCalled(); await checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted."); }); }); diff --git a/test/unit-tests/models/LocalRoom-test.ts b/test/unit-tests/models/LocalRoom-test.ts index 64e3f51240..26d4845c6c 100644 --- a/test/unit-tests/models/LocalRoom-test.ts +++ b/test/unit-tests/models/LocalRoom-test.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { Direction, EventType, type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom"; import { createTestClient } from "../../test-utils"; @@ -79,4 +79,26 @@ describe("LocalRoom", () => { }); }); }); + + it("should return false for isEncryptionEnabled with no state", () => { + expect(room.isEncryptionEnabled()).toBe(false); + }); + + it("should return true for isEncryptionEnabled with an encryption state event", () => { + const encryptionEvent = new MatrixEvent({ + type: EventType.RoomEncryption, + state_key: "", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + sender: "@test:localhost", + room_id: room.roomId, + event_id: "$test:localhost", + }); + + const roomState = room.getLiveTimeline().getState(Direction.Forward); + roomState?.setStateEvents([encryptionEvent]); + + expect(room.isEncryptionEnabled()).toBe(true); + }); });