Refactor UnreadNotificationBadge to shared MVVM view (#33672)
* Refactor notification badge to shared MVVM view * Bage icons Images * Narrow unread notification badge refactor scope * Align unread badge MVVM pattern * Reduce unread badge viewmodel duplication * Document unread badge viewmodel props * Remove legacy badge classes from shared view * Update css for knocked door * Update thread badge e2e selector * Update read receipt badge e2e selectors
This commit is contained in:
@@ -1007,21 +1007,25 @@ describe("EventTile", () => {
|
||||
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
|
||||
|
||||
// By default, the thread will assume it is read.
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
||||
expect(container.querySelectorAll('[data-testid="notification-badge"]')).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Total, 3);
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge_level_highlight")).toHaveLength(0);
|
||||
let badges = container.querySelectorAll('[data-testid="notification-badge"]');
|
||||
expect(badges).toHaveLength(1);
|
||||
expect(badges[0]).toHaveAttribute("data-badge-type", "dot");
|
||||
expect(badges[0]).toHaveAttribute("data-notification-level", "notification");
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Highlight, 1);
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge_level_highlight")).toHaveLength(1);
|
||||
badges = container.querySelectorAll('[data-testid="notification-badge"]');
|
||||
expect(badges).toHaveLength(1);
|
||||
expect(badges[0]).toHaveAttribute("data-badge-type", "dot");
|
||||
expect(badges[0]).toHaveAttribute("data-notification-level", "highlight");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+15
-11
@@ -38,6 +38,10 @@ describe("UnreadNotificationBadge", () => {
|
||||
return <UnreadNotificationBadge room={room} threadId={threadId} />;
|
||||
}
|
||||
|
||||
function getBadge(container: HTMLElement): Element | null {
|
||||
return container.querySelector('[data-testid="notification-badge"]');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
client.supportsThreads = () => true;
|
||||
@@ -84,27 +88,27 @@ describe("UnreadNotificationBadge", () => {
|
||||
it("renders unread notification badge", () => {
|
||||
const { container } = render(getComponent());
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeFalsy();
|
||||
expect(getBadge(container)).toHaveTextContent("1");
|
||||
expect(getBadge(container)).toHaveAttribute("data-notification-level", "notification");
|
||||
|
||||
act(() => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
||||
});
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeTruthy();
|
||||
expect(getBadge(container)).toHaveAttribute("data-notification-level", "highlight");
|
||||
});
|
||||
|
||||
it("renders unread thread notification badge", () => {
|
||||
const { container } = render(getComponent(THREAD_ID));
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeFalsy();
|
||||
expect(getBadge(container)).toHaveTextContent("1");
|
||||
expect(getBadge(container)).toHaveAttribute("data-notification-level", "notification");
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1);
|
||||
});
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeTruthy();
|
||||
expect(getBadge(container)).toHaveAttribute("data-notification-level", "highlight");
|
||||
});
|
||||
|
||||
it("hides unread notification badge", () => {
|
||||
@@ -112,7 +116,7 @@ describe("UnreadNotificationBadge", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
const { container } = render(getComponent(THREAD_ID));
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeFalsy();
|
||||
expect(getBadge(container)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +146,7 @@ describe("UnreadNotificationBadge", () => {
|
||||
muteRoom(room);
|
||||
|
||||
const { container } = render(getComponent());
|
||||
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
|
||||
expect(getBadge(container)).toBeNull();
|
||||
});
|
||||
|
||||
it("activity renders unread notification badge", () => {
|
||||
@@ -168,8 +172,8 @@ describe("UnreadNotificationBadge", () => {
|
||||
room.addLiveEvents([event], { addToState: true });
|
||||
|
||||
const { container } = render(getComponent(THREAD_ID));
|
||||
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_level_highlight")).toBeFalsy();
|
||||
expect(getBadge(container)).toBeInTheDocument();
|
||||
expect(getBadge(container)).toHaveAttribute("data-badge-type", "dot");
|
||||
expect(getBadge(container)).not.toHaveAttribute("data-notification-level", "highlight");
|
||||
});
|
||||
});
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2026 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 { NotificationCountType, PendingEventOrdering, Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { stubClient } from "../../../test-utils/test-utils";
|
||||
import { UnreadNotificationBadgeViewModel } from "../../../../src/viewmodels/room/notification-badge/UnreadNotificationBadgeViewModel";
|
||||
|
||||
describe("UnreadNotificationBadgeViewModel", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
const trackedRoomEvents = [
|
||||
RoomEvent.UnreadNotifications,
|
||||
RoomEvent.Receipt,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.Redaction,
|
||||
RoomEvent.LocalEchoUpdated,
|
||||
RoomEvent.MyMembership,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room("!room:example.org", client, "@user:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
});
|
||||
|
||||
function setUnreads(greys: number, reds: number): void {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, greys);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, reds);
|
||||
}
|
||||
|
||||
it("computes the initial snapshot from unread state", () => {
|
||||
setUnreads(12, 0);
|
||||
|
||||
const vm = new UnreadNotificationBadgeViewModel({ room });
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
shouldRender: true,
|
||||
isVisible: true,
|
||||
isNotification: true,
|
||||
isHighlight: false,
|
||||
badgeType: "badge_2char",
|
||||
symbol: "12",
|
||||
});
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("updates when the room unread state changes", () => {
|
||||
const vm = new UnreadNotificationBadgeViewModel({ room });
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
setUnreads(0, 2);
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
isHighlight: true,
|
||||
symbol: "2",
|
||||
});
|
||||
expect(listener).toHaveBeenCalled();
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("skips unchanged force-dot setter updates", () => {
|
||||
setUnreads(1, 0);
|
||||
const vm = new UnreadNotificationBadgeViewModel({ room, forceDot: false });
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setForceDot(false);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
vm.setForceDot(true);
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(vm.getSnapshot().badgeType).toBe("dot");
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("moves room event listeners when the room changes", () => {
|
||||
const nextRoom = new Room("!next:example.org", client, "@user:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
const initialRoomListenerCounts = new Map(
|
||||
trackedRoomEvents.map((eventName) => [eventName, room.listenerCount(eventName)]),
|
||||
);
|
||||
const initialNextRoomListenerCounts = new Map(
|
||||
trackedRoomEvents.map((eventName) => [eventName, nextRoom.listenerCount(eventName)]),
|
||||
);
|
||||
const vm = new UnreadNotificationBadgeViewModel({ room });
|
||||
|
||||
for (const eventName of trackedRoomEvents) {
|
||||
expect(room.listenerCount(eventName)).toBe(initialRoomListenerCounts.get(eventName)! + 1);
|
||||
}
|
||||
|
||||
vm.setRoom(nextRoom);
|
||||
|
||||
for (const eventName of trackedRoomEvents) {
|
||||
expect(room.listenerCount(eventName)).toBe(initialRoomListenerCounts.get(eventName));
|
||||
expect(nextRoom.listenerCount(eventName)).toBe(initialNextRoomListenerCounts.get(eventName)! + 1);
|
||||
}
|
||||
|
||||
vm.dispose();
|
||||
for (const eventName of trackedRoomEvents) {
|
||||
expect(nextRoom.listenerCount(eventName)).toBe(initialNextRoomListenerCounts.get(eventName));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user