Skip to content
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

New room list: basic flat list #29368

Draft
wants to merge 32 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
199ab71
Implement enough of the new store to get a list of rooms
MidhunSureshR Feb 24, 2025
257cd9e
Make it possible to swap sorting algorithm
MidhunSureshR Feb 24, 2025
757cd88
Don't attach to window object
MidhunSureshR Feb 25, 2025
933fa08
Remove the store class
MidhunSureshR Feb 25, 2025
8060411
Create a new room list store that wraps around the skip list
MidhunSureshR Feb 25, 2025
d115235
Create a minimal view model
MidhunSureshR Feb 25, 2025
db25c0d
Fix CI
MidhunSureshR Feb 25, 2025
06d9852
Add some basic tests for the store
MidhunSureshR Feb 28, 2025
3da4576
Write more tests
MidhunSureshR Feb 28, 2025
67ed03f
Add some jsdoc comments
MidhunSureshR Feb 28, 2025
cf50965
Add more documentation
MidhunSureshR Feb 28, 2025
a7817e5
Add more docs
MidhunSureshR Mar 2, 2025
87d8ef7
Update the store on action
MidhunSureshR Mar 2, 2025
0fe0260
Add more tests
MidhunSureshR Mar 2, 2025
35a9cbe
Add newlines between case blocks
MidhunSureshR Mar 2, 2025
44a867f
Make code more readable
MidhunSureshR Mar 2, 2025
9f0f782
Add more tests
MidhunSureshR Mar 2, 2025
9fae001
Remove redundant code
MidhunSureshR Mar 2, 2025
c989bbf
Fix test
MidhunSureshR Mar 2, 2025
6b61ba9
Remove more redundant code
MidhunSureshR Mar 2, 2025
2c41ade
Add more tests
MidhunSureshR Mar 2, 2025
b093789
Add method to await space store setup
MidhunSureshR Mar 3, 2025
c254ae0
Implement a way to filter by active space
MidhunSureshR Mar 3, 2025
19ed3e9
Fix broken jest tests
MidhunSureshR Mar 3, 2025
3719642
Add more vm functionality
MidhunSureshR Mar 3, 2025
4beeffc
chore: make the room list panel a flexbox
florianduros Feb 27, 2025
aa0dc11
draft
florianduros Feb 26, 2025
7f4a142
test(new room list): add test for room cell
florianduros Feb 27, 2025
463249b
test(new room list): update room list panel tests
florianduros Feb 28, 2025
d8f4834
test(new room list): add test to virtualized room list
florianduros Feb 28, 2025
6f6a214
test(e2e): add room list tests
florianduros Feb 28, 2025
e329640
test(e2e): update room panel tests
florianduros Mar 3, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,17 @@ test.describe("Room list panel", () => {
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();

// Populate the room list
for (let i = 0; i < 20; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});

test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomListView(page);
// Wait for the last room list to be visible
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
});
});
50 changes: 50 additions & 0 deletions playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { type Page } from "@playwright/test";

import { test, expect } from "../../../element-web-test";

test.describe("Room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});

/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}

test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
for (let i = 0; i < 30; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});

test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");

await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});

test("should open the room when it is clicked", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@
@import "./views/right_panel/_VerificationPanel.pcss";
@import "./views/right_panel/_WidgetCard.pcss";
@import "./views/room_settings/_AliasSettings.pcss";
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
@import "./views/rooms/RoomListPanel/_RoomListCell.pcss";
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
@import "./views/rooms/RoomListPanel/_RoomListPanel.pcss";
@import "./views/rooms/RoomListPanel/_RoomListSearch.pcss";
Expand Down
15 changes: 15 additions & 0 deletions res/css/views/rooms/RoomListPanel/_RoomList.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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_RoomList {
height: 100%;

.mx_RoomList_List {
/* Avoid when on hover, the background color to be on top of the right border */
padding-right: 1px;
}
}
44 changes: 44 additions & 0 deletions res/css/views/rooms/RoomListPanel/_RoomListCell.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.
*/

/**
* The RoomCell has the following structure:
* button----------------------------------------|
* | <-12px-> container--------------------------|
* | | room avatar <-12px-> content-----|
* | | | room_name |
* | | | ----------| <-- border
* |---------------------------------------------|
*/
.mx_RoomListCell {
all: unset;

&:hover {
background-color: var(--cpd-color-bg-action-secondary-hovered);
}

.mx_RoomListCell_container {
padding-left: var(--cpd-space-3x);
font: var(--cpd-font-body-md-regular);
height: 100%;

.mx_RoomListCell_content {
height: 100%;
flex: 1;
/* The border is only under the room name and the future hover menu */
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
box-sizing: border-box;
min-width: 0;

span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

.mx_RoomListHeaderView {
height: 60px;
flex: 0 0 60px;
padding: 0 var(--cpd-space-3x);

.mx_RoomListHeaderView_title {
Expand Down
2 changes: 1 addition & 1 deletion res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

.mx_RoomListSearch {
/* From figma, this should be aligned with the room header */
height: 64px;
flex: 0 0 64px;
box-sizing: border-box;
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
padding: 0 var(--cpd-space-3x);
Expand Down
51 changes: 51 additions & 0 deletions src/components/viewmodels/roomlist/RoomListViewModel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
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 { useCallback, useState } from "react";

import type { Room } from "matrix-js-sdk/src/matrix";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";

export interface RoomListViewState {
/**
* A list of rooms to be displayed in the left panel.
*/
rooms: Room[];

/**
* Open the room having given roomId.
*/
openRoom: (roomId: string) => void;
}

/**
* View model for the new room list
* @see {@link RoomListViewState} for more information about what this view model returns.
*/
export function useRoomListViewModel(): RoomListViewState {
const [rooms, setRooms] = useState(RoomListStoreV3.instance.getSortedRoomInActiveSpace());

useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => {
const newRooms = RoomListStoreV3.instance.getSortedRoomInActiveSpace();
setRooms(newRooms);
});

const openRoom = useCallback((roomId: string): void => {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
metricsTrigger: "RoomList",
});
}, []);

return { rooms, openRoom };
}
51 changes: 51 additions & 0 deletions src/components/views/rooms/RoomListPanel/RoomList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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, { useCallback, type JSX } from "react";
import { AutoSizer, List, type ListRowProps } from "react-virtualized";

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

interface RoomListProps {
/**
* The view model state for the room list.
*/
vm: RoomListViewState;
}

/**
* A virtualized list of rooms.
*/
export function RoomList({ vm: { rooms, openRoom } }: RoomListProps): JSX.Element {
const roomRendererMemoized = useCallback(
({ key, index, style }: ListRowProps) => (
<RoomListCell room={rooms[index]} key={key} style={style} onClick={() => openRoom(rooms[index].roomId)} />
),
[rooms, openRoom],
);

// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
return (
<div className="mx_RoomList" data-testid="room-list">
<AutoSizer>
{({ height, width }) => (
<List
aria-label={_t("room_list|list_title")}
className="mx_RoomList_List"
rowRenderer={roomRendererMemoized}
rowCount={rooms.length}
rowHeight={48}
height={height}
width={width}
/>
)}
</AutoSizer>
</div>
);
}
44 changes: 44 additions & 0 deletions src/components/views/rooms/RoomListPanel/RoomListCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { type Room } from "matrix-js-sdk/src/matrix";

import { _t } from "../../../../languageHandler";
import { Flex } from "../../../utils/Flex";
import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar";

interface RoomListCellProps extends React.HTMLAttributes<HTMLButtonElement> {
/**
* The room to display
*/
room: Room;
}

/**
* A cell in the room list
*/
export function RoomListCell({ room, ...props }: RoomListCellProps): JSX.Element {
return (
<button
className="mx_RoomListCell"
type="button"
aria-label={_t("room_list|room|open_room", { roomName: room.name })}
{...props}
>
{/* We need this extra div between the button and the content in order to add a padding which is not messing with the virtualized list */}
<Flex className="mx_RoomListCell_container" gap="var(--cpd-space-3x)" align="center">
<DecoratedRoomAvatar room={room} size="32px" />
<Flex className="mx_RoomListCell_content" align="center">
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
<span title={room.name}>{room.name}</span>
{/* Future hover menu et notification badges */}
</Flex>
</Flex>
</button>
);
}
13 changes: 11 additions & 2 deletions src/components/views/rooms/RoomListPanel/RoomListPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { shouldShowComponent } from "../../../../customisations/helpers/UICompon
import { UIComponent } from "../../../../settings/UIFeature";
import { RoomListSearch } from "./RoomListSearch";
import { RoomListHeaderView } from "./RoomListHeaderView";
import { RoomListView } from "./RoomListView";
import { Flex } from "../../../utils/Flex";

type RoomListPanelProps = {
/**
Expand All @@ -27,9 +29,16 @@ export const RoomListPanel: React.FC<RoomListPanelProps> = ({ activeSpace }) =>
const displayRoomSearch = shouldShowComponent(UIComponent.FilterContainer);

return (
<section className="mx_RoomListPanel" data-testid="room-list-panel">
<Flex
as="section"
className="mx_RoomListPanel"
data-testid="room-list-panel"
direction="column"
align="stretch"
>
{displayRoomSearch && <RoomListSearch activeSpace={activeSpace} />}
<RoomListHeaderView />
</section>
<RoomListView />
</Flex>
);
};
17 changes: 17 additions & 0 deletions src/components/views/rooms/RoomListPanel/RoomListView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel";
import { RoomList } from "./RoomList";

export function RoomListView(): JSX.Element {
const vm = useRoomListViewModel();
// Room filters will be added soon
return <RoomList vm={vm} />;
}
4 changes: 4 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2097,12 +2097,16 @@
"one": "Currently joining %(count)s room",
"other": "Currently joining %(count)s rooms"
},
"list_title": "Room list",
"notification_options": "Notification options",
"open_space_menu": "Open space menu",
"redacting_messages_status": {
"one": "Currently removing messages in %(count)s room",
"other": "Currently removing messages in %(count)s rooms"
},
"room": {
"open_room": "Open room %(roomName)s"
},
"show_less": "Show less",
"show_n_more": {
"one": "Show %(count)s more",
Expand Down
Loading