diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts new file mode 100644 index 0000000000..59f5a2fcab --- /dev/null +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -0,0 +1,88 @@ +/* + * 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 { expect, test } from "../../../element-web-test"; +import type { Page } from "@playwright/test"; + +test.describe("Room list filters and sort", () => { + test.use({ + displayName: "Alice", + botCreateOpts: { + displayName: "BotBob", + autoAcceptInvites: true, + }, + labsFlags: ["feature_new_room_list"], + }); + + /** + * Get the room list + * @param page + */ + function getRoomList(page: Page) { + return page.getByTestId("room-list"); + } + + function getPrimaryFilters(page: Page) { + return page.getByRole("listbox", { name: "Room list filters" }); + } + + test.beforeEach(async ({ page, app, bot, user }) => { + // The notification toast is displayed above the search section + await app.closeNotificationToast(); + + await app.client.createRoom({ name: "empty room" }); + + const unReadDmId = await bot.createRoom({ + name: "unread dm", + invite: [user.userId], + is_direct: true, + }); + await bot.sendMessage(unReadDmId, "I am a robot. Beep."); + + const unReadRoomId = await app.client.createRoom({ name: "unread room" }); + await app.client.inviteUser(unReadRoomId, bot.credentials.userId); + await bot.joinRoom(unReadRoomId); + await bot.sendMessage(unReadRoomId, "I am a robot. Beep."); + + const favouriteId = await app.client.createRoom({ name: "favourite room" }); + await app.client.evaluate(async (client, favouriteId) => { + await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 }); + }, favouriteId); + }); + + test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => { + const roomList = getRoomList(page); + const primaryFilters = getPrimaryFilters(page); + + const allFilters = await primaryFilters.locator("option").all(); + for (const filter of allFilters) { + expect(await filter.getAttribute("aria-selected")).toBe("false"); + } + await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png"); + + await primaryFilters.getByRole("option", { name: "Unread" }).click(); + // only one room should be visible + await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); + await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(2); + await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png"); + + await primaryFilters.getByRole("option", { name: "Favourite" }).click(); + await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(1); + + await primaryFilters.getByRole("option", { name: "People" }).click(); + await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(1); + + await primaryFilters.getByRole("option", { name: "Rooms" }).click(); + await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); + await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible(); + await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(3); + }); +}); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png new file mode 100644 index 0000000000..8c056bc754 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png new file mode 100644 index 0000000000..06314b5213 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png index 1ebfde5ea7..cba5fa86d4 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png index f9275dd211..5d88f45aa5 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png index 79c9254f0a..157b0ce156 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 8501ed7bd1..a8536a2d4d 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -273,6 +273,7 @@ @import "./views/rooms/RoomListPanel/_RoomListCell.pcss"; @import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss"; @import "./views/rooms/RoomListPanel/_RoomListPanel.pcss"; +@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss"; @import "./views/rooms/RoomListPanel/_RoomListSearch.pcss"; @import "./views/rooms/_AppsDrawer.pcss"; @import "./views/rooms/_Autocomplete.pcss"; diff --git a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss new file mode 100644 index 0000000000..ac85782bbd --- /dev/null +++ b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss @@ -0,0 +1,12 @@ +/* + * 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. + */ + +.mx_RoomListPrimaryFilters { + margin: unset; + list-style-type: none; + padding: var(--cpd-space-2x) var(--cpd-space-3x); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx new file mode 100644 index 0000000000..ebf972d361 --- /dev/null +++ b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx @@ -0,0 +1,45 @@ +/* + * 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, { type JSX } from "react"; +import { ChatFilter } from "@vector-im/compound-web"; + +import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; +import { Flex } from "../../../utils/Flex"; +import { _t } from "../../../../languageHandler"; + +interface RoomListPrimaryFiltersProps { + /** + * The view model for the room list + */ + vm: RoomListViewState; +} + +/** + * The primary filters for the room list + */ +export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX.Element { + return ( + + {vm.primaryFilters.map((filter) => ( +
  • + + {filter.name} + +
  • + ))} +
    + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/src/components/views/rooms/RoomListPanel/RoomListView.tsx index 14b456852c..36fab95658 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListView.tsx @@ -9,12 +9,17 @@ import React, { type JSX } from "react"; import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel"; import { RoomList } from "./RoomList"; +import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; /** * Host the room list and the (future) room filters */ export function RoomListView(): JSX.Element { const vm = useRoomListViewModel(); - // Room filters will be added soon - return ; + return ( + <> + + + + ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 133ab7360b..9281382715 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2114,6 +2114,7 @@ "list_title": "Room list", "notification_options": "Notification options", "open_space_menu": "Open space menu", + "primary_filters": "Room list filters", "redacting_messages_status": { "one": "Currently removing messages in %(count)s room", "other": "Currently removing messages in %(count)s rooms" diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx new file mode 100644 index 0000000000..3b1b29a5ff --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; +import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms"; +import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters"; + +describe("", () => { + let vm: RoomListViewState; + + beforeEach(() => { + vm = { + rooms: [], + openRoom: jest.fn(), + primaryFilters: [ + { name: "People", active: false, toggle: jest.fn() }, + { name: "Rooms", active: true, toggle: jest.fn() }, + ], + activateSecondaryFilter: () => {}, + activeSecondaryFilter: SecondaryFilters.AllActivity, + }; + }); + + it("should render primary filters", async () => { + const user = userEvent.setup(); + + const { asFragment } = render(); + expect(screen.getByRole("option", { name: "People" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Rooms" })).toHaveAttribute("aria-selected", "true"); + expect(asFragment()).toMatchSnapshot(); + + await user.click(screen.getByRole("button", { name: "People" })); + expect(vm.primaryFilters[0].toggle).toHaveBeenCalled(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap index b6690510b3..6421601173 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap @@ -24,6 +24,65 @@ exports[` should not render the RoomListSearch component when U +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    should render the RoomListSearch component when UICom
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    should render primary filters 1`] = ` + +
      +
    • + +
    • +
    • + +
    • +
    +
    +`;