From ce9c66ba4c25f3de5ceca5d244591d8aa1183ce8 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Fri, 19 Dec 2025 15:41:09 +0000 Subject: [PATCH] Update algorithm for history visible banner. (#31577) * feat: Update algorithm for history visible banner. - The banner now only shows for rooms with `shared` or `worldReadable` history visibility. - The banner does not show in rooms in which the current user cannot send messages. * tests: Add `getHistoryVisibility` to stub room. * docs: Add description to `visible` condition check. * docs: Fix spelling. Co-authored-by: Florian Duros * chore: Remove `jest-sonar.xml`. --------- Co-authored-by: Florian Duros --- .../views/composer/HistoryVisibleBanner.tsx | 3 + .../views/rooms/MessageComposer.tsx | 6 +- .../HistoryVisibleBannerViewModel.tsx | 40 +++++++++---- test/test-utils/test-utils.ts | 1 + .../HistoryVisibleBannerViewModel-test.tsx | 58 +++++++++++++++++-- 5 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/components/views/composer/HistoryVisibleBanner.tsx b/src/components/views/composer/HistoryVisibleBanner.tsx index 09286ae8e1..85a6bd7fb9 100644 --- a/src/components/views/composer/HistoryVisibleBanner.tsx +++ b/src/components/views/composer/HistoryVisibleBanner.tsx @@ -16,6 +16,9 @@ export const HistoryVisibleBanner: React.FC<{ /** The room instance associated with this banner view model. */ room: Room; + /** Whether the current user can send messages in the room. */ + canSendMessages: boolean; + /** * If not null, specifies 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. diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index b2546b02a3..c23bc7917d 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -675,7 +675,11 @@ export class MessageComposer extends React.Component { return (
- +
{ + 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 && - !threadId && room.hasEncryptionStateEvent() && - room.getHistoryVisibility() !== HistoryVisibility.Joined && + canSendMessages && + isHistoryVisible && + !threadId && !acknowledged, }; }; @@ -92,7 +112,7 @@ export class HistoryVisibleBannerViewModel * @param props - Properties for this view model. See {@link Props}. */ public constructor(props: Props) { - super(props, HistoryVisibleBannerViewModel.computeSnapshot(props.room, props.threadId)); + super(props, HistoryVisibleBannerViewModel.computeSnapshot(props)); this.disposables.trackListener(props.room, RoomStateEvent.Update, () => this.setSnapshot()); @@ -126,7 +146,7 @@ export class HistoryVisibleBannerViewModel ); } - this.snapshot.set(HistoryVisibleBannerViewModel.computeSnapshot(this.props.room, this.props.threadId)); + this.snapshot.set(HistoryVisibleBannerViewModel.computeSnapshot(this.props)); } /** diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 525f2e4ed8..9757905882 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -677,6 +677,7 @@ 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 index 764376aba3..b1ab6ef7ff 100644 --- a/test/unit-tests/components/viewmodels/composer/HistoryVisibleBannerViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/composer/HistoryVisibleBannerViewModel-test.tsx @@ -54,7 +54,7 @@ describe("HistoryVisibleBannerViewModel", () => { }); it("should not show the banner in unencrypted rooms", () => { - const vm = new HistoryVisibleBannerViewModel({ room, threadId: null }); + const vm = new HistoryVisibleBannerViewModel({ room, canSendMessages: true, threadId: null }); expect(vm.getSnapshot().visible).toBe(false); }); @@ -76,7 +76,7 @@ describe("HistoryVisibleBannerViewModel", () => { }), ]); - const vm = new HistoryVisibleBannerViewModel({ room, threadId: null }); + const vm = new HistoryVisibleBannerViewModel({ room, canSendMessages: true, threadId: null }); expect(vm.getSnapshot().visible).toBe(false); }); @@ -99,7 +99,7 @@ describe("HistoryVisibleBannerViewModel", () => { }), ]); - const vm = new HistoryVisibleBannerViewModel({ room, threadId: null }); + const vm = new HistoryVisibleBannerViewModel({ room, canSendMessages: true, threadId: null }); expect(vm.getSnapshot().visible).toBe(false); vm.dispose(); }); @@ -122,12 +122,12 @@ describe("HistoryVisibleBannerViewModel", () => { }), ]); - const vm = new HistoryVisibleBannerViewModel({ room, threadId: "some thread ID" }); + const vm = new HistoryVisibleBannerViewModel({ room, canSendMessages: true, threadId: "some thread ID" }); expect(vm.getSnapshot().visible).toBe(false); vm.dispose(); }); - it("should show the banner in encrypted rooms with non-joined history visibility", async () => { + it("should not show the banner if the user cannot send messages", () => { upsertRoomStateEvents(room, [ mkEvent({ event: true, @@ -145,7 +145,53 @@ describe("HistoryVisibleBannerViewModel", () => { }), ]); - const vm = new HistoryVisibleBannerViewModel({ room, threadId: null }); + 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);