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.
This commit is contained in:
17
src/components/views/composer/HistoryVisibleBanner.tsx
Normal file
17
src/components/views/composer/HistoryVisibleBanner.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { HistoryVisibleBannerView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
|
||||
import React from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { HistoryVisibleBannerViewModel } from "../../../viewmodels/composer/HistoryVisibleBannerViewModel";
|
||||
|
||||
export const HistoryVisibleBanner: React.FC<{ room: Room }> = ({ room }) => {
|
||||
const vm = useCreateAutoDisposedViewModel(() => new HistoryVisibleBannerViewModel({ room }));
|
||||
return <HistoryVisibleBannerView vm={vm} />;
|
||||
};
|
||||
@@ -54,6 +54,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 { HistoryVisibleBanner } from "../composer/HistoryVisibleBanner";
|
||||
|
||||
// The prefix used when persisting editor drafts to localstorage.
|
||||
export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_";
|
||||
@@ -674,6 +675,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||
|
||||
return (
|
||||
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
|
||||
<HistoryVisibleBanner room={this.props.room} />
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<UserIdentityWarning room={this.props.room} key={this.props.room.roomId} />
|
||||
<ReplyPreview
|
||||
|
||||
@@ -2123,6 +2123,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>.",
|
||||
|
||||
@@ -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;
|
||||
@@ -1488,4 +1489,8 @@ export const SETTINGS: Settings = {
|
||||
displayName: _td("devtools|settings|elementCallUrl"),
|
||||
default: "",
|
||||
},
|
||||
"acknowledgedHistoryVisibility": {
|
||||
supportedLevels: [SettingLevel.ROOM_ACCOUNT],
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
112
src/viewmodels/composer/HistoryVisibleBannerViewModel.tsx
Normal file
112
src/viewmodels/composer/HistoryVisibleBannerViewModel.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user