From d6d647f56debd060195ed9704cd452d6a7b40896 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 22 Jan 2026 15:26:47 +0530 Subject: [PATCH] Unread Sorting - Add option for sorting in `OptionsMenuView` (#31754) * Add new sort option * Support new sorting algorithm in vm * Add option item for unread sorter * Add tests --- .../src/i18n/strings/en_EN.json | 3 +- .../RoomListHeaderView/RoomListHeaderView.tsx | 2 +- .../menu/OptionMenuView.test.tsx | 30 +++++++++++++++++++ .../menu/OptionMenuView.tsx | 5 ++++ .../room-list/RoomListHeaderViewModel.ts | 29 ++++++++++++++++-- .../room-list/RoomListHeaderViewModel-test.ts | 1 + 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index 96e0f04531..a1d2276ba5 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -51,7 +51,8 @@ "sort": "Sort", "sort_type": { "activity": "Activity", - "atoz": "A-Z" + "atoz": "A-Z", + "unread_first": "Unread first" }, "space_menu": { "home": "Space home", diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx index 4659625555..5da7cf8d37 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx @@ -19,7 +19,7 @@ import styles from "./RoomListHeaderView.module.css"; /** * The available sorting options for the room list. */ -export type SortOption = "recent" | "alphabetical"; +export type SortOption = "recent" | "alphabetical" | "unread-first"; export interface RoomListHeaderViewSnapshot { /** diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx index f067c1db3b..83f0703c8a 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx @@ -36,6 +36,7 @@ describe("", () => { expect(screen.getByRole("menuitemradio", { name: "A-Z" })).toBeChecked(); expect(screen.getByRole("menuitemradio", { name: "Activity" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Unread first" })).not.toBeChecked(); }); it("should show Activity selected if activeSortOption is recent", async () => { @@ -49,9 +50,25 @@ describe("", () => { await user.click(button); expect(screen.getByRole("menuitemradio", { name: "A-Z" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Unread first" })).not.toBeChecked(); expect(screen.getByRole("menuitemradio", { name: "Activity" })).toBeChecked(); }); + it("should show `Unread First` selected if activeSortOption is unread-first", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "unread-first" }); + render(); + + // Open the menu + const button = screen.getByRole("button", { name: "Room Options" }); + await user.click(button); + + expect(screen.getByRole("menuitemradio", { name: "A-Z" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Activity" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Unread first" })).toBeChecked(); + }); + it("should sort A to Z", async () => { const user = userEvent.setup(); @@ -78,6 +95,19 @@ describe("", () => { expect(vm.sort).toHaveBeenCalledWith("recent"); }); + it("should sort by unread first", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "recent" }); + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + await user.click(screen.getByRole("menuitemradio", { name: "Unread first" })); + + expect(vm.sort).toHaveBeenCalledWith("unread-first"); + }); + it("should toggle message preview", async () => { const user = userEvent.setup(); diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx index bf6a5d80c2..3509d841e4 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx @@ -60,6 +60,11 @@ export function OptionMenuView({ vm }: OptionMenuViewProps): JSX.Element { checked={activeSortOption === "recent"} onSelect={() => vm.sort("recent")} /> + vm.sort("unread-first")} + /> { - const sortingAlgorithm = option === "recent" ? SortingAlgorithm.Recency : SortingAlgorithm.Alphabetic; + let sortingAlgorithm: SortingAlgorithm; + switch (option) { + case "alphabetical": + sortingAlgorithm = SortingAlgorithm.Alphabetic; + break; + case "recent": + sortingAlgorithm = SortingAlgorithm.Recency; + break; + case "unread-first": + sortingAlgorithm = SortingAlgorithm.Unread; + break; + } RoomListStoreV3.instance.resort(sortingAlgorithm); this.snapshot.merge({ activeSortOption: option }); }; @@ -192,8 +203,20 @@ export class RoomListHeaderViewModel */ function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixClient): RoomListHeaderViewSnapshot { const sortingAlgorithm = SettingsStore.getValue("RoomList.preferredSorting"); - const activeSortOption = - sortingAlgorithm === SortingAlgorithm.Recency ? ("recent" as const) : ("alphabetical" as const); + + let activeSortOption: SortOption; + switch (sortingAlgorithm) { + case SortingAlgorithm.Alphabetic: + activeSortOption = "alphabetical"; + break; + case SortingAlgorithm.Recency: + activeSortOption = "recent"; + break; + case SortingAlgorithm.Unread: + activeSortOption = "unread-first"; + break; + } + const isMessagePreviewEnabled = SettingsStore.getValue("RoomList.showMessagePreview"); return { diff --git a/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index 3d6083bc36..ca96762fa8 100644 --- a/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -271,6 +271,7 @@ describe("RoomListHeaderViewModel", () => { it.each([ ["recent" as const, SortingAlgorithm.Recency], ["alphabetical" as const, SortingAlgorithm.Alphabetic], + ["unread-first" as const, SortingAlgorithm.Unread], ])("should resort when sort is called with '%s'", (option, expectedAlgorithm) => { const resortSpy = jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn()); vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });