diff --git a/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx b/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx new file mode 100644 index 000000000000..96bc0b616bef --- /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: check leaving modal for teams +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 => { + 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( + 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 000000000000..70284057aee7 --- /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 000000000000..133acd9b0f78 --- /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 000000000000..46308772b429 --- /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 9fbdc8be019c..c628afc34595 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['options'] | Record => { +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 { - return ( - <> - {label} - - ); - }, []); - - return useMemo['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>((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 bad31ac8af3f..e41ef88a2415 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, 'name' | 'color'> & { showUnprioritized?: boolean; }; -const PRIORITY_ICONS: Record = { +export const PRIORITY_ICONS: Record = { + [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 ( - - - - ); + if (!showUnprioritized && level === LivechatPriorityWeight.NOT_SPECIFIED) { + return null; } return iconName ? : null; diff --git a/apps/meteor/client/sidebar/Item/Condensed.tsx b/apps/meteor/client/sidebar/Item/Condensed.tsx index 792c39903393..331a01949aad 100644 --- a/apps/meteor/client/sidebar/Item/Condensed.tsx +++ b/apps/meteor/client/sidebar/Item/Condensed.tsx @@ -1,5 +1,5 @@ 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 } from 'react'; import { memo, useState } from 'react'; @@ -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) => { - setMenuVisibility(e.target.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 e236f8660bc7..53015d17e036 100644 --- a/apps/meteor/client/sidebar/Item/Extended.tsx +++ b/apps/meteor/client/sidebar/Item/Extended.tsx @@ -1,5 +1,5 @@ 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 } from 'react'; import { memo, useState } from 'react'; @@ -42,15 +42,10 @@ const Extended = ({ }: ExtendedProps) => { const formatDate = useShortTimeAgo(); const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION); - const isReduceMotionEnabled = usePrefersReducedMotion(); - const handleMenu = useEffectEvent((e) => { - setMenuVisibility(e.target.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 f1f37047ece4..2b9dd8fef490 100644 --- a/apps/meteor/client/sidebar/Item/Medium.tsx +++ b/apps/meteor/client/sidebar/Item/Medium.tsx @@ -1,5 +1,5 @@ import { Sidebar, IconButton } from '@rocket.chat/fuselage'; -import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks'; +import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks'; import type { ReactNode } from 'react'; import { memo, useState } from 'react'; @@ -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) => { - setMenuVisibility(e.target.offsetWidth > 0 && Boolean(menu)); - }); const handleMenuEvent = { - [isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu, + [isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility, }; return ( diff --git a/apps/meteor/client/sidebar/RoomMenu.tsx b/apps/meteor/client/sidebar/RoomMenu.tsx index 14e453da5401..508753263954 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 => { - 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( - , - ); - }); - - 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 ( -