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 15:26:47 +05:30 committed by GitHub
parent b9cdc0390a
commit d6d647f56d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 65 additions and 5 deletions

View File

@ -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",

View File

@ -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 {
/**

View File

@ -36,6 +36,7 @@ describe("<OptionMenuView />", () => {
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("<OptionMenuView />", () => {
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(<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 () => {
const user = userEvent.setup();
@ -78,6 +95,19 @@ describe("<OptionMenuView />", () => {
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 () => {
const user = userEvent.setup();

View File

@ -60,6 +60,11 @@ export function OptionMenuView({ vm }: OptionMenuViewProps): JSX.Element {
checked={activeSortOption === "recent"}
onSelect={() => vm.sort("recent")}
/>
<RadioMenuItem
label={_t("room_list|sort_type|unread_first")}
checked={activeSortOption === "unread-first"}
onSelect={() => vm.sort("unread-first")}
/>
<RadioMenuItem
label={_t("room_list|sort_type|atoz")}
checked={activeSortOption === "alphabetical"}

View File

@ -170,7 +170,18 @@ export class RoomListHeaderViewModel
};
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);
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 {

View File

@ -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 });