Skip to content

Add room list sorting #29951

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.10.2",
"@vector-im/compound-web": "^7.11.0",
"@vector-im/matrix-wysiwyg": "2.38.3",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ test.describe("Room list filters and sort", () => {
return page.getByRole("button", { name: "Filter" });
}

function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}

/**
* Get the room list
* @param page
Expand Down Expand Up @@ -252,6 +256,23 @@ test.describe("Room list filters and sort", () => {
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
},
);

test("should sort the room list alphabetically", async ({ page }) => {
const roomListView = getRoomList(page);

await getRoomOptionsMenu(page).click();
await page.getByRole("menuitemradio", { name: "A-Z" }).click();

await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
});

test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
const roomListView = getRoomList(page);

await bot.sendMessage(unReadDmId, "Hello!");

await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
});
});

test.describe("Empty room list", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
* Please see LICENSE files in the repository root for full details.
*/

import { IconButton, Menu, MenuTitle, CheckboxMenuItem, Tooltip } from "@vector-im/compound-web";
import React, { type Ref, type JSX, useState } from "react";
import { IconButton, Menu, MenuTitle, CheckboxMenuItem, Tooltip, RadioMenuItem } from "@vector-im/compound-web";
import React, { type Ref, type JSX, useState, useCallback } from "react";
import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";

import { _t } from "../../../../languageHandler";
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { SortOption } from "../../../viewmodels/roomlist/useSorter";

interface MenuTriggerProps extends React.ComponentProps<typeof IconButton> {
ref?: Ref<HTMLButtonElement>;
Expand Down Expand Up @@ -39,6 +40,14 @@ interface Props {
export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
const [open, setOpen] = useState(false);

const onActivitySelected = useCallback(() => {
vm.sort(SortOption.Activity);
}, [vm]);

const onAtoZSelected = useCallback(() => {
vm.sort(SortOption.AToZ);
}, [vm]);

return (
<Menu
open={open}
Expand All @@ -48,6 +57,17 @@ export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
align="start"
trigger={<MenuTrigger />}
>
<MenuTitle title={_t("room_list|sort")} />
<RadioMenuItem
label={_t("room_list|sort_type|activity")}
checked={vm.activeSortOption === SortOption.Activity}
onSelect={onActivitySelected}
/>
<RadioMenuItem
label={_t("room_list|sort_type|atoz")}
checked={vm.activeSortOption === SortOption.AToZ}
onSelect={onAtoZSelected}
/>
<MenuTitle title={_t("room_list|appearance")} />
<CheckboxMenuItem
label={_t("room_list|show_message_previews")}
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2167,9 +2167,14 @@
"other": "Show %(count)s more"
},
"show_previews": "Show previews of messages",
"sort": "Sort",
"sort_by": "Sort by",
"sort_by_activity": "Activity",
"sort_by_alphabet": "A-Z",
"sort_type": {
"activity": "Activity",
"atoz": "A-Z"
},
"sort_unread_first": "Show rooms with unread messages first",
"space_menu": {
"home": "Space home",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";

import { RoomListOptionsMenu } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListOptionsMenu";
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";

describe("<RoomListOptionsMenu />", () => {
it("should match snapshot", () => {
const vm = {
sort: jest.fn(),
} as unknown as RoomListViewState;

const { asFragment } = render(<RoomListOptionsMenu vm={vm} />);

expect(asFragment()).toMatchSnapshot();
});

it("should show A to Z selected if activeSortOption is Alphabetic", async () => {
const user = userEvent.setup();

const vm = {
sort: jest.fn(),
activeSortOption: "Alphabetic",
} as unknown as RoomListViewState;

render(<RoomListOptionsMenu vm={vm} />);

// Open the menu
const button = screen.getByRole("button", { name: "Room Options" });
await user.click(button);

expect(screen.getByRole("menuitemradio", { name: "A-Z" })).toBeChecked();
expect(screen.getByRole("menuitemradio", { name: "Activity" })).not.toBeChecked();
});

it("should show Activity selected if activeSortOption is Recency", async () => {
const user = userEvent.setup();

const vm = {
sort: jest.fn(),
activeSortOption: "Recency",
} as unknown as RoomListViewState;

render(<RoomListOptionsMenu 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" })).toBeChecked();
});

it("should sort A to Z", async () => {
const user = userEvent.setup();

const vm = {
sort: jest.fn(),
} as unknown as RoomListViewState;

render(<RoomListOptionsMenu vm={vm} />);

await user.click(screen.getByRole("button", { name: "Room Options" }));

await user.click(screen.getByRole("menuitemradio", { name: "A-Z" }));

expect(vm.sort).toHaveBeenCalledWith("Alphabetic");
});

it("should sort by activity", async () => {
const user = userEvent.setup();

const vm = {
sort: jest.fn(),
activeSortOption: "Alphabetic",
} as unknown as RoomListViewState;

render(<RoomListOptionsMenu vm={vm} />);

await user.click(screen.getByRole("button", { name: "Room Options" }));

await user.click(screen.getByRole("menuitemradio", { name: "Activity" }));

expect(vm.sort).toHaveBeenCalledWith("Recency");
});

it("should show message previews disabled", async () => {
const user = userEvent.setup();

const vm = {
shouldShowMessagePreview: false,
} as unknown as RoomListViewState;

render(<RoomListOptionsMenu vm={vm} />);

await user.click(screen.getByRole("button", { name: "Room Options" }));

expect(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })).not.toBeChecked();
});

it("should show message previews enabled", async () => {
const user = userEvent.setup();

const vm = {
shouldShowMessagePreview: true,
} as unknown as RoomListViewState;

render(<RoomListOptionsMenu vm={vm} />);

await user.click(screen.getByRole("button", { name: "Room Options" }));

expect(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })).toBeChecked();
});

it("should toggle message previews", async () => {
const user = userEvent.setup();

const vm = {
toggleMessagePreview: jest.fn(),
} as unknown as RoomListViewState;

render(<RoomListOptionsMenu vm={vm} />);

await user.click(screen.getByRole("button", { name: "Room Options" }));

await user.click(screen.getByRole("menuitemcheckbox", { name: "Show message previews" }));

expect(vm.toggleMessagePreview).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<RoomListOptionsMenu /> should match snapshot 1`] = `
<DocumentFragment>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«r2»"
class="_icon-button_m2erp_8 mx_RoomListSecondaryFilters_roomOptionsButton"
data-state="closed"
id="radix-«r0»"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</button>
</DocumentFragment>
`;
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3757,10 +3757,10 @@
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.2.tgz#27363d26446eaa21880ab126fa51fec112e6fd86"
integrity sha512-y13bhPyJ5OzbGRl21F6+Y2adrjyK+mu67yKTx+o8MfmIpJzMSn4KkHZtcujMquWSh0e5ZAufsnk4VYvxbSpr1A==

"@vector-im/compound-web@^7.10.2":
version "7.10.2"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.10.2.tgz#2f62c6ab83269e5b957f53bb53413a74fb65e04d"
integrity sha512-K9gA1Ah9CTJMeZTkcDFpAdVRNbu/rQEgV3PoDcEPI3e9iDds8Dhbo7EfOciPvtXCZw6Hr83lnhWDnwTFHVlahQ==
"@vector-im/compound-web@^7.11.0":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.11.0.tgz#b7c466e64089320b41f8eaf6f2b30950e9692ca2"
integrity sha512-lRxXUOQJHdBswhykpNs/J/cBW4fPY1qbwyDexlWxX5zCVAYiuMCWo2tI+Y7/SK4tNbDr7nwoTDRh4H9CO1L5LQ==
dependencies:
"@floating-ui/react" "^0.27.0"
"@radix-ui/react-context-menu" "^2.2.1"
Expand Down
Loading