Skip to content

Commit bbd798e

Browse files
authored
New room list: add notification decoration (#29552)
* chore: update @compound-web * feat(notification decoration): add NotificationDecoration component * feat(room list item): get notification state in view model * feat(room list item): use notification decoration in RoomListItemView * test(notification decoration): add tests * test(room list item view model): add a11yLabel tests * test(room list item): update tests * test(e2e): add decoration tests
1 parent f3f0587 commit bbd798e

File tree

17 files changed

+573
-76
lines changed

17 files changed

+573
-76
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
"@types/png-chunks-extract": "^1.0.2",
9393
"@types/react-virtualized": "^9.21.30",
9494
"@vector-im/compound-design-tokens": "^4.0.0",
95-
"@vector-im/compound-web": "^7.7.2",
95+
"@vector-im/compound-web": "^7.9.0",
9696
"@vector-im/matrix-wysiwyg": "2.38.2",
9797
"@zxcvbn-ts/core": "^3.0.4",
9898
"@zxcvbn-ts/language-common": "^3.0.4",

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

Lines changed: 189 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ test.describe("Room list", () => {
1313
test.use({
1414
displayName: "Alice",
1515
labsFlags: ["feature_new_room_list"],
16+
botCreateOpts: {
17+
displayName: "BotBob",
18+
},
1619
});
1720

1821
/**
@@ -26,71 +29,195 @@ test.describe("Room list", () => {
2629
test.beforeEach(async ({ page, app, user }) => {
2730
// The notification toast is displayed above the search section
2831
await app.closeNotificationToast();
29-
for (let i = 0; i < 30; i++) {
30-
await app.client.createRoom({ name: `room${i}` });
31-
}
3232
});
3333

34-
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
35-
const roomListView = getRoomList(page);
36-
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
37-
await expect(roomListView).toMatchScreenshot("room-list.png");
38-
39-
await roomListView.hover();
40-
// Scroll to the end of the room list
41-
await page.mouse.wheel(0, 1000);
42-
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
43-
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
44-
});
45-
46-
test("should open the room when it is clicked", async ({ page, app, user }) => {
47-
const roomListView = getRoomList(page);
48-
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
49-
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
50-
});
51-
52-
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
53-
const roomListView = getRoomList(page);
54-
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
55-
await roomItem.hover();
56-
57-
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
58-
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
59-
await roomItemMenu.click();
60-
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
61-
62-
// It should make the room favourited
63-
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
64-
65-
// Check that the room is favourited
66-
await roomItem.hover();
67-
await roomItemMenu.click();
68-
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
69-
// It should show the invite dialog
70-
await page.getByRole("menuitem", { name: "invite" }).click();
71-
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
72-
await app.closeDialog();
73-
74-
// It should leave the room
75-
await roomItem.hover();
76-
await roomItemMenu.click();
77-
await page.getByRole("menuitem", { name: "leave room" }).click();
78-
await expect(roomItem).not.toBeVisible();
34+
test.describe("Room list", () => {
35+
test.beforeEach(async ({ page, app, user }) => {
36+
for (let i = 0; i < 30; i++) {
37+
await app.client.createRoom({ name: `room${i}` });
38+
}
39+
});
40+
41+
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
42+
const roomListView = getRoomList(page);
43+
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
44+
await expect(roomListView).toMatchScreenshot("room-list.png");
45+
46+
await roomListView.hover();
47+
// Scroll to the end of the room list
48+
await page.mouse.wheel(0, 1000);
49+
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
50+
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
51+
});
52+
53+
test("should open the room when it is clicked", async ({ page, app, user }) => {
54+
const roomListView = getRoomList(page);
55+
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
56+
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
57+
});
58+
59+
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
60+
const roomListView = getRoomList(page);
61+
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
62+
await roomItem.hover();
63+
64+
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
65+
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
66+
await roomItemMenu.click();
67+
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
68+
69+
// It should make the room favourited
70+
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
71+
72+
// Check that the room is favourited
73+
await roomItem.hover();
74+
await roomItemMenu.click();
75+
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
76+
// It should show the invite dialog
77+
await page.getByRole("menuitem", { name: "invite" }).click();
78+
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
79+
await app.closeDialog();
80+
81+
// It should leave the room
82+
await roomItem.hover();
83+
await roomItemMenu.click();
84+
await page.getByRole("menuitem", { name: "leave room" }).click();
85+
await expect(roomItem).not.toBeVisible();
86+
});
87+
88+
test("should scroll to the current room", async ({ page, app, user }) => {
89+
const roomListView = getRoomList(page);
90+
await roomListView.hover();
91+
// Scroll to the end of the room list
92+
await page.mouse.wheel(0, 1000);
93+
94+
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
95+
96+
const filters = page.getByRole("listbox", { name: "Room list filters" });
97+
await filters.getByRole("option", { name: "People" }).click();
98+
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
99+
100+
await filters.getByRole("option", { name: "People" }).click();
101+
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
102+
});
79103
});
80104

81-
test("should scroll to the current room", async ({ page, app, user }) => {
82-
const roomListView = getRoomList(page);
83-
await roomListView.hover();
84-
// Scroll to the end of the room list
85-
await page.mouse.wheel(0, 1000);
86-
87-
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
88-
89-
const filters = page.getByRole("listbox", { name: "Room list filters" });
90-
await filters.getByRole("option", { name: "People" }).click();
91-
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
92-
93-
await filters.getByRole("option", { name: "People" }).click();
94-
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
105+
test.describe("Notification decoration", () => {
106+
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
107+
const roomListView = getRoomList(page);
108+
109+
await bot.createRoom({
110+
name: "invited room",
111+
invite: [user.userId],
112+
is_direct: true,
113+
});
114+
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
115+
await expect(invitedRoom).toBeVisible();
116+
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
117+
});
118+
119+
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
120+
const roomListView = getRoomList(page);
121+
122+
const roomId = await app.client.createRoom({ name: "2 notifications" });
123+
await app.client.inviteUser(roomId, bot.credentials.userId);
124+
await bot.joinRoom(roomId);
125+
126+
await bot.sendMessage(roomId, "I am a robot. Beep.");
127+
await bot.sendMessage(roomId, "I am a robot. Beep.");
128+
129+
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
130+
await expect(room).toBeVisible();
131+
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
132+
await expect(room).toMatchScreenshot("room-list-item-notification.png");
133+
});
134+
135+
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
136+
const roomListView = getRoomList(page);
137+
138+
const roomId = await app.client.createRoom({ name: "mention" });
139+
await app.client.inviteUser(roomId, bot.credentials.userId);
140+
await bot.joinRoom(roomId);
141+
142+
const clientBot = await bot.prepareClient();
143+
await clientBot.evaluate(
144+
async (client, { roomId, userId }) => {
145+
await client.sendMessage(roomId, {
146+
// @ts-ignore ignore usage of MsgType.text
147+
"msgtype": "m.text",
148+
"body": "User",
149+
"format": "org.matrix.custom.html",
150+
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
151+
"m.mentions": {
152+
user_ids: [userId],
153+
},
154+
});
155+
},
156+
{ roomId, userId: user.userId },
157+
);
158+
await bot.sendMessage(roomId, "I am a robot. Beep.");
159+
160+
const room = roomListView.getByRole("gridcell", { name: "mention" });
161+
await expect(room).toBeVisible();
162+
await expect(room).toMatchScreenshot("room-list-item-mention.png");
163+
});
164+
165+
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
166+
const roomListView = getRoomList(page);
167+
168+
const roomId = await app.client.createRoom({ name: "activity" });
169+
await app.client.inviteUser(roomId, bot.credentials.userId);
170+
await bot.joinRoom(roomId);
171+
172+
await app.viewRoomById(roomId);
173+
await app.settings.openRoomSettings("Notifications");
174+
await page.getByText("@mentions & keywords").click();
175+
await app.settings.closeDialog();
176+
177+
await app.settings.openUserSettings("Notifications");
178+
await page.getByText("Show all activity in the room list (dots or number of unread messages)").click();
179+
await app.settings.closeDialog();
180+
181+
await bot.sendMessage(roomId, "I am a robot. Beep.");
182+
183+
const room = roomListView.getByRole("gridcell", { name: "activity" });
184+
await expect(room.getByTestId("notification-decoration")).toBeVisible();
185+
await expect(room).toMatchScreenshot("room-list-item-activity.png");
186+
});
187+
188+
test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
189+
const roomListView = getRoomList(page);
190+
191+
const roomId = await app.client.createRoom({ name: "mark as unread" });
192+
await app.client.inviteUser(roomId, bot.credentials.userId);
193+
await bot.joinRoom(roomId);
194+
195+
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
196+
await room.hover();
197+
await room.getByRole("button", { name: "More Options" }).click();
198+
await page.getByRole("menuitem", { name: "mark as unread" }).click();
199+
200+
// Remove hover on the room list item
201+
await roomListView.hover();
202+
203+
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
204+
});
205+
206+
test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
207+
const roomListView = getRoomList(page);
208+
209+
const roomId = await app.client.createRoom({ name: "silent" });
210+
await app.client.inviteUser(roomId, bot.credentials.userId);
211+
await bot.joinRoom(roomId);
212+
213+
await app.viewRoomById(roomId);
214+
await app.settings.openRoomSettings("Notifications");
215+
await page.getByText("Off").click();
216+
await app.settings.closeDialog();
217+
218+
const room = roomListView.getByRole("gridcell", { name: "silent" });
219+
await expect(room.getByTestId("notification-decoration")).toBeVisible();
220+
await expect(room).toMatchScreenshot("room-list-item-silent.png");
221+
});
95222
});
96223
});

src/components/viewmodels/roomlist/RoomListItemViewModel.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
import { useCallback } from "react";
8+
import { useCallback, useMemo } from "react";
99
import { type Room } from "matrix-js-sdk/src/matrix";
1010

1111
import dispatcher from "../../../dispatcher/dispatcher";
1212
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
1313
import { Action } from "../../../dispatcher/actions";
1414
import { hasAccessToOptionsMenu } from "./utils";
15+
import { _t } from "../../../languageHandler";
16+
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
17+
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
1518

1619
export interface RoomListItemViewState {
1720
/**
@@ -22,6 +25,14 @@ export interface RoomListItemViewState {
2225
* Open the room having given roomId.
2326
*/
2427
openRoom: () => void;
28+
/**
29+
* The a11y label for the room list item.
30+
*/
31+
a11yLabel: string;
32+
/**
33+
* The notification state of the room.
34+
*/
35+
notificationState: RoomNotificationState;
2536
}
2637

2738
/**
@@ -31,6 +42,8 @@ export interface RoomListItemViewState {
3142
export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
3243
// incoming: Check notification menu rights
3344
const showHoverMenu = hasAccessToOptionsMenu(room);
45+
const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]);
46+
const a11yLabel = getA11yLabel(room, notificationState);
3447

3548
// Actions
3649

@@ -43,7 +56,38 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
4356
}, [room]);
4457

4558
return {
59+
notificationState,
4660
showHoverMenu,
4761
openRoom,
62+
a11yLabel,
4863
};
4964
}
65+
66+
/**
67+
* Get the a11y label for the room list item
68+
* @param room
69+
* @param notificationState
70+
*/
71+
function getA11yLabel(room: Room, notificationState: RoomNotificationState): string {
72+
if (notificationState.isUnsetMessage) {
73+
return _t("a11y|room_messsage_not_sent", {
74+
roomName: room.name,
75+
});
76+
} else if (notificationState.invited) {
77+
return _t("a11y|room_n_unread_invite", {
78+
roomName: room.name,
79+
});
80+
} else if (notificationState.isMention) {
81+
return _t("a11y|room_n_unread_messages_mentions", {
82+
roomName: room.name,
83+
count: notificationState.count,
84+
});
85+
} else if (notificationState.hasUnreadCount) {
86+
return _t("a11y|room_n_unread_messages", {
87+
roomName: room.name,
88+
count: notificationState.count,
89+
});
90+
} else {
91+
return _t("room_list|room|open_room", { roomName: room.name });
92+
}
93+
}

0 commit comments

Comments
 (0)