Files
element-web/src/viewmodels/composer/HistoryVisibleBannerViewModel.tsx
Skye Elliot cff9119324 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: https://github.com/element-hq/element-meta/issues/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.
2025-12-10 10:37:04 +00:00

113 lines
3.8 KiB
TypeScript

/*
* 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
{
/**
* Watcher ID for the "feature_share_history_on_invite" setting.
*/
private readonly featureWatcher: string;
/**
* Watcher ID for the "acknowledgedHistoryVisibility" setting specific to the room.
*/
private readonly acknowledgedWatcher: string;
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);
}
}