Compare commits

...

9 Commits

Author SHA1 Message Date
Florian Duros
21668d6012 refactor: move room encryption check in a dedicated function 2025-09-09 16:32:22 +02:00
Florian Duros
e0fac5e9ee test(e2e): update existing tests 2025-09-09 16:25:46 +02:00
Florian Duros
875bdb4dab test: update existing tests 2025-09-09 15:38:51 +02:00
Florian Duros
6ad823782b test(e2e): check composer doesn't display unencrypted state 2025-09-09 15:30:35 +02:00
Florian Duros
8cf397a912 doc: add docs to LocalRoom.isEncryptionEnabled 2025-09-09 15:05:44 +02:00
Florian Duros
3c20661fa1 fix: look at e2eStatus in composer of local room 2025-09-09 15:02:02 +02:00
Florian Duros
6529731563 refactor: put back the e2e test after merge 2025-09-09 11:24:17 +02:00
Florian Duros
37018ade6d Merge branch 'develop' into valere/fix_start_message_encryption
# Conflicts:
#	playwright/e2e/create-room/create-room.spec.ts
2025-09-09 11:23:34 +02:00
Valere
49a1b1874a Fix local room encryption status always not enabled 2025-08-01 18:29:29 +02:00
8 changed files with 81 additions and 34 deletions

View File

@@ -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.

View File

@@ -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);

View File

@@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise<v
await result.first().click();
// send first message to start DM
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
const locator = page.getByRole("textbox", { name: "Send a message…" });
await expect(locator).toBeFocused();
await locator.fill("Hey!");
await locator.press("Enter");
@@ -260,7 +260,7 @@ test.describe("Spotlight", () => {
// 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");

View File

@@ -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<HTMLElement | null>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
mainSplitContentType: MainSplitContentType;
e2eStatus?: E2EStatus;
}
/**
@@ -304,6 +306,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
} else {
composer = (
<MessageComposer
e2eStatus={props.e2eStatus}
room={props.localRoom}
resizeNotifier={props.resizeNotifier}
permalinkCreator={props.permalinkCreator}
@@ -1397,10 +1400,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
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<void> {
@@ -2061,6 +2067,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return (
<ScopedRoomContextProvider {...this.state}>
<LocalRoomView
e2eStatus={this.state.e2eStatus}
localRoom={localRoom}
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator}

View File

@@ -7,9 +7,29 @@ Please see LICENSE files in the repository root for full details.
*/
import { type MatrixClient, type MatrixEvent, type Room, EventType } from "matrix-js-sdk/src/matrix";
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { useRoomState } from "./useRoomState.ts";
import { useAsyncMemo } from "./useAsyncMemo.ts";
import { LocalRoom } from "../models/LocalRoom.ts";
/**
* Check if a room is encrypted.
* If the room is a LocalRoom, check the state directly.
* Otherwise, use the crypto API to check if encryption is enabled in the room.
*
* @param room - The room to check.
* @param cryptoApi - The crypto API from the Matrix client.
*/
export async function isRoomEncrypted(room: Room, cryptoApi: CryptoApi): Promise<boolean> {
if (room instanceof LocalRoom) {
// For local room check the state.
// The crypto check fails because the eventId 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,

View File

@@ -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,18 @@ 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 {
// check the local room state
const encryptionState = this.getLiveTimeline()
.getState(Direction.Forward)
?.getStateEvents(EventType.RoomEncryption)[0];
// if there is an encryption state event, it is encrypted.
// Regardless of the content/algorithm, we assume it is encrypted.
return encryptionState instanceof MatrixEvent;
}
}

View File

@@ -1183,7 +1183,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</div>
<div
aria-label="Message composer"
class="mx_MessageComposer mx_MessageComposer_e2eStatus"
class="mx_MessageComposer"
role="region"
>
<div
@@ -1192,25 +1192,6 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<div
class="mx_MessageComposer_row"
>
<div
class="mx_MessageComposer_e2eIconWrapper"
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="«rfm»"
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
class="mx_SendMessageComposer"
>
@@ -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"
>

View File

@@ -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.");
});
});