New room list: add more options menu on room list item (#29445)
* refactor(room list item): rename `RoomListCell` into `RoomListItemView` * refactor(room list item): move open room action to new room list item view model * feat(hover menu): add `hasAccessToOptionsMenu` * feat(hover menu): add to `RoomListItemViewModel` the condition to display or not the hover menu * feat(hover menu): add view model for the hover menu * feat(hover menu): add hover menu view * feat(hover menu): add hover menu to room list item * feat(hover menu): update i18n * test(view model list item): update test and add test to `showHoverMenu` * test(room list): update snapshot * test(room list item menu): add tests for view model * test(room list item menu): add tests for view * test(room list item): add tests * test(e2e): add tests for more options menu * chore: update compound web * test(e2e): fix typo
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 { renderHook } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { mkStubRoom, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
|
||||
import { useRoomListItemMenuViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel";
|
||||
import { hasAccessToOptionsMenu } from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
|
||||
import { useUnreadNotifications } from "../../../../../src/hooks/useUnreadNotifications";
|
||||
import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel";
|
||||
import { clearRoomNotification, setMarkedUnreadState } from "../../../../../src/utils/notifications";
|
||||
import { tagRoom } from "../../../../../src/utils/room/tagRoom";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/hooks/useUnreadNotifications", () => ({
|
||||
useUnreadNotifications: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/utils/notifications", () => ({
|
||||
clearRoomNotification: jest.fn(),
|
||||
setMarkedUnreadState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/utils/room/tagRoom", () => ({
|
||||
tagRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomListItemMenuViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
|
||||
DMRoomMap.makeShared(matrixClient);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
|
||||
|
||||
mocked(useUnreadNotifications).mockReturnValue({ symbol: null, count: 0, level: NotificationLevel.None });
|
||||
jest.spyOn(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function render() {
|
||||
return renderHook(() => useRoomListItemMenuViewModel(room), withClientContextRenderOptions(matrixClient));
|
||||
}
|
||||
|
||||
it("default", () => {
|
||||
const { result } = render();
|
||||
expect(result.current.showMoreOptionsMenu).toBe(false);
|
||||
expect(result.current.canInvite).toBe(false);
|
||||
expect(result.current.isFavourite).toBe(false);
|
||||
expect(result.current.canCopyRoomLink).toBe(true);
|
||||
expect(result.current.canMarkAsRead).toBe(false);
|
||||
expect(result.current.canMarkAsUnread).toBe(true);
|
||||
});
|
||||
|
||||
it("should has showMoreOptionsMenu to be true", () => {
|
||||
mocked(hasAccessToOptionsMenu).mockReturnValue(true);
|
||||
const { result } = render();
|
||||
expect(result.current.showMoreOptionsMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("should be able to invite", () => {
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
const { result } = render();
|
||||
expect(result.current.canInvite).toBe(true);
|
||||
});
|
||||
|
||||
it("should be a favourite", () => {
|
||||
room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
|
||||
const { result } = render();
|
||||
expect(result.current.isFavourite).toBe(true);
|
||||
});
|
||||
|
||||
it("should not be able to copy the room link", () => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue("userId");
|
||||
const { result } = render();
|
||||
expect(result.current.canCopyRoomLink).toBe(false);
|
||||
});
|
||||
|
||||
it("should be able to mark as read", () => {
|
||||
// Add a notification
|
||||
mocked(useUnreadNotifications).mockReturnValue({
|
||||
symbol: null,
|
||||
count: 1,
|
||||
level: NotificationLevel.Notification,
|
||||
});
|
||||
const { result } = render();
|
||||
expect(result.current.canMarkAsRead).toBe(true);
|
||||
expect(result.current.canMarkAsUnread).toBe(false);
|
||||
});
|
||||
|
||||
// Actions
|
||||
|
||||
it("should mark as read", () => {
|
||||
const { result } = render();
|
||||
result.current.markAsRead(new Event("click"));
|
||||
expect(mocked(clearRoomNotification)).toHaveBeenCalledWith(room, matrixClient);
|
||||
});
|
||||
|
||||
it("should mark as unread", () => {
|
||||
const { result } = render();
|
||||
result.current.markAsUnread(new Event("click"));
|
||||
expect(mocked(setMarkedUnreadState)).toHaveBeenCalledWith(room, matrixClient, true);
|
||||
});
|
||||
|
||||
it("should tag a room as favourite", () => {
|
||||
const { result } = render();
|
||||
result.current.toggleFavorite(new Event("click"));
|
||||
expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.Favourite);
|
||||
});
|
||||
|
||||
it("should tag a room as low priority", () => {
|
||||
const { result } = render();
|
||||
result.current.toggleLowPriority();
|
||||
expect(mocked(tagRoom)).toHaveBeenCalledWith(room, DefaultTagID.LowPriority);
|
||||
});
|
||||
|
||||
it("should dispatch invite action", () => {
|
||||
const { result } = render();
|
||||
result.current.invite(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_invite",
|
||||
roomId: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch a copy room action", () => {
|
||||
const { result } = render();
|
||||
result.current.copyRoomLink(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "copy_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch forget room action", () => {
|
||||
// forget room is only available for archived rooms
|
||||
room.tags = { [DefaultTagID.Archived]: { order: 0 } };
|
||||
|
||||
const { result } = render();
|
||||
result.current.leaveRoom(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "forget_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch leave room action", () => {
|
||||
const { result } = render();
|
||||
result.current.leaveRoom(new Event("click"));
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "leave_room",
|
||||
room_id: room.roomId,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { renderHook } from "jest-matrix-react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { useRoomListItemViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel";
|
||||
import { createTestClient, mkStubRoom } from "../../../../test-utils";
|
||||
import { hasAccessToOptionsMenu } from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
describe("RoomListItemViewModel", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
const matrixClient = createTestClient();
|
||||
room = mkStubRoom("roomId", "roomName", matrixClient);
|
||||
});
|
||||
|
||||
it("should dispatch view room action on openRoom", async () => {
|
||||
const { result: vm } = renderHook(() => useRoomListItemViewModel(room));
|
||||
|
||||
const fn = jest.spyOn(dispatcher, "dispatch");
|
||||
vm.current.openRoom();
|
||||
expect(fn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: "RoomList",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show hover menu if user has access to options menu", async () => {
|
||||
mocked(hasAccessToOptionsMenu).mockReturnValue(true);
|
||||
const { result: vm } = renderHook(() => useRoomListItemViewModel(room));
|
||||
expect(vm.current.showHoverMenu).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -12,8 +12,6 @@ import RoomListStoreV3 from "../../../../../src/stores/room-list-v3/RoomListStor
|
||||
import { mkStubRoom } from "../../../../test-utils";
|
||||
import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/SlidingRoomListStore";
|
||||
import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { SecondaryFilters } from "../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||
import { SortingAlgorithm } from "../../../../../src/stores/room-list-v3/skip-list/sorters";
|
||||
@@ -56,21 +54,6 @@ describe("RoomListViewModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch view room action on openRoom", async () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
const fn = jest.spyOn(dispatcher, "dispatch");
|
||||
act(() => vm.current.openRoom(rooms[7].roomId));
|
||||
expect(fn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: Action.ViewRoom,
|
||||
room_id: rooms[7].roomId,
|
||||
metricsTrigger: "RoomList",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Filters", () => {
|
||||
it("should provide list of available filters", () => {
|
||||
mockAndCreateRooms();
|
||||
|
||||
Reference in New Issue
Block a user