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 <florian.duros@ormaz.fr>

* chore: Remove `jest-sonar.xml`.

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
Skye Elliot
2025-12-19 15:41:09 +00:00
committed by GitHub
parent aa84b2e07c
commit ce9c66ba4c
5 changed files with 91 additions and 17 deletions

View File

@@ -16,6 +16,9 @@ export const HistoryVisibleBanner: React.FC<{
/** The room instance associated with this banner view model. */ /** The room instance associated with this banner view model. */
room: Room; 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, * 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. * where the banner view is displayed as a child of the message composer.

View File

@@ -675,7 +675,11 @@ export class MessageComposer extends React.Component<IProps, IState> {
return ( return (
<div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}> <div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}>
<HistoryVisibleBanner room={this.props.room} threadId={threadId ?? null} /> <HistoryVisibleBanner
room={this.props.room}
canSendMessages={canSendMessages}
threadId={threadId ?? null}
/>
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
<UserIdentityWarning room={this.props.room} key={this.props.room.roomId} /> <UserIdentityWarning room={this.props.room} key={this.props.room.roomId} />
<ReplyPreview <ReplyPreview

View File

@@ -15,12 +15,22 @@ import { HistoryVisibility, RoomStateEvent, type Room } from "matrix-js-sdk/src/
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { SettingLevel } from "../../settings/SettingLevel"; 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 { interface Props {
/** /**
* The room instance associated with this banner view model. * The room instance associated with this banner view model.
*/ */
room: Room; 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 * 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 * timeline side view, where the banner view is displayed as a child of the message
@@ -66,23 +76,33 @@ export class HistoryVisibleBannerViewModel
/** /**
* Computes the latest banner snapshot given the VM's props. * Computes the latest banner snapshot given the VM's props.
* @param room - The room the banner will be shown in. * @param props - See {@link Props}.
* @param threadId - The thread ID passed in from the parent {@link MessageComposer}.
* @returns The latest snapshot. See {@link HistoryVisibleBannerViewSnapshot}. * @returns The latest snapshot. See {@link HistoryVisibleBannerViewSnapshot}.
*/ */
private static readonly computeSnapshot = ( private static readonly computeSnapshot = ({
room: Room, room,
threadId?: string | null, canSendMessages,
): HistoryVisibleBannerViewSnapshot => { threadId,
}: Props): HistoryVisibleBannerViewSnapshot => {
const featureEnabled = SettingsStore.getValue("feature_share_history_on_invite"); const featureEnabled = SettingsStore.getValue("feature_share_history_on_invite");
const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", room.roomId); 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 { return {
visible: visible:
featureEnabled && featureEnabled &&
!threadId &&
room.hasEncryptionStateEvent() && room.hasEncryptionStateEvent() &&
room.getHistoryVisibility() !== HistoryVisibility.Joined && canSendMessages &&
isHistoryVisible &&
!threadId &&
!acknowledged, !acknowledged,
}; };
}; };
@@ -92,7 +112,7 @@ export class HistoryVisibleBannerViewModel
* @param props - Properties for this view model. See {@link Props}. * @param props - Properties for this view model. See {@link Props}.
*/ */
public constructor(props: 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()); 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));
} }
/** /**

View File

@@ -677,6 +677,7 @@ export function mkStubRoom(
getCanonicalAlias: jest.fn(), getCanonicalAlias: jest.fn(),
getDMInviter: jest.fn(), getDMInviter: jest.fn(),
getEventReadUpTo: jest.fn(() => null), getEventReadUpTo: jest.fn(() => null),
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Joined),
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1), getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1),
getJoinRule: jest.fn().mockReturnValue("invite"), getJoinRule: jest.fn().mockReturnValue("invite"),
getJoinedMemberCount: jest.fn().mockReturnValue(1), getJoinedMemberCount: jest.fn().mockReturnValue(1),

View File

@@ -54,7 +54,7 @@ describe("HistoryVisibleBannerViewModel", () => {
}); });
it("should not show the banner in unencrypted rooms", () => { 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); 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); 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); expect(vm.getSnapshot().visible).toBe(false);
vm.dispose(); 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); expect(vm.getSnapshot().visible).toBe(false);
vm.dispose(); 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, [ upsertRoomStateEvents(room, [
mkEvent({ mkEvent({
event: true, 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); expect(vm.getSnapshot().visible).toBe(true);
await vm.onClose(); await vm.onClose();
expect(vm.getSnapshot().visible).toBe(false); expect(vm.getSnapshot().visible).toBe(false);