From f7f35c39aff6e09656ce8e20ef1e7050b74e43aa Mon Sep 17 00:00:00 2001
From: Douglas Fabris <devfabris@gmail.com>
Date: Fri, 17 Jan 2025 12:01:47 -0300
Subject: [PATCH] refactor: Replace `Menu` in favor of `GenericMenu` in
 RoomMenu (#34927)

---
 .../client/hooks/menuActions/useLeaveRoom.tsx |  65 ++++++
 .../menuActions/useToggleFavoriteAction.ts    |  18 ++
 .../hooks/menuActions/useToggleReadAction.ts  |  48 ++++
 .../meteor/client/hooks/useRoomMenuActions.ts | 118 ++++++++++
 .../hooks/useOmnichannelPrioritiesMenu.tsx    |  54 ++---
 .../omnichannel/priorities/PriorityIcon.tsx   |  15 +-
 apps/meteor/client/sidebar/Item/Condensed.tsx |  10 +-
 apps/meteor/client/sidebar/Item/Extended.tsx  |  11 +-
 apps/meteor/client/sidebar/Item/Medium.tsx    |  10 +-
 apps/meteor/client/sidebar/RoomMenu.spec.tsx  |  14 +-
 apps/meteor/client/sidebar/RoomMenu.tsx       | 201 +----------------
 .../client/sidebarv2/Item/Condensed.tsx       |  10 +-
 .../meteor/client/sidebarv2/Item/Extended.tsx |  12 +-
 apps/meteor/client/sidebarv2/Item/Medium.tsx  |  10 +-
 apps/meteor/client/sidebarv2/RoomMenu.tsx     | 210 +-----------------
 .../account/profile/ActionConfirmModal.tsx    |   1 +
 .../page-objects/fragments/home-sidenav.ts    |   4 +-
 .../e2e/page-objects/fragments/sidebar.ts     |   2 +-
 .../GenericMenu/GenericMenuItem.tsx           |   5 +-
 19 files changed, 320 insertions(+), 498 deletions(-)
 create mode 100644 apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx
 create mode 100644 apps/meteor/client/hooks/menuActions/useToggleFavoriteAction.ts
 create mode 100644 apps/meteor/client/hooks/menuActions/useToggleReadAction.ts
 create mode 100644 apps/meteor/client/hooks/useRoomMenuActions.ts

diff --git a/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx b/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx
new file mode 100644
index 00000000000..b5c90c8f42d
--- /dev/null
+++ b/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx
@@ -0,0 +1,65 @@
+import type { RoomType } from '@rocket.chat/core-typings';
+import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
+import type { TranslationKey } from '@rocket.chat/ui-contexts';
+import { useEndpoint, useRouter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
+import { useTranslation } from 'react-i18next';
+
+import { LegacyRoomManager } from '../../../app/ui-utils/client';
+import { UiTextContext } from '../../../definition/IRoomTypeConfig';
+import WarningModal from '../../components/WarningModal';
+import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
+
+const leaveEndpoints = {
+	p: '/v1/groups.leave',
+	c: '/v1/channels.leave',
+	d: '/v1/im.leave',
+	v: '/v1/channels.leave',
+	l: '/v1/groups.leave',
+} as const;
+
+type LeaveRoomProps = {
+	rid: string;
+	type: RoomType;
+	name: string;
+	roomOpen?: boolean;
+};
+
+// TODO: this menu action should consider team leaving
+export const useLeaveRoomAction = ({ rid, type, name, roomOpen }: LeaveRoomProps) => {
+	const { t } = useTranslation();
+	const setModal = useSetModal();
+	const dispatchToastMessage = useToastMessageDispatch();
+	const router = useRouter();
+
+	const leaveRoom = useEndpoint('POST', leaveEndpoints[type]);
+
+	const handleLeave = useEffectEvent(() => {
+		const leave = async (): Promise<void> => {
+			try {
+				await leaveRoom({ roomId: rid });
+				if (roomOpen) {
+					router.navigate('/home');
+				}
+				LegacyRoomManager.close(rid);
+			} catch (error) {
+				dispatchToastMessage({ type: 'error', message: error });
+			} finally {
+				setModal(null);
+			}
+		};
+
+		const warnText = roomCoordinator.getRoomDirectives(type).getUiText(UiTextContext.LEAVE_WARNING);
+
+		setModal(
+			<WarningModal
+				text={t(warnText as TranslationKey, name)}
+				confirmText={t('Leave_room')}
+				close={() => setModal(null)}
+				cancelText={t('Cancel')}
+				confirm={leave}
+			/>,
+		);
+	});
+
+	return handleLeave;
+};
diff --git a/apps/meteor/client/hooks/menuActions/useToggleFavoriteAction.ts b/apps/meteor/client/hooks/menuActions/useToggleFavoriteAction.ts
new file mode 100644
index 00000000000..70284057aee
--- /dev/null
+++ b/apps/meteor/client/hooks/menuActions/useToggleFavoriteAction.ts
@@ -0,0 +1,18 @@
+import type { IRoom } from '@rocket.chat/core-typings';
+import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
+import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
+
+export const useToggleFavoriteAction = ({ rid, isFavorite }: { rid: IRoom['_id']; isFavorite: boolean }) => {
+	const toggleFavorite = useEndpoint('POST', '/v1/rooms.favorite');
+	const dispatchToastMessage = useToastMessageDispatch();
+
+	const handleToggleFavorite = useEffectEvent(async () => {
+		try {
+			await toggleFavorite({ roomId: rid, favorite: !isFavorite });
+		} catch (error) {
+			dispatchToastMessage({ type: 'error', message: error });
+		}
+	});
+
+	return handleToggleFavorite;
+};
diff --git a/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts b/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts
new file mode 100644
index 00000000000..133acd9b0f7
--- /dev/null
+++ b/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts
@@ -0,0 +1,48 @@
+import type { ISubscription } from '@rocket.chat/core-typings';
+import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
+import { useEndpoint, useMethod, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
+import { useQueryClient } from '@tanstack/react-query';
+
+import { LegacyRoomManager } from '../../../app/ui-utils/client';
+
+type ToggleReadActionProps = {
+	rid: string;
+	isUnread?: boolean;
+	subscription?: ISubscription;
+};
+
+export const useToggleReadAction = ({ rid, isUnread, subscription }: ToggleReadActionProps) => {
+	const dispatchToastMessage = useToastMessageDispatch();
+	const queryClient = useQueryClient();
+	const router = useRouter();
+
+	const readMessages = useEndpoint('POST', '/v1/subscriptions.read');
+	const unreadMessages = useMethod('unreadMessages');
+
+	const handleToggleRead = useEffectEvent(async () => {
+		try {
+			queryClient.invalidateQueries({
+				queryKey: ['sidebar/search/spotlight'],
+			});
+
+			if (isUnread) {
+				await readMessages({ rid, readThreads: true });
+				return;
+			}
+
+			if (subscription == null) {
+				return;
+			}
+
+			LegacyRoomManager.close(subscription.t + subscription.name);
+
+			router.navigate('/home');
+
+			await unreadMessages(undefined, rid);
+		} catch (error) {
+			dispatchToastMessage({ type: 'error', message: error });
+		}
+	});
+
+	return handleToggleRead;
+};
diff --git a/apps/meteor/client/hooks/useRoomMenuActions.ts b/apps/meteor/client/hooks/useRoomMenuActions.ts
new file mode 100644
index 00000000000..46308772b42
--- /dev/null
+++ b/apps/meteor/client/hooks/useRoomMenuActions.ts
@@ -0,0 +1,118 @@
+import type { RoomType } from '@rocket.chat/core-typings';
+import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
+import { usePermission, useSetting, useUserSubscription } from '@rocket.chat/ui-contexts';
+import type { Fields } from '@rocket.chat/ui-contexts';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { useLeaveRoomAction } from './menuActions/useLeaveRoom';
+import { useToggleFavoriteAction } from './menuActions/useToggleFavoriteAction';
+import { useToggleReadAction } from './menuActions/useToggleReadAction';
+import { useHideRoomAction } from './useHideRoomAction';
+import { useOmnichannelPrioritiesMenu } from '../omnichannel/hooks/useOmnichannelPrioritiesMenu';
+
+const fields: Fields = {
+	f: true,
+	t: true,
+	name: true,
+};
+
+type RoomMenuActionsProps = {
+	rid: string;
+	type: RoomType;
+	name: string;
+	isUnread?: boolean;
+	cl?: boolean;
+	roomOpen?: boolean;
+	hideDefaultOptions: boolean;
+};
+
+export const useRoomMenuActions = ({
+	rid,
+	type,
+	name,
+	isUnread,
+	cl,
+	roomOpen,
+	hideDefaultOptions,
+}: RoomMenuActionsProps): { title: string; items: GenericMenuItemProps[] }[] => {
+	const { t } = useTranslation();
+	const subscription = useUserSubscription(rid, fields);
+
+	const isFavorite = Boolean(subscription?.f);
+	const canLeaveChannel = usePermission('leave-c');
+	const canLeavePrivate = usePermission('leave-p');
+	const canFavorite = useSetting('Favorite_Rooms') as boolean;
+
+	const canLeave = ((): boolean => {
+		if (type === 'c' && !canLeaveChannel) {
+			return false;
+		}
+		if (type === 'p' && !canLeavePrivate) {
+			return false;
+		}
+		return !((cl != null && !cl) || ['d', 'l'].includes(type));
+	})();
+
+	const handleHide = useHideRoomAction({ rid, type, name }, { redirect: false });
+	const handleToggleFavorite = useToggleFavoriteAction({ rid, isFavorite });
+	const handleToggleRead = useToggleReadAction({ rid, isUnread, subscription });
+	const handleLeave = useLeaveRoomAction({ rid, type, name, roomOpen });
+
+	const isOmnichannelRoom = type === 'l';
+	const prioritiesMenu = useOmnichannelPrioritiesMenu(rid);
+
+	const menuOptions = useMemo(
+		() =>
+			!hideDefaultOptions
+				? [
+						!isOmnichannelRoom && {
+							id: 'hideRoom',
+							icon: 'eye-off',
+							content: t('Hide'),
+							onClick: handleHide,
+						},
+						{
+							id: 'toggleRead',
+							icon: 'flag',
+							content: isUnread ? t('Mark_read') : t('Mark_unread'),
+							onClick: handleToggleRead,
+						},
+						canFavorite && {
+							id: 'toggleFavorite',
+							icon: isFavorite ? 'star-filled' : 'star',
+							content: isFavorite ? t('Unfavorite') : t('Favorite'),
+							onClick: handleToggleFavorite,
+						},
+						canLeave && {
+							id: 'leaveRoom',
+							icon: 'sign-out',
+							content: t('Leave_room'),
+							onClick: handleLeave,
+						},
+					]
+				: [],
+		[
+			hideDefaultOptions,
+			t,
+			handleHide,
+			isUnread,
+			handleToggleRead,
+			canFavorite,
+			isFavorite,
+			handleToggleFavorite,
+			canLeave,
+			handleLeave,
+			isOmnichannelRoom,
+		],
+	);
+
+	if (isOmnichannelRoom && prioritiesMenu.length > 0) {
+		return [
+			{ title: '', items: menuOptions.filter(Boolean) as GenericMenuItemProps[] },
+			{ title: t('Priorities'), items: prioritiesMenu },
+		];
+	}
+
+	return [{ title: '', items: menuOptions.filter(Boolean) as GenericMenuItemProps[] }];
+};
diff --git a/apps/meteor/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx b/apps/meteor/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx
index 9fbdc8be019..c628afc3459 100644
--- a/apps/meteor/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx
+++ b/apps/meteor/client/omnichannel/hooks/useOmnichannelPrioritiesMenu.tsx
@@ -1,17 +1,15 @@
 import { LivechatPriorityWeight } from '@rocket.chat/core-typings';
-import type { Menu } from '@rocket.chat/fuselage';
 import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
 import { useEndpoint } from '@rocket.chat/ui-contexts';
 import { useQueryClient } from '@tanstack/react-query';
-import type { ComponentProps } from 'react';
-import { useCallback, useMemo } from 'react';
+import { useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useOmnichannelPriorities } from './useOmnichannelPriorities';
 import { dispatchToastMessage } from '../../lib/toast';
-import { PriorityIcon } from '../priorities/PriorityIcon';
+import { PRIORITY_ICONS } from '../priorities/PriorityIcon';
 
-export const useOmnichannelPrioritiesMenu = (rid: string): ComponentProps<typeof Menu>['options'] | Record<string, never> => {
+export const useOmnichannelPrioritiesMenu = (rid: string) => {
 	const { t } = useTranslation();
 	const queryClient = useQueryClient();
 	const updateRoomPriority = useEndpoint('POST', '/v1/livechat/room/:rid/priority', { rid });
@@ -32,41 +30,27 @@ export const useOmnichannelPrioritiesMenu = (rid: string): ComponentProps<typeof
 		}
 	});
 
-	const renderOption = useCallback((label: string, weight: LivechatPriorityWeight) => {
-		return (
-			<>
-				<PriorityIcon level={weight || LivechatPriorityWeight.NOT_SPECIFIED} showUnprioritized /> {label}
-			</>
-		);
-	}, []);
-
-	return useMemo<ComponentProps<typeof Menu>['options']>(() => {
-		const menuHeading = {
-			type: 'heading',
-			label: t('Priorities'),
-		};
-
+	return useMemo(() => {
 		const unprioritizedOption = {
-			type: 'option',
-			action: handlePriorityChange(''),
-			label: {
-				label: renderOption(t('Unprioritized'), LivechatPriorityWeight.NOT_SPECIFIED),
-			},
+			id: 'unprioritized',
+			icon: PRIORITY_ICONS[LivechatPriorityWeight.NOT_SPECIFIED].iconName,
+			iconColor: PRIORITY_ICONS[LivechatPriorityWeight.NOT_SPECIFIED].color,
+			content: t('Unprioritized'),
+			onClick: handlePriorityChange(''),
 		};
 
-		const options = priorities.reduce<Record<string, object>>((items, { _id: priorityId, name, i18n, dirty, sortItem }) => {
+		const options = priorities.map(({ _id: priorityId, name, i18n, dirty, sortItem }) => {
 			const label = dirty && name ? name : i18n;
 
-			items[label] = {
-				action: handlePriorityChange(priorityId),
-				label: {
-					label: renderOption(label, sortItem),
-				},
+			return {
+				id: priorityId,
+				icon: PRIORITY_ICONS[sortItem].iconName,
+				iconColor: PRIORITY_ICONS[sortItem].color,
+				content: label,
+				onClick: handlePriorityChange(priorityId),
 			};
+		});
 
-			return items;
-		}, {});
-
-		return priorities.length ? { menuHeading, Unprioritized: unprioritizedOption, ...options } : {};
-	}, [t, handlePriorityChange, priorities, renderOption]);
+		return priorities.length ? [unprioritizedOption, ...options] : [];
+	}, [t, handlePriorityChange, priorities]);
 };
diff --git a/apps/meteor/client/omnichannel/priorities/PriorityIcon.tsx b/apps/meteor/client/omnichannel/priorities/PriorityIcon.tsx
index bad31ac8af3..e41ef88a241 100644
--- a/apps/meteor/client/omnichannel/priorities/PriorityIcon.tsx
+++ b/apps/meteor/client/omnichannel/priorities/PriorityIcon.tsx
@@ -1,5 +1,5 @@
 import { LivechatPriorityWeight } from '@rocket.chat/core-typings';
-import { Box, Icon, Palette, StatusBullet } from '@rocket.chat/fuselage';
+import { Icon, Palette } from '@rocket.chat/fuselage';
 import type { Keys } from '@rocket.chat/icons';
 import type { TranslationKey } from '@rocket.chat/ui-contexts';
 import type { ComponentProps, ReactElement } from 'react';
@@ -13,7 +13,10 @@ type PriorityIconProps = Omit<ComponentProps<typeof Icon>, 'name' | 'color'> & {
 	showUnprioritized?: boolean;
 };
 
-const PRIORITY_ICONS: Record<number, { iconName: Keys; color: string }> = {
+export const PRIORITY_ICONS: Record<number, { iconName: Keys; color?: string }> = {
+	[LivechatPriorityWeight.NOT_SPECIFIED]: {
+		iconName: 'circle-unfilled',
+	},
 	[LivechatPriorityWeight.HIGHEST]: {
 		iconName: 'chevron-double-up',
 		color: Palette.badge['badge-background-level-4'].toString(),
@@ -51,12 +54,8 @@ export const PriorityIcon = ({ level, size = 20, showUnprioritized = false, ...p
 		return dirty ? name : t(i18n as TranslationKey);
 	}, [level, priorities, t]);
 
-	if (showUnprioritized && level === LivechatPriorityWeight.NOT_SPECIFIED) {
-		return (
-			<Box is='i' mi='4px' title={t('Unprioritized')}>
-				<StatusBullet status='offline' />
-			</Box>
-		);
+	if (!showUnprioritized && level === LivechatPriorityWeight.NOT_SPECIFIED) {
+		return null;
 	}
 
 	return iconName ? <Icon {...props} name={iconName} color={color} size={size} title={name} /> : null;
diff --git a/apps/meteor/client/sidebar/Item/Condensed.tsx b/apps/meteor/client/sidebar/Item/Condensed.tsx
index 6dd5027b95c..331a01949aa 100644
--- a/apps/meteor/client/sidebar/Item/Condensed.tsx
+++ b/apps/meteor/client/sidebar/Item/Condensed.tsx
@@ -1,7 +1,7 @@
 import { IconButton, Sidebar } from '@rocket.chat/fuselage';
-import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
+import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
 import type { Keys as IconName } from '@rocket.chat/icons';
-import type { ReactElement, UIEvent } from 'react';
+import type { ReactElement } from 'react';
 import { memo, useState } from 'react';
 
 type CondensedProps = {
@@ -21,14 +21,10 @@ type CondensedProps = {
 
 const Condensed = ({ icon, title = '', avatar, actions, href, unread, menu, badges, ...props }: CondensedProps) => {
 	const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
-
 	const isReduceMotionEnabled = usePrefersReducedMotion();
 
-	const handleMenu = useEffectEvent((e: UIEvent<HTMLElement>) => {
-		setMenuVisibility(e.currentTarget.offsetWidth > 0 && Boolean(menu));
-	});
 	const handleMenuEvent = {
-		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
+		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility,
 	};
 
 	return (
diff --git a/apps/meteor/client/sidebar/Item/Extended.tsx b/apps/meteor/client/sidebar/Item/Extended.tsx
index 7a09e6d04d1..53015d17e03 100644
--- a/apps/meteor/client/sidebar/Item/Extended.tsx
+++ b/apps/meteor/client/sidebar/Item/Extended.tsx
@@ -1,7 +1,7 @@
 import { Sidebar, IconButton } from '@rocket.chat/fuselage';
-import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
+import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
 import type { Keys as IconName } from '@rocket.chat/icons';
-import type { ReactNode, UIEvent } from 'react';
+import type { ReactNode } from 'react';
 import { memo, useState } from 'react';
 
 import { useShortTimeAgo } from '../../hooks/useTimeAgo';
@@ -42,15 +42,10 @@ const Extended = ({
 }: ExtendedProps) => {
 	const formatDate = useShortTimeAgo();
 	const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
-
 	const isReduceMotionEnabled = usePrefersReducedMotion();
 
-	const handleMenu = useEffectEvent((e: UIEvent<HTMLElement>) => {
-		setMenuVisibility(e.currentTarget.offsetWidth > 0 && Boolean(menu));
-	});
-
 	const handleMenuEvent = {
-		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
+		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility,
 	};
 
 	return (
diff --git a/apps/meteor/client/sidebar/Item/Medium.tsx b/apps/meteor/client/sidebar/Item/Medium.tsx
index 5ee19b1fed6..2b9dd8fef49 100644
--- a/apps/meteor/client/sidebar/Item/Medium.tsx
+++ b/apps/meteor/client/sidebar/Item/Medium.tsx
@@ -1,6 +1,6 @@
 import { Sidebar, IconButton } from '@rocket.chat/fuselage';
-import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
-import type { ReactNode, UIEvent } from 'react';
+import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
+import type { ReactNode } from 'react';
 import { memo, useState } from 'react';
 
 type MediumProps = {
@@ -19,14 +19,10 @@ type MediumProps = {
 
 const Medium = ({ icon, title = '', avatar, actions, href, badges, unread, menu, ...props }: MediumProps) => {
 	const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
-
 	const isReduceMotionEnabled = usePrefersReducedMotion();
 
-	const handleMenu = useEffectEvent((e: UIEvent<HTMLElement>) => {
-		setMenuVisibility(e.currentTarget.offsetWidth > 0 && Boolean(menu));
-	});
 	const handleMenuEvent = {
-		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
+		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility,
 	};
 
 	return (
diff --git a/apps/meteor/client/sidebar/RoomMenu.spec.tsx b/apps/meteor/client/sidebar/RoomMenu.spec.tsx
index a8f2104cea4..2e75a345718 100644
--- a/apps/meteor/client/sidebar/RoomMenu.spec.tsx
+++ b/apps/meteor/client/sidebar/RoomMenu.spec.tsx
@@ -48,10 +48,10 @@ it('should display all the menu options for regular rooms', async () => {
 	const menu = screen.queryByRole('button');
 	await userEvent.click(menu as HTMLElement);
 
-	expect(await screen.findByRole('option', { name: 'Hide' })).toBeInTheDocument();
-	expect(await screen.findByRole('option', { name: 'Favorite' })).toBeInTheDocument();
-	expect(await screen.findByRole('option', { name: 'Mark Unread' })).toBeInTheDocument();
-	expect(await screen.findByRole('option', { name: 'Leave' })).toBeInTheDocument();
+	expect(await screen.findByRole('menuitem', { name: 'Hide' })).toBeInTheDocument();
+	expect(await screen.findByRole('menuitem', { name: 'Favorite' })).toBeInTheDocument();
+	expect(await screen.findByRole('menuitem', { name: 'Mark Unread' })).toBeInTheDocument();
+	expect(await screen.findByRole('menuitem', { name: 'Leave' })).toBeInTheDocument();
 });
 
 it('should display only mark unread and favorite for omnichannel rooms', async () => {
@@ -60,7 +60,7 @@ it('should display only mark unread and favorite for omnichannel rooms', async (
 	const menu = screen.queryByRole('button');
 	await userEvent.click(menu as HTMLElement);
 
-	expect(await screen.findAllByRole('option')).toHaveLength(2);
-	expect(screen.queryByRole('option', { name: 'Hide' })).not.toBeInTheDocument();
-	expect(screen.queryByRole('option', { name: 'Leave' })).not.toBeInTheDocument();
+	expect(await screen.findAllByRole('menuitem')).toHaveLength(2);
+	expect(screen.queryByRole('menuitem', { name: 'Hide' })).not.toBeInTheDocument();
+	expect(screen.queryByRole('menuitem', { name: 'Leave' })).not.toBeInTheDocument();
 });
diff --git a/apps/meteor/client/sidebar/RoomMenu.tsx b/apps/meteor/client/sidebar/RoomMenu.tsx
index 14e453da540..50875326395 100644
--- a/apps/meteor/client/sidebar/RoomMenu.tsx
+++ b/apps/meteor/client/sidebar/RoomMenu.tsx
@@ -1,34 +1,10 @@
 import type { RoomType } from '@rocket.chat/core-typings';
-import { Option, Menu } from '@rocket.chat/fuselage';
-import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
-import type { TranslationKey, Fields } from '@rocket.chat/ui-contexts';
-import {
-	useRouter,
-	useSetModal,
-	useToastMessageDispatch,
-	useUserSubscription,
-	useSetting,
-	usePermission,
-	useMethod,
-	useTranslation,
-	useEndpoint,
-} from '@rocket.chat/ui-contexts';
-import { useQueryClient } from '@tanstack/react-query';
+import { GenericMenu } from '@rocket.chat/ui-client';
+import { useTranslation } from '@rocket.chat/ui-contexts';
 import type { ReactElement } from 'react';
-import { memo, useMemo } from 'react';
+import { memo } from 'react';
 
-import { LegacyRoomManager } from '../../app/ui-utils/client';
-import { UiTextContext } from '../../definition/IRoomTypeConfig';
-import WarningModal from '../components/WarningModal';
-import { useHideRoomAction } from '../hooks/useHideRoomAction';
-import { roomCoordinator } from '../lib/rooms/roomCoordinator';
-import { useOmnichannelPrioritiesMenu } from '../omnichannel/hooks/useOmnichannelPrioritiesMenu';
-
-const fields: Fields = {
-	f: true,
-	t: true,
-	name: true,
-};
+import { useRoomMenuActions } from '../hooks/useRoomMenuActions';
 
 type RoomMenuProps = {
 	rid: string;
@@ -42,15 +18,6 @@ type RoomMenuProps = {
 	hideDefaultOptions: boolean;
 };
 
-const leaveEndpoints = {
-	p: '/v1/groups.leave',
-	c: '/v1/channels.leave',
-	d: '/v1/im.leave',
-
-	v: '/v1/channels.leave',
-	l: '/v1/groups.leave',
-} as const;
-
 const RoomMenu = ({
 	rid,
 	unread,
@@ -63,167 +30,11 @@ const RoomMenu = ({
 	hideDefaultOptions = false,
 }: RoomMenuProps): ReactElement | null => {
 	const t = useTranslation();
-	const dispatchToastMessage = useToastMessageDispatch();
-	const setModal = useSetModal();
-
-	const closeModal = useEffectEvent(() => setModal());
-
-	const router = useRouter();
-
-	const subscription = useUserSubscription(rid, fields);
-	const canFavorite = useSetting('Favorite_Rooms');
-	const isFavorite = Boolean(subscription?.f);
-
-	const readMessages = useEndpoint('POST', '/v1/subscriptions.read');
-	const toggleFavorite = useEndpoint('POST', '/v1/rooms.favorite');
-	const leaveRoom = useEndpoint('POST', leaveEndpoints[type]);
-
-	const unreadMessages = useMethod('unreadMessages');
 
 	const isUnread = alert || unread || threadUnread;
+	const sections = useRoomMenuActions({ rid, type, name, isUnread, cl, roomOpen, hideDefaultOptions });
 
-	const canLeaveChannel = usePermission('leave-c');
-	const canLeavePrivate = usePermission('leave-p');
-
-	const isOmnichannelRoom = type === 'l';
-	const prioritiesMenu = useOmnichannelPrioritiesMenu(rid);
-
-	const queryClient = useQueryClient();
-
-	const handleHide = useHideRoomAction({ rid, type, name }, { redirect: false });
-
-	const canLeave = ((): boolean => {
-		if (type === 'c' && !canLeaveChannel) {
-			return false;
-		}
-		if (type === 'p' && !canLeavePrivate) {
-			return false;
-		}
-		return !((cl != null && !cl) || ['d', 'l'].includes(type));
-	})();
-
-	const handleLeave = useEffectEvent(() => {
-		const leave = async (): Promise<void> => {
-			try {
-				await leaveRoom({ roomId: rid });
-				if (roomOpen) {
-					router.navigate('/home');
-				}
-				LegacyRoomManager.close(rid);
-			} catch (error) {
-				dispatchToastMessage({ type: 'error', message: error });
-			}
-			closeModal();
-		};
-
-		const warnText = roomCoordinator.getRoomDirectives(type).getUiText(UiTextContext.LEAVE_WARNING);
-
-		setModal(
-			<WarningModal
-				text={t(warnText as TranslationKey, name)}
-				confirmText={t('Leave_room')}
-				close={closeModal}
-				cancelText={t('Cancel')}
-				confirm={leave}
-			/>,
-		);
-	});
-
-	const handleToggleRead = useEffectEvent(async () => {
-		try {
-			queryClient.invalidateQueries({
-				queryKey: ['sidebar/search/spotlight'],
-			});
-
-			if (isUnread) {
-				await readMessages({ rid, readThreads: true });
-				return;
-			}
-
-			if (subscription == null) {
-				return;
-			}
-
-			LegacyRoomManager.close(subscription.t + subscription.name);
-
-			router.navigate('/home');
-
-			await unreadMessages(undefined, rid);
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-	});
-
-	const handleToggleFavorite = useEffectEvent(async () => {
-		try {
-			await toggleFavorite({ roomId: rid, favorite: !isFavorite });
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-	});
-
-	const menuOptions = useMemo(
-		() => ({
-			...(!hideDefaultOptions && {
-				...(isOmnichannelRoom
-					? {}
-					: {
-							hideRoom: {
-								label: { label: t('Hide'), icon: 'eye-off' },
-								action: handleHide,
-							},
-						}),
-				toggleRead: {
-					label: { label: isUnread ? t('Mark_read') : t('Mark_unread'), icon: 'flag' },
-					action: handleToggleRead,
-				},
-				...(canFavorite
-					? {
-							toggleFavorite: {
-								label: {
-									label: isFavorite ? t('Unfavorite') : t('Favorite'),
-									icon: isFavorite ? 'star-filled' : 'star',
-								},
-								action: handleToggleFavorite,
-							},
-						}
-					: {}),
-				...(canLeave && {
-					leaveRoom: {
-						label: { label: t('Leave_room'), icon: 'sign-out' },
-						action: handleLeave,
-					},
-				}),
-			}),
-			...(isOmnichannelRoom && prioritiesMenu),
-		}),
-		[
-			hideDefaultOptions,
-			t,
-			handleHide,
-			isUnread,
-			handleToggleRead,
-			canFavorite,
-			isFavorite,
-			handleToggleFavorite,
-			canLeave,
-			handleLeave,
-			isOmnichannelRoom,
-			prioritiesMenu,
-		],
-	);
-
-	return (
-		<Menu
-			rcx-sidebar-item__menu
-			title={t('Options')}
-			mini
-			aria-keyshortcuts='alt'
-			options={menuOptions}
-			maxHeight={300}
-			renderItem={({ label: { label, icon }, ...props }): JSX.Element => <Option label={label} icon={icon} {...props} />}
-		/>
-	);
+	return <GenericMenu detached className='rcx-sidebar-item__menu' title={t('Options')} mini aria-keyshortcuts='alt' sections={sections} />;
 };
 
 export default memo(RoomMenu);
diff --git a/apps/meteor/client/sidebarv2/Item/Condensed.tsx b/apps/meteor/client/sidebarv2/Item/Condensed.tsx
index ffdf0eb2080..9c4edf7159e 100644
--- a/apps/meteor/client/sidebarv2/Item/Condensed.tsx
+++ b/apps/meteor/client/sidebarv2/Item/Condensed.tsx
@@ -1,7 +1,7 @@
 import { IconButton, SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemMenu, SidebarV2ItemTitle } from '@rocket.chat/fuselage';
-import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
+import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
 import type { Keys as IconName } from '@rocket.chat/icons';
-import type { HTMLAttributes, ReactElement, UIEvent } from 'react';
+import type { HTMLAttributes, ReactElement } from 'react';
 import { memo, useState } from 'react';
 
 type CondensedProps = {
@@ -21,14 +21,10 @@ type CondensedProps = {
 
 const Condensed = ({ icon, title, avatar, actions, unread, menu, badges, ...props }: CondensedProps) => {
 	const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
-
 	const isReduceMotionEnabled = usePrefersReducedMotion();
 
-	const handleMenu = useEffectEvent((e: UIEvent<HTMLDivElement>) => {
-		setMenuVisibility(e.currentTarget.offsetWidth > 0 && Boolean(menu));
-	});
 	const handleMenuEvent = {
-		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
+		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility,
 	};
 
 	return (
diff --git a/apps/meteor/client/sidebarv2/Item/Extended.tsx b/apps/meteor/client/sidebarv2/Item/Extended.tsx
index 12d999c5a3d..9a2f49c711a 100644
--- a/apps/meteor/client/sidebarv2/Item/Extended.tsx
+++ b/apps/meteor/client/sidebarv2/Item/Extended.tsx
@@ -9,9 +9,9 @@ import {
 	SidebarV2ItemMenu,
 	IconButton,
 } from '@rocket.chat/fuselage';
-import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
+import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
 import type { Keys as IconName } from '@rocket.chat/icons';
-import type { HTMLAttributes, ReactNode, UIEvent } from 'react';
+import type { HTMLAttributes, ReactNode } from 'react';
 import { memo, useState } from 'react';
 
 import { useShortTimeAgo } from '../../hooks/useTimeAgo';
@@ -52,27 +52,21 @@ const Extended = ({
 }: ExtendedProps) => {
 	const formatDate = useShortTimeAgo();
 	const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
-
 	const isReduceMotionEnabled = usePrefersReducedMotion();
 
-	const handleMenu = useEffectEvent((e: UIEvent<HTMLDivElement>) => {
-		setMenuVisibility(e.currentTarget.offsetWidth > 0 && Boolean(menu));
-	});
 	const handleMenuEvent = {
-		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
+		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility,
 	};
 
 	return (
 		<SidebarV2Item href={href} selected={selected} {...props}>
 			{avatar && <SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper>}
-
 			<SidebarV2ItemCol>
 				<SidebarV2ItemRow>
 					{icon && icon}
 					<SidebarV2ItemTitle unread={unread}>{title}</SidebarV2ItemTitle>
 					{time && <SidebarV2ItemTimestamp>{formatDate(time)}</SidebarV2ItemTimestamp>}
 				</SidebarV2ItemRow>
-
 				<SidebarV2ItemRow>
 					<SidebarV2ItemContent unread={unread}>{subtitle}</SidebarV2ItemContent>
 					{badges && badges}
diff --git a/apps/meteor/client/sidebarv2/Item/Medium.tsx b/apps/meteor/client/sidebarv2/Item/Medium.tsx
index 0b4e04c031d..d4fbde625b7 100644
--- a/apps/meteor/client/sidebarv2/Item/Medium.tsx
+++ b/apps/meteor/client/sidebarv2/Item/Medium.tsx
@@ -1,7 +1,7 @@
 import { IconButton, SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemMenu, SidebarV2ItemTitle } from '@rocket.chat/fuselage';
-import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
+import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
 import type { Keys as IconName } from '@rocket.chat/icons';
-import type { HTMLAttributes, ReactNode, UIEvent } from 'react';
+import type { HTMLAttributes, ReactNode } from 'react';
 import { memo, useState } from 'react';
 
 type MediumProps = {
@@ -20,14 +20,10 @@ type MediumProps = {
 
 const Medium = ({ icon, title, avatar, actions, badges, unread, menu, ...props }: MediumProps) => {
 	const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
-
 	const isReduceMotionEnabled = usePrefersReducedMotion();
 
-	const handleMenu = useEffectEvent((e: UIEvent<HTMLDivElement>) => {
-		setMenuVisibility(e.currentTarget.offsetWidth > 0 && Boolean(menu));
-	});
 	const handleMenuEvent = {
-		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
+		[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility,
 	};
 
 	return (
diff --git a/apps/meteor/client/sidebarv2/RoomMenu.tsx b/apps/meteor/client/sidebarv2/RoomMenu.tsx
index f74a231c613..fa1f236edb9 100644
--- a/apps/meteor/client/sidebarv2/RoomMenu.tsx
+++ b/apps/meteor/client/sidebarv2/RoomMenu.tsx
@@ -1,34 +1,9 @@
 import type { RoomType } from '@rocket.chat/core-typings';
-import { Option, Menu } from '@rocket.chat/fuselage';
-import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
-import type { TranslationKey, Fields } from '@rocket.chat/ui-contexts';
-import {
-	useRouter,
-	useSetModal,
-	useToastMessageDispatch,
-	useUserSubscription,
-	useSetting,
-	usePermission,
-	useMethod,
-	useTranslation,
-	useEndpoint,
-} from '@rocket.chat/ui-contexts';
-import { useQueryClient } from '@tanstack/react-query';
-import type { ReactElement } from 'react';
-import { memo, useMemo } from 'react';
+import { GenericMenu } from '@rocket.chat/ui-client';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import { memo } from 'react';
 
-import { LegacyRoomManager } from '../../app/ui-utils/client';
-import { UiTextContext } from '../../definition/IRoomTypeConfig';
-import WarningModal from '../components/WarningModal';
-import { useHideRoomAction } from '../hooks/useHideRoomAction';
-import { roomCoordinator } from '../lib/rooms/roomCoordinator';
-import { useOmnichannelPrioritiesMenu } from '../omnichannel/hooks/useOmnichannelPrioritiesMenu';
-
-const fields: Fields = {
-	f: true,
-	t: true,
-	name: true,
-};
+import { useRoomMenuActions } from '../hooks/useRoomMenuActions';
 
 type RoomMenuProps = {
 	rid: string;
@@ -42,184 +17,13 @@ type RoomMenuProps = {
 	hideDefaultOptions: boolean;
 };
 
-const leaveEndpoints = {
-	p: '/v1/groups.leave',
-	c: '/v1/channels.leave',
-	d: '/v1/im.leave',
-
-	v: '/v1/channels.leave',
-	l: '/v1/groups.leave',
-} as const;
-
-const RoomMenu = ({
-	rid,
-	unread,
-	threadUnread,
-	alert,
-	roomOpen,
-	type,
-	cl,
-	name = '',
-	hideDefaultOptions = false,
-}: RoomMenuProps): ReactElement | null => {
+const RoomMenu = ({ rid, unread, threadUnread, alert, roomOpen, type, cl, name = '', hideDefaultOptions = false }: RoomMenuProps) => {
 	const t = useTranslation();
-	const dispatchToastMessage = useToastMessageDispatch();
-	const setModal = useSetModal();
-
-	const closeModal = useEffectEvent(() => setModal());
-
-	const router = useRouter();
-
-	const subscription = useUserSubscription(rid, fields);
-	const canFavorite = useSetting('Favorite_Rooms');
-	const isFavorite = Boolean(subscription?.f);
-
-	const readMessages = useEndpoint('POST', '/v1/subscriptions.read');
-	const toggleFavorite = useEndpoint('POST', '/v1/rooms.favorite');
-	const leaveRoom = useEndpoint('POST', leaveEndpoints[type]);
-
-	const unreadMessages = useMethod('unreadMessages');
 
 	const isUnread = alert || unread || threadUnread;
+	const sections = useRoomMenuActions({ rid, type, name, isUnread, cl, roomOpen, hideDefaultOptions });
 
-	const canLeaveChannel = usePermission('leave-c');
-	const canLeavePrivate = usePermission('leave-p');
-
-	const isOmnichannelRoom = type === 'l';
-	const prioritiesMenu = useOmnichannelPrioritiesMenu(rid);
-
-	const queryClient = useQueryClient();
-
-	const handleHide = useHideRoomAction({ rid, type, name }, { redirect: false });
-
-	const canLeave = ((): boolean => {
-		if (type === 'c' && !canLeaveChannel) {
-			return false;
-		}
-		if (type === 'p' && !canLeavePrivate) {
-			return false;
-		}
-		return !((cl != null && !cl) || ['d', 'l'].includes(type));
-	})();
-
-	const handleLeave = useEffectEvent(() => {
-		const leave = async (): Promise<void> => {
-			try {
-				await leaveRoom({ roomId: rid });
-				if (roomOpen) {
-					router.navigate('/home');
-				}
-				LegacyRoomManager.close(rid);
-			} catch (error) {
-				dispatchToastMessage({ type: 'error', message: error });
-			}
-			closeModal();
-		};
-
-		const warnText = roomCoordinator.getRoomDirectives(type).getUiText(UiTextContext.LEAVE_WARNING);
-
-		setModal(
-			<WarningModal
-				text={t(warnText as TranslationKey, name)}
-				confirmText={t('Leave_room')}
-				close={closeModal}
-				cancelText={t('Cancel')}
-				confirm={leave}
-			/>,
-		);
-	});
-
-	const handleToggleRead = useEffectEvent(async () => {
-		try {
-			queryClient.invalidateQueries({
-				queryKey: ['sidebar/search/spotlight'],
-			});
-
-			if (isUnread) {
-				await readMessages({ rid, readThreads: true });
-				return;
-			}
-
-			if (subscription == null) {
-				return;
-			}
-
-			LegacyRoomManager.close(subscription.t + subscription.name);
-
-			router.navigate('/home');
-
-			await unreadMessages(undefined, rid);
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-	});
-
-	const handleToggleFavorite = useEffectEvent(async () => {
-		try {
-			await toggleFavorite({ roomId: rid, favorite: !isFavorite });
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-	});
-
-	const menuOptions = useMemo(
-		() => ({
-			...(!hideDefaultOptions && {
-				hideRoom: {
-					label: { label: t('Hide'), icon: 'eye-off' },
-					action: handleHide,
-				},
-				toggleRead: {
-					label: { label: isUnread ? t('Mark_read') : t('Mark_unread'), icon: 'flag' },
-					action: handleToggleRead,
-				},
-				...(canFavorite
-					? {
-							toggleFavorite: {
-								label: {
-									label: isFavorite ? t('Unfavorite') : t('Favorite'),
-									icon: isFavorite ? 'star-filled' : 'star',
-								},
-								action: handleToggleFavorite,
-							},
-						}
-					: {}),
-				...(canLeave && {
-					leaveRoom: {
-						label: { label: t('Leave_room'), icon: 'sign-out' },
-						action: handleLeave,
-					},
-				}),
-			}),
-			...(isOmnichannelRoom && prioritiesMenu),
-		}),
-		[
-			hideDefaultOptions,
-			t,
-			handleHide,
-			isUnread,
-			handleToggleRead,
-			canFavorite,
-			isFavorite,
-			handleToggleFavorite,
-			canLeave,
-			handleLeave,
-			isOmnichannelRoom,
-			prioritiesMenu,
-		],
-	);
-
-	return (
-		<Menu
-			rcx-sidebar-item__menu
-			title={t('Options')}
-			mini
-			aria-keyshortcuts='alt'
-			options={menuOptions}
-			maxHeight={300}
-			renderItem={({ label: { label, icon }, ...props }): JSX.Element => <Option label={label} icon={icon} {...props} />}
-		/>
-	);
+	return <GenericMenu detached className='rcx-sidebar-item__menu' title={t('Options')} mini aria-keyshortcuts='alt' sections={sections} />;
 };
 
 export default memo(RoomMenu);
diff --git a/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx b/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx
index 49179f12c73..ee6c1a505e0 100644
--- a/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx
+++ b/apps/meteor/client/views/account/profile/ActionConfirmModal.tsx
@@ -11,6 +11,7 @@ type ActionConfirmModalProps = {
 	onCancel: () => void;
 };
 
+// TODO: Use react-hook-form
 const ActionConfirmModal = ({ isPassword, onConfirm, onCancel }: ActionConfirmModalProps) => {
 	const { t } = useTranslation();
 	const [inputText, setInputText] = useState('');
diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts
index 46f80610442..5c8d83a9d1f 100644
--- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts
+++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts
@@ -79,14 +79,14 @@ export class HomeSidenav {
 		const sidebarItem = this.getSidebarItemByName(name);
 		await sidebarItem.focus();
 		await sidebarItem.locator('.rcx-sidebar-item__menu').click();
-		await this.page.getByRole('option', { name: 'Mark Unread' }).click();
+		await this.page.getByRole('menuitem', { name: 'Mark Unread' }).click();
 	}
 
 	async selectPriority(name: string, priority: string) {
 		const sidebarItem = this.getSidebarItemByName(name);
 		await sidebarItem.focus();
 		await sidebarItem.locator('.rcx-sidebar-item__menu').click();
-		await this.page.locator(`li[value="${priority}"]`).click();
+		await this.page.getByRole('menuitem', { name: priority }).click();
 	}
 
 	async openAdministrationByLabel(text: string): Promise<void> {
diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts
index a34a1af480a..9cc548438a7 100644
--- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts
+++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts
@@ -79,7 +79,7 @@ export class Sidebar {
 		await item.hover();
 		await item.focus();
 		await item.locator('.rcx-sidebar-item__menu').click();
-		await this.page.getByRole('option', { name: 'Mark Unread' }).click();
+		await this.page.getByRole('menuitem', { name: 'Mark Unread' }).click();
 	}
 
 	getCollapseGroupByName(name: string): Locator {
diff --git a/packages/ui-client/src/components/GenericMenu/GenericMenuItem.tsx b/packages/ui-client/src/components/GenericMenu/GenericMenuItem.tsx
index 41576a2a40a..dcf6043d5a2 100644
--- a/packages/ui-client/src/components/GenericMenu/GenericMenuItem.tsx
+++ b/packages/ui-client/src/components/GenericMenu/GenericMenuItem.tsx
@@ -4,6 +4,7 @@ import type { ComponentProps, MouseEvent, ReactNode } from 'react';
 export type GenericMenuItemProps = {
 	id: string;
 	icon?: ComponentProps<typeof MenuItemIcon>['name'];
+	iconColor?: ComponentProps<typeof MenuItemIcon>['color'];
 	content?: ReactNode;
 	addon?: ReactNode;
 	onClick?: (e?: MouseEvent<HTMLElement>) => void;
@@ -15,10 +16,10 @@ export type GenericMenuItemProps = {
 	variant?: string;
 };
 
-const GenericMenuItem = ({ icon, content, addon, status, gap, tooltip }: GenericMenuItemProps) => (
+const GenericMenuItem = ({ icon, iconColor, content, addon, status, gap, tooltip }: GenericMenuItemProps) => (
 	<>
 		{gap && <MenuItemColumn />}
-		{icon && <MenuItemIcon name={icon} />}
+		{icon && <MenuItemIcon name={icon} color={iconColor} />}
 		{status && <MenuItemColumn>{status}</MenuItemColumn>}
 		{content && <MenuItemContent title={tooltip}>{content}</MenuItemContent>}
 		{addon && <MenuItemInput>{addon}</MenuItemInput>}
-- 
GitLab