Skip to content
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
47ca834
feat: Implement UI for history visibility acknowledgement.
kaylendog Nov 3, 2025
efe05c7
tests: Add test suite for `RoomStatusBarHistoryVisible`.
kaylendog Nov 3, 2025
568adbe
docs: Document `RoomStatusBarHistoryVisible` and props interface.
kaylendog Nov 3, 2025
426c957
feat: Use newer `@vector-im/compound` components.
kaylendog Nov 3, 2025
a21f850
test: Update snapshots for `RoomStatusBarHistoryVisible` tests.
kaylendog Nov 3, 2025
409d1eb
chore: Update playwright screenshots.
kaylendog Nov 4, 2025
a899629
feat: Move `RoomStatusBarHistoryVisible` to `shared-components`.
kaylendog Nov 4, 2025
6a51c18
fix: Address review comments on `RoomStatusBarHistoryVisible`.
kaylendog Nov 7, 2025
a5fdc5b
fix: Address review comments on `RoomStatusBar` and tests.
kaylendog Nov 7, 2025
7bc5d67
chore: Move `RoomStatusBarHistoryVisible` to `room/RoomStatusBarHisto…
kaylendog Nov 7, 2025
636ecc9
chore: Fix linting issues.
kaylendog Nov 7, 2025
4ae0b01
feat: Gate behind history visibility labs flag.
kaylendog Nov 13, 2025
6d7bff7
feat: Add link to history sharing docs.
kaylendog Nov 13, 2025
a729c86
fix: Resolve build issue with shared-components.
kaylendog Nov 13, 2025
30c41d1
tests: Enable history sharing lab for unit tests.
kaylendog Nov 13, 2025
7444e3b
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Nov 13, 2025
6e49704
tests: Set labs flag in SettingsStore mock.
kaylendog Nov 13, 2025
8620722
fix: Remove non-existent arg - documentation should be updated!
kaylendog Nov 13, 2025
ccec28f
chore: Remove old CSS rule filter.
kaylendog Nov 13, 2025
b8a54b9
fix: Use package name for import over relative path.
kaylendog Nov 13, 2025
bed81c3
fix: Mark styles as important due to improper CSS load order.
kaylendog Nov 13, 2025
bdc1067
docs: Add doc comments to `!important` directives.
kaylendog Nov 13, 2025
746acc9
docs: Correct license header.
kaylendog Nov 13, 2025
f0377f8
tests: Update `RoomStatusBarHistoryVisible` snapshot.
kaylendog Nov 13, 2025
90a0cab
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Nov 17, 2025
4e77fab
tests: Update shared history invite screenshot.
kaylendog Nov 17, 2025
4c84126
tests: Revert spurious screenshot changes.
kaylendog Nov 17, 2025
c2ec426
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Nov 26, 2025
137d633
feat: Update to use `Banner` component.
kaylendog Nov 28, 2025
b8a1b6b
chore: Remove broken import.
kaylendog Nov 28, 2025
656d90f
chore: Remove unused translation string.
kaylendog Nov 28, 2025
3e6bf40
tests: Add `getHistoryVisibility` to `currentState` of stub room.
kaylendog Nov 28, 2025
3072fe6
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Nov 28, 2025
641456d
tests: Update screenshot.
kaylendog Nov 28, 2025
ad48665
chore: Remove old snapshots.
kaylendog Nov 28, 2025
54b938e
Merge branch 'develop' into kaylendog/history-sharing/status
kaylendog Nov 28, 2025
b14779a
Merge branch 'develop' into kaylendog/history-sharing/status
kaylendog Dec 1, 2025
658c3d3
tests: Update playwright screenshot.
kaylendog Dec 1, 2025
be0fb6d
Merge branch 'develop' into kaylendog/history-sharing/status
kaylendog Dec 1, 2025
14f2cbb
feat: Separate `HistoryVisibleBanner` hooks into MVVM architecture.
kaylendog Dec 1, 2025
4bc039b
chore: Remove unused imports.
kaylendog Dec 1, 2025
bd78960
feat: Use info link over action button for `HistoryVisibleBanner`
kaylendog Dec 1, 2025
21cb3c8
tests: Update snapshot for `HistoryVisibleBanner`.
kaylendog Dec 1, 2025
86fa5a3
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Dec 1, 2025
321b5c2
chore: Remove unused imports.
kaylendog Dec 1, 2025
69a8ae2
feat: Switch to MVVM architecture per style guide.
kaylendog Dec 3, 2025
773e92c
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Dec 3, 2025
49dfc24
tests: Update snapshot for `HistoryVisibleBanner`.
kaylendog Dec 3, 2025
5f22fa6
tests: Update shared components snapshots.
kaylendog Dec 3, 2025
7a8c489
tests: Add unit tests for `HistoryVisibleBannerView` stories.
kaylendog Dec 3, 2025
e41e64c
fix: Linting errors from SonarCloud.
kaylendog Dec 3, 2025
e5bf5c3
feat: Finalise conversion to MVVM.
kaylendog Dec 3, 2025
e0e7017
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Dec 3, 2025
f1b6112
fix: Silent `this` binding issue.
kaylendog Dec 3, 2025
3602023
tests: Update playwright snapshot.
kaylendog Dec 4, 2025
c49a600
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Dec 8, 2025
50b5785
feat: Introduce wrapper component for `HistoryVisibleBanner`.
kaylendog Dec 9, 2025
8477189
tests: Update playwright screenshots for `HistoryVisibleBanner`.
kaylendog Dec 9, 2025
b6b65b0
docs: Add doc comments to fields in `HistoryVisibleBannerViewModel`.
kaylendog Dec 9, 2025
15843cc
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Dec 9, 2025
70e9baf
Merge remote-tracking branch 'upstream/develop' into kaylendog/histor…
kaylendog Dec 10, 2025
042698a
tests: Update playwright snapshot.
kaylendog Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 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, type StoryFn } from "@storybook/react-vite";
import React, { type JSX } from "react";
import { fn } from "storybook/test";

import { useMockedViewModel } from "../../useMockedViewModel";
import {
HistoryVisibleBannerView,
type HistoryVisibleBannerViewActions,
type HistoryVisibleBannerViewSnapshot,
} from "./HistoryVisibleBannerView";

type HistoryVisibleBannerProps = HistoryVisibleBannerViewSnapshot & HistoryVisibleBannerViewActions;

const HistoryVisibleBannerViewWrapper = ({ onClose, ...rest }: HistoryVisibleBannerProps): JSX.Element => {
const vm = useMockedViewModel(rest, {
onClose,
});
return <HistoryVisibleBannerView vm={vm} />;
};

export default {
title: "composer/HistoryVisibleBannerView",
component: HistoryVisibleBannerViewWrapper,
tags: ["autodocs"],
argTypes: {},
args: {
visible: true,
onClose: fn(),
},
} as Meta<typeof HistoryVisibleBannerViewWrapper>;

const Template: StoryFn<typeof HistoryVisibleBannerViewWrapper> = (args) => (
<HistoryVisibleBannerViewWrapper {...args} />
);

export const Default = Template.bind({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2025 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 { render } from "jest-matrix-react";
import { composeStories } from "@storybook/react-vite";

import * as stories from "./HistoryVisibleBannerView.stories.tsx";

const { Default } = composeStories(stories);

describe("HistoryVisibleBannerView", () => {
it("renders a history visible banner", () => {
const dismissFn = jest.fn();

const { container } = render(<Default onClose={dismissFn} />);
expect(container).toMatchSnapshot();

const button = container.querySelector("button");
expect(button).not.toBeNull();
button?.click();
expect(dismissFn).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 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 { Link } from "@vector-im/compound-web";
import React, { type JSX } from "react";

import { useViewModel } from "../../useViewModel";
import { _t } from "../../utils/i18n";
import { type ViewModel } from "../../viewmodel";
import { Banner } from "../Banner";

export interface HistoryVisibleBannerViewActions {
/**
* Called when the user dismisses the banner.
*/
onClose: () => void;
}

export interface HistoryVisibleBannerViewSnapshot {
/**
* Whether the banner is currently visible.
*/
visible: boolean;
}

/**
* The view model for the banner.
*/
export type HistoryVisibleBannerViewModel = ViewModel<HistoryVisibleBannerViewSnapshot> &
HistoryVisibleBannerViewActions;

interface HistoryVisibleBannerViewProps {
/**
* The view model for the banner.
*/
vm: HistoryVisibleBannerViewModel;
}

/**
* A component to alert that history is shared to new members of the room.
*
* @example
* ```tsx
* <HistoryVisibleBannerView vm={historyVisibleBannerViewModel} />
* ```
*/
export function HistoryVisibleBannerView({ vm }: Readonly<HistoryVisibleBannerViewProps>): JSX.Element {
const { visible } = useViewModel(vm);

const contents = _t(
"room|status_bar|history_visible",
{},
{
a: substituteATag,
},
);

return (
<>
{visible && (
<Banner type="info" onClose={() => vm.onClose()}>
{contents}
</Banner>
)}
</>
);
}

function substituteATag(sub: string): JSX.Element {
return (
<Link href="https://element.io/en/help#e2ee-history-sharing" target="_blank">
{sub}
</Link>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`HistoryVisibleBannerView renders a history visible banner 1`] = `
<div>
<div
class="banner"
data-type="info"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
/>
<path
clip-rule="evenodd"
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"
fill-rule="evenodd"
/>
</svg>
</div>
<span
class="content"
>
<span>
Messages you send will be shared with new members invited to this room.
<a
class="_link_1v5rz_8"
data-kind="primary"
data-size="medium"
href="https://element.io/en/help#e2ee-history-sharing"
rel="noreferrer noopener"
target="_blank"
>
Learn more
</a>
</span>
</span>
<div
class="actions"
>
<button
class="_button_187yx_8"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
Dismiss
</button>
</div>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright (c) 2025 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 "./HistoryVisibleBannerView";
1 change: 1 addition & 0 deletions packages/shared-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./audio/PlayPauseButton";
export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails";
export * from "./composer/Banner";
export * from "./composer/HistoryVisibleBannerView";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./pill-input/Pill";
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { type Optional } from "matrix-events-sdk";
import { Tooltip } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { LockOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { HistoryVisibleBannerView } from "@element-hq/web-shared-components";

import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
Expand Down Expand Up @@ -55,6 +56,7 @@ import { type MatrixClientProps, withMatrixClientHOC } from "../../../contexts/M
import { UIFeature } from "../../../settings/UIFeature";
import { formatTimeLeft } from "../../../DateUtils";
import RoomReplacedSvg from "../../../../res/img/room_replaced.svg";
import { HistoryVisibleBannerViewModel } from "../../../viewmodels/composer/HistoryVisibleBannerViewModel";

// The prefix used when persisting editor drafts to localstorage.
export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_";
Expand Down Expand Up @@ -666,6 +668,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
return (
<Tooltip open={isTooltipOpen} description={formatTimeLeft(secondsLeft)} placement="bottom">
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
<HistoryVisibleBannerView vm={new HistoryVisibleBannerViewModel({ room: this.props.room })} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At every render of the MessageComposer, the vm will be recreated. We want to avoid that.
You can:

  1. Create the vm in the message component (constructor/componentDidMount), put in the state, dispose the vm in componentWillUnMount
  2. Create a functional component where you create the vm with Introduce a hook to auto dispose view models #31178 and render the view

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 50b5785

<div className="mx_MessageComposer_wrapper">
<UserIdentityWarning room={this.props.room} key={this.props.room.roomId} />
<ReplyPreview
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2112,6 +2112,7 @@
"status_bar": {
"delete_all": "Delete all",
"exceeded_resource_limit": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
"history_visible": "Messages you send will be shared with new members invited to this room. <a>Learn more</a>",
"homeserver_blocked": "Your message wasn't sent because this homeserver has been blocked by its administrator. Please <a>contact your service administrator</a> to continue using the service.",
"monthly_user_limit_reached": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
"requires_consent_agreement": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
Expand Down
5 changes: 5 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export interface Settings {
"inviteRules": IBaseSetting<ComputedInviteConfig>;
"blockInvites": IBaseSetting<boolean>;
"Developer.elementCallUrl": IBaseSetting<string>;
"acknowledgedHistoryVisibility": IBaseSetting<boolean>;
}

export type SettingKey = keyof Settings;
Expand Down Expand Up @@ -1480,4 +1481,8 @@ export const SETTINGS: Settings = {
displayName: _td("devtools|settings|elementCallUrl"),
default: "",
},
"acknowledgedHistoryVisibility": {
supportedLevels: [SettingLevel.ROOM_ACCOUNT],
default: false,
},
};
105 changes: 105 additions & 0 deletions src/viewmodels/composer/HistoryVisibleBannerViewModel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2025 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 {
BaseViewModel,
type HistoryVisibleBannerViewModel as HistoryVisibleBannerViewModelInterface,
type HistoryVisibleBannerViewSnapshot,
} from "@element-hq/web-shared-components";
import { HistoryVisibility, RoomStateEvent, type Room } from "matrix-js-sdk/src/matrix";

import SettingsStore from "../../settings/SettingsStore";
import { SettingLevel } from "../../settings/SettingLevel";

interface Props {
room: Room;
}

export class HistoryVisibleBannerViewModel
extends BaseViewModel<HistoryVisibleBannerViewSnapshot, Props>
implements HistoryVisibleBannerViewModelInterface
{
private readonly featureWatcher: string;
private readonly acknowledgedWatcher: string;
Comment on lines 29 to 34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing tsdoc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b6b65b0


private static readonly computeSnapshot = (room: Room): HistoryVisibleBannerViewSnapshot => {
const featureEnabled = SettingsStore.getValue("feature_share_history_on_invite");
const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", room.roomId);

return {
visible:
featureEnabled &&
room.hasEncryptionStateEvent() &&
room.getHistoryVisibility() !== HistoryVisibility.Joined &&
!acknowledged,
};
};

public constructor(props: Props) {
super(props, HistoryVisibleBannerViewModel.computeSnapshot(props.room));

this.disposables.trackListener(props.room, RoomStateEvent.Update, () => this.setSnapshot());

// `SettingsStore` is not an `EventListener`, so we must manage these manually.
this.featureWatcher = SettingsStore.watchSetting(
"feature_share_history_on_invite",
null,
(_key, _roomId, _level, value: boolean) => this.setSnapshot(),
);
this.acknowledgedWatcher = SettingsStore.watchSetting(
"acknowledgedHistoryVisibility",
props.room.roomId,
(_key, _roomId, _level, value: boolean) => this.setSnapshot(),
);
}

private setSnapshot(): void {
const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", this.props.room.roomId);

// Reset the acknowleded flag when the history visibility is set back to joined.
if (this.props.room.getHistoryVisibility() === HistoryVisibility.Joined && acknowledged) {
SettingsStore.setValue(
"acknowledgedHistoryVisibility",
this.props.room.roomId,
SettingLevel.ROOM_ACCOUNT,
false,
);
}

this.snapshot.set(HistoryVisibleBannerViewModel.computeSnapshot(this.props.room));
}

/**
* Revoke the banner's acknoledgement status.
*/
public async revoke(): Promise<void> {
await SettingsStore.setValue(
"acknowledgedHistoryVisibility",
this.props.room.roomId,
SettingLevel.ROOM_ACCOUNT,
false,
);
}

/**
* Called when the user dismisses the banner.
*/
public async onClose(): Promise<void> {
await SettingsStore.setValue(
"acknowledgedHistoryVisibility",
this.props.room.roomId,
SettingLevel.ROOM_ACCOUNT,
true,
);
}

public dispose(): void {
super.dispose();
SettingsStore.unwatchSetting(this.featureWatcher);
SettingsStore.unwatchSetting(this.acknowledgedWatcher);
}
}
Loading
Loading