New room list: add notification options menu (#29639)
* feat: add `utils.hasAccessToNotificationMenu` * feat(room list item view model): use `hasAccessToNotificationMenu` to compute `showHoverMenu` * feat(room list item menu view model): add notification options menu attributes * feat(room list item menu view): add notification options * test: add tests for `utils.hasAccessToNotificationMenu` * test(room list item view model): add test for `showHoverMenu` * test(room list item menu view model): add tests for new attributes * test(room list item menu view): add tests for notification options menu * chore: update i18n * test(e2e): update screenshots * test(e2e): add tests for notification options menu
@ -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();
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 77 KiB |
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
<Flex className="mx_RoomListItemMenuView" align="center" gap="var(--cpd-space-0-5x)">
|
||||
{vm.showMoreOptionsMenu && <MoreOptionsMenu setMenuOpen={setMenuOpen} vm={vm} />}
|
||||
{vm.showNotificationMenu && <NotificationMenu setMenuOpen={setMenuOpen} vm={vm} />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@ -152,3 +157,93 @@ export const MoreOptionsButton = forwardRef<HTMLButtonElement, MoreOptionsButton
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface NotificationMenuProps {
|
||||
/**
|
||||
* The view model state for the menu.
|
||||
*/
|
||||
vm: RoomListItemMenuViewState;
|
||||
/**
|
||||
* Set the menu open state.
|
||||
* @param isOpen
|
||||
*/
|
||||
setMenuOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
title={_t("room_list|notification_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<NotificationButton isRoomMuted={vm.isNotificationMute} size="24px" />}
|
||||
>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessage}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|default_settings")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessages)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessage && <CheckIcon width="24px" height="24px" />}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessageLoud}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|all_messages")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessageLoud && <CheckIcon width="24px" height="24px" />}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMentionOnly}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mentions_keywords")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.MentionsOnly)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMentionOnly && <CheckIcon width="24px" height="24px" />}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMute}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mute_room")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.Mute)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMute && <CheckIcon width="24px" height="24px" />}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
interface NotificationButtonProps extends ComponentProps<typeof IconButton> {
|
||||
/**
|
||||
* Whether the room is muted.
|
||||
*/
|
||||
isRoomMuted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to trigger the notification menu.
|
||||
*/
|
||||
export const NotificationButton = forwardRef<HTMLButtonElement, NotificationButtonProps>(function MoreOptionsButton(
|
||||
{ isRoomMuted, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<Tooltip label={_t("room_list|notification_options")}>
|
||||
<IconButton aria-label={_t("room_list|notification_options")} {...props} ref={ref}>
|
||||
{isRoomMuted ? <NotificationOffIcon /> : <NotificationIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
@ -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 <a>settings</a>",
|
||||
"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"
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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("<RoomListItemMenuView />", () => {
|
||||
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("<RoomListItemMenuView />", () => {
|
||||
toggleLowPriority: jest.fn(),
|
||||
toggleFavorite: jest.fn(),
|
||||
invite: jest.fn(),
|
||||
setRoomNotifState: jest.fn(),
|
||||
};
|
||||
|
||||
let matrixClient: MatrixClient;
|
||||
@ -58,22 +65,37 @@ describe("<RoomListItemMenuView />", () => {
|
||||
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 () => {
|
||||
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.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: "More Options" }));
|
||||
await user.click(screen.getByRole("button", { name: label }));
|
||||
expect(setMenuOpen).toHaveBeenCalledWith(true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should display all the buttons and have the actions linked", async () => {
|
||||
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("<RoomListItemMenuView />", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -37,6 +37,115 @@ exports[`<RoomListItemMenuView /> should render the more options menu 1`] = `
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby=":r9:"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
id="radix-:r7:"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListItemMenuView /> should render the notification options menu 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_Flex mx_RoomListItemMenuView"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-0-5x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="More Options"
|
||||
aria-labelledby=":ri:"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
id="radix-:rg:"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby=":rp:"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
id="radix-:rn:"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|