From f3dbe81ef490296629c76b2066bac2095a0a1004 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 14 Mar 2025 17:22:45 +0100 Subject: [PATCH] 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 --- package.json | 2 +- .../room-list-panel/room-list.spec.ts | 30 +++ .../room-list-item-hover-linux.png | Bin 0 -> 2293 bytes ...room-list-item-open-more-options-linux.png | Bin 0 -> 81877 bytes res/css/_components.pcss | 3 +- .../RoomListPanel/_RoomListItemMenuView.pcss | 12 ++ ...omListCell.pcss => _RoomListItemView.pcss} | 13 +- .../roomlist/RoomListItemMenuViewModel.tsx | 180 ++++++++++++++++++ .../roomlist/RoomListItemViewModel.tsx | 49 +++++ .../viewmodels/roomlist/RoomListViewModel.tsx | 20 -- src/components/viewmodels/roomlist/utils.ts | 25 +++ .../views/rooms/RoomListPanel/RoomList.tsx | 10 +- .../rooms/RoomListPanel/RoomListCell.tsx | 44 ----- .../RoomListPanel/RoomListItemMenuView.tsx | 154 +++++++++++++++ .../rooms/RoomListPanel/RoomListItemView.tsx | 76 ++++++++ src/i18n/strings/en_EN.json | 9 + test/test-utils/test-utils.ts | 2 +- .../RoomListItemMenuViewModel-test.tsx | 173 +++++++++++++++++ .../roomlist/RoomListItemViewModel-test.tsx | 49 +++++ .../roomlist/RoomListViewModel-test.tsx | 17 -- .../rooms/RoomListPanel/RoomList-test.tsx | 15 +- .../rooms/RoomListPanel/RoomListCell-test.tsx | 44 ----- .../RoomListItemMenuView-test.tsx | 110 +++++++++++ .../RoomListPanel/RoomListItemView-test.tsx | 68 +++++++ .../RoomListPrimaryFilters-test.tsx | 1 - .../__snapshots__/RoomList-test.tsx.snap | 80 ++++---- .../RoomListItemMenuView-test.tsx.snap | 42 ++++ ...sx.snap => RoomListItemView-test.tsx.snap} | 10 +- yarn.lock | 8 +- 29 files changed, 1044 insertions(+), 202 deletions(-) create mode 100644 playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png create mode 100644 playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png create mode 100644 res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss rename res/css/views/rooms/RoomListPanel/{_RoomListCell.pcss => _RoomListItemView.pcss} (79%) create mode 100644 src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx create mode 100644 src/components/viewmodels/roomlist/RoomListItemViewModel.tsx create mode 100644 src/components/viewmodels/roomlist/utils.ts delete mode 100644 src/components/views/rooms/RoomListPanel/RoomListCell.tsx create mode 100644 src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx create mode 100644 src/components/views/rooms/RoomListPanel/RoomListItemView.tsx create mode 100644 test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx create mode 100644 test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx delete mode 100644 test/unit-tests/components/views/rooms/RoomListPanel/RoomListCell-test.tsx create mode 100644 test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx create mode 100644 test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx create mode 100644 test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap rename test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/{RoomListCell-test.tsx.snap => RoomListItemView-test.tsx.snap} (78%) diff --git a/package.json b/package.json index acfeff4c48..6581f0c2e0 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 ff06eda0aa..493ed0d1ab 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 0000000000000000000000000000000000000000..45d5588733d88ba0b0061e92c2e7c436ab95195a GIT binary patch literal 2293 zcmaKuc|6mPAICqgwdBfI!yMDp+=ggdIcGVNMLAX&O3M)=V`7DJFH|ZS@^h3BD;;hl znq#4ejF4gIAd%+E&+qsD@AuE^{r>0mc)#C|=O3?ksuSK;QXDD{0Dz>u9qtqW2zl=G zhG5|xRSZaS*eOEcr);r6Rj=|q0DzhHI4hT^ydPs%TuyUUcKyt4{H~)0exj+V=3puu z=n6Slbg60zeZe!DJ)~MX5RL1wI#aG*WS4J2dOtL@1x6Rexjt7#r5H#Fy+XH_Wr(M^ z=4Dx?AS^rA*KO|}F6MDKbl%FJwdLlA<|oHdPFBCof;{*H}xpXEi%u!Qer4scRs2EQ)M?vEG{b;vI1I@iDM^4xmj zoBHw6tD@A&R7`5nxpUbkeV!S}nh68r5=MpZOAjd%oWmAJcO6*IIsnoe#bm4dWev*% zKS5|^@$&msTO5u{rB_LV@)7p=x2?y<{8m#21@{n+T&ud6Cf9$^50#$K~4^WQVhTK93X4+Iz=@WNb~e4K++4Qj6x zJYo$NbTbbt=^__~KLwj^yhQ~Zy`utq&B^KEk1)v1=4VGfmVkFFHVE2Y=Dd(4=t8h| z%ufZF@Kwr;;<2oAb52aLJ$yZ{*B>2quZ>!R;-Cp6@#xCQuS*DbN^Iqc!cS07XRacI z{-|*i*9${Q!OTrHG;}mI{lC55?C(EMdcpNKRz-(~ejD0!R=4HP%ped5$UYU!vu&RO z-}g;>fX@BKr~Rv9Ed88iYIu?}C^E5@?olxI=R;uYJ-x5l_$xc5LEiJ9Fv9Z}c?>viGB8ddb>^PKj0$cdyW7&Yc zeX6XzU{n#HY&`<(x3$&d8&hd@lVqbb^7t-7!_^m8CByzD6_0rr=8r6(n>*-iB??4`?8=0{TmK zd+N*YZ}&4&FNSw>v>CY`YH}@oB84pJAKRzuLqyx@N|Njfccvb}fK~nLF2#lx@q4Bx zwIKwfmmVXbpL^ zhML*sudQ!hH29UE>qa0vDVP95)01QZV>fG?n|#krlFIfY;%X|wU2;4j3nA+nF$4KlxF{|t=X&Dc?d@zoQkA@TBDk={Fn(=xk6c==v$HdEL=Fl`^BsN}Fz~qJD~k=v+?V=g zP9(H2FV8VJSb7KR<(lceeiXggGG;@(NXplgI9kKRt32;XF}F! z{q3n+;X>Zt6Ua!p>1jW+Xn}viM9O_Wsjjj0^?ji@`tUXOC2t*`-n0xu3*02Y5rLJ={G-uL<3s(CEXRsL5C{}4wL}|4NT^2- zz9up!8mQh8t`U)uk6oZAOD#73b@%unJ3W`vtu0Dx#>w{1ga!q%RCx1~i;E|DUVx=j zWF#UVUpg?)J)@LTbIaQQvD%8P|A^PCTZk3eTU-atsMyIEk6*7LkcBil-IZESPdg5W zvrj%S@Qrg^H#fV@vnhUFTU$~q==@w4o&fi?-f~TlN#;mNNkJM?F`js<)&d4YPS!pu z-;;sTRe0oyLZaa5JU z4Xw|n)ITk0Y3}zGz8GIr?pi&Z*V$m)vgQDj-!`JK6?P>{0-^h`EOPvZVo!>+$xVCd zw8YpcKmv|iY@}S;!mQm^x=Xc)2$=rTs3sn_?Sb3gUHX)M;hEM$U4vmlppEh?6CnT( zhl<0=Rx8z-fg2B-dGga4w!(mCN4&}M$D6gjg+!MK)60#EPgnf^n+`ln&CrDJ_udAa zgBg|X+ibt(90JQO2@}?2;FNWePe#U zf6;?Dw0hDA6(*UnyzT!-*iIAx>D|dpZ>x;5F5|(D2BvsVX|7B^BAqCx_A^bFa#s{F zh{F7i<2Hv(y?s-!lgwnT3cWC7r7`ZQO47lA#5H{4^>nUJ-RT(C(u&?p(`;r@M}b9} z`S793A^uM&5~gXYaa1v}LI4n8;$flxFZQ*`OADt!ZI-jPJD2=|J#20Ip^Zir?ffx- M{V_bQ3hQ(I--uU5+W-In literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0501bf1e4c848e3338b9b4f15e22ee9d21b2d20a GIT binary patch literal 81877 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ɦsT^-ew6 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 FKLA9!cm4nX literal 0 HcmV?d00001 diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 89948acd20..1b4dc79296 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 0000000000..cabd9b2d20 --- /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 812145a73e..e53ba3dc79 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 0000000000..6b089495a0 --- /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 0000000000..9e38e6e8d8 --- /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 c195613d7d..b1b39e7f0c 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"; import { type SortOption, useSorter } from "./useSorter"; @@ -19,12 +14,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. @@ -60,17 +49,8 @@ export function useRoomListViewModel(): RoomListViewState { const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms(); const { activeSortOption, sort } = useSorter(); - 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 0000000000..3886d0e3b0 --- /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 3645a72bb9..006d1b9732 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 a5e9cc5df2..0000000000 --- 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 0000000000..ca08bf698c --- /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 0000000000..20173e324e --- /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 d85b15d25b..34e0e98be3 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 321ff8b27e..1f6351b7b9 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 0000000000..89dd644208 --- /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 0000000000..2854c433e7 --- /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 e1e2f6ac57..99f1483d4a 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"; 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(); 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 cd6ed29e27..11764df7b7 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"; @@ -31,7 +30,6 @@ describe("", () => { const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`)); vm = { rooms, - openRoom: jest.fn(), primaryFilters: [], activateSecondaryFilter: () => {}, activeSecondaryFilter: SecondaryFilters.AllActivity, @@ -48,15 +46,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 3bbde9fb92..0000000000 --- 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 0000000000..de1d37ed08 --- /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 0000000000..3023f9a9a7 --- /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 3bb1968371..a3e60f765d 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx @@ -20,7 +20,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 8d3559031a..37f8a0364a 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 3a28a1ad27..e63d9f91a6 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`] = `