Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
dougfabris committed Jan 9, 2025
1 parent 33dbe1d commit ab5b1a4
Show file tree
Hide file tree
Showing 18 changed files with 307 additions and 483 deletions.
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: 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<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
8 changes: 2 additions & 6 deletions apps/meteor/client/sidebar/Item/Condensed.tsx
Original file line number Diff line number Diff line change
@@ -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';
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) => {
setMenuVisibility(e.target.offsetWidth > 0 && Boolean(menu));
});
const handleMenuEvent = {
[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setMenuVisibility,
};

return (
Expand Down
Loading

0 comments on commit ab5b1a4

Please sign in to comment.