Skip to content

New room list: add notification decoration #29552

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
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 @@ -92,7 +92,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.7.2",
"@vector-im/compound-web": "^7.9.0",
"@vector-im/matrix-wysiwyg": "2.38.2",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
Expand Down
251 changes: 189 additions & 62 deletions playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
test.use({
displayName: "Alice",
labsFlags: ["feature_new_room_list"],
botCreateOpts: {
displayName: "BotBob",
},
});

/**
Expand All @@ -26,71 +29,195 @@
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();
});

test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
await roomItem.hover();

await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
await roomItemMenu.click();
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");

// It should make the room favourited
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();

// Check that the room is favourited
await roomItem.hover();
await roomItemMenu.click();
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
// It should show the invite dialog
await page.getByRole("menuitem", { name: "invite" }).click();
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
await app.closeDialog();

// It should leave the room
await roomItem.hover();
await roomItemMenu.click();
await page.getByRole("menuitem", { name: "leave room" }).click();
await expect(roomItem).not.toBeVisible();
test.describe("Room list", () => {
test.beforeEach(async ({ page, app, user }) => {
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();
});

test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
await roomItem.hover();

await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
await roomItemMenu.click();
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");

// It should make the room favourited
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();

// Check that the room is favourited
await roomItem.hover();
await roomItemMenu.click();
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
// It should show the invite dialog
await page.getByRole("menuitem", { name: "invite" }).click();
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
await app.closeDialog();

// It should leave the room
await roomItem.hover();
await roomItemMenu.click();
await page.getByRole("menuitem", { name: "leave room" }).click();
await expect(roomItem).not.toBeVisible();
});

test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);

await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();

const filters = page.getByRole("listbox", { name: "Room list filters" });
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();

await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
});
});

test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.hover();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);

await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();

const filters = page.getByRole("listbox", { name: "Room list filters" });
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();

await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
test.describe("Notification decoration", () => {
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);

await bot.createRoom({
name: "invited room",
invite: [user.userId],
is_direct: true,
});
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
await expect(invitedRoom).toBeVisible();
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
});

test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);

const roomId = await app.client.createRoom({ name: "2 notifications" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);

await bot.sendMessage(roomId, "I am a robot. Beep.");
await bot.sendMessage(roomId, "I am a robot. Beep.");

const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
await expect(room).toBeVisible();
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
await expect(room).toMatchScreenshot("room-list-item-notification.png");
});

test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);

const roomId = await app.client.createRoom({ name: "mention" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);

const clientBot = await bot.prepareClient();
await clientBot.evaluate(
async (client, { roomId, userId }) => {
await client.sendMessage(roomId, {
// @ts-ignore ignore usage of MsgType.text
"msgtype": "m.text",
"body": "User",
"format": "org.matrix.custom.html",
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
"m.mentions": {
user_ids: [userId],
},
});
},
{ roomId, userId: user.userId },
);
await bot.sendMessage(roomId, "I am a robot. Beep.");

const room = roomListView.getByRole("gridcell", { name: "mention" });
await expect(room).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-mention.png");
});

test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);

const roomId = await app.client.createRoom({ name: "activity" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);

await app.viewRoomById(roomId);
await app.settings.openRoomSettings("Notifications");
await page.getByText("@mentions & keywords").click();
await app.settings.closeDialog();

await app.settings.openUserSettings("Notifications");
await page.getByText("Show all activity in the room list (dots or number of unread messages)").click();
await app.settings.closeDialog();

await bot.sendMessage(roomId, "I am a robot. Beep.");

const room = roomListView.getByRole("gridcell", { name: "activity" });
await expect(room.getByTestId("notification-decoration")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-activity.png");

Check failure on line 185 in playwright/e2e/left-panel/room-list-panel/room-list.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 2/6

[Chrome] › playwright/e2e/left-panel/room-list-panel/room-list.spec.ts:165:13 › Room list › Notification decoration › should render an activity decoration @screenshot

1) [Chrome] › playwright/e2e/left-panel/room-list-panel/room-list.spec.ts:165:13 › Room list › Notification decoration › should render an activity decoration @screenshot Error: expect(locator).toHaveScreenshot(expected) 36 pixels (ratio 0.01 of all image pixels) are different. Expected: /home/runner/work/element-web/element-web/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png Received: /home/runner/work/element-web/element-web/playwright/test-results/left-panel-room-list-panel-1a022-nder-an-activity-decoration-Chrome/room-list-item-activity-actual.png Diff: /home/runner/work/element-web/element-web/playwright/test-results/left-panel-room-list-panel-1a022-nder-an-activity-decoration-Chrome/room-list-item-activity-diff.png Call log: - expect.toHaveScreenshot(room-list-item-activity.png) with timeout 5000ms - verifying given screenshot expectation - waiting for getByTestId('room-list').getByRole('gridcell', { name: 'activity' }) - locator resolved to <button type="button" role="gridcell" aria-selected="true" aria-label="Open room activity" class="mx_RoomListItemView mx_RoomListItemView_selected">…</button> - taking element screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - attempting scroll into view action - waiting for element to be stable - 36 pixels (ratio 0.01 of all image pixels) are different. - waiting 100ms before taking screenshot - waiting for getByTestId('room-list').getByRole('gridcell', { name: 'activity' }) - locator resolved to <button type="button" role="gridcell" aria-selected="true" aria-label="Open room activity" class="mx_RoomListItemView mx_RoomListItemView_selected">…</button> - taking element screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - attempting scroll into view action - waiting for element to be stable - captured a stable screenshot - 36 pixels (ratio 0.01 of all image pixels) are different. 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 }) => { at /home/runner/work/element-web/element-web/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts:185:32
});

test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);

const roomId = await app.client.createRoom({ name: "mark as unread" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);

const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
await room.hover();
await room.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "mark as unread" }).click();

// Remove hover on the room list item
await roomListView.hover();

await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
});

test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);

const roomId = await app.client.createRoom({ name: "silent" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);

await app.viewRoomById(roomId);
await app.settings.openRoomSettings("Notifications");
await page.getByText("Off").click();
await app.settings.closeDialog();

const room = roomListView.getByRole("gridcell", { name: "silent" });
await expect(room.getByTestId("notification-decoration")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-silent.png");
});
});
});
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.
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.
46 changes: 45 additions & 1 deletion src/components/viewmodels/roomlist/RoomListItemViewModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
* Please see LICENSE files in the repository root for full details.
*/

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

import dispatcher from "../../../dispatcher/dispatcher";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { hasAccessToOptionsMenu } from "./utils";
import { _t } from "../../../languageHandler";
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";

export interface RoomListItemViewState {
/**
Expand All @@ -22,6 +25,14 @@ export interface RoomListItemViewState {
* Open the room having given roomId.
*/
openRoom: () => void;
/**
* The a11y label for the room list item.
*/
a11yLabel: string;
/**
* The notification state of the room.
*/
notificationState: RoomNotificationState;
}

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

// Actions

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

return {
notificationState,
showHoverMenu,
openRoom,
a11yLabel,
};
}

/**
* Get the a11y label for the room list item
* @param room
* @param notificationState
*/
function getA11yLabel(room: Room, notificationState: RoomNotificationState): string {
if (notificationState.isUnsetMessage) {
return _t("a11y|room_messsage_not_sent", {
roomName: room.name,
});
} else if (notificationState.invited) {
return _t("a11y|room_n_unread_invite", {
roomName: room.name,
});
} else if (notificationState.isMention) {
return _t("a11y|room_n_unread_messages_mentions", {
roomName: room.name,
count: notificationState.count,
});
} else if (notificationState.hasUnreadCount) {
return _t("a11y|room_n_unread_messages", {
roomName: room.name,
count: notificationState.count,
});
} else {
return _t("room_list|room|open_room", { roomName: room.name });
}
}
Loading
Loading