Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Replace Menu in favor of GenericMenu in RoomMenu #34927

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
18 changes: 18 additions & 0 deletions apps/meteor/client/hooks/menuActions/useToggleFavoriteAction.ts
Original file line number Diff line number Diff line change
@@ -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;
};
48 changes: 48 additions & 0 deletions apps/meteor/client/hooks/menuActions/useToggleReadAction.ts
Original file line number Diff line number Diff line change
@@ -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;
};
118 changes: 118 additions & 0 deletions apps/meteor/client/hooks/useRoomMenuActions.ts
Original file line number Diff line number Diff line change
@@ -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[] }];
};
Original file line number Diff line number Diff line change
@@ -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 });
Expand All @@ -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]);
};
15 changes: 7 additions & 8 deletions apps/meteor/client/omnichannel/priorities/PriorityIcon.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(),
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 3 additions & 7 deletions apps/meteor/client/sidebar/Item/Condensed.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 (
Expand Down
Loading
Loading