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
This commit is contained in:
R Midhun Suresh
2026-01-22 09:56:47 +00:00
committed by GitHub
parent b9cdc0390a
commit d6d647f56d
6 changed files with 65 additions and 5 deletions
@@ -51,7 +51,8 @@
"sort": "Sort", "sort": "Sort",
"sort_type": { "sort_type": {
"activity": "Activity", "activity": "Activity",
"atoz": "A-Z" "atoz": "A-Z",
"unread_first": "Unread first"
}, },
"space_menu": { "space_menu": {
"home": "Space home", "home": "Space home",
@@ -19,7 +19,7 @@ import styles from "./RoomListHeaderView.module.css";
/** /**
* The available sorting options for the room list. * The available sorting options for the room list.
*/ */
export type SortOption = "recent" | "alphabetical"; export type SortOption = "recent" | "alphabetical" | "unread-first";
export interface RoomListHeaderViewSnapshot { export interface RoomListHeaderViewSnapshot {
/** /**
@@ -36,6 +36,7 @@ describe("<OptionMenuView />", () => {
expect(screen.getByRole("menuitemradio", { name: "A-Z" })).toBeChecked(); expect(screen.getByRole("menuitemradio", { name: "A-Z" })).toBeChecked();
expect(screen.getByRole("menuitemradio", { name: "Activity" })).not.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 () => { it("should show Activity selected if activeSortOption is recent", async () => {
@@ -49,9 +50,25 @@ describe("<OptionMenuView />", () => {
await user.click(button); await user.click(button);
expect(screen.getByRole("menuitemradio", { name: "A-Z" })).not.toBeChecked(); 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(); 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(<OptionMenuView vm={vm} />);
// 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 () => { it("should sort A to Z", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
@@ -78,6 +95,19 @@ describe("<OptionMenuView />", () => {
expect(vm.sort).toHaveBeenCalledWith("recent"); expect(vm.sort).toHaveBeenCalledWith("recent");
}); });
it("should sort by unread first", async () => {
const user = userEvent.setup();
const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "recent" });
render(<OptionMenuView vm={vm} />);
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 () => { it("should toggle message preview", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
@@ -60,6 +60,11 @@ export function OptionMenuView({ vm }: OptionMenuViewProps): JSX.Element {
checked={activeSortOption === "recent"} checked={activeSortOption === "recent"}
onSelect={() => vm.sort("recent")} onSelect={() => vm.sort("recent")}
/> />
<RadioMenuItem
label={_t("room_list|sort_type|unread_first")}
checked={activeSortOption === "unread-first"}
onSelect={() => vm.sort("unread-first")}
/>
<RadioMenuItem <RadioMenuItem
label={_t("room_list|sort_type|atoz")} label={_t("room_list|sort_type|atoz")}
checked={activeSortOption === "alphabetical"} checked={activeSortOption === "alphabetical"}
@@ -170,7 +170,18 @@ export class RoomListHeaderViewModel
}; };
public sort = (option: SortOption): void => { public sort = (option: SortOption): void => {
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); RoomListStoreV3.instance.resort(sortingAlgorithm);
this.snapshot.merge({ activeSortOption: option }); this.snapshot.merge({ activeSortOption: option });
}; };
@@ -192,8 +203,20 @@ export class RoomListHeaderViewModel
*/ */
function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixClient): RoomListHeaderViewSnapshot { function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixClient): RoomListHeaderViewSnapshot {
const sortingAlgorithm = SettingsStore.getValue("RoomList.preferredSorting"); 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"); const isMessagePreviewEnabled = SettingsStore.getValue("RoomList.showMessagePreview");
return { return {
@@ -271,6 +271,7 @@ describe("RoomListHeaderViewModel", () => {
it.each([ it.each([
["recent" as const, SortingAlgorithm.Recency], ["recent" as const, SortingAlgorithm.Recency],
["alphabetical" as const, SortingAlgorithm.Alphabetic], ["alphabetical" as const, SortingAlgorithm.Alphabetic],
["unread-first" as const, SortingAlgorithm.Unread],
])("should resort when sort is called with '%s'", (option, expectedAlgorithm) => { ])("should resort when sort is called with '%s'", (option, expectedAlgorithm) => {
const resortSpy = jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn()); const resortSpy = jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn());
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });