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%;"
>