diff --git a/.changeset/red-crews-behave.md b/.changeset/red-crews-behave.md new file mode 100644 index 0000000000000..19608792e73d0 --- /dev/null +++ b/.changeset/red-crews-behave.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-kit': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduces new property `category` for Rocket.Chat Apps to register UI action buttons. This property is used to group buttons in the UI. diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx index 183aea7190d9b..17357be8b6ab7 100644 --- a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx @@ -19,6 +19,7 @@ import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoT import { useChat } from '../../../views/room/contexts/ChatContext'; import { useRoomToolbox } from '../../../views/room/contexts/RoomToolboxContext'; import MessageActionMenu from './MessageActionMenu'; +import MessageToolbarStarsActionMenu from './MessageToolbarStarsActionMenu'; import { useWebDAVMessageAction } from './useWebDAVMessageAction'; const getMessageContext = (message: IMessage, room: IRoom, context?: MessageActionContext): MessageActionContext => { @@ -78,6 +79,8 @@ const MessageToolbar = ({ const actionButtonApps = useMessageActionAppsActionButtons(context); + const starsAction = useMessageActionAppsActionButtons(context, 'ai'); + const { messageToolbox: hiddenActions } = useLayoutHiddenActions(); // TODO: move this to another place @@ -135,6 +138,19 @@ const MessageToolbar = ({ disabled={action?.disabled?.({ message, room, user, subscription, settings: mapSettings, chat, context })} /> ))} + {starsAction.data && starsAction.data.length > 0 && ( + ({ + ...action, + action: (e) => action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions }), + }))} + onChangeMenuVisibility={onChangeMenuVisibility} + data-qa-type='message-action-stars-menu-options' + context={{ message, room, user, subscription, settings: mapSettings, chat, context }} + isMessageEncrypted={isE2EEMessage(message)} + /> + )} + {actionsQueryResult.isSuccess && actionsQueryResult.data.menu.length > 0 && ( ({ diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx new file mode 100644 index 0000000000000..7c88d89ac2057 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx @@ -0,0 +1,89 @@ +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu, type GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { MouseEvent, ReactElement } from 'react'; +import React from 'react'; + +import type { MessageActionConditionProps, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; + +type MessageActionConfigOption = Omit & { + action: (e?: MouseEvent) => void; +}; + +type MessageActionSection = { + id: string; + title: string; + items: GenericMenuItemProps[]; +}; + +type MessageActionMenuProps = { + onChangeMenuVisibility: (visible: boolean) => void; + options: MessageActionConfigOption[]; + context: MessageActionConditionProps; + isMessageEncrypted: boolean; +}; + +const MessageToolbarStarsActionMenu = ({ + options, + onChangeMenuVisibility, + context, + isMessageEncrypted, +}: MessageActionMenuProps): ReactElement => { + const t = useTranslation(); + const id = useUniqueId(); + + const groupOptions = options.reduce((acc, option) => { + const transformedOption = { + variant: option.color === 'alert' ? 'danger' : '', + id: option.id, + icon: option.icon, + content: t(option.label), + onClick: option.action, + type: option.type, + ...(option.disabled && { disabled: option?.disabled?.(context) }), + ...(option.disabled && + option?.disabled?.(context) && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }), + }; + + const group = option.type || ''; + let section = acc.find((section: { id: string }) => section.id === group); + + if (!section) { + section = { id: group, title: '', items: [] }; + acc.push(section); + } + + // Add option to the appropriate section + section.items.push(transformedOption); + + // Handle the "apps" section if message is encrypted + if (group === 'apps' && isMessageEncrypted) { + section.items = [ + { + content: t('Unavailable'), + id, + disabled: true, + gap: false, + tooltip: t('Action_not_available_encrypted_content', { action: t('Apps') }), + }, + ]; + } + + return acc; + }, [] as MessageActionSection[]); + + return ( + + ); +}; + +export default MessageToolbarStarsActionMenu; diff --git a/apps/meteor/client/hooks/useAppActionButtons.ts b/apps/meteor/client/hooks/useAppActionButtons.ts index 64c46f370a1f4..3e21ca6a668d1 100644 --- a/apps/meteor/client/hooks/useAppActionButtons.ts +++ b/apps/meteor/client/hooks/useAppActionButtons.ts @@ -1,4 +1,4 @@ -import type { IUIActionButton, UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui'; +import { type IUIActionButton, type UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useStream, useToastMessageDispatch, useUserId } from '@rocket.chat/ui-contexts'; @@ -13,6 +13,7 @@ import type { MessageBoxAction } from '../../app/ui-utils/client/lib/messageBox' import { Utilities } from '../../ee/lib/misc/Utilities'; import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; import { useApplyButtonFilters, useApplyButtonAuthFilter } from './useApplyButtonFilters'; +import { useFilterActionsByContextAndCategory } from './useFilterActions'; const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${appId}/${actionId}`; @@ -160,20 +161,18 @@ export const useUserDropdownAppsActionButtons = () => { } as UseQueryResult; }; -export const useMessageActionAppsActionButtons = (context?: MessageActionContext) => { +export const useMessageActionAppsActionButtons = (context?: MessageActionContext, category?: string) => { const result = useAppActionButtons('messageAction'); const actionManager = useUiKitActionManager(); const applyButtonFilters = useApplyButtonFilters(); const dispatchToastMessage = useToastMessageDispatch(); const { t } = useTranslation(); + const filterActionsByContextAndCategory = useFilterActionsByContextAndCategory(context, category); const data = useMemo( () => result.data ?.filter((action) => { - if ( - context && - !(action.when?.messageActionContext || ['message', 'message-mobile', 'threads', 'starred']).includes(context as any) - ) { + if (!filterActionsByContextAndCategory(action)) { return false; } return applyButtonFilters(action); @@ -212,7 +211,7 @@ export const useMessageActionAppsActionButtons = (context?: MessageActionContext return item; }), - [actionManager, applyButtonFilters, context, dispatchToastMessage, result.data, t], + [actionManager, applyButtonFilters, dispatchToastMessage, filterActionsByContextAndCategory, result.data, t], ); return { ...result, diff --git a/apps/meteor/client/hooks/useFilterActions.ts b/apps/meteor/client/hooks/useFilterActions.ts new file mode 100644 index 0000000000000..5eab0b795a59b --- /dev/null +++ b/apps/meteor/client/hooks/useFilterActions.ts @@ -0,0 +1,24 @@ +import { MessageActionContext } from '@rocket.chat/apps-engine/definition/ui'; +import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; +import { useCallback } from 'react'; + +const DEFAULT_CATEGORY = 'default'; + +export const useFilterActionsByContextAndCategory = (context: string | undefined, category = 'default') => { + return useCallback( + (action: IUIActionButton) => { + if (!context) { + return true; + } + + const actionCategory = action?.category ?? DEFAULT_CATEGORY; + const messageActionContext = action.when?.messageActionContext || Object.values(MessageActionContext); + const isContextMatch = messageActionContext.includes(context as MessageActionContext); + + const isCategoryMatch = category === DEFAULT_CATEGORY ? actionCategory === DEFAULT_CATEGORY : actionCategory === category; + + return isContextMatch && isCategoryMatch; + }, + [context, category], + ); +}; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index d77854e2e1469..b89b760caadf1 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -392,6 +392,7 @@ "Agent_Without_Extensions": "Agent Without Extensions", "Agents": "Agents", "Agree": "Agree", + "AI_Actions": "AI Actions", "Alerts": "Alerts", "Alias": "Alias", "Alias_Format": "Alias Format",