diff --git a/playwright/e2e/crypto/history-sharing.spec.ts b/playwright/e2e/crypto/history-sharing.spec.ts index 820f5edc63..3974ba79c5 100644 --- a/playwright/e2e/crypto/history-sharing.spec.ts +++ b/playwright/e2e/crypto/history-sharing.spec.ts @@ -38,6 +38,14 @@ test.describe("History sharing", function () { // Create the room and send a message await createRoom(alicePage, "TestRoom", true); + + // The default history visibility for private rooms is "invited", + // so we need to change it to "shared" to ensure Bob can see the message when he joins. + const roomId = await aliceElementApp.getCurrentRoomIdFromUrl(); + await aliceElementApp.client.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "shared", + }); + await sendMessageInCurrentRoom(alicePage, "A message from Alice"); // Send the invite to Bob @@ -101,6 +109,12 @@ test.describe("History sharing", function () { await bobPage.getByRole("option", { name: "TestRoom" }).click(); await bobPage.getByRole("button", { name: "Accept" }).click(); + // The room now defaults to "invited" history visibility, so we need to set it to "shared" first + await aliceElementApp.client.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "shared", + }); + await expect(bobPage.getByText("Alice made future room history visible to all room members.")).toBeVisible(); + // Bob sends a message with "shared" visibility await sendMessageInCurrentRoom(bobPage, "Message1: 'shared' visibility"); await expect(alicePage.getByText("Message1")).toBeVisible(); diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 433d80278d..b94d605559 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/src/createRoom.ts b/src/createRoom.ts index 30020bc2ce..193e9dd2cc 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -16,7 +16,7 @@ import { RoomCreateTypeField, RoomType, type ICreateRoomOpts, - type HistoryVisibility, + HistoryVisibility, JoinRule, Preset, RestrictedAllowType, @@ -294,11 +294,24 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro } } - if (opts.historyVisibility) { + // Set history visibility to "invited" for DMs and non-public rooms unless explicitly overridden + // This ensures that room history is only visible to invited members by default + let historyVisibility = opts.historyVisibility; + if (!historyVisibility) { + // Determine if the room will be public based on the join rule or preset + const isPublicRoom = opts.joinRule === JoinRule.Public || createOpts.preset === Preset.PublicChat; + + // For DMs and non-public rooms, set history visibility to "invited" + if (opts.dmUserId || !isPublicRoom) { + historyVisibility = HistoryVisibility.Invited; + } + } + + if (historyVisibility) { createOpts.initial_state.push({ type: EventType.RoomHistoryVisibility, content: { - history_visibility: opts.historyVisibility, + history_visibility: historyVisibility, }, }); } diff --git a/test/unit-tests/createRoom-test.ts b/test/unit-tests/createRoom-test.ts index 2775ad5409..3474020f65 100644 --- a/test/unit-tests/createRoom-test.ts +++ b/test/unit-tests/createRoom-test.ts @@ -11,6 +11,7 @@ import { mocked, type Mocked } from "jest-mock"; import { type MatrixClient, type Device, + HistoryVisibility, Preset, RoomType, JoinRule, @@ -58,7 +59,10 @@ describe("createRoom", () => { expect(client.createRoom).toHaveBeenCalledWith({ preset: "private_chat", visibility: "private", - initial_state: [{ state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }], + initial_state: [ + { state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }, + { type: "m.room.history_visibility", content: { history_visibility: "invited" } }, + ], }); }); @@ -77,6 +81,7 @@ describe("createRoom", () => { algorithm: "m.megolm.v1.aes-sha2", }, }, + { type: "m.room.history_visibility", content: { history_visibility: "invited" } }, ], }); }); @@ -104,6 +109,7 @@ describe("createRoom", () => { "io.element.msc4362.encrypt_state_events": true, }, }, + { type: "m.room.history_visibility", content: { history_visibility: "invited" } }, // Room name is NOT included, since it needs to be encrypted. ], }); @@ -146,6 +152,7 @@ describe("createRoom", () => { "io.element.msc4362.encrypt_state_events": true, }, }, + { type: "m.room.history_visibility", content: { history_visibility: "invited" } }, // Room name is NOT included, since it needs to be encrypted. ], }); @@ -178,6 +185,7 @@ describe("createRoom", () => { "io.element.msc4362.encrypt_state_events": true, }, }, + { type: "m.room.history_visibility", content: { history_visibility: "invited" } }, // Room name is NOT included, since it needs to be encrypted. ], }); @@ -218,6 +226,7 @@ describe("createRoom", () => { initial_state: [ { state_key: "", type: "m.room.guest_access", content: { guest_access: "can_join" } }, { type: "m.space.parent", state_key: parentSpace.roomId, content: { canonical: true, via: [] } }, + { type: "m.room.history_visibility", content: { history_visibility: "invited" } }, ], }); }); @@ -354,6 +363,31 @@ describe("createRoom", () => { ); }); + it("should set history visibility to invited for DMs", async () => { + await createRoom(client, { dmUserId: "@bob:example.org" }); + expect(client.createRoom).toHaveBeenCalledWith( + expect.objectContaining({ + initial_state: expect.arrayContaining([ + { type: "m.room.history_visibility", content: { history_visibility: "invited" } }, + ]), + }), + ); + }); + + it("should respect an explicit history visibility override", async () => { + await createRoom(client, { + createOpts: { preset: Preset.PrivateChat }, + historyVisibility: HistoryVisibility.Shared, + }); + expect(client.createRoom).toHaveBeenCalledWith( + expect.objectContaining({ + initial_state: expect.arrayContaining([ + { type: "m.room.history_visibility", content: { history_visibility: "shared" } }, + ]), + }), + ); + }); + describe("room versions", () => { afterEach(() => { jest.clearAllMocks();