diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 2ac857c7ce6..d099265754a 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -204,36 +204,45 @@ test.describe("Room list filters and sort", () => { await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites"); }); - test( - "unread filter should only match unread rooms that have a count", - { tag: "@screenshot" }, - async ({ page, app, bot }) => { - const roomListView = getRoomList(page); - const primaryFilters = getPrimaryFilters(page); - - // Let's configure unread dm room so that we only get notification for mentions and keywords - await app.viewRoomById(unReadDmId); - await app.settings.openRoomSettings("Notifications"); - await page.getByText("@mentions & keywords").click(); - await app.settings.closeDialog(); - - // Let's open a room other than unread room or unread dm - await roomListView.getByRole("button", { name: "Open room favourite room" }).click(); - - // Let's make the bot send a new message in both rooms - await bot.sendMessage(unReadDmId, "Hello!"); - await bot.sendMessage(unReadRoomId, "Hello!"); - - // Let's activate the unread filter now - await primaryFilters.getByRole("option", { name: "Unread" }).click(); - - // Unread filter should only show unread room and not unread dm! - const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" }); - await expect(unreadDm).toBeVisible(); - await expect(unreadDm).toMatchScreenshot("unread-dm.png"); - await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible(); - }, - ); + test("unread filter should obey the showbold setting", async ({ page, app, bot }) => { + const roomListView = getRoomList(page); + const primaryFilters = getPrimaryFilters(page); + + // Set the "showbold" setting to off + await app.settings.setValue("Notifications.showbold", null, SettingLevel.DEVICE, false); + + // The unread DM room only notifies for mentions and keywords + await app.viewRoomById(unReadDmId); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("@mentions & keywords").click(); + await app.settings.closeDialog(); + + // (The unread non-DM room still notifies for all activity) + + // Open some other room + await roomListView.getByRole("button", { name: "Open room favourite room" }).click(); + + // Send a message in the unread DM room and the unread non-DM room + await bot.sendMessage(unReadDmId, "Hello!"); + await bot.sendMessage(unReadRoomId, "Hello!"); + + // Turn the "Unreads" filter on + await primaryFilters.getByRole("option", { name: "Unreads" }).click(); + + const unreadRoom = roomListView.getByRole("option", { name: "Open room unread room" }); + const unreadDm = roomListView.getByRole("option", { name: "Open room unread dm" }); + + // Only the unread room is visible. The DM room is hidden. + await expect(unreadRoom).toBeVisible(); + await expect(unreadDm).not.toBeVisible(); + + // Now set "showbold" to on + await app.settings.setValue("Notifications.showbold", null, SettingLevel.DEVICE, true); + + // Now both unread rooms are visible + await expect(unreadRoom).toBeVisible(); + await expect(unreadDm).toBeVisible(); + }); test("should sort the room list alphabetically", async ({ page }) => { const roomListView = getRoomList(page); diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png deleted file mode 100644 index e1f451d1211..00000000000 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png and /dev/null differ diff --git a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts index 200abab9be4..b8d13e221e4 100644 --- a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts @@ -136,6 +136,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { }); SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged()); SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () => this.onOrderedCustomSectionsChange()); + SettingsStore.watchSetting("Notifications.showbold", null, () => this.onShowBoldChange()); this.loadCustomSections(); SettingsStore.watchSetting("RoomList.showSections", null, () => this.scheduleEmit()); @@ -517,6 +518,16 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { this.scheduleEmit(); } + /** + * Handle changes to the showbold setting. + * Updates the skip list filters to reflect the new setting and emits an update. + * Emit {@link LISTS_UPDATE_EVENT}. + */ + private onShowBoldChange(): void { + this.roomSkipList?.useNewFilters(this.getSkipListFilters()); + this.scheduleEmit(); + } + /** * Create a new section. * Emits {@link SECTION_CREATED_EVENT} if the section was successfully created. diff --git a/apps/web/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts b/apps/web/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts index 0c6100eb131..094f95bb141 100644 --- a/apps/web/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts +++ b/apps/web/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts @@ -8,10 +8,18 @@ import type { Room } from "matrix-js-sdk/src/matrix"; import { type Filter, FilterEnum } from "."; import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore"; import { getMarkedUnreadState } from "../../../../utils/notifications"; +import SettingsStore from "../../../../settings/SettingsStore"; export class UnreadFilter implements Filter { public matches(room: Room): boolean { - return RoomNotificationStateStore.instance.getRoomState(room).hasUnreadCount || !!getMarkedUnreadState(room); + // If the user marked this room as unread, it's unread + if (getMarkedUnreadState(room)) { + return true; + } + + const showBold = SettingsStore.getValue("Notifications.showbold"); + const notifState = RoomNotificationStateStore.instance.getRoomState(room); + return showBold ? notifState.hasAnyNotificationOrActivity : notifState.hasUnreadCount; } public get key(): FilterEnum.UnreadFilter { diff --git a/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts b/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts index 73e1ec82d6a..e6b35ee2047 100644 --- a/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts +++ b/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts @@ -593,7 +593,10 @@ describe("RoomListStoreV3", () => { } }); - it("supports filtering unread rooms", async () => { + it("supports filtering unread rooms when showbold is off", async () => { + // Ensure that Notifications.showbold is off + jest.spyOn(SettingsStore, "getValue").mockImplementation(() => false); + const { client, rooms } = getClientAndRooms(); // Let's choose 5 rooms to put in space const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client); @@ -601,7 +604,47 @@ describe("RoomListStoreV3", () => { // Let's say 8, 27 are unread jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { const state = { + // This should be used, because showbold is off hasUnreadCount: [rooms[8], rooms[27]].includes(room), + + // This should not be used + hasAnyNotificationOrActivity: false, + } as unknown as RoomNotificationState; + return state; + }); + + setupMocks(spaceRoom, roomIds); + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + // Should only give us rooms at index 8 and 27 + const result = store + .getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter]) + .sections.flatMap((s) => s.rooms); + expect(result).toHaveLength(2); + for (const i of [8, 27]) { + expect(result).toContain(rooms[i]); + } + }); + + it("supports filtering unread rooms when showbold is on", async () => { + // Ensure that Notifications.showbold is on + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => { + return setting === "Notifications.showbold"; + }); + + const { client, rooms } = getClientAndRooms(); + // Let's choose 5 rooms to put in space + const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client); + + // Let's say 8, 27 are unread + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { + const state = { + // This should be used, because showbold is off + hasUnreadCount: false, + + // This should not be used + hasAnyNotificationOrActivity: [rooms[8], rooms[27]].includes(room), } as unknown as RoomNotificationState; return state; }); @@ -777,6 +820,7 @@ describe("RoomListStoreV3", () => { jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { const state = { hasUnreadCount: [rooms[8], rooms[27]].includes(room), + hasAnyNotificationOrActivity: [rooms[8], rooms[27]].includes(room), } as unknown as RoomNotificationState; return state; }); @@ -825,6 +869,40 @@ describe("RoomListStoreV3", () => { store.getSortedRoomsInActiveSpace([FilterEnum.InvitesFilter]).sections.flatMap((s) => s.rooms), ).toContain(room); }); + + it("updates filters when showbold setting changes", async () => { + // Given one room is "bold" (unread) and one has a notification + const { store, rooms } = await getRoomListStore(); + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { + const state = { + // Only 27 has notifications + hasUnreadCount: [rooms[27]].includes(room), + // But both 8 and 27 have unread messages (bold) + hasAnyNotificationOrActivity: [rooms[8], rooms[27]].includes(room), + } as unknown as RoomNotificationState; + return state; + }); + + // When showbold is set to true + await SettingsStore.setValue("Notifications.showbold", null, SettingLevel.DEVICE, true); + + // Then both rooms are in the room list + const showboldRooms = store + .getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter]) + .sections.flatMap((s) => s.rooms); + expect(showboldRooms).toContain(rooms[27]); + expect(showboldRooms).toContain(rooms[8]); + + // But when showbold is set to false + await SettingsStore.setValue("Notifications.showbold", null, SettingLevel.DEVICE, false); + + // Then only the room with a notification is in the room list + const noShowboldRooms = store + .getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter]) + .sections.flatMap((s) => s.rooms); + expect(noShowboldRooms).toContain(rooms[27]); + expect(noShowboldRooms).not.toContain(rooms[8]); + }); }); describe("getServerNoticeRooms", () => {