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:
Skye Elliot
2025-12-10 10:37:04 +00:00
committed by GitHub
parent a13e9c1285
commit cff9119324
15 changed files with 490 additions and 1 deletions

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2025 New Vector 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 { Room } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import SettingsStore, { type CallbackFn } from "../../../../../src/settings/SettingsStore";
import { mkEvent, stubClient, upsertRoomStateEvents } from "../../../../test-utils";
import { HistoryVisibleBannerViewModel } from "../../../../../src/viewmodels/composer/HistoryVisibleBannerViewModel";
describe("HistoryVisibleBannerViewModel", () => {
const ROOM_ID = "!roomId:example.org";
let room: Room;
let watcherCallbacks: CallbackFn[];
let acknowledgedHistoryVisibility: boolean;
beforeEach(() => {
watcherCallbacks = [];
acknowledgedHistoryVisibility = false;
jest.spyOn(SettingsStore, "setValue").mockImplementation(async (settingName, roomId, level, value) => {
if (settingName === "acknowledgedHistoryVisibility") {
acknowledgedHistoryVisibility = value;
}
watcherCallbacks.forEach((callbackFn) => callbackFn(settingName, roomId, level, value, value));
});
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId) => {
if (settingName === "acknowledgedHistoryVisibility") {
return acknowledgedHistoryVisibility;
}
if (settingName === "feature_share_history_on_invite") {
return true;
}
return SettingsStore.getDefaultValue(settingName);
});
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, roomId, callbackFn) => {
watcherCallbacks.push(callbackFn);
return `mockWatcherId-${settingName}-${roomId}`;
});
stubClient();
room = new Room(ROOM_ID, {} as any, "@user:example.org");
});
afterEach(() => {
jest.clearAllMocks();
});
it("should not show the banner in unencrypted rooms", () => {
const vm = new HistoryVisibleBannerViewModel({ room });
expect(vm.getSnapshot().visible).toBe(false);
});
it("should not show the banner in encrypted rooms with joined history visibility", () => {
upsertRoomStateEvents(room, [
mkEvent({
event: true,
type: "m.room.encryption",
user: "@user1:server",
content: {},
}),
mkEvent({
event: true,
type: "m.room.history_visibility",
content: {
history_visibility: "joined",
},
user: "@user1:server",
}),
]);
const vm = new HistoryVisibleBannerViewModel({ room });
expect(vm.getSnapshot().visible).toBe(false);
});
it("should not show the banner if it has been dismissed", async () => {
await SettingsStore.setValue("acknowledgedHistoryVisibility", ROOM_ID, SettingLevel.ROOM_ACCOUNT, true);
upsertRoomStateEvents(room, [
mkEvent({
event: true,
type: "m.room.encryption",
user: "@user1:server",
content: {},
}),
mkEvent({
event: true,
type: "m.room.history_visibility",
user: "@user1:server",
content: {
history_visibility: "shared",
},
}),
]);
const vm = new HistoryVisibleBannerViewModel({ room });
expect(vm.getSnapshot().visible).toBe(false);
vm.dispose();
});
it("should show the banner in encrypted rooms with non-joined history visibility", async () => {
upsertRoomStateEvents(room, [
mkEvent({
event: true,
type: "m.room.encryption",
user: "@user1:server",
content: {},
}),
mkEvent({
event: true,
type: "m.room.history_visibility",
user: "@user1:server",
content: {
history_visibility: "shared",
},
}),
]);
const vm = new HistoryVisibleBannerViewModel({ room });
expect(vm.getSnapshot().visible).toBe(true);
await vm.onClose();
expect(vm.getSnapshot().visible).toBe(false);
});
});