diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/invited-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/invited-history-visibility-auto.png new file mode 100644 index 0000000000..6e1e62c3e7 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/invited-history-visibility-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/joined-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/joined-history-visibility-auto.png new file mode 100644 index 0000000000..6e1e62c3e7 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/joined-history-visibility-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/shared-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/shared-history-visibility-auto.png new file mode 100644 index 0000000000..cc31e1b3a9 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/shared-history-visibility-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/world-readable-history-visibility-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/world-readable-history-visibility-auto.png new file mode 100644 index 0000000000..d59731e529 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx/world-readable-history-visibility-auto.png differ diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index a76f3a54a0..04bc3596ec 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -29,6 +29,11 @@ "context_menu": { "title": "Room options" }, + "history_visibility_badge": { + "private": "New members don't see history", + "shared": "New members see history", + "world_readable": "Anyone can see history" + }, "status_bar": { "delete_all": "Delete all", "exceeded_resource_limit_description": "Please contact your service administrator to continue using the service.", diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 840342eea7..270b805781 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -20,6 +20,7 @@ export * from "./message-body/ReactionsRowButtonTooltip"; export * from "./pill-input/Pill"; export * from "./pill-input/PillInput"; export * from "./room/RoomStatusBar"; +export * from "./room/HistoryVisibilityBadge"; export * from "./rich-list/RichItem"; export * from "./rich-list/RichList"; export * from "./room-list/RoomListHeaderView"; diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx new file mode 100644 index 0000000000..33983bd473 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { HistoryVisibilityBadge } from "./HistoryVisibilityBadge"; + +const meta = { + title: "Room/HistoryVisibilityBadge", + component: HistoryVisibilityBadge, + tags: ["autodocs"], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/IXcnmuaIwtm3F3vBuFCPUp/Room-History-Sharing?node-id=39-10758&t=MKC8KCGCpykDbrcX-1", + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; +export const InvitedHistoryVisibility: Story = { args: { historyVisibility: "invited" } }; +export const JoinedHistoryVisibility: Story = { args: { historyVisibility: "joined" } }; +export const SharedHistoryVisibility: Story = { args: { historyVisibility: "shared" } }; +export const WorldReadableHistoryVisibility: Story = { args: { historyVisibility: "world_readable" } }; diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.test.tsx b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.test.tsx new file mode 100644 index 0000000000..6b8ea1c398 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { describe, expect, it } from "vitest"; +import { render } from "@testing-library/react"; + +import { HistoryVisibilityBadge } from "./HistoryVisibilityBadge.tsx"; + +describe("HistoryVisibilityBadge", () => { + for (const visibility of ["invited", "joined", "shared", "world_readable"]) { + it(`renders the badge for ${visibility}`, () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + } +}); diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.tsx b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.tsx new file mode 100644 index 0000000000..f03e69dd00 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.tsx @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { Badge } from "@vector-im/compound-web"; +import { + HistoryIcon, + UserProfileSolidIcon, + VisibilityOffIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { _t } from "../../utils/i18n"; + +interface Props { + /** The history visibility of the room, according to the room state. */ + historyVisibility: "invited" | "joined" | "shared" | "world_readable"; +} + +/** A badge showing the history visibility of a room. */ +export function HistoryVisibilityBadge({ historyVisibility }: Props): JSX.Element | null { + const iconProps = { + color: "var(--cpd-color-icon-info-primary)", + width: "1rem", // 16px at the default font size, per the design + height: "1rem", + }; + switch (historyVisibility) { + case "invited": + case "joined": + return ( + + + {_t("room|history_visibility_badge|private")} + + ); + case "shared": + return ( + + + {_t("room|history_visibility_badge|shared")} + + ); + case "world_readable": + return ( + + + {_t("room|history_visibility_badge|world_readable")} + + ); + default: + return null; + } +} diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/__snapshots__/HistoryVisibilityBadge.test.tsx.snap b/packages/shared-components/src/room/HistoryVisibilityBadge/__snapshots__/HistoryVisibilityBadge.test.tsx.snap new file mode 100644 index 0000000000..e284eb7a51 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/__snapshots__/HistoryVisibilityBadge.test.tsx.snap @@ -0,0 +1,99 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`HistoryVisibilityBadge > renders the badge for invited 1`] = ` +
+ + + + + New members don't see history + +
+`; + +exports[`HistoryVisibilityBadge > renders the badge for joined 1`] = ` +
+ + + + + New members don't see history + +
+`; + +exports[`HistoryVisibilityBadge > renders the badge for shared 1`] = ` +
+ + + + + + New members see history + +
+`; + +exports[`HistoryVisibilityBadge > renders the badge for world_readable 1`] = ` +
+ + + + + + Anyone can see history + +
+`; diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/index.ts b/packages/shared-components/src/room/HistoryVisibilityBadge/index.ts new file mode 100644 index 0000000000..e8958a9913 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export * from "./HistoryVisibilityBadge"; 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 14cb5ce372..433d80278d 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/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png index febb73c235..775857a5f7 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index 0645b780b8..abe08d0433 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx b/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx index 45018e7e20..db3bdb51ac 100644 --- a/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx +++ b/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx @@ -5,7 +5,7 @@ Please see LICENSE files in the repository root for full details. */ import { useEffect, useRef, useState } from "react"; -import { EventType, type JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { EventType, type HistoryVisibility, type JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; @@ -49,6 +49,10 @@ export interface RoomSummaryCardState { * The join rule of the room, used to display the correct badge and icon */ roomJoinRule: JoinRule; + /** + * The history visibility of the room, used to display the correct badge. + */ + historyVisibility: HistoryVisibility; /** * if it is a video room, it should not display export chat, polls, files, extensions */ @@ -158,8 +162,9 @@ export function useRoomSummaryCardViewModel( const e2eStatus = roomContext.e2eStatus; const isVideoRoom = calcIsVideoRoom(room); - const { roomJoinRule } = useRoomState(room, (state) => ({ + const { historyVisibility, roomJoinRule } = useRoomState(room, (state) => ({ roomJoinRule: state.getJoinRule(), + historyVisibility: state.getHistoryVisibility(), })); const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || ""; const pinCount = usePinnedEvents(room).length; @@ -254,6 +259,7 @@ export function useRoomSummaryCardViewModel( isRoomEncrypted, roomJoinRule, e2eStatus, + historyVisibility, isVideoRoom, alias, isFavorite, diff --git a/src/components/views/right_panel/RoomSummaryCardView.tsx b/src/components/views/right_panel/RoomSummaryCardView.tsx index 67eea91478..be316fdd14 100644 --- a/src/components/views/right_panel/RoomSummaryCardView.tsx +++ b/src/components/views/right_panel/RoomSummaryCardView.tsx @@ -39,7 +39,7 @@ import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error" import ErrorSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; -import { Box, Flex } from "@element-hq/web-shared-components"; +import { Box, Flex, HistoryVisibilityBadge } from "@element-hq/web-shared-components"; import BaseCard from "./BaseCard.tsx"; import { _t } from "../../../languageHandler.tsx"; @@ -165,34 +165,42 @@ const RoomSummaryCardView: React.FC = ({ {vm.alias} - + {!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && ( - + {_t("common|public_room")} )} {vm.isRoomEncrypted && vm.e2eStatus !== E2EStatus.Warning && ( - + {_t("common|encrypted")} )} {!vm.isRoomEncrypted && ( - + {_t("common|unencrypted")} )} {vm.e2eStatus === E2EStatus.Warning && ( - + {_t("common|not_trusted")} )} + + diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 9bafa55c10..1159fa3fb7 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -32,6 +32,7 @@ import { type EventStatus, type ICreateRoomOpts, RoomState, + HistoryVisibility, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { normalize } from "matrix-js-sdk/src/utils"; @@ -673,6 +674,7 @@ export function mkStubRoom( maySendRedactionForEvent: jest.fn().mockReturnValue(true), maySendEvent: jest.fn().mockReturnValue(true), members: {}, + getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Shared), getJoinRule: jest.fn().mockReturnValue(JoinRule.Invite), on: jest.fn(), off: jest.fn(), diff --git a/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx b/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx index 0e5c7efffe..44b7fe8317 100644 --- a/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx +++ b/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { render, fireEvent, screen } from "jest-matrix-react"; -import { Room, type MatrixClient, JoinRule, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { Room, type MatrixClient, JoinRule, MatrixEvent, HistoryVisibility } from "matrix-js-sdk/src/matrix"; import { mocked, type MockedObject } from "jest-mock"; import userEvent from "@testing-library/user-event"; @@ -56,6 +56,7 @@ describe("", () => { e2eStatus: undefined, isVideoRoom: false, roomJoinRule: JoinRule.Public, + historyVisibility: HistoryVisibility.Shared, alias: "", isFavorite: false, canInviteToState: true, diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap index c1e6782909..23da533e9f 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap @@ -70,7 +70,7 @@ exports[` has button to edit topic 1`] = ` />
has button to edit topic 1`] = ` fill="currentColor" height="1em" viewBox="0 0 24 24" - width="1em" + width="1rem" xmlns="http://www.w3.org/2000/svg" > has button to edit topic 1`] = ` fill="currentColor" height="1em" viewBox="0 0 24 24" - width="1em" + width="1rem" xmlns="http://www.w3.org/2000/svg" > has button to edit topic 1`] = ` Not encrypted + + + + + + New members see history +
renders the room summary 1`] = ` />
renders the room summary 1`] = ` fill="currentColor" height="1em" viewBox="0 0 24 24" - width="1em" + width="1rem" xmlns="http://www.w3.org/2000/svg" > renders the room summary 1`] = ` fill="currentColor" height="1em" viewBox="0 0 24 24" - width="1em" + width="1rem" xmlns="http://www.w3.org/2000/svg" > renders the room summary 1`] = ` Not encrypted + + + + + + New members see history +
renders the room topic in the summary 1`] = ` />
renders the room topic in the summary 1`] = ` fill="currentColor" height="1em" viewBox="0 0 24 24" - width="1em" + width="1rem" xmlns="http://www.w3.org/2000/svg" > renders the room topic in the summary 1`] = ` fill="currentColor" height="1em" viewBox="0 0 24 24" - width="1em" + width="1rem" xmlns="http://www.w3.org/2000/svg" > renders the room topic in the summary 1`] = ` Not encrypted + + + + + + New members see history +