New room list: avoid extra render for room list item (#29752)
* fix: avoid extra render in the new room list * fix: listen to room name changes * fix: trigger render when notification state change * test: fix room list item tests * chore: fix typo `RoomNotificationState.isUnsentMessage` * refactor: move `isNotificationDecorationVisible` into `useRoomListItemViewModel` * refactor: recalculate notification values on notification state changes * refactor: rename `isNotificationDecorationVisible` to `showNotificationDecoration` * test: add test for room list item view * test: add notification tests in room list item vm * fix: listen to notification updates in `NotificationDecoration` * test: update notification decoration tests * refactor: display notification decoration according to vm * test: update room list item view tests * fix: a11y label computation after room name change * refactor: improve notification handling
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import * as UseCallModule from "../../../../../src/hooks/useCall";
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
|
||||
@@ -86,6 +87,49 @@ describe("RoomListItemViewModel", () => {
|
||||
expect(vm.current.showHoverMenu).toBe(true);
|
||||
});
|
||||
|
||||
describe("notification", () => {
|
||||
let notificationState: RoomNotificationState;
|
||||
beforeEach(() => {
|
||||
notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
|
||||
});
|
||||
|
||||
it("should show notification decoration if there is call has participant", () => {
|
||||
jest.spyOn(UseCallModule, "useParticipantCount").mockReturnValue(1);
|
||||
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showNotificationDecoration).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "hasAnyNotificationOrActivity",
|
||||
mock: () => jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true),
|
||||
},
|
||||
{ label: "muted", mock: () => jest.spyOn(notificationState, "muted", "get").mockReturnValue(true) },
|
||||
])("should show notification decoration if $label=true", ({ mock }) => {
|
||||
mock();
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.showNotificationDecoration).toBe(true);
|
||||
});
|
||||
|
||||
it("should be bold if there is a notification", () => {
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
|
||||
const { result: vm } = renderHook(
|
||||
() => useRoomListItemViewModel(room),
|
||||
withClientContextRenderOptions(room.client),
|
||||
);
|
||||
expect(vm.current.isBold).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a11yLabel", () => {
|
||||
let notificationState: RoomNotificationState;
|
||||
beforeEach(() => {
|
||||
@@ -96,7 +140,7 @@ describe("RoomListItemViewModel", () => {
|
||||
it.each([
|
||||
{
|
||||
label: "unsent message",
|
||||
mock: () => jest.spyOn(notificationState, "isUnsetMessage", "get").mockReturnValue(true),
|
||||
mock: () => jest.spyOn(notificationState, "isUnsentMessage", "get").mockReturnValue(true),
|
||||
expected: "Open room roomName with an unsent message.",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,60 +8,94 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
|
||||
import { type RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
|
||||
import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration";
|
||||
import { createTestClient, mkStubRoom } from "../../../../test-utils";
|
||||
|
||||
describe("<NotificationDecoration />", () => {
|
||||
it("should not render if RoomNotificationState.isSilent=true", () => {
|
||||
const state = { hasAnyNotificationOrActivity: false } as RoomNotificationState;
|
||||
render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
let roomNotificationState: RoomNotificationState;
|
||||
beforeEach(() => {
|
||||
const matrixClient = createTestClient();
|
||||
const room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
roomNotificationState = new RoomNotificationState(room, false);
|
||||
});
|
||||
|
||||
it("should not render if RoomNotificationState.hasAnyNotificationOrActivity=true", () => {
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
||||
render(<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />);
|
||||
expect(screen.queryByTestId("notification-decoration")).toBeNull();
|
||||
});
|
||||
|
||||
it("should render the unset message decoration", () => {
|
||||
const state = { hasAnyNotificationOrActivity: true, isUnsetMessage: true } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "isUnsentMessage", "get").mockReturnValue(true);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the invitation decoration", () => {
|
||||
const state = { hasAnyNotificationOrActivity: true, invited: true } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "invited", "get").mockReturnValue(true);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the mention decoration", () => {
|
||||
const state = { hasAnyNotificationOrActivity: true, isMention: true, count: 1 } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "isMention", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the notification decoration", () => {
|
||||
const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 1 } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the notification decoration without count", () => {
|
||||
const state = { hasAnyNotificationOrActivity: true, isNotification: true, count: 0 } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(0);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the activity decoration", () => {
|
||||
const state = { hasAnyNotificationOrActivity: true, isActivityNotification: true } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "isActivityNotification", "get").mockReturnValue(true);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the muted decoration", () => {
|
||||
const state = { hasAnyNotificationOrActivity: true, muted: true } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={false} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(roomNotificationState, "muted", "get").mockReturnValue(true);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
it("should render the video decoration", () => {
|
||||
const state = { hasAnyNotificationOrActivity: false } as RoomNotificationState;
|
||||
const { asFragment } = render(<NotificationDecoration notificationState={state} hasVideoCall={true} />);
|
||||
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
|
||||
const { asFragment } = render(
|
||||
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={true} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ describe("<RoomListItemView />", () => {
|
||||
let defaultValue: RoomListItemViewState;
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
room = mkRoom(matrixClient, "room1");
|
||||
@@ -36,15 +35,22 @@ describe("<RoomListItemView />", () => {
|
||||
DMRoomMap.makeShared(matrixClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
|
||||
|
||||
const notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "isNotification", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(1);
|
||||
|
||||
defaultValue = {
|
||||
openRoom: jest.fn(),
|
||||
showHoverMenu: false,
|
||||
notificationState: new RoomNotificationState(room, false),
|
||||
notificationState,
|
||||
a11yLabel: "Open room room1",
|
||||
isBold: false,
|
||||
isVideoRoom: false,
|
||||
callConnectionState: null,
|
||||
hasParticipantInCall: false,
|
||||
name: room.name,
|
||||
showNotificationDecoration: false,
|
||||
};
|
||||
|
||||
mocked(useRoomListItemViewModel).mockReturnValue(defaultValue);
|
||||
@@ -84,4 +90,30 @@ describe("<RoomListItemView />", () => {
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should display notification decoration", async () => {
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
const { asFragment } = render(<RoomListItemView room={room} isSelected={false} />);
|
||||
expect(screen.getByTestId("notification-decoration")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should not display notification decoration when hovered", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
render(<RoomListItemView room={room} isSelected={false} />);
|
||||
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
await user.hover(listItem);
|
||||
|
||||
expect(screen.queryByRole("notification-decoration")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,65 @@ exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListItemView /> should display notification decoration 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-selected="false"
|
||||
class="mx_RoomListItemView mx_RoomListItemView_notification_decoration"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="mx_Flex"
|
||||
data-testid="notification-decoration"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1-5x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_unread-counter_9mg0k_8"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListItemView /> should render a room item 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
|
||||
@@ -223,7 +223,7 @@ describe("RoomNotificationState", () => {
|
||||
it("should has isUnsetMessage at true", () => {
|
||||
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]);
|
||||
const roomNotifState = new RoomNotificationState(room, false);
|
||||
expect(roomNotifState.isUnsetMessage).toBe(true);
|
||||
expect(roomNotifState.isUnsentMessage).toBe(true);
|
||||
});
|
||||
|
||||
it("should has isMention at false if the notification is invitation, an unset message or a knock", () => {
|
||||
|
||||
Reference in New Issue
Block a user