diff --git a/package.json b/package.json index 974be1666e7..ae1a2d1f4e1 100644 --- a/package.json +++ b/package.json @@ -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.6.4", + "@vector-im/compound-web": "^7.7.2", "@vector-im/matrix-wysiwyg": "2.38.2", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index ff06eda0aa5..493ed0d1aba 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -11,6 +11,7 @@ import { test, expect } from "../../../element-web-test"; test.describe("Room list", () => { test.use({ + displayName: "Alice", labsFlags: ["feature_new_room_list"], }); @@ -47,4 +48,33 @@ test.describe("Room list", () => { 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(); + }); }); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png new file mode 100644 index 00000000000..45d5588733d Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png new file mode 100644 index 00000000000..0501bf1e4c8 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 89948acd20f..1b4dc79296e 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -271,8 +271,9 @@ @import "./views/right_panel/_WidgetCard.pcss"; @import "./views/room_settings/_AliasSettings.pcss"; @import "./views/rooms/RoomListPanel/_RoomList.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListCell.pcss"; @import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss"; +@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss"; +@import "./views/rooms/RoomListPanel/_RoomListItemView.pcss"; @import "./views/rooms/RoomListPanel/_RoomListPanel.pcss"; @import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss"; @import "./views/rooms/RoomListPanel/_RoomListSearch.pcss"; diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss new file mode 100644 index 00000000000..cabd9b2d205 --- /dev/null +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss @@ -0,0 +1,12 @@ +/* + * 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. + */ + +.mx_RoomListItemMenuView { + svg { + fill: var(--cpd-color-icon-primary); + } +} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListCell.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss similarity index 79% rename from res/css/views/rooms/RoomListPanel/_RoomListCell.pcss rename to res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss index 812145a73e3..e53ba3dc79e 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListCell.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss @@ -6,7 +6,7 @@ */ /** - * The RoomCell has the following structure: + * The RoomListItemView has the following structure: * button----------------------------------------| * | <-12px-> container--------------------------| * | | room avatar <-12px-> content-----| @@ -14,19 +14,20 @@ * | | | ----------| <-- border * |---------------------------------------------| */ -.mx_RoomListCell { +.mx_RoomListItemView { all: unset; &:hover { background-color: var(--cpd-color-bg-action-secondary-hovered); } - .mx_RoomListCell_container { + .mx_RoomListItemView_container { padding-left: var(--cpd-space-3x); font: var(--cpd-font-body-md-regular); height: 100%; - .mx_RoomListCell_content { + .mx_RoomListItemView_content { + padding-right: var(--cpd-space-3x); height: 100%; flex: 1; /* The border is only under the room name and the future hover menu */ @@ -42,3 +43,7 @@ } } } + +.mx_RoomListItemView_menu_open { + background-color: var(--cpd-color-bg-action-secondary-hovered); +} diff --git a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx new file mode 100644 index 00000000000..6b089495a0b --- /dev/null +++ b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx @@ -0,0 +1,180 @@ +/* + * 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 { useCallback } from "react"; +import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; + +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; +import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; +import { hasAccessToOptionsMenu } from "./utils"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import { DefaultTagID } from "../../../stores/room-list/models"; +import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; +import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../../settings/UIFeature"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications"; +import PosthogTrackers from "../../../PosthogTrackers"; +import { tagRoom } from "../../../utils/room/tagRoom"; + +export interface RoomListItemMenuViewState { + /** + * Whether the more options menu should be shown. + */ + showMoreOptionsMenu: boolean; + /** + * Whether the room is a favourite room. + */ + isFavourite: boolean; + /** + * Can invite other user's in the room. + */ + canInvite: boolean; + /** + * Can copy the room link. + */ + canCopyRoomLink: boolean; + /** + * Can mark the room as read. + */ + canMarkAsRead: boolean; + /** + * Can mark the room as unread. + */ + canMarkAsUnread: boolean; + /** + * Mark the room as read. + * @param evt + */ + markAsRead: (evt: Event) => void; + /** + * Mark the room as unread. + * @param evt + */ + markAsUnread: (evt: Event) => void; + /** + * Toggle the room as favourite. + * @param evt + */ + toggleFavorite: (evt: Event) => void; + /** + * Toggle the room as low priority. + */ + toggleLowPriority: () => void; + /** + * Invite other users in the room. + * @param evt + */ + invite: (evt: Event) => void; + /** + * Copy the room link in the clipboard. + * @param evt + */ + copyRoomLink: (evt: Event) => void; + /** + * Leave the room. + * @param evt + */ + leaveRoom: (evt: Event) => void; +} + +export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewState { + const matrixClient = useMatrixClientContext(); + const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); + const { level: notificationLevel } = useUnreadNotifications(room); + + const showMoreOptionsMenu = hasAccessToOptionsMenu(room); + + const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); + const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]); + const isArchived = Boolean(roomTags[DefaultTagID.Archived]); + + const canMarkAsRead = notificationLevel > NotificationLevel.None; + const canMarkAsUnread = !canMarkAsRead && !isArchived; + + const canInvite = + room.canInvite(matrixClient.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers); + const canCopyRoomLink = !isDm; + + // Actions + + const markAsRead = useCallback( + async (evt: Event): Promise => { + await clearRoomNotification(room, matrixClient); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", evt); + }, + [room, matrixClient], + ); + + const markAsUnread = useCallback( + async (evt: Event): Promise => { + await setMarkedUnreadState(room, matrixClient, true); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", evt); + }, + [room, matrixClient], + ); + + const toggleFavorite = useCallback( + (evt: Event): void => { + tagRoom(room, DefaultTagID.Favourite); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); + }, + [room], + ); + + const toggleLowPriority = useCallback((): void => tagRoom(room, DefaultTagID.LowPriority), [room]); + + const invite = useCallback( + (evt: Event): void => { + dispatcher.dispatch({ + action: "view_invite", + roomId: room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", evt); + }, + [room], + ); + + const copyRoomLink = useCallback( + (evt: Event): void => { + dispatcher.dispatch({ + action: "copy_room", + room_id: room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); + }, + [room], + ); + + const leaveRoom = useCallback( + (evt: Event): void => { + dispatcher.dispatch({ + action: isArchived ? "forget_room" : "leave_room", + room_id: room.roomId, + }); + PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", evt); + }, + [room, isArchived], + ); + + return { + showMoreOptionsMenu, + isFavourite, + canInvite, + canCopyRoomLink, + canMarkAsRead, + canMarkAsUnread, + markAsRead, + markAsUnread, + toggleFavorite, + toggleLowPriority, + invite, + copyRoomLink, + leaveRoom, + }; +} diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx new file mode 100644 index 00000000000..9e38e6e8d89 --- /dev/null +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -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 { useCallback } 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"; + +export interface RoomListItemViewState { + /** + * Whether the hover menu should be shown. + */ + showHoverMenu: boolean; + /** + * Open the room having given roomId. + */ + openRoom: () => void; +} + +/** + * View model for the room list item + * @see {@link RoomListItemViewState} for more information about what this view model returns. + */ +export function useRoomListItemViewModel(room: Room): RoomListItemViewState { + // incoming: Check notification menu rights + const showHoverMenu = hasAccessToOptionsMenu(room); + + // Actions + + const openRoom = useCallback((): void => { + dispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + metricsTrigger: "RoomList", + }); + }, [room]); + + return { + showHoverMenu, + openRoom, + }; +} diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx index fb827c48891..09fe68e5a21 100644 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { useCallback } from "react"; - import type { Room } from "matrix-js-sdk/src/matrix"; -import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms"; export interface RoomListViewState { @@ -18,12 +13,6 @@ export interface RoomListViewState { * A list of rooms to be displayed in the left panel. */ rooms: Room[]; - - /** - * Open the room having given roomId. - */ - openRoom: (roomId: string) => void; - /** * A list of objects that provide the view enough information * to render primary room filters. @@ -48,17 +37,8 @@ export interface RoomListViewState { export function useRoomListViewModel(): RoomListViewState { const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms(); - const openRoom = useCallback((roomId: string): void => { - dispatcher.dispatch({ - action: Action.ViewRoom, - room_id: roomId, - metricsTrigger: "RoomList", - }); - }, []); - return { rooms, - openRoom, primaryFilters, activateSecondaryFilter, activeSecondaryFilter, diff --git a/src/components/viewmodels/roomlist/utils.ts b/src/components/viewmodels/roomlist/utils.ts new file mode 100644 index 00000000000..3886d0e3b07 --- /dev/null +++ b/src/components/viewmodels/roomlist/utils.ts @@ -0,0 +1,25 @@ +/* + * 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 { type Room, KnownMembership } from "matrix-js-sdk/src/matrix"; + +import { isKnockDenied } from "../../../utils/membership"; +import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; +import { UIComponent } from "../../../settings/UIFeature"; + +/** + * Check if the user has access to the options menu. + * @param room + */ +export function hasAccessToOptionsMenu(room: Room): boolean { + return ( + room.getMyMembership() === KnownMembership.Invite || + (room.getMyMembership() !== KnownMembership.Knock && + !isKnockDenied(room) && + shouldShowComponent(UIComponent.RoomOptionsMenu)) + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomList.tsx b/src/components/views/rooms/RoomListPanel/RoomList.tsx index 3645a72bb91..006d1b97320 100644 --- a/src/components/views/rooms/RoomListPanel/RoomList.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomList.tsx @@ -10,7 +10,7 @@ import { AutoSizer, List, type ListRowProps } from "react-virtualized"; import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; import { _t } from "../../../../languageHandler"; -import { RoomListCell } from "./RoomListCell"; +import { RoomListItemView } from "./RoomListItemView"; interface RoomListProps { /** @@ -22,12 +22,10 @@ interface RoomListProps { /** * A virtualized list of rooms. */ -export function RoomList({ vm: { rooms, openRoom } }: RoomListProps): JSX.Element { +export function RoomList({ vm: { rooms } }: RoomListProps): JSX.Element { const roomRendererMemoized = useCallback( - ({ key, index, style }: ListRowProps) => ( - openRoom(rooms[index].roomId)} /> - ), - [rooms, openRoom], + ({ key, index, style }: ListRowProps) => , + [rooms], ); // The first div is needed to make the virtualized list take all the remaining space and scroll correctly diff --git a/src/components/views/rooms/RoomListPanel/RoomListCell.tsx b/src/components/views/rooms/RoomListPanel/RoomListCell.tsx deleted file mode 100644 index a5e9cc5df23..00000000000 --- a/src/components/views/rooms/RoomListPanel/RoomListCell.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 React, { type JSX } from "react"; -import { type Room } from "matrix-js-sdk/src/matrix"; - -import { _t } from "../../../../languageHandler"; -import { Flex } from "../../../utils/Flex"; -import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar"; - -interface RoomListCellProps extends React.HTMLAttributes { - /** - * The room to display - */ - room: Room; -} - -/** - * A cell in the room list - */ -export function RoomListCell({ room, ...props }: RoomListCellProps): JSX.Element { - return ( - - ); -} diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx new file mode 100644 index 00000000000..ca08bf698cb --- /dev/null +++ b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx @@ -0,0 +1,154 @@ +/* + * 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 React, { type ComponentProps, forwardRef, type JSX, useState } from "react"; +import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem, Tooltip } from "@vector-im/compound-web"; +import MarkAsReadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-read"; +import MarkAsUnreadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-unread"; +import FavouriteIcon from "@vector-im/compound-design-tokens/assets/web/icons/favourite"; +import ArrowDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/arrow-down"; +import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add"; +import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link"; +import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave"; +import OverflowIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; +import { type Room } from "matrix-js-sdk/src/matrix"; + +import { _t } from "../../../../languageHandler"; +import { Flex } from "../../../utils/Flex"; +import { + type RoomListItemMenuViewState, + useRoomListItemMenuViewModel, +} from "../../../viewmodels/roomlist/RoomListItemMenuViewModel"; + +interface RoomListItemMenuViewProps { + /** + * The room to display the menu for. + */ + room: Room; + /** + * Set the menu open state. + * @param isOpen + */ + setMenuOpen: (isOpen: boolean) => void; +} + +/** + * A view for the room list item menu. + */ +export function RoomListItemMenuView({ room, setMenuOpen }: RoomListItemMenuViewProps): JSX.Element { + const vm = useRoomListItemMenuViewModel(room); + + return ( + + {vm.showMoreOptionsMenu && } + + ); +} + +interface MoreOptionsMenuProps { + /** + * The view model state for the menu. + */ + vm: RoomListItemMenuViewState; + /** + * Set the menu open state. + * @param isOpen + */ + setMenuOpen: (isOpen: boolean) => void; +} + +/** + * The more options menu for the room list item. + */ +function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + { + setOpen(isOpen); + setMenuOpen(isOpen); + }} + title={_t("room_list|room|more_options")} + showTitle={false} + align="start" + trigger={} + > + {vm.canMarkAsRead && ( + evt.stopPropagation()} + /> + )} + {vm.canMarkAsUnread && ( + evt.stopPropagation()} + /> + )} + evt.stopPropagation()} + /> + evt.stopPropagation()} + /> + {vm.canInvite && ( + evt.stopPropagation()} + /> + )} + {vm.canCopyRoomLink && ( + evt.stopPropagation()} + /> + )} + + evt.stopPropagation()} + /> + + ); +} + +interface MoreOptionsButtonProps extends ComponentProps {} + +/** + * A button to trigger the more options menu. + */ +export const MoreOptionsButton = forwardRef( + function MoreOptionsButton(props, ref) { + return ( + + + + + + ); + }, +); diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx new file mode 100644 index 00000000000..20173e324e7 --- /dev/null +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -0,0 +1,76 @@ +/* + * 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 React, { type JSX, useState } from "react"; +import { type Room } from "matrix-js-sdk/src/matrix"; +import classNames from "classnames"; + +import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel"; +import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar"; +import { Flex } from "../../../utils/Flex"; +import { _t } from "../../../../languageHandler"; +import { RoomListItemMenuView } from "./RoomListItemMenuView"; + +interface RoomListItemViewPropsProps extends React.HTMLAttributes { + /** + * The room to display + */ + room: Room; +} + +/** + * An item in the room list + */ +export function RoomListItemView({ room, ...props }: RoomListItemViewPropsProps): JSX.Element { + const vm = useRoomListItemViewModel(room); + + const [isHover, setIsHover] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + // The compound menu in RoomListItemMenuView needs to be rendered when the hover menu is shown + // Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned + const showHoverDecoration = (isMenuOpen || isHover) && vm.showHoverMenu; + + return ( + + ); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 55702791159..920e19ac826 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2112,6 +2112,14 @@ "other": "Currently joining %(count)s rooms" }, "list_title": "Room list", + "more_options": { + "copy_link": "Copy room link", + "favourited": "Favourited", + "leave_room": "Leave room", + "low_priority": "Low priority", + "mark_read": "Mark as read", + "mark_unread": "Mark as unread" + }, "notification_options": "Notification options", "open_space_menu": "Open space menu", "primary_filters": "Room list filters", @@ -2120,6 +2128,7 @@ "other": "Currently removing messages in %(count)s rooms" }, "room": { + "more_options": "More Options", "open_room": "Open room %(roomName)s" }, "show_less": "Show less", diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 321ff8b27e0..1f6351b7b9e 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -606,7 +606,7 @@ export function mkStubRoom( getState: (): RoomState | undefined => undefined, } as unknown as EventTimeline; return { - canInvite: jest.fn(), + canInvite: jest.fn().mockReturnValue(false), client, findThreadForEvent: jest.fn(), createThreadsTimelineSets: jest.fn().mockReturnValue(new Promise(() => {})), diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx new file mode 100644 index 00000000000..89dd644208a --- /dev/null +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx @@ -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, + }); + }); +}); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx new file mode 100644 index 00000000000..2854c433e7b --- /dev/null +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx @@ -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); + }); +}); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 055feb84e62..985a5d7f92e 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -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"; @@ -49,21 +47,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(); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx index e720798f040..11725c7a760 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx @@ -7,8 +7,7 @@ import React from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; -import { render, screen, waitFor } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; +import { render } from "jest-matrix-react"; import { mkRoom, stubClient } from "../../../../../test-utils"; import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; @@ -30,7 +29,6 @@ describe("", () => { const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`)); vm = { rooms, - openRoom: jest.fn(), primaryFilters: [], activateSecondaryFilter: () => {}, activeSecondaryFilter: SecondaryFilters.AllActivity, @@ -45,15 +43,4 @@ describe("", () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); - - it("should open the room", async () => { - const user = userEvent.setup(); - - render(); - await waitFor(async () => { - expect(screen.getByRole("gridcell", { name: "Open room room9" })).toBeVisible(); - await user.click(screen.getByRole("gridcell", { name: "Open room room9" })); - }); - expect(vm.openRoom).toHaveBeenCalledWith(vm.rooms[9].roomId); - }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListCell-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListCell-test.tsx deleted file mode 100644 index 3bbde9fb929..00000000000 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListCell-test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 React from "react"; -import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; -import { render, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { mkRoom, stubClient } from "../../../../../test-utils"; -import { RoomListCell } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListCell"; -import DMRoomMap from "../../../../../../src/utils/DMRoomMap"; - -describe("", () => { - let matrixClient: MatrixClient; - let room: Room; - - beforeEach(() => { - matrixClient = stubClient(); - room = mkRoom(matrixClient, "room1"); - - DMRoomMap.makeShared(matrixClient); - jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null); - }); - - test("should render a room cell", () => { - const onClick = jest.fn(); - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); - - test("should call onClick when clicked", async () => { - const user = userEvent.setup(); - - const onClick = jest.fn(); - render(); - - await user.click(screen.getByRole("button", { name: `Open room ${room.name}` })); - expect(onClick).toHaveBeenCalled(); - }); -}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx new file mode 100644 index 00000000000..de1d37ed088 --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx @@ -0,0 +1,110 @@ +/* + * 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 React from "react"; +import { mocked } from "jest-mock"; +import { render, screen } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; + +import { + type RoomListItemMenuViewState, + useRoomListItemMenuViewModel, +} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel"; +import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { mkRoom, stubClient } from "../../../../../test-utils"; +import { RoomListItemMenuView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemMenuView"; + +jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel", () => ({ + useRoomListItemMenuViewModel: jest.fn(), +})); + +describe("", () => { + const defaultValue: RoomListItemMenuViewState = { + showMoreOptionsMenu: true, + isFavourite: true, + canInvite: true, + canMarkAsUnread: true, + canMarkAsRead: true, + canCopyRoomLink: true, + copyRoomLink: jest.fn(), + markAsUnread: jest.fn(), + markAsRead: jest.fn(), + leaveRoom: jest.fn(), + toggleLowPriority: jest.fn(), + toggleFavorite: jest.fn(), + invite: jest.fn(), + }; + + let matrixClient: MatrixClient; + let room: Room; + + beforeEach(() => { + mocked(useRoomListItemMenuViewModel).mockReturnValue(defaultValue); + matrixClient = stubClient(); + room = mkRoom(matrixClient, "room1"); + }); + + function renderMenu(setMenuOpen = jest.fn()) { + return render(); + } + + it("should render the more options menu", () => { + const { asFragment } = renderMenu(); + expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should not render the more options menu when showMoreOptionsMenu is false", () => { + mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showMoreOptionsMenu: false }); + renderMenu(); + expect(screen.queryByRole("button", { name: "More Options" })).toBeNull(); + }); + + it("should call setMenuOpen when the menu is opened", async () => { + const user = userEvent.setup(); + const setMenuOpen = jest.fn(); + renderMenu(setMenuOpen); + + await user.click(screen.getByRole("button", { name: "More Options" })); + expect(setMenuOpen).toHaveBeenCalledWith(true); + }); + + it("should display all the buttons and have the actions linked", async () => { + const user = userEvent.setup(); + renderMenu(); + + const openMenu = screen.getByRole("button", { name: "More Options" }); + await user.click(openMenu); + + await user.click(screen.getByRole("menuitem", { name: "Mark as read" })); + expect(defaultValue.markAsRead).toHaveBeenCalled(); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "Mark as unread" })); + expect(defaultValue.markAsUnread).toHaveBeenCalled(); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitemcheckbox", { name: "Favourited" })); + expect(defaultValue.toggleFavorite).toHaveBeenCalled(); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "Low priority" })); + expect(defaultValue.toggleLowPriority).toHaveBeenCalled(); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "Invite" })); + expect(defaultValue.invite).toHaveBeenCalled(); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "Copy room link" })); + expect(defaultValue.copyRoomLink).toHaveBeenCalled(); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "Leave room" })); + expect(defaultValue.leaveRoom).toHaveBeenCalled(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx new file mode 100644 index 00000000000..3023f9a9a79 --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx @@ -0,0 +1,68 @@ +/* + * 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 React from "react"; +import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; +import { render, screen, waitFor } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; + +import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils"; +import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView"; +import DMRoomMap from "../../../../../../src/utils/DMRoomMap"; +import { + type RoomListItemViewState, + useRoomListItemViewModel, +} from "../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel"; + +jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemViewModel", () => ({ + useRoomListItemViewModel: jest.fn(), +})); + +describe("", () => { + const defaultValue: RoomListItemViewState = { + openRoom: jest.fn(), + showHoverMenu: false, + }; + let matrixClient: MatrixClient; + let room: Room; + + beforeEach(() => { + mocked(useRoomListItemViewModel).mockReturnValue(defaultValue); + matrixClient = stubClient(); + room = mkRoom(matrixClient, "room1"); + + DMRoomMap.makeShared(matrixClient); + jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null); + }); + + test("should render a room item", () => { + const onClick = jest.fn(); + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + test("should call openRoom when clicked", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: `Open room ${room.name}` })); + expect(defaultValue.openRoom).toHaveBeenCalled(); + }); + + test("should hover decoration if hovered", async () => { + mocked(useRoomListItemViewModel).mockReturnValue({ ...defaultValue, showHoverMenu: true }); + + const user = userEvent.setup(); + render(, withClientContextRenderOptions(matrixClient)); + const listItem = screen.getByRole("button", { name: `Open room ${room.name}` }); + expect(screen.queryByRole("button", { name: "More Options" })).toBeNull(); + + await user.hover(listItem); + await waitFor(() => expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument()); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx index 3b1b29a5ff9..3500e039bf3 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx @@ -19,7 +19,6 @@ describe("", () => { beforeEach(() => { vm = { rooms: [], - openRoom: jest.fn(), primaryFilters: [ { name: "People", active: false, toggle: jest.fn() }, { name: "Rooms", active: true, toggle: jest.fn() }, diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap index 8d3559031ab..37f8a0364a6 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap @@ -24,13 +24,13 @@ exports[` should render a room list 1`] = ` > + + +`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListCell-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap similarity index 78% rename from test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListCell-test.tsx.snap rename to test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap index 3a28a1ad278..e63d9f91a63 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListCell-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render a room cell 1`] = ` +exports[` should render a room item 1`] = `