Skip to content

Commit f95218e

Browse files
authored
New room list: add space menu in room header (#29352)
* feat(new room list): add space menu in view model * test(new room list): add space menu in view model * feat(new room list): add space menu in room list header * chore: update i18n * test(new room list): add tests for space menu * test(new room list): update room list tests * test(e2e): add tests for space menu
1 parent 62a2872 commit f95218e

File tree

11 files changed

+603
-68
lines changed

11 files changed

+603
-68
lines changed

playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,41 @@ test.describe("Header section of the room list", () => {
4747
await app.closeDialog();
4848
});
4949

50-
test("should render the header section for a space", async ({ page, app, user }) => {
50+
test("should render the header section for a space", { tag: "@screenshot" }, async ({ page, app, user }) => {
5151
await app.client.createSpace({ name: "MySpace" });
5252
await page.getByRole("button", { name: "MySpace" }).click();
5353

5454
const roomListHeader = getHeaderSection(page);
55+
await expect(roomListHeader).toMatchScreenshot("room-list-space-header.png");
56+
5557
await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible();
5658
await expect(roomListHeader.getByRole("button", { name: "Add" })).toBeVisible();
59+
60+
const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" });
61+
await spaceMenu.click();
62+
63+
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-space-menu.png");
64+
65+
// It should open the space home
66+
await page.getByRole("menuitem", { name: "Space home" }).click();
67+
await expect(page.getByRole("main").getByRole("heading", { name: "MySpace" })).toBeVisible();
68+
69+
// It should open the invite dialog
70+
await spaceMenu.click();
71+
await page.getByRole("menuitem", { name: "Invite" }).click();
72+
await expect(page.getByRole("heading", { name: "Invite to MySpace" })).toBeVisible();
73+
await app.closeDialog();
74+
75+
// It should open the space preferences
76+
await spaceMenu.click();
77+
await page.getByRole("menuitem", { name: "Preferences" }).click();
78+
await expect(page.getByRole("heading", { name: "Preferences" })).toBeVisible();
79+
await app.closeDialog();
80+
81+
// It should open the space settings
82+
await spaceMenu.click();
83+
await page.getByRole("menuitem", { name: "Space Settings" }).click();
84+
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
85+
await app.closeDialog();
5786
});
5887
});

res/css/views/rooms/RoomListView/_RoomListHeaderView.pcss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,16 @@
1717
button {
1818
color: var(--cpd-color-icon-secondary);
1919
}
20+
21+
.mx_SpaceMenu_button {
22+
svg {
23+
transition: transform 0.1s linear;
24+
}
25+
}
26+
27+
.mx_SpaceMenu_button[aria-expanded="true"] {
28+
svg {
29+
transform: rotate(180deg);
30+
}
31+
}
2032
}

src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { useCallback } from "react";
9-
import { type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
9+
import { JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
1010

1111
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
1212
import { UIComponent } from "../../../settings/UIFeature";
@@ -23,7 +23,15 @@ import {
2323
UPDATE_SELECTED_SPACE,
2424
} from "../../../stores/spaces";
2525
import SpaceStore from "../../../stores/spaces/SpaceStore";
26-
import { showCreateNewRoom } from "../../../utils/space";
26+
import {
27+
shouldShowSpaceSettings,
28+
showCreateNewRoom,
29+
showSpaceInvite,
30+
showSpacePreferences,
31+
showSpaceSettings,
32+
} from "../../../utils/space";
33+
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
34+
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
2735

2836
/**
2937
* Hook to get the active space and its title.
@@ -59,6 +67,11 @@ export interface RoomListHeaderViewState {
5967
* True if the user can create rooms
6068
*/
6169
displayComposeMenu: boolean;
70+
/**
71+
* Whether to display the space menu
72+
* True if there is an active space
73+
*/
74+
displaySpaceMenu: boolean;
6275
/**
6376
* Whether the user can create rooms
6477
*/
@@ -67,6 +80,14 @@ export interface RoomListHeaderViewState {
6780
* Whether the user can create video rooms
6881
*/
6982
canCreateVideoRoom: boolean;
83+
/**
84+
* Whether the user can invite in the active space
85+
*/
86+
canInviteInSpace: boolean;
87+
/**
88+
* Whether the user can access space settings
89+
*/
90+
canAccessSpaceSettings: boolean;
7091
/**
7192
* Create a chat room
7293
* @param e - The click event
@@ -81,17 +102,39 @@ export interface RoomListHeaderViewState {
81102
* Create a video room
82103
*/
83104
createVideoRoom: () => void;
105+
/**
106+
* Open the active space home
107+
*/
108+
openSpaceHome: () => void;
109+
/**
110+
* Display the space invite dialog
111+
*/
112+
inviteInSpace: () => void;
113+
/**
114+
* Open the space preferences
115+
*/
116+
openSpacePreferences: () => void;
117+
/**
118+
* Open the space settings
119+
*/
120+
openSpaceSettings: () => void;
84121
}
85122

86123
/**
87124
* View model for the RoomListHeader.
88125
*/
89126
export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
127+
const matrixClient = useMatrixClientContext();
90128
const { activeSpace, title } = useSpace();
91129

92130
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
93131
const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms");
94132
const displayComposeMenu = canCreateRoom;
133+
const displaySpaceMenu = Boolean(activeSpace);
134+
const canInviteInSpace = Boolean(
135+
activeSpace?.getJoinRule() === JoinRule.Public || activeSpace?.canInvite(matrixClient.getSafeUserId()),
136+
);
137+
const canAccessSpaceSettings = Boolean(activeSpace && shouldShowSpaceSettings(activeSpace));
95138

96139
/* Actions */
97140

@@ -125,13 +168,48 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
125168
}
126169
}, [activeSpace, elementCallVideoRoomsEnabled]);
127170

171+
const openSpaceHome = useCallback(() => {
172+
// openSpaceHome is only available when there is an active space
173+
if (!activeSpace) return;
174+
defaultDispatcher.dispatch<ViewRoomPayload>({
175+
action: Action.ViewRoom,
176+
room_id: activeSpace.roomId,
177+
metricsTrigger: undefined,
178+
});
179+
}, [activeSpace]);
180+
181+
const inviteInSpace = useCallback(() => {
182+
// inviteInSpace is only available when there is an active space
183+
if (!activeSpace) return;
184+
showSpaceInvite(activeSpace);
185+
}, [activeSpace]);
186+
187+
const openSpacePreferences = useCallback(() => {
188+
// openSpacePreferences is only available when there is an active space
189+
if (!activeSpace) return;
190+
showSpacePreferences(activeSpace);
191+
}, [activeSpace]);
192+
193+
const openSpaceSettings = useCallback(() => {
194+
// openSpaceSettings is only available when there is an active space
195+
if (!activeSpace) return;
196+
showSpaceSettings(activeSpace);
197+
}, [activeSpace]);
198+
128199
return {
129200
title,
130201
displayComposeMenu,
202+
displaySpaceMenu,
131203
canCreateRoom,
132204
canCreateVideoRoom,
205+
canInviteInSpace,
206+
canAccessSpaceSettings,
133207
createChatRoom,
134208
createRoom,
135209
createVideoRoom,
210+
openSpaceHome,
211+
inviteInSpace,
212+
openSpacePreferences,
213+
openSpaceSettings,
136214
};
137215
}

src/components/views/rooms/RoomListView/RoomListHeaderView.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import React, { type JSX, useState } from "react";
88
import { IconButton, Menu, MenuItem } from "@vector-im/compound-web";
99
import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose";
1010
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
11+
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
1112
import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
13+
import HomeIcon from "@vector-im/compound-design-tokens/assets/web/icons/home";
14+
import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences";
15+
import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings";
1216
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call";
1317

1418
import { _t } from "../../../../languageHandler";
@@ -34,12 +38,57 @@ export function RoomListHeaderView(): JSX.Element {
3438
align="center"
3539
data-testid="room-list-header"
3640
>
37-
<h1>{vm.title}</h1>
41+
<Flex align="center" gap="var(--cpd-space-1x)">
42+
<h1>{vm.title}</h1>
43+
{vm.displaySpaceMenu && <SpaceMenu vm={vm} />}
44+
</Flex>
3845
{vm.displayComposeMenu && <ComposeMenu vm={vm} />}
3946
</Flex>
4047
);
4148
}
4249

50+
interface SpaceMenuProps {
51+
/**
52+
* The view model for the room list header
53+
*/
54+
vm: RoomListHeaderViewState;
55+
}
56+
57+
/**
58+
* The space menu for the room list header
59+
*/
60+
function SpaceMenu({ vm }: SpaceMenuProps): JSX.Element {
61+
const [open, setOpen] = useState(false);
62+
63+
return (
64+
<Menu
65+
open={open}
66+
onOpenChange={setOpen}
67+
title={vm.title}
68+
side="right"
69+
align="start"
70+
trigger={
71+
<IconButton className="mx_SpaceMenu_button" aria-label={_t("room_list|open_space_menu")} size="20px">
72+
<ChevronDownIcon />
73+
</IconButton>
74+
}
75+
>
76+
<MenuItem Icon={HomeIcon} label={_t("room_list|space_menu|home")} onSelect={vm.openSpaceHome} />
77+
{vm.canInviteInSpace && (
78+
<MenuItem Icon={UserAddIcon} label={_t("action|invite")} onSelect={vm.inviteInSpace} />
79+
)}
80+
<MenuItem Icon={PreferencesIcon} label={_t("common|preferences")} onSelect={vm.openSpacePreferences} />
81+
{vm.canAccessSpaceSettings && (
82+
<MenuItem
83+
Icon={SettingsIcon}
84+
label={_t("room_list|space_menu|space_settings")}
85+
onSelect={vm.openSpaceSettings}
86+
/>
87+
)}
88+
</Menu>
89+
);
90+
}
91+
4392
interface ComposeMenuProps {
4493
/**
4594
* The view model for the room list header

src/i18n/strings/en_EN.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,6 +2098,7 @@
20982098
"other": "Currently joining %(count)s rooms"
20992099
},
21002100
"notification_options": "Notification options",
2101+
"open_space_menu": "Open space menu",
21012102
"redacting_messages_status": {
21022103
"one": "Currently removing messages in %(count)s room",
21032104
"other": "Currently removing messages in %(count)s rooms"
@@ -2112,6 +2113,10 @@
21122113
"sort_by_activity": "Activity",
21132114
"sort_by_alphabet": "A-Z",
21142115
"sort_unread_first": "Show rooms with unread messages first",
2116+
"space_menu": {
2117+
"home": "Space home",
2118+
"space_settings": "Space Settings"
2119+
},
21152120
"space_menu_label": "%(spaceName)s menu",
21162121
"sublist_options": "List options",
21172122
"suggested_rooms_heading": "Suggested Rooms"

0 commit comments

Comments
 (0)