diff --git a/playwright/e2e/crypto/history-sharing.spec.ts b/playwright/e2e/crypto/history-sharing.spec.ts index ee275da84ac..820f5edc637 100644 --- a/playwright/e2e/crypto/history-sharing.spec.ts +++ b/playwright/e2e/crypto/history-sharing.spec.ts @@ -50,7 +50,9 @@ test.describe("History sharing", function () { // Bob should now be able to decrypt the event await expect(bobPage.getByText("A message from Alice")).toBeVisible(); - const mask = [bobPage.locator(".mx_MessageTimestamp")]; + // Exclude message timestamps and RR avatars from the screenshot. Bob sometimes sees Alice's RR on the + // previous event, which is surprising but not what we're testing here. + const mask = [bobPage.locator(".mx_MessageTimestamp"), bobPage.locator(".mx_ReadReceiptGroup_container")]; await expect(bobPage.locator(".mx_RoomView_body")).toMatchScreenshot("shared-history-invite-accepted.png", { mask, }); diff --git a/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png b/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png index 9131b77c243..211ce86cf15 100644 Binary files a/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png and b/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png differ diff --git a/src/components/views/rooms/RoomHeader/RoomHeader.tsx b/src/components/views/rooms/RoomHeader/RoomHeader.tsx index 1cf48f66189..2434d5900e7 100644 --- a/src/components/views/rooms/RoomHeader/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader/RoomHeader.tsx @@ -18,10 +18,11 @@ import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icon import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; -import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; +import { HistoryVisibility, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import { Flex, Box } from "@element-hq/web-shared-components"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; +import { HistoryIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { useRoomName } from "../../../../hooks/useRoomName.ts"; import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases.ts"; @@ -55,6 +56,7 @@ import { useScopedRoomContext } from "../../../../contexts/ScopedRoomContext.tsx import { ToggleableIcon } from "./toggle/ToggleableIcon.tsx"; import { CurrentRightPanelPhaseContextProvider } from "../../../../contexts/CurrentRightPanelPhaseContext.tsx"; import { LocalRoom } from "../../../../models/LocalRoom.ts"; +import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts"; function RoomHeaderButtons({ room, @@ -401,8 +403,11 @@ export default function RoomHeader({ const client = useMatrixClientContext(); const roomName = useRoomName(room); const joinRule = useRoomState(room, (state) => state.getJoinRule()); + const historyVisibility = useRoomState(room, (state) => state.getHistoryVisibility()); + const historySharingEnabled = useFeatureEnabled("feature_share_history_on_invite"); const dmMember = useDmMember(room); const isDirectMessage = !!dmMember; + const isRoomEncrypted = useIsEncrypted(client, room); const e2eStatus = useEncryptionStatus(client, room); const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); const onAvatarClick = (): void => { @@ -484,6 +489,21 @@ export default function RoomHeader({ /> )} + + {isRoomEncrypted && + historySharingEnabled && + (historyVisibility === HistoryVisibility.Shared || + historyVisibility === HistoryVisibility.WorldReadable) && ( + + + + )} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8299d762a6e..03989ab9eaa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2031,7 +2031,8 @@ "one": "Asking to join", "other": "%(count)s people asking to join" }, - "room_is_public": "This room is public" + "room_is_public": "This room is public", + "shared_history_tooltip": "New members see history" }, "header_avatar_open_settings_label": "Open room settings", "header_face_pile_tooltip": "People", diff --git a/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx index 5d5c9fb3378..7c1b5429cdc 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx @@ -60,6 +60,7 @@ import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore import { UIFeature } from "../../../../../../src/settings/UIFeature"; import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; import { ElementCallMemberEventType } from "../../../../../../src/call-types"; +import { defaultWatchManager } from "../../../../../../src/settings/Settings.tsx"; jest.mock("../../../../../../src/utils/ShieldUtils"); jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({ @@ -100,7 +101,7 @@ describe("RoomHeader", () => { }; } - beforeEach(async () => { + beforeEach(() => { client = stubClient(); room = new Room(ROOM_ID, client, "@alice:example.org", { pendingEventOrdering: PendingEventOrdering.Detached, @@ -708,6 +709,41 @@ describe("RoomHeader", () => { }); }); + it("shows a history icon if the room is encrypted and has shared history", async () => { + mocked(client.getCrypto()!).isEncryptionEnabledInRoom.mockResolvedValue(true); + await room.addLiveEvents( + [ + new MatrixEvent({ + type: "m.room.history_visibility", + content: { history_visibility: "shared" }, + sender: MatrixClientPeg.get()!.getSafeUserId(), + state_key: "", + room_id: room.roomId, + }), + ], + { addToState: true }, + ); + let featureEnabled = true; + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (flag) => flag === "feature_share_history_on_invite" && featureEnabled, + ); + + render(, getWrapper()); + await waitFor(() => getByLabelText(document.body, "New members see history")); + + // Disable the labs flag and check the icon disappears + featureEnabled = false; + act(() => + defaultWatchManager.notifyUpdate( + "feature_share_history_on_invite", + null, + SettingLevel.DEVICE, + featureEnabled, + ), + ); + expect(queryByLabelText(document.body, "New members see history")).not.toBeInTheDocument(); + }); + describe("dm", () => { beforeEach(() => { // Make the mocked room a DM diff --git a/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap index 4a169c029a1..404edd3c6aa 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomHeader/__snapshots__/RoomHeader-test.tsx.snap @@ -56,7 +56,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = ` style="--cpd-icon-button-size: 100%;" >