Room list: add activity marker to sections (#33024)

* feat: add unread status to section view

* feat: add unread tracking in room list section

* feat: populate rooms into section header vm

* test: add units for unread in section view model

* test(e2e): add unread tests
This commit is contained in:
Florian Duros
2026-04-06 20:05:45 +01:00
committed by GitHub
parent e53a148da2
commit 46bff1f9e6
9 changed files with 220 additions and 6 deletions
@@ -5,13 +5,25 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { RoomListSectionHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListSectionHeaderViewModel";
import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState";
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
import { createTestClient, mkRoom } from "../../test-utils";
describe("RoomListSectionHeaderViewModel", () => {
let onToggleExpanded: jest.Mock;
let matrixClient: MatrixClient;
beforeEach(() => {
onToggleExpanded = jest.fn();
matrixClient = createTestClient();
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should initialize snapshot from props", () => {
@@ -45,4 +57,100 @@ describe("RoomListSectionHeaderViewModel", () => {
expect(vm.getSnapshot().isExpanded).toBe(true);
expect(onToggleExpanded).toHaveBeenCalledWith(true);
});
describe("unread status", () => {
let room: Room;
let notificationState: RoomNotificationState;
beforeEach(() => {
room = mkRoom(matrixClient, "!room:server");
notificationState = new RoomNotificationState(room, false);
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
});
it("should set isUnread to false when no rooms have notifications", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
onToggleExpanded,
});
vm.setRooms([room]);
expect(vm.getSnapshot().isUnread).toBe(false);
});
it("should set isUnread to true when a room has notifications", () => {
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
onToggleExpanded,
});
vm.setRooms([room]);
expect(vm.getSnapshot().isUnread).toBe(true);
});
it("should subscribe to new rooms and unsubscribe from removed rooms", () => {
const room2 = mkRoom(matrixClient, "!room2:server");
const notificationState2 = new RoomNotificationState(room2, false);
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState")
.mockReturnValueOnce(notificationState)
.mockReturnValue(notificationState2);
jest.spyOn(notificationState, "on");
jest.spyOn(notificationState, "off");
jest.spyOn(notificationState2, "on");
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
onToggleExpanded,
});
vm.setRooms([room]);
expect(notificationState.on).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
vm.setRooms([room2]);
expect(notificationState.off).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
expect(notificationState2.on).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
// Calling setRooms again with the same room should not re-subscribe
vm.setRooms([room2]);
expect(notificationState2.on).toHaveBeenCalledTimes(1);
});
it("should update isUnread when a notification state update event fires", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
onToggleExpanded,
});
vm.setRooms([room]);
expect(vm.getSnapshot().isUnread).toBe(false);
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
notificationState.emit(NotificationStateEvents.Update);
expect(vm.getSnapshot().isUnread).toBe(true);
});
it("should unsubscribe from all notification states on dispose", () => {
jest.spyOn(notificationState, "off");
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
onToggleExpanded,
});
vm.setRooms([room]);
vm.dispose();
expect(notificationState.off).toHaveBeenCalledWith(NotificationStateEvents.Update, expect.any(Function));
});
});
});