Skip to content

Commit cff9119

Browse files
authored
Implement UI for history visibility acknowledgement. (#31156)
* feat: Implement UI for history visibility acknowledgement. Shows a banner above the message composer whenever a user opens a room with non-join history visibility, which they can dismiss. - Whenever a user opens an encrypted room with non-join history visibility, show them a banner, unless we have already marked it as dismissed. - Whenever a user opens an encrypted room with joined history visibility, we unmark it as dismissed. Issue: element-hq/element-meta#2875 * tests: Add test suite for `RoomStatusBarHistoryVisible`. * docs: Document `RoomStatusBarHistoryVisible` and props interface. * feat: Use newer `@vector-im/compound` components. * test: Update snapshots for `RoomStatusBarHistoryVisible` tests. * chore: Update playwright screenshots. * feat: Move `RoomStatusBarHistoryVisible` to `shared-components`. * fix: Address review comments on `RoomStatusBarHistoryVisible`. * fix: Address review comments on `RoomStatusBar` and tests. * chore: Move `RoomStatusBarHistoryVisible` to `room/RoomStatusBarHistoryVisible` * chore: Fix linting issues. * feat: Gate behind history visibility labs flag. * feat: Add link to history sharing docs. * fix: Resolve build issue with shared-components. * tests: Enable history sharing lab for unit tests. * tests: Set labs flag in SettingsStore mock. * fix: Remove non-existent arg - documentation should be updated! * chore: Remove old CSS rule filter. * fix: Use package name for import over relative path. * fix: Mark styles as important due to improper CSS load order. * docs: Add doc comments to `!important` directives. This change should restore my status as a good person. * docs: Correct license header. * tests: Update `RoomStatusBarHistoryVisible` snapshot. * tests: Update shared history invite screenshot. * tests: Revert spurious screenshot changes. * feat: Update to use `Banner` component. * chore: Remove broken import. * chore: Remove unused translation string. * tests: Add `getHistoryVisibility` to `currentState` of stub room. * tests: Update screenshot. * chore: Remove old snapshots. * tests: Update playwright screenshot. * feat: Separate `HistoryVisibleBanner` hooks into MVVM architecture. * chore: Remove unused imports. * feat: Use info link over action button for `HistoryVisibleBanner` * tests: Update snapshot for `HistoryVisibleBanner`. * chore: Remove unused imports. * feat: Switch to MVVM architecture per style guide. * tests: Update snapshot for `HistoryVisibleBanner`. * tests: Update shared components snapshots. * tests: Add unit tests for `HistoryVisibleBannerView` stories. * fix: Linting errors from SonarCloud. * feat: Finalise conversion to MVVM. * fix: Silent `this` binding issue. * tests: Update playwright snapshot. * feat: Introduce wrapper component for `HistoryVisibleBanner`. * tests: Update playwright screenshots for `HistoryVisibleBanner`. * docs: Add doc comments to fields in `HistoryVisibleBannerViewModel`. * tests: Update playwright snapshot.
1 parent a13e9c1 commit cff9119

File tree

15 files changed

+490
-1
lines changed

15 files changed

+490
-1
lines changed
19.5 KB
Loading
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
import { type Meta, type StoryFn } from "@storybook/react-vite";
8+
import React, { type JSX } from "react";
9+
import { fn } from "storybook/test";
10+
11+
import { useMockedViewModel } from "../../useMockedViewModel";
12+
import {
13+
HistoryVisibleBannerView,
14+
type HistoryVisibleBannerViewActions,
15+
type HistoryVisibleBannerViewSnapshot,
16+
} from "./HistoryVisibleBannerView";
17+
18+
type HistoryVisibleBannerProps = HistoryVisibleBannerViewSnapshot & HistoryVisibleBannerViewActions;
19+
20+
const HistoryVisibleBannerViewWrapper = ({ onClose, ...rest }: HistoryVisibleBannerProps): JSX.Element => {
21+
const vm = useMockedViewModel(rest, {
22+
onClose,
23+
});
24+
return <HistoryVisibleBannerView vm={vm} />;
25+
};
26+
27+
export default {
28+
title: "composer/HistoryVisibleBannerView",
29+
component: HistoryVisibleBannerViewWrapper,
30+
tags: ["autodocs"],
31+
argTypes: {},
32+
args: {
33+
visible: true,
34+
onClose: fn(),
35+
},
36+
} as Meta<typeof HistoryVisibleBannerViewWrapper>;
37+
38+
const Template: StoryFn<typeof HistoryVisibleBannerViewWrapper> = (args) => (
39+
<HistoryVisibleBannerViewWrapper {...args} />
40+
);
41+
42+
export const Default = Template.bind({});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import { render } from "jest-matrix-react";
10+
import { composeStories } from "@storybook/react-vite";
11+
12+
import * as stories from "./HistoryVisibleBannerView.stories.tsx";
13+
14+
const { Default } = composeStories(stories);
15+
16+
describe("HistoryVisibleBannerView", () => {
17+
it("renders a history visible banner", () => {
18+
const dismissFn = jest.fn();
19+
20+
const { container } = render(<Default onClose={dismissFn} />);
21+
expect(container).toMatchSnapshot();
22+
23+
const button = container.querySelector("button");
24+
expect(button).not.toBeNull();
25+
button?.click();
26+
expect(dismissFn).toHaveBeenCalled();
27+
});
28+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { Link } from "@vector-im/compound-web";
9+
import React, { type JSX } from "react";
10+
11+
import { useViewModel } from "../../useViewModel";
12+
import { _t } from "../../utils/i18n";
13+
import { type ViewModel } from "../../viewmodel";
14+
import { Banner } from "../Banner";
15+
16+
export interface HistoryVisibleBannerViewActions {
17+
/**
18+
* Called when the user dismisses the banner.
19+
*/
20+
onClose: () => void;
21+
}
22+
23+
export interface HistoryVisibleBannerViewSnapshot {
24+
/**
25+
* Whether the banner is currently visible.
26+
*/
27+
visible: boolean;
28+
}
29+
30+
/**
31+
* The view model for the banner.
32+
*/
33+
export type HistoryVisibleBannerViewModel = ViewModel<HistoryVisibleBannerViewSnapshot> &
34+
HistoryVisibleBannerViewActions;
35+
36+
interface HistoryVisibleBannerViewProps {
37+
/**
38+
* The view model for the banner.
39+
*/
40+
vm: HistoryVisibleBannerViewModel;
41+
}
42+
43+
/**
44+
* A component to alert that history is shared to new members of the room.
45+
*
46+
* @example
47+
* ```tsx
48+
* <HistoryVisibleBannerView vm={historyVisibleBannerViewModel} />
49+
* ```
50+
*/
51+
export function HistoryVisibleBannerView({ vm }: Readonly<HistoryVisibleBannerViewProps>): JSX.Element {
52+
const { visible } = useViewModel(vm);
53+
54+
const contents = _t(
55+
"room|status_bar|history_visible",
56+
{},
57+
{
58+
a: substituteATag,
59+
},
60+
);
61+
62+
return (
63+
<>
64+
{visible && (
65+
<Banner type="info" onClose={() => vm.onClose()}>
66+
{contents}
67+
</Banner>
68+
)}
69+
</>
70+
);
71+
}
72+
73+
function substituteATag(sub: string): JSX.Element {
74+
return (
75+
<Link href="https://element.io/en/help#e2ee-history-sharing" target="_blank">
76+
{sub}
77+
</Link>
78+
);
79+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`HistoryVisibleBannerView renders a history visible banner 1`] = `
4+
<div>
5+
<div
6+
class="banner"
7+
data-type="info"
8+
>
9+
<div
10+
class="icon"
11+
>
12+
<svg
13+
fill="currentColor"
14+
font-size="24"
15+
height="1em"
16+
viewBox="0 0 24 24"
17+
width="1em"
18+
xmlns="http://www.w3.org/2000/svg"
19+
>
20+
<path
21+
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
22+
/>
23+
<path
24+
clip-rule="evenodd"
25+
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
26+
fill-rule="evenodd"
27+
/>
28+
</svg>
29+
</div>
30+
<span
31+
class="content"
32+
>
33+
<span>
34+
Messages you send will be shared with new members invited to this room.
35+
<a
36+
class="_link_1v5rz_8"
37+
data-kind="primary"
38+
data-size="medium"
39+
href="https://element.io/en/help#e2ee-history-sharing"
40+
rel="noreferrer noopener"
41+
target="_blank"
42+
>
43+
Learn more
44+
</a>
45+
</span>
46+
</span>
47+
<div
48+
class="actions"
49+
>
50+
<button
51+
class="_button_187yx_8"
52+
data-kind="secondary"
53+
data-size="sm"
54+
role="button"
55+
tabindex="0"
56+
>
57+
Dismiss
58+
</button>
59+
</div>
60+
</div>
61+
</div>
62+
`;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
export * from "./HistoryVisibleBannerView";

packages/shared-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from "./audio/PlayPauseButton";
1212
export * from "./audio/SeekBar";
1313
export * from "./avatar/AvatarWithDetails";
1414
export * from "./composer/Banner";
15+
export * from "./composer/HistoryVisibleBannerView";
1516
export * from "./event-tiles/TextualEventView";
1617
export * from "./message-body/MediaBody";
1718
export * from "./pill-input/Pill";
4.13 KB
Loading
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { HistoryVisibleBannerView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
9+
import React from "react";
10+
import { type Room } from "matrix-js-sdk/src/matrix";
11+
12+
import { HistoryVisibleBannerViewModel } from "../../../viewmodels/composer/HistoryVisibleBannerViewModel";
13+
14+
export const HistoryVisibleBanner: React.FC<{ room: Room }> = ({ room }) => {
15+
const vm = useCreateAutoDisposedViewModel(() => new HistoryVisibleBannerViewModel({ room }));
16+
return <HistoryVisibleBannerView vm={vm} />;
17+
};

src/components/views/rooms/MessageComposer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { type MatrixClientProps, withMatrixClientHOC } from "../../../contexts/M
5454
import { UIFeature } from "../../../settings/UIFeature";
5555
import { formatTimeLeft } from "../../../DateUtils";
5656
import RoomReplacedSvg from "../../../../res/img/room_replaced.svg";
57+
import { HistoryVisibleBanner } from "../composer/HistoryVisibleBanner";
5758

5859
// The prefix used when persisting editor drafts to localstorage.
5960
export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_";
@@ -674,6 +675,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
674675

675676
return (
676677
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
678+
<HistoryVisibleBanner room={this.props.room} />
677679
<div className="mx_MessageComposer_wrapper">
678680
<UserIdentityWarning room={this.props.room} key={this.props.room.roomId} />
679681
<ReplyPreview

0 commit comments

Comments
 (0)