-
;
"blockInvites": IBaseSetting;
"Developer.elementCallUrl": IBaseSetting;
- "acknowledgedHistoryVisibility": IBaseSetting;
}
export type SettingKey = keyof Settings;
@@ -1499,8 +1498,4 @@ export const SETTINGS: Settings = {
displayName: _td("devtools|settings|elementCallUrl"),
default: "",
},
- "acknowledgedHistoryVisibility": {
- supportedLevels: [SettingLevel.ROOM_ACCOUNT],
- default: false,
- },
};
diff --git a/src/viewmodels/composer/HistoryVisibleBannerViewModel.tsx b/src/viewmodels/composer/HistoryVisibleBannerViewModel.tsx
deleted file mode 100644
index 7d1ce9ec81..0000000000
--- a/src/viewmodels/composer/HistoryVisibleBannerViewModel.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * 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";
-
-/**
- * A collection of {@link HistoryVisibility} levels that trigger the display of the history visible banner.
- */
-const BANNER_VISIBLE_LEVELS = [HistoryVisibility.Shared, HistoryVisibility.WorldReadable];
-
-interface Props {
- /**
- * The room instance associated with this banner view model.
- */
- room: Room;
-
- /**
- * Whether or not the current user is able to send messages in this room.
- */
- canSendMessages: boolean;
-
- /**
- * If not null, indicates the ID of the thread currently being viewed in the thread
- * timeline side view, where the banner view is displayed as a child of the message
- * composer.
- */
- threadId: string | null;
-}
-
-/**
- * View model for the history visible banner, which prompts users that the current room
- * history may be shared with new invitees, if they have not already acknowledged the
- * banner.
- *
- * The view model operates using a simple 2-case algorithm:
- *
- * 1. When a user opens an encrypted room where `history_visibility` is not set to `joined`,
- * and the user hasn't previously dismissed it for this particular room, display a banner.
- * If the user dismisses the banner, update the client's local store to record that the
- * banner has been dismissed.
- * 2. When the user opens an encrypted room where `history_visibility` is set to `joined`, clear
- * the dismissal flag if it was previously set. This ensures that if the room's history
- * visibility changes from public to private and back to public, the banner will reappear
- * when appropriate.
- *
- * This banner is only shown in the regular timeline view, not the thread timeline view, which is
- * done by conditioning on the presence of `threadId` in the viewmodel's {@link Props}.
- *
- * See https://github.com/element-hq/element-meta/issues/2875 for more information.
- */
-export class HistoryVisibleBannerViewModel
- extends BaseViewModel
- 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;
-
- /**
- * Computes the latest banner snapshot given the VM's props.
- * @param props - See {@link Props}.
- * @returns The latest snapshot. See {@link HistoryVisibleBannerViewSnapshot}.
- */
- private static readonly computeSnapshot = ({
- room,
- canSendMessages,
- threadId,
- }: Props): HistoryVisibleBannerViewSnapshot => {
- const featureEnabled = SettingsStore.getValue("feature_share_history_on_invite");
- const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", room.roomId);
- const isHistoryVisible = BANNER_VISIBLE_LEVELS.includes(room.getHistoryVisibility());
-
- // This implements point 1. of the algorithm described above. In the order below, all
- // of the following must be true for the banner to display:
- // - The room history sharing feature must be enabled.
- // - The room must be encrypted.
- // - The user must be able to send messages.
- // - The history must be visible.
- // - The view should not be part of a thread timeline.
- // - The user must not have acknowledged the banner.
- return {
- visible:
- featureEnabled &&
- room.hasEncryptionStateEvent() &&
- canSendMessages &&
- isHistoryVisible &&
- !threadId &&
- !acknowledged,
- };
- };
-
- /**
- * Creates a new view model instance.
- * @param props - Properties for this view model. See {@link Props}.
- */
- public constructor(props: Props) {
- super(props, HistoryVisibleBannerViewModel.computeSnapshot(props));
-
- 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(),
- );
- }
-
- /**
- * Recompute and update this VM instance's snapshot. This will update the `acknowledgedHistoryVisibility`
- * store entry if necessary.
- */
- 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));
- }
-
- /**
- * Revoke the banner's acknoledgement status.
- */
- public async revoke(): Promise {
- await SettingsStore.setValue(
- "acknowledgedHistoryVisibility",
- this.props.room.roomId,
- SettingLevel.ROOM_ACCOUNT,
- false,
- );
- }
-
- /**
- * Called when the user dismisses the banner.
- */
- public async onClose(): Promise {
- await SettingsStore.setValue(
- "acknowledgedHistoryVisibility",
- this.props.room.roomId,
- SettingLevel.ROOM_ACCOUNT,
- true,
- );
- }
-
- /**
- * Dispose of the viewmodel and its settings listeners.
- */
- public dispose(): void {
- super.dispose();
- SettingsStore.unwatchSetting(this.featureWatcher);
- SettingsStore.unwatchSetting(this.acknowledgedWatcher);
- }
-}
diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts
index 9ebfdbf1f3..9bafa55c10 100644
--- a/test/test-utils/test-utils.ts
+++ b/test/test-utils/test-utils.ts
@@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
import EventEmitter from "events";
import { mocked, type MockedObject } from "jest-mock";
import {
- type EventTimeline,
MatrixEvent,
type Room,
type User,
@@ -17,7 +16,7 @@ import {
type IEvent,
type RoomMember,
type MatrixClient,
- RoomState,
+ type EventTimeline,
EventType,
type IEventRelation,
type IUnsigned,
@@ -30,9 +29,9 @@ import {
JoinRule,
type OidcClientConfig,
type GroupCall,
- HistoryVisibility,
- type ICreateRoomOpts,
type EventStatus,
+ type ICreateRoomOpts,
+ RoomState,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { normalize } from "matrix-js-sdk/src/utils";
@@ -668,7 +667,6 @@ export function mkStubRoom(
createThreadsTimelineSets: jest.fn().mockReturnValue(new Promise(() => {})),
currentState: {
getStateEvents: jest.fn((_type, key) => (key === undefined ? [] : null)),
- getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Joined),
getMember: jest.fn(),
mayClientSendStateEvent: jest.fn().mockReturnValue(true),
maySendStateEvent: jest.fn().mockReturnValue(true),
@@ -689,7 +687,6 @@ export function mkStubRoom(
getCanonicalAlias: jest.fn(),
getDMInviter: jest.fn(),
getEventReadUpTo: jest.fn(() => null),
- getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Joined),
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1),
getJoinRule: jest.fn().mockReturnValue("invite"),
getJoinedMemberCount: jest.fn().mockReturnValue(1),
diff --git a/test/unit-tests/components/viewmodels/composer/HistoryVisibleBannerViewModel-test.tsx b/test/unit-tests/components/viewmodels/composer/HistoryVisibleBannerViewModel-test.tsx
deleted file mode 100644
index b1ab6ef7ff..0000000000
--- a/test/unit-tests/components/viewmodels/composer/HistoryVisibleBannerViewModel-test.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * 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, canSendMessages: true, threadId: null });
- 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, canSendMessages: true, threadId: null });
- 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, canSendMessages: true, threadId: null });
- expect(vm.getSnapshot().visible).toBe(false);
- vm.dispose();
- });
-
- it("should not show the banner in threads", () => {
- 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, canSendMessages: true, threadId: "some thread ID" });
- expect(vm.getSnapshot().visible).toBe(false);
- vm.dispose();
- });
-
- it("should not show the banner if the user cannot send messages", () => {
- 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, canSendMessages: false, threadId: null });
- expect(vm.getSnapshot().visible).toBe(false);
- vm.dispose();
- });
-
- it("should not show the banner if history visibility is `invited`", () => {
- 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: "invited",
- },
- }),
- ]);
-
- const vm = new HistoryVisibleBannerViewModel({ room, canSendMessages: true, threadId: null });
- expect(vm.getSnapshot().visible).toBe(false);
- vm.dispose();
- });
-
- it("should show the banner in encrypted rooms with shared 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, canSendMessages: true, threadId: null });
- expect(vm.getSnapshot().visible).toBe(true);
- await vm.onClose();
- expect(vm.getSnapshot().visible).toBe(false);
- });
-});