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 ebca899a13..ccf9489e8c 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 @@ -85,6 +85,43 @@ test.describe("Room list", () => { await expect(roomItem).not.toBeVisible(); }); + test("should open the notification 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"); + let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" }); + await roomItemMenu.click(); + + // Default settings should be selected + await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png"); + + // It should make the room muted + await page.getByRole("menuitem", { name: "Mute room" }).click(); + + // Remove hover on the room list item + await roomListView.hover(); + + // The room decoration should have the muted icon + await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); + + await roomItem.hover(); + // On hover, the room should show the muted icon + await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png"); + + roomItemMenu = roomItem.getByRole("button", { name: "Notification options" }); + await roomItemMenu.click(); + // The Mute room option should be selected + await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true"); + await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png"); + }); + test("should scroll to the current room", async ({ page, app, user }) => { const roomListView = getRoomList(page); await roomListView.hover(); 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 index eccf48a528..b0cab67c66 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png 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-hover-silent-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png new file mode 100644 index 0000000000..a69315feff Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-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 index 40206cc22b..a1106b4dc3 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png 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/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png new file mode 100644 index 0000000000..d360a8e31d Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png new file mode 100644 index 0000000000..2ae4b2e417 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ diff --git a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx index 6b089495a0..997b515f27 100644 --- a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx @@ -11,7 +11,7 @@ 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 { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils"; import DMRoomMap from "../../../utils/DMRoomMap"; import { DefaultTagID } from "../../../stores/room-list/models"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; @@ -21,12 +21,18 @@ import dispatcher from "../../../dispatcher/dispatcher"; import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications"; import PosthogTrackers from "../../../PosthogTrackers"; import { tagRoom } from "../../../utils/room/tagRoom"; +import { RoomNotifState } from "../../../RoomNotifs"; +import { useNotificationState } from "../../../hooks/useRoomNotificationState"; export interface RoomListItemMenuViewState { /** * Whether the more options menu should be shown. */ showMoreOptionsMenu: boolean; + /** + * Whether the notification menu should be shown. + */ + showNotificationMenu: boolean; /** * Whether the room is a favourite room. */ @@ -47,6 +53,22 @@ export interface RoomListItemMenuViewState { * Can mark the room as unread. */ canMarkAsUnread: boolean; + /** + * Whether the notification is set to all messages. + */ + isNotificationAllMessage: boolean; + /** + * Whether the notification is set to all messages loud. + */ + isNotificationAllMessageLoud: boolean; + /** + * Whether the notification is set to mentions and keywords only. + */ + isNotificationMentionOnly: boolean; + /** + * Whether the notification is muted. + */ + isNotificationMute: boolean; /** * Mark the room as read. * @param evt @@ -81,6 +103,11 @@ export interface RoomListItemMenuViewState { * @param evt */ leaveRoom: (evt: Event) => void; + /** + * Set the room notification state. + * @param state + */ + setRoomNotifState: (state: RoomNotifState) => void; } export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewState { @@ -88,12 +115,13 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt 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 showMoreOptionsMenu = hasAccessToOptionsMenu(room); + const showNotificationMenu = hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived); + const canMarkAsRead = notificationLevel > NotificationLevel.None; const canMarkAsUnread = !canMarkAsRead && !isArchived; @@ -101,6 +129,12 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt room.canInvite(matrixClient.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers); const canCopyRoomLink = !isDm; + const [roomNotifState, setRoomNotifState] = useNotificationState(room); + const isNotificationAllMessage = roomNotifState === RoomNotifState.AllMessages; + const isNotificationAllMessageLoud = roomNotifState === RoomNotifState.AllMessagesLoud; + const isNotificationMentionOnly = roomNotifState === RoomNotifState.MentionsOnly; + const isNotificationMute = roomNotifState === RoomNotifState.Mute; + // Actions const markAsRead = useCallback( @@ -164,11 +198,16 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt return { showMoreOptionsMenu, + showNotificationMenu, isFavourite, canInvite, canCopyRoomLink, canMarkAsRead, canMarkAsUnread, + isNotificationAllMessage, + isNotificationAllMessageLoud, + isNotificationMentionOnly, + isNotificationMute, markAsRead, markAsUnread, toggleFavorite, @@ -176,5 +215,6 @@ export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewSt invite, copyRoomLink, leaveRoom, + setRoomNotifState, }; } diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index 1d5d9aba11..d1c9fef4ab 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -6,15 +6,18 @@ */ import { useCallback, useMemo } from "react"; -import { type Room } from "matrix-js-sdk/src/matrix"; +import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import dispatcher from "../../../dispatcher/dispatcher"; import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; -import { hasAccessToOptionsMenu } from "./utils"; +import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils"; import { _t } from "../../../languageHandler"; import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; +import { DefaultTagID } from "../../../stores/room-list/models"; export interface RoomListItemViewState { /** @@ -40,8 +43,12 @@ export interface RoomListItemViewState { * @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); + const matrixClient = useMatrixClientContext(); + const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); + const isArchived = Boolean(roomTags[DefaultTagID.Archived]); + + const showHoverMenu = + hasAccessToOptionsMenu(room) || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived); const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); const a11yLabel = getA11yLabel(room, notificationState); diff --git a/src/components/viewmodels/roomlist/utils.ts b/src/components/viewmodels/roomlist/utils.ts index 6220c3b961..dfa20e0d1c 100644 --- a/src/components/viewmodels/roomlist/utils.ts +++ b/src/components/viewmodels/roomlist/utils.ts @@ -27,6 +27,16 @@ export function hasAccessToOptionsMenu(room: Room): boolean { ); } +/** + * Check if the user has access to the notification menu. + * @param room + * @param isGuest + * @param isArchived + */ +export function hasAccessToNotificationMenu(room: Room, isGuest: boolean, isArchived: boolean): boolean { + return !isGuest && !isArchived && hasAccessToOptionsMenu(room); +} + /** * Create a room * @param space - The space to create the room in diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx index 00d23cc2d2..71e84984c5 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx @@ -15,6 +15,9 @@ import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user 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 NotificationIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-solid"; +import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-off-solid"; +import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; import { type Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../languageHandler"; @@ -23,6 +26,7 @@ import { type RoomListItemMenuViewState, useRoomListItemMenuViewModel, } from "../../../viewmodels/roomlist/RoomListItemMenuViewModel"; +import { RoomNotifState } from "../../../../RoomNotifs"; interface RoomListItemMenuViewProps { /** @@ -45,6 +49,7 @@ export function RoomListItemMenuView({ room, setMenuOpen }: RoomListItemMenuView return ( {vm.showMoreOptionsMenu && } + {vm.showNotificationMenu && } ); } @@ -152,3 +157,93 @@ export const MoreOptionsButton = forwardRef void; +} + +function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + { + setOpen(isOpen); + setMenuOpen(isOpen); + }} + title={_t("room_list|notification_options")} + showTitle={false} + align="start" + trigger={} + > + vm.setRoomNotifState(RoomNotifState.AllMessages)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationAllMessage && } + + vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationAllMessageLoud && } + + vm.setRoomNotifState(RoomNotifState.MentionsOnly)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationMentionOnly && } + + vm.setRoomNotifState(RoomNotifState.Mute)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationMute && } + + + ); +} + +interface NotificationButtonProps extends ComponentProps { + /** + * Whether the room is muted. + */ + isRoomMuted: boolean; +} + +/** + * A button to trigger the notification menu. + */ +export const NotificationButton = forwardRef(function MoreOptionsButton( + { isRoomMuted, ...props }, + ref, +) { + return ( + + + {isRoomMuted ? : } + + + ); +}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ce1c2e503b..98b0b7af96 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1677,6 +1677,7 @@ "class_global": "Global", "class_other": "Other", "default": "Default", + "default_settings": "Match default settings", "email_pusher_app_display_name": "Email Notifications", "enable_prompt_toast_description": "Enable desktop notifications", "enable_prompt_toast_title": "Notifications", @@ -1693,9 +1694,10 @@ "mark_all_read": "Mark all as read", "mentions_and_keywords": "@mentions & keywords", "mentions_and_keywords_description": "Get notified only with mentions and keywords as set up in your settings", - "mentions_keywords": "Mentions & keywords", + "mentions_keywords": "Mentions and keywords", "message_didnt_send": "Message didn't send. Click for info.", - "mute_description": "You won't get any notifications" + "mute_description": "You won't get any notifications", + "mute_room": "Mute room" }, "notifier": { "m.key.verification.request": "%(name)s is requesting verification" diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx index 89dd644208..ddb3cba609 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemMenuViewModel-test.tsx @@ -11,7 +11,10 @@ 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 { + hasAccessToNotificationMenu, + 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"; @@ -19,15 +22,22 @@ import { NotificationLevel } from "../../../../../src/stores/notifications/Notif import { clearRoomNotification, setMarkedUnreadState } from "../../../../../src/utils/notifications"; import { tagRoom } from "../../../../../src/utils/room/tagRoom"; import dispatcher from "../../../../../src/dispatcher/dispatcher"; +import { useNotificationState } from "../../../../../src/hooks/useRoomNotificationState"; +import { RoomNotifState } from "../../../../../src/RoomNotifs"; jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ hasAccessToOptionsMenu: jest.fn().mockReturnValue(false), + hasAccessToNotificationMenu: jest.fn().mockReturnValue(false), })); jest.mock("../../../../../src/hooks/useUnreadNotifications", () => ({ useUnreadNotifications: jest.fn(), })); +jest.mock("../../../../../src/hooks/useRoomNotificationState", () => ({ + useNotificationState: jest.fn(), +})); + jest.mock("../../../../../src/utils/notifications", () => ({ clearRoomNotification: jest.fn(), setMarkedUnreadState: jest.fn(), @@ -49,6 +59,7 @@ describe("RoomListItemMenuViewModel", () => { jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null); mocked(useUnreadNotifications).mockReturnValue({ symbol: null, count: 0, level: NotificationLevel.None }); + mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, jest.fn()]); jest.spyOn(dispatcher, "dispatch"); }); @@ -76,6 +87,12 @@ describe("RoomListItemMenuViewModel", () => { expect(result.current.showMoreOptionsMenu).toBe(true); }); + it("should has showNotificationMenu to be true", () => { + mocked(hasAccessToNotificationMenu).mockReturnValue(true); + const { result } = render(); + expect(result.current.showNotificationMenu).toBe(true); + }); + it("should be able to invite", () => { jest.spyOn(room, "canInvite").mockReturnValue(true); const { result } = render(); @@ -106,6 +123,29 @@ describe("RoomListItemMenuViewModel", () => { expect(result.current.canMarkAsUnread).toBe(false); }); + it("should has isNotificationAllMessage to be true", () => { + const { result } = render(); + expect(result.current.isNotificationAllMessage).toBe(true); + }); + + it("should has isNotificationAllMessageLoud to be true", () => { + mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessagesLoud, jest.fn()]); + const { result } = render(); + expect(result.current.isNotificationAllMessageLoud).toBe(true); + }); + + it("should has isNotificationMentionOnly to be true", () => { + mocked(useNotificationState).mockReturnValue([RoomNotifState.MentionsOnly, jest.fn()]); + const { result } = render(); + expect(result.current.isNotificationMentionOnly).toBe(true); + }); + + it("should has isNotificationMute to be true", () => { + mocked(useNotificationState).mockReturnValue([RoomNotifState.Mute, jest.fn()]); + const { result } = render(); + expect(result.current.isNotificationMute).toBe(true); + }); + // Actions it("should mark as read", () => { @@ -170,4 +210,12 @@ describe("RoomListItemMenuViewModel", () => { room_id: room.roomId, }); }); + + it("should call setRoomNotifState", () => { + const setRoomNotifState = jest.fn(); + mocked(useNotificationState).mockReturnValue([RoomNotifState.AllMessages, setRoomNotifState]); + const { result } = render(); + result.current.setRoomNotifState(RoomNotifState.Mute); + expect(setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute); + }); }); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx index caba9abd1e..9558ced09b 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx @@ -12,13 +12,17 @@ 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"; +import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils"; +import { + hasAccessToNotificationMenu, + hasAccessToOptionsMenu, +} from "../../../../../src/components/viewmodels/roomlist/utils"; import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ hasAccessToOptionsMenu: jest.fn().mockReturnValue(false), + hasAccessToNotificationMenu: jest.fn().mockReturnValue(false), })); describe("RoomListItemViewModel", () => { @@ -30,7 +34,10 @@ describe("RoomListItemViewModel", () => { }); it("should dispatch view room action on openRoom", async () => { - const { result: vm } = renderHook(() => useRoomListItemViewModel(room)); + const { result: vm } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); const fn = jest.spyOn(dispatcher, "dispatch"); vm.current.openRoom(); @@ -45,7 +52,19 @@ describe("RoomListItemViewModel", () => { it("should show hover menu if user has access to options menu", async () => { mocked(hasAccessToOptionsMenu).mockReturnValue(true); - const { result: vm } = renderHook(() => useRoomListItemViewModel(room)); + const { result: vm } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); + expect(vm.current.showHoverMenu).toBe(true); + }); + + it("should show hover menu if user has access to notification menu", async () => { + mocked(hasAccessToNotificationMenu).mockReturnValue(true); + const { result: vm } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); expect(vm.current.showHoverMenu).toBe(true); }); diff --git a/test/unit-tests/components/viewmodels/roomlist/utils-test.ts b/test/unit-tests/components/viewmodels/roomlist/utils-test.ts index 1fd4fc1b4a..322d2a5cc6 100644 --- a/test/unit-tests/components/viewmodels/roomlist/utils-test.ts +++ b/test/unit-tests/components/viewmodels/roomlist/utils-test.ts @@ -10,7 +10,11 @@ import { mocked } from "jest-mock"; import type { MatrixClient, Room, RoomState } from "matrix-js-sdk/src/matrix"; import { createTestClient, mkStubRoom } from "../../../../test-utils"; import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; -import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils"; +import { + hasCreateRoomRights, + createRoom, + hasAccessToNotificationMenu, +} from "../../../../../src/components/viewmodels/roomlist/utils"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { showCreateNewRoom } from "../../../../../src/utils/space"; @@ -66,4 +70,13 @@ describe("utils", () => { expect(hasCreateRoomRights(matrixClient)).toBe(true); }); }); + + it("hasAccessToNotificationMenu", () => { + mocked(shouldShowComponent).mockReturnValue(true); + const room = mkStubRoom("roomId", "roomName", matrixClient); + const isGuest = false; + const isArchived = false; + + expect(hasAccessToNotificationMenu(room, isGuest, isArchived)).toBe(true); + }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx index de1d37ed08..2994d7629c 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemMenuView-test.tsx @@ -17,6 +17,7 @@ import { 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"; +import { RoomNotifState } from "../../../../../../src/RoomNotifs"; jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemMenuViewModel", () => ({ useRoomListItemMenuViewModel: jest.fn(), @@ -25,11 +26,16 @@ jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListItemMenu describe("", () => { const defaultValue: RoomListItemMenuViewState = { showMoreOptionsMenu: true, + showNotificationMenu: true, isFavourite: true, canInvite: true, canMarkAsUnread: true, canMarkAsRead: true, canCopyRoomLink: true, + isNotificationAllMessage: true, + isNotificationMentionOnly: true, + isNotificationAllMessageLoud: true, + isNotificationMute: true, copyRoomLink: jest.fn(), markAsUnread: jest.fn(), markAsRead: jest.fn(), @@ -37,6 +43,7 @@ describe("", () => { toggleLowPriority: jest.fn(), toggleFavorite: jest.fn(), invite: jest.fn(), + setRoomNotifState: jest.fn(), }; let matrixClient: MatrixClient; @@ -58,22 +65,37 @@ describe("", () => { expect(asFragment()).toMatchSnapshot(); }); + it("should render the notification options menu", () => { + const { asFragment } = renderMenu(); + expect(screen.getByRole("button", { name: "Notification 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 not render the notification options menu when showNotificationMenu is false", () => { + mocked(useRoomListItemMenuViewModel).mockReturnValue({ ...defaultValue, showNotificationMenu: false }); + renderMenu(); + expect(screen.queryByRole("button", { name: "Notification options" })).toBeNull(); }); - it("should display all the buttons and have the actions linked", async () => { + it.each([["More Options"], ["Notification options"]])( + "should call setMenuOpen when the menu is opened for %s menu", + async (label) => { + const user = userEvent.setup(); + const setMenuOpen = jest.fn(); + renderMenu(setMenuOpen); + + await user.click(screen.getByRole("button", { name: label })); + expect(setMenuOpen).toHaveBeenCalledWith(true); + }, + ); + + it("should display all the buttons and have the actions linked for the more options menu", async () => { const user = userEvent.setup(); renderMenu(); @@ -107,4 +129,27 @@ describe("", () => { await user.click(screen.getByRole("menuitem", { name: "Leave room" })); expect(defaultValue.leaveRoom).toHaveBeenCalled(); }); + + it("should display all the buttons and have the actions linked for the notification options menu", async () => { + const user = userEvent.setup(); + renderMenu(); + + const openMenu = screen.getByRole("button", { name: "Notification options" }); + await user.click(openMenu); + + await user.click(screen.getByRole("menuitem", { name: "Match default settings" })); + expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessages); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "All messages" })); + expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessagesLoud); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "Mentions and keywords" })); + expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.MentionsOnly); + + await user.click(openMenu); + await user.click(screen.getByRole("menuitem", { name: "Mute room" })); + expect(defaultValue.setRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute); + }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap index 0910706d9b..6489d61c35 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap @@ -37,6 +37,115 @@ exports[` should render the more options menu 1`] = ` + + + +`; + +exports[` should render the notification options menu 1`] = ` + +
+ +
`;