Show an icon in the room header for shared history (#31879)

* Show an icon in the room header for shared history

Add a decoration to the header for encrypted rooms with `history_visibility:
{shared|public}`.

Fixes: #31858

* Gate "shared history icon" behind labs flag

... since history isn't actually shared unless the flag is on

* Update snapshots

* update screenshot

* update screenshots, again

* exclude RRs from screenshot test
This commit is contained in:
Richard van der Hoff 2026-01-27 15:06:22 +00:00 committed by GitHub
parent a972340216
commit 617722018c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 8 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -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({
/>
</Tooltip>
)}
{isRoomEncrypted &&
historySharingEnabled &&
(historyVisibility === HistoryVisibility.Shared ||
historyVisibility === HistoryVisibility.WorldReadable) && (
<Tooltip label={_t("room|header|shared_history_tooltip")} placement="right">
<HistoryIcon
width="16px"
height="16px"
className="mx_RoomHeader_icon"
color="var(--cpd-color-icon-info-primary)"
aria-label={_t("room|header|shared_history_tooltip")}
/>
</Tooltip>
)}
</Text>
</Box>
</button>

View File

@ -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",

View File

@ -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(<RoomHeader room={room} />, 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

View File

@ -56,7 +56,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_12c_"
aria-labelledby="_r_134_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -83,7 +83,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="_r_12h_"
aria-labelledby="_r_139_"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@ -98,7 +98,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby="_r_12m_"
aria-labelledby="_r_13e_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"
@ -125,7 +125,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby="_r_12r_"
aria-labelledby="_r_13j_"
class="_icon-button_1215g_8"
data-kind="primary"
role="button"