diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index 3901ef2df8e7..742ea4c9a5b7 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -1,96 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; import { AutoTranslate } from './autotranslate'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { - hasTranslationLanguageInAttachments, - hasTranslationLanguageInMessage, -} from '../../../../client/views/room/MessageList/lib/autoTranslate'; -import { hasAtLeastOnePermission } from '../../../authorization/client'; -import { Messages } from '../../../models/client'; -import { settings } from '../../../settings/client'; -import { MessageAction } from '../../../ui-utils/client/lib/MessageAction'; -import { sdk } from '../../../utils/client/lib/SDKClient'; Meteor.startup(() => { AutoTranslate.init(); - - Tracker.autorun(() => { - if (settings.get('AutoTranslate_Enabled') && hasAtLeastOnePermission(['auto-translate'])) { - MessageAction.addButton({ - id: 'translate', - icon: 'language', - label: 'Translate', - context: ['message', 'message-mobile', 'threads'], - type: 'interaction', - action(_, { message }) { - const language = AutoTranslate.getLanguage(message.rid); - if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { - (AutoTranslate.messageIdsToWait as any)[message._id] = true; - Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); - void sdk.call('autoTranslate.translateMessage', message, language); - } - const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; - Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); - }, - condition({ message, subscription, user, room }) { - if (!user) { - return false; - } - const language = subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid) || ''; - const isLivechatRoom = roomCoordinator.isLivechatRoom(room?.t); - const isDifferentUser = message?.u && message.u._id !== user._id; - const autoTranslateEnabled = subscription?.autoTranslate || isLivechatRoom; - const hasLanguage = - hasTranslationLanguageInMessage(message, language) || hasTranslationLanguageInAttachments(message.attachments, language); - - return Boolean( - (message as { autoTranslateShowInverse?: boolean }).autoTranslateShowInverse || - (isDifferentUser && autoTranslateEnabled && !hasLanguage), - ); - }, - order: 90, - }); - MessageAction.addButton({ - id: 'view-original', - icon: 'language', - label: 'View_original', - context: ['message', 'message-mobile', 'threads'], - type: 'interaction', - action(_, props) { - const { message } = props; - const language = AutoTranslate.getLanguage(message.rid); - if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { - (AutoTranslate.messageIdsToWait as any)[message._id] = true; - Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); - void sdk.call('autoTranslate.translateMessage', message, language); - } - const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; - Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); - }, - condition({ message, subscription, user, room }) { - const language = subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid) || ''; - const isLivechatRoom = roomCoordinator.isLivechatRoom(room?.t); - if (!user) { - return false; - } - const isDifferentUser = message?.u && message.u._id !== user._id; - const autoTranslateEnabled = subscription?.autoTranslate || isLivechatRoom; - const hasLanguage = - hasTranslationLanguageInMessage(message, language) || hasTranslationLanguageInAttachments(message.attachments, language); - - return Boolean( - !(message as { autoTranslateShowInverse?: boolean }).autoTranslateShowInverse && - isDifferentUser && - autoTranslateEnabled && - hasLanguage, - ); - }, - order: 90, - }); - } else { - MessageAction.removeButton('toggle-language'); - } - }); }); diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 1cf02277878a..0309b406feb7 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -37,7 +37,7 @@ Meteor.startup(() => { export const AutoTranslate = { initialized: false, providersMetadata: {} as { [providerNamer: string]: { name: string; displayName: string } }, - messageIdsToWait: {} as { [messageId: string]: string }, + messageIdsToWait: {} as { [messageId: string]: boolean }, supportedLanguages: [] as ISupportedLanguage[] | undefined, findSubscriptionByRid: mem((rid) => Subscriptions.findOne({ rid })), diff --git a/apps/meteor/app/ui-utils/client/index.ts b/apps/meteor/app/ui-utils/client/index.ts index 6409db5a3592..7fdb76e73d4e 100644 --- a/apps/meteor/app/ui-utils/client/index.ts +++ b/apps/meteor/app/ui-utils/client/index.ts @@ -1,6 +1,3 @@ -import './lib/messageActionDefault'; - -export { MessageAction } from './lib/MessageAction'; export { messageBox } from './lib/messageBox'; export { LegacyRoomManager } from './lib/LegacyRoomManager'; export { upsertMessage, RoomHistoryManager } from './lib/RoomHistoryManager'; diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 0a5483c4acaa..5a1710dc4830 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -1,14 +1,7 @@ -import type { IMessage, IUser, ISubscription, IRoom, SettingValue, ITranslatedMessage } from '@rocket.chat/core-typings'; import type { Keys as IconName } from '@rocket.chat/icons'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import mem from 'mem'; -import type { ContextType } from 'react'; -import type { AutoTranslateOptions } from '../../../../client/views/room/MessageList/hooks/useAutoTranslate'; -import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; -import type { RoomToolboxContextValue } from '../../../../client/views/room/contexts/RoomToolboxContext'; - -type MessageActionGroup = 'message' | 'menu'; +type MessageActionGroup = 'menu'; export type MessageActionContext = | 'message' @@ -25,92 +18,17 @@ export type MessageActionContext = type MessageActionType = 'communication' | 'interaction' | 'duplication' | 'apps' | 'management'; -export type MessageActionConditionProps = { - message: IMessage; - user: IUser | undefined; - room: IRoom; - subscription?: ISubscription; - context?: MessageActionContext; - settings: { [key: string]: SettingValue }; - chat: ContextType; -}; - export type MessageActionConfig = { id: string; icon: IconName; variant?: 'danger' | 'success' | 'warning'; label: TranslationKey; - order?: number; - /* @deprecated */ - color?: string; - role?: string; - group?: MessageActionGroup | MessageActionGroup[]; + order: number; + /** @deprecated */ + color?: 'alert'; + group: MessageActionGroup; context?: MessageActionContext[]; - action: ( - e: Pick | undefined, - { - message, - tabbar, - room, - chat, - autoTranslateOptions, - }: { - message: IMessage & Partial; - tabbar: RoomToolboxContextValue; - room?: IRoom; - chat: ContextType; - autoTranslateOptions?: AutoTranslateOptions; - }, - ) => any; - condition?: (props: MessageActionConditionProps) => Promise | boolean; + action: (e: Pick | undefined) => any; type?: MessageActionType; - disabled?: (props: MessageActionConditionProps) => boolean; + disabled?: boolean; }; - -class MessageAction { - public buttons: Record = {}; - - public addButton(config: MessageActionConfig): void { - if (!config?.id) { - return; - } - - if (!config.group) { - config.group = 'menu'; - } - - if (config.condition) { - config.condition = mem(config.condition, { maxAge: 1000, cacheKey: JSON.stringify }); - } - - this.buttons[config.id] = config; - } - - public removeButton(id: MessageActionConfig['id']): void { - delete this.buttons[id]; - } - - public async getAll( - props: MessageActionConditionProps, - context: MessageActionContext, - group: MessageActionGroup, - ): Promise { - return ( - await Promise.all( - Object.values(this.buttons) - .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) - .filter((button) => !button.group || (Array.isArray(button.group) ? button.group.includes(group) : button.group === group)) - .filter((button) => !button.context || button.context.includes(context)) - .map(async (button) => { - return [button, !button.condition || (await button.condition({ ...props, context }))] as const; - }), - ) - ) - .filter(([, condition]) => condition) - .map(([button]) => button); - } -} - -const instance = new MessageAction(); - -export { instance as MessageAction }; diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts deleted file mode 100644 index 1301863b7e53..000000000000 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import { isE2EEMessage, isRoomFederated } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; - -import { MessageAction } from './MessageAction'; -import { getPermaLink } from '../../../../client/lib/getPermaLink'; -import { imperativeModal } from '../../../../client/lib/imperativeModal'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { dispatchToastMessage } from '../../../../client/lib/toast'; -import { router } from '../../../../client/providers/RouterProvider'; -import ForwardMessageModal from '../../../../client/views/room/modals/ForwardMessageModal/ForwardMessageModal'; -import ReactionListModal from '../../../../client/views/room/modals/ReactionListModal'; -import ReportMessageModal from '../../../../client/views/room/modals/ReportMessageModal'; -import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/client'; -import { Rooms, Subscriptions } from '../../../models/client'; -import { t } from '../../../utils/lib/i18n'; - -const getMainMessageText = (message: IMessage): IMessage => { - const newMessage = { ...message }; - newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.description || newMessage.attachments?.[0]?.title || ''; - newMessage.md = newMessage.md || newMessage.attachments?.[0]?.descriptionMd || undefined; - return { ...newMessage }; -}; - -Meteor.startup(async () => { - MessageAction.addButton({ - id: 'reply-directly', - icon: 'reply-directly', - label: 'Reply_in_direct_message', - context: ['message', 'message-mobile', 'threads', 'federated'], - role: 'link', - type: 'communication', - action(_, { message }) { - roomCoordinator.openRouteLink( - 'd', - { name: message.u.username }, - { - ...router.getSearchParameters(), - reply: message._id, - }, - ); - }, - condition({ subscription, room, message, user }) { - if (subscription == null) { - return false; - } - if (room.t === 'd' || room.t === 'l') { - return false; - } - - // Check if we already have a DM started with the message user (not ourselves) or we can start one - if (!!user && user._id !== message.u._id && !hasPermission('create-d')) { - const dmRoom = Rooms.findOne({ _id: [user._id, message.u._id].sort().join('') }); - if (!dmRoom || !Subscriptions.findOne({ 'rid': dmRoom._id, 'u._id': user._id })) { - return false; - } - } - - return true; - }, - order: 0, - group: 'menu', - disabled({ message }) { - return isE2EEMessage(message); - }, - }); - - MessageAction.addButton({ - id: 'forward-message', - icon: 'arrow-forward', - label: 'Forward_message', - context: ['message', 'message-mobile', 'threads'], - type: 'communication', - async action(_, { message }) { - const permalink = await getPermaLink(message._id); - imperativeModal.open({ - component: ForwardMessageModal, - props: { - message, - permalink, - onClose: (): void => { - imperativeModal.close(); - }, - }, - }); - }, - order: 0, - group: 'message', - disabled({ message }) { - return isE2EEMessage(message); - }, - }); - - MessageAction.addButton({ - id: 'quote-message', - icon: 'quote', - label: 'Quote', - context: ['message', 'message-mobile', 'threads', 'federated'], - async action(_, { message, chat, autoTranslateOptions }) { - if (message && autoTranslateOptions?.autoTranslateEnabled && autoTranslateOptions.showAutoTranslate(message)) { - message.msg = - message.translations && autoTranslateOptions.autoTranslateLanguage - ? message.translations[autoTranslateOptions.autoTranslateLanguage] - : message.msg; - } - - await chat?.composer?.quoteMessage(message); - }, - condition({ subscription }) { - if (subscription == null) { - return false; - } - - return true; - }, - order: -2, - group: 'message', - }); - - MessageAction.addButton({ - id: 'copy', - icon: 'copy', - label: 'Copy_text', - // classes: 'clipboard', - context: ['message', 'message-mobile', 'threads', 'federated'], - type: 'duplication', - async action(_, { message }) { - const msgText = getMainMessageText(message).msg; - await navigator.clipboard.writeText(msgText); - dispatchToastMessage({ type: 'success', message: t('Copied') }); - }, - condition({ subscription }) { - return !!subscription; - }, - order: 6, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'edit-message', - icon: 'edit', - label: 'Edit', - context: ['message', 'message-mobile', 'threads', 'federated'], - type: 'management', - async action(_, { message, chat }) { - await chat?.messageEditing.editMessage(message); - }, - condition({ message, subscription, settings, room, user }) { - if (subscription == null) { - return false; - } - if (isRoomFederated(room)) { - return message.u._id === user?._id; - } - const canEditMessage = hasAtLeastOnePermission('edit-message', message.rid); - const isEditAllowed = settings.Message_AllowEditing; - const editOwn = message.u && message.u._id === user?._id; - if (!(canEditMessage || (isEditAllowed && editOwn))) { - return false; - } - const blockEditInMinutes = settings.Message_AllowEditing_BlockEditInMinutes as number; - const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete', message.rid); - - if (!bypassBlockTimeLimit && blockEditInMinutes) { - let msgTs; - if (message.ts != null) { - msgTs = moment(message.ts); - } - let currentTsDiff; - if (msgTs != null) { - currentTsDiff = moment().diff(msgTs, 'minutes'); - } - return (!!currentTsDiff || currentTsDiff === 0) && currentTsDiff < blockEditInMinutes; - } - return true; - }, - order: 8, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'delete-message', - icon: 'trash', - label: 'Delete', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - color: 'alert', - type: 'management', - async action(_, { message, chat }) { - await chat?.flows.requestMessageDeletion(message); - }, - condition({ message, subscription, room, chat, user }) { - if (!subscription) { - return false; - } - if (isRoomFederated(room)) { - return message.u._id === user?._id; - } - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { - return false; - } - - return chat?.data.canDeleteMessage(message) ?? false; - }, - order: 10, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'report-message', - icon: 'report', - label: 'Report', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - color: 'alert', - type: 'management', - action(_, { message }) { - imperativeModal.open({ - component: ReportMessageModal, - props: { - message: getMainMessageText(message), - onClose: imperativeModal.close, - }, - }); - }, - condition({ subscription, room, message, user }) { - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom || message.u._id === user?._id) { - return false; - } - - return Boolean(subscription); - }, - order: 9, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'reaction-list', - icon: 'emoji', - label: 'Reactions', - context: ['message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], - type: 'interaction', - action(_, { message: { reactions = {} } }) { - imperativeModal.open({ - component: ReactionListModal, - props: { reactions, onClose: imperativeModal.close }, - }); - }, - condition({ message: { reactions } }) { - return !!reactions; - }, - order: 9, - group: 'menu', - }); -}); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx index 034ab0367e81..80e61896bed3 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx @@ -3,7 +3,7 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, usePermission, useRouter } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useUserDropdownAppsActionButtons } from '../../../hooks/useAppActionButtons'; +import { useUserDropdownAppsActionButtons } from '../../../hooks/useUserDropdownAppsActionButtons'; import { useAppRequestStats } from '../../../views/marketplace/hooks/useAppRequestStats'; export const useMarketPlaceMenu = () => { diff --git a/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts b/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts index ce8ec3f9cd81..3405512dbe8b 100644 --- a/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts +++ b/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts @@ -27,7 +27,6 @@ export const usePinMessageMutation = () => { }, onSettled: (_data, _error, message) => { queryClient.invalidateQueries(roomsQueryKeys.pinnedMessages(message.rid)); - queryClient.invalidateQueries(roomsQueryKeys.messageActions(message.rid, message._id)); }, }); }; diff --git a/apps/meteor/client/components/message/hooks/useStarMessageMutation.ts b/apps/meteor/client/components/message/hooks/useStarMessageMutation.ts index da73b73eacd6..eabfc8692643 100644 --- a/apps/meteor/client/components/message/hooks/useStarMessageMutation.ts +++ b/apps/meteor/client/components/message/hooks/useStarMessageMutation.ts @@ -27,7 +27,6 @@ export const useStarMessageMutation = () => { }, onSettled: (_data, _error, message) => { queryClient.invalidateQueries(roomsQueryKeys.starredMessages(message.rid)); - queryClient.invalidateQueries(roomsQueryKeys.messageActions(message.rid, message._id)); }, }); }; diff --git a/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts b/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts index a3c4c2882b0b..f777929d5689 100644 --- a/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts +++ b/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts @@ -27,7 +27,6 @@ export const useUnpinMessageMutation = () => { }, onSettled: (_data, _error, message) => { queryClient.invalidateQueries(roomsQueryKeys.pinnedMessages(message.rid)); - queryClient.invalidateQueries(roomsQueryKeys.messageActions(message.rid, message._id)); }, }); }; diff --git a/apps/meteor/client/components/message/hooks/useUnstarMessageMutation.ts b/apps/meteor/client/components/message/hooks/useUnstarMessageMutation.ts index 7cb29fd0bc3f..329e931fe116 100644 --- a/apps/meteor/client/components/message/hooks/useUnstarMessageMutation.ts +++ b/apps/meteor/client/components/message/hooks/useUnstarMessageMutation.ts @@ -27,7 +27,6 @@ export const useUnstarMessageMutation = () => { }, onSettled: (_data, _error, message) => { queryClient.invalidateQueries(roomsQueryKeys.starredMessages(message.rid)); - queryClient.invalidateQueries(roomsQueryKeys.messageActions(message.rid, message._id)); }, }); }; diff --git a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx deleted file mode 100644 index 143c0a3fe46a..000000000000 --- a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { GenericMenu, type GenericMenuItemProps } from '@rocket.chat/ui-client'; -import type { MouseEvent, ReactElement } from 'react'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -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 MessageActionMenu = ({ options, onChangeMenuVisibility, context, isMessageEncrypted }: MessageActionMenuProps): ReactElement => { - const { t } = useTranslation(); - const id = useUniqueId(); - const groupOptions = options - .map((option) => ({ - 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) }) }), - })) - .reduce( - (acc, option) => { - const group = option.type ? option.type : ''; - const section = acc.find((section: { id: string }) => section.id === group); - if (section) { - section.items.push(option); - return acc; - } - const newSection = { id: group, title: group === 'apps' ? t('Apps') : '', items: [option] }; - acc.push(newSection); - - return acc; - }, - [] as unknown as MessageActionSection[], - ) - .map((section) => { - if (section.id !== 'apps') { - return section; - } - - if (!isMessageEncrypted) { - return section; - } - - return { - id: 'apps', - title: t('Apps'), - items: [ - { - content: t('Unavailable'), - type: 'apps', - id, - disabled: true, - gap: false, - tooltip: t('Action_not_available_encrypted_content', { action: t('Apps') }), - }, - ], - }; - }); - - return ( - - ); -}; - -export default MessageActionMenu; diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx index 9e5a0f10f85f..113b5278cdff 100644 --- a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx @@ -1,39 +1,25 @@ import { useToolbar } from '@react-aria/toolbar'; import type { IMessage, IRoom, ISubscription, ITranslatedMessage } from '@rocket.chat/core-typings'; -import { isThreadMessage, isRoomFederated, isVideoConfMessage, isE2EEMessage } from '@rocket.chat/core-typings'; -import { MessageToolbar as FuselageMessageToolbar, MessageToolbarItem } from '@rocket.chat/fuselage'; -import { useFeaturePreview } from '@rocket.chat/ui-client'; -import { useUser, useSettings, useTranslation, useMethod, useLayoutHiddenActions, useSetting } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import type { ComponentProps, ReactElement } from 'react'; -import React, { memo, useMemo, useRef } from 'react'; +import { isThreadMessage, isRoomFederated, isVideoConfMessage } from '@rocket.chat/core-typings'; +import { MessageToolbar as FuselageMessageToolbar } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, ElementType, ReactElement } from 'react'; +import React, { memo, useRef } from 'react'; -import MessageActionMenu from './MessageActionMenu'; +import MessageToolbarActionMenu from './MessageToolbarActionMenu'; import MessageToolbarStarsActionMenu from './MessageToolbarStarsActionMenu'; -import { useFollowMessageAction } from './useFollowMessageAction'; -import { useJumpToMessageContextAction } from './useJumpToMessageContextAction'; -import { useMarkAsUnreadMessageAction } from './useMarkAsUnreadMessageAction'; -import { useNewDiscussionMessageAction } from './useNewDiscussionMessageAction'; -import { usePermalinkAction } from './usePermalinkAction'; -import { usePinMessageAction } from './usePinMessageAction'; -import { useReactionMessageAction } from './useReactionMessageAction'; -import { useReplyInThreadMessageAction } from './useReplyInThreadMessageAction'; -import { useStarMessageAction } from './useStarMessageAction'; -import { useUnFollowMessageAction } from './useUnFollowMessageAction'; -import { useUnpinMessageAction } from './useUnpinMessageAction'; -import { useUnstarMessageAction } from './useUnstarMessageAction'; -import { useWebDAVMessageAction } from './useWebDAVMessageAction'; +import DefaultItems from './items/DefaultItems'; +import DirectItems from './items/DirectItems'; +import FederatedItems from './items/FederatedItems'; +import MentionsItems from './items/MentionsItems'; +import MobileItems from './items/MobileItems'; +import PinnedItems from './items/PinnedItems'; +import SearchItems from './items/SearchItems'; +import StarredItems from './items/StarredItems'; +import ThreadsItems from './items/ThreadsItems'; +import VideoconfItems from './items/VideoconfItems'; +import VideoconfThreadsItems from './items/VideoconfThreadsItems'; import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; -import { useEmojiPickerData } from '../../../contexts/EmojiPickerContext'; -import { useMessageActionAppsActionButtons } from '../../../hooks/useAppActionButtons'; -import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; -import { roomsQueryKeys } from '../../../lib/queryKeys'; -import EmojiElement from '../../../views/composer/EmojiPicker/EmojiElement'; -import { useIsSelecting } from '../../../views/room/MessageList/contexts/SelectedMessagesContext'; -import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; -import { useChat } from '../../../views/room/contexts/ChatContext'; -import { useRoomToolbox } from '../../../views/room/contexts/RoomToolboxContext'; const getMessageContext = (message: IMessage, room: IRoom, context?: MessageActionContext): MessageActionContext => { if (context) { @@ -55,6 +41,23 @@ const getMessageContext = (message: IMessage, room: IRoom, context?: MessageActi return 'message'; }; +const itemsByContext: Record< + MessageActionContext, + ElementType<{ message: IMessage; room: IRoom; subscription: ISubscription | undefined }> +> = { + 'message': DefaultItems, + 'message-mobile': MobileItems, + 'threads': ThreadsItems, + 'videoconf': VideoconfItems, + 'videoconf-threads': VideoconfThreadsItems, + 'pinned': PinnedItems, + 'direct': DirectItems, + 'starred': StarredItems, + 'mentions': MentionsItems, + 'federated': FederatedItems, + 'search': SearchItems, +}; + type MessageToolbarProps = { message: IMessage & Partial; messageContext?: MessageActionContext; @@ -72,151 +75,25 @@ const MessageToolbar = ({ ...props }: MessageToolbarProps): ReactElement | null => { const t = useTranslation(); - const user = useUser() ?? undefined; - const settings = useSettings(); - const isLayoutEmbedded = useEmbeddedLayout(); const toolbarRef = useRef(null); const { toolbarProps } = useToolbar(props, toolbarRef); - const quickReactionsEnabled = useFeaturePreview('quickReactions'); - - const setReaction = useMethod('setReaction'); - const context = getMessageContext(message, room, messageContext); - const mapSettings = useMemo(() => Object.fromEntries(settings.map((setting) => [setting._id, setting.value])), [settings]); - - const chat = useChat(); - const { quickReactions, addRecentEmoji } = useEmojiPickerData(); - - const actionButtonApps = useMessageActionAppsActionButtons(context); - - const starsAction = useMessageActionAppsActionButtons(context, 'ai'); - - const { messageToolbox: hiddenActions } = useLayoutHiddenActions(); - const allowStarring = useSetting('Message_AllowStarring'); - - // TODO: move this to another place - useWebDAVMessageAction(); - useNewDiscussionMessageAction(); - useUnpinMessageAction(message, { room, subscription }); - usePinMessageAction(message, { room, subscription }); - useStarMessageAction(message, { room, user }); - useUnstarMessageAction(message, { room, user }); - usePermalinkAction(message, { subscription, id: 'permalink-star', context: ['starred'], order: 10 }); - usePermalinkAction(message, { subscription, id: 'permalink-pinned', context: ['pinned'], order: 5 }); - usePermalinkAction(message, { - subscription, - id: 'permalink', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - type: 'duplication', - order: 5, - }); - useFollowMessageAction(message, { room, user, context }); - useUnFollowMessageAction(message, { room, user, context }); - useReplyInThreadMessageAction(message, { room, subscription }); - useJumpToMessageContextAction(message, { - id: 'jump-to-message', - order: 100, - context: ['mentions', 'threads', 'videoconf-threads', 'message-mobile', 'search'], - }); - useJumpToMessageContextAction(message, { - id: 'jump-to-pin-message', - order: 100, - hidden: !subscription, - context: ['pinned', 'direct'], - }); - useJumpToMessageContextAction(message, { - id: 'jump-to-star-message', - hidden: !allowStarring || !subscription, - order: 100, - context: ['starred'], - }); - useReactionMessageAction(message, { user, room, subscription }); - useMarkAsUnreadMessageAction(message, { user, room, subscription }); - - const actionsQueryResult = useQuery({ - queryKey: roomsQueryKeys.messageActionsWithParameters(room._id, message), - queryFn: async () => { - const props = { message, room, user, subscription, settings: mapSettings, chat }; - - const toolboxItems = await MessageAction.getAll(props, context, 'message'); - const menuItems = await MessageAction.getAll(props, context, 'menu'); - - return { - message: toolboxItems.filter((action) => !hiddenActions.includes(action.id)), - menu: menuItems.filter((action) => !(isLayoutEmbedded && action.id === 'reply-directly') && !hiddenActions.includes(action.id)), - }; - }, - keepPreviousData: true, - }); - - const toolbox = useRoomToolbox(); - - const selecting = useIsSelecting(); - - const autoTranslateOptions = useAutoTranslate(subscription); - - if (selecting || (!actionsQueryResult.data?.message.length && !actionsQueryResult.data?.menu.length)) { - return null; - } - - const isReactionAllowed = actionsQueryResult.data?.message.find(({ id }) => id === 'reaction-message'); - - const handleSetReaction = (emoji: string) => { - setReaction(`:${emoji}:`, message._id); - addRecentEmoji(emoji); - }; + const MessageToolbarItems = itemsByContext[context]; return ( - {quickReactionsEnabled && - isReactionAllowed && - quickReactions.slice(0, 3).map(({ emoji, image }) => { - return handleSetReaction(emoji)} />; - })} - {actionsQueryResult.isSuccess && - actionsQueryResult.data.message.map((action) => ( - action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions })} - key={action.id} - icon={action.icon} - title={ - action?.disabled?.({ message, room, user, subscription, settings: mapSettings, chat, context }) - ? t('Action_not_available_encrypted_content', { action: t(action.label) }) - : t(action.label) - } - data-qa-id={action.label} - data-qa-type='message-action-menu' - 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 && ( - ({ - ...action, - action: (e) => action.action(e, { message, tabbar: toolbox, room, chat, autoTranslateOptions }), - }))} - onChangeMenuVisibility={onChangeMenuVisibility} - data-qa-type='message-action-menu-options' - context={{ message, room, user, subscription, settings: mapSettings, chat, context }} - isMessageEncrypted={isE2EEMessage(message)} - /> - )} + + + ); }; diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx new file mode 100644 index 000000000000..7c0e0e509eaa --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/MessageToolbarActionMenu.tsx @@ -0,0 +1,157 @@ +import { isE2EEMessage, type IMessage, type IRoom, type ISubscription } from '@rocket.chat/core-typings'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu, type GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useCopyAction } from './useCopyAction'; +import { useDeleteMessageAction } from './useDeleteMessageAction'; +import { useEditMessageAction } from './useEditMessageAction'; +import { useFollowMessageAction } from './useFollowMessageAction'; +import { useMarkAsUnreadMessageAction } from './useMarkAsUnreadMessageAction'; +import { useMessageActionAppsActionButtons } from './useMessageActionAppsActionButtons'; +import { useNewDiscussionMessageAction } from './useNewDiscussionMessageAction'; +import { usePermalinkAction } from './usePermalinkAction'; +import { usePinMessageAction } from './usePinMessageAction'; +import { useReadReceiptsDetailsAction } from './useReadReceiptsDetailsAction'; +import { useReplyInDMAction } from './useReplyInDMAction'; +import { useReportMessageAction } from './useReportMessageAction'; +import { useShowMessageReactionsAction } from './useShowMessageReactionsAction'; +import { useStarMessageAction } from './useStarMessageAction'; +import { useTranslateAction } from './useTranslateAction'; +import { useUnFollowMessageAction } from './useUnFollowMessageAction'; +import { useUnpinMessageAction } from './useUnpinMessageAction'; +import { useUnstarMessageAction } from './useUnstarMessageAction'; +import { useViewOriginalTranslationAction } from './useViewOriginalTranslationAction'; +import { useWebDAVMessageAction } from './useWebDAVMessageAction'; +import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { isTruthy } from '../../../../lib/isTruthy'; + +type MessageActionSection = { + id: string; + title: string; + items: GenericMenuItemProps[]; +}; + +type MessageToolbarActionMenuProps = { + message: IMessage; + context: MessageActionContext; + room: IRoom; + subscription: ISubscription | undefined; + onChangeMenuVisibility: (visible: boolean) => void; +}; + +const MessageToolbarActionMenu = ({ message, context, room, subscription, onChangeMenuVisibility }: MessageToolbarActionMenuProps) => { + // TODO: move this to another place + const menuItems = [ + useWebDAVMessageAction(message, { subscription }), + useNewDiscussionMessageAction(message, { room, subscription }), + useUnpinMessageAction(message, { room, subscription }), + usePinMessageAction(message, { room, subscription }), + useStarMessageAction(message, { room }), + useUnstarMessageAction(message, { room }), + usePermalinkAction(message, { id: 'permalink-star', context: ['starred'], order: 10 }), + usePermalinkAction(message, { id: 'permalink-pinned', context: ['pinned'], order: 5 }), + usePermalinkAction(message, { + id: 'permalink', + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + type: 'duplication', + order: 5, + }), + useFollowMessageAction(message, { room, context }), + useUnFollowMessageAction(message, { room, context }), + useMarkAsUnreadMessageAction(message, { room, subscription }), + useTranslateAction(message, { room, subscription }), + useViewOriginalTranslationAction(message, { room, subscription }), + useReplyInDMAction(message, { room, subscription }), + useCopyAction(message, { subscription }), + useEditMessageAction(message, { room, subscription }), + useDeleteMessageAction(message, { room, subscription }), + useReportMessageAction(message, { room, subscription }), + useShowMessageReactionsAction(message), + useReadReceiptsDetailsAction(message), + ]; + + const hiddenActions = useLayoutHiddenActions().messageToolbox; + const data = menuItems + .filter(isTruthy) + .filter((button) => button.group === 'menu') + .filter((button) => !button.context || button.context.includes(context)) + .filter((action) => !hiddenActions.includes(action.id)) + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + + const actionButtonApps = useMessageActionAppsActionButtons(message, context); + + const id = useUniqueId(); + const { t } = useTranslation(); + + if (data.length === 0) { + return null; + } + + const isMessageEncrypted = isE2EEMessage(message); + + const groupOptions = [...data, ...(actionButtonApps.data ?? [])] + .map((option) => ({ + variant: option.color === 'alert' ? 'danger' : '', + id: option.id, + icon: option.icon, + content: t(option.label), + onClick: option.action, + type: option.type, + ...(typeof option.disabled === 'boolean' && { disabled: option.disabled }), + ...(typeof option.disabled === 'boolean' && + option.disabled && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }), + })) + .reduce((acc, option) => { + const group = option.type ? option.type : ''; + const section = acc.find((section: { id: string }) => section.id === group); + if (section) { + section.items.push(option); + return acc; + } + const newSection = { id: group, title: group === 'apps' ? t('Apps') : '', items: [option] }; + acc.push(newSection); + + return acc; + }, [] as MessageActionSection[]) + .map((section) => { + if (section.id !== 'apps') { + return section; + } + + if (!isMessageEncrypted) { + return section; + } + + return { + id: 'apps', + title: t('Apps'), + items: [ + { + content: t('Unavailable'), + type: 'apps', + id, + disabled: true, + gap: false, + tooltip: t('Action_not_available_encrypted_content', { action: t('Apps') }), + }, + ], + }; + }); + + return ( + + ); +}; + +export default MessageToolbarActionMenu; diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarItem.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarItem.tsx new file mode 100644 index 000000000000..368a3b6d6acd --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/MessageToolbarItem.tsx @@ -0,0 +1,35 @@ +import { MessageToolbarItem as FuselageMessageToolbarItem } from '@rocket.chat/fuselage'; +import type { Keys as IconName } from '@rocket.chat/icons'; +import { useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; +import type { MouseEventHandler } from 'react'; +import React from 'react'; + +type MessageToolbarItemProps = { + id: string; + icon: IconName; + title: string; + disabled?: boolean; + qa: string; + onClick: MouseEventHandler; +}; + +const MessageToolbarItem = ({ id, icon, title, disabled, qa, onClick }: MessageToolbarItemProps) => { + const hiddenActions = useLayoutHiddenActions().messageToolbox; + + if (hiddenActions.includes(id)) { + return null; + } + + return ( + + ); +}; + +export default MessageToolbarItem; diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx index 44f9bc558ddd..4c5699c58f7a 100644 --- a/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageToolbarStarsActionMenu.tsx @@ -1,14 +1,11 @@ +import { isE2EEMessage, type IMessage } from '@rocket.chat/core-typings'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { GenericMenu, type GenericMenuItemProps } from '@rocket.chat/ui-client'; -import type { MouseEvent, ReactElement } from 'react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import type { MessageActionConditionProps, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; - -type MessageActionConfigOption = Omit & { - action: (e?: MouseEvent) => void; -}; +import { useMessageActionAppsActionButtons } from './useMessageActionAppsActionButtons'; +import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; type MessageActionSection = { id: string; @@ -17,22 +14,23 @@ type MessageActionSection = { }; type MessageActionMenuProps = { + message: IMessage; + context: MessageActionContext; onChangeMenuVisibility: (visible: boolean) => void; - options: MessageActionConfigOption[]; - context: MessageActionConditionProps; - isMessageEncrypted: boolean; }; -const MessageToolbarStarsActionMenu = ({ - options, - onChangeMenuVisibility, - context, - isMessageEncrypted, -}: MessageActionMenuProps): ReactElement => { +const MessageToolbarStarsActionMenu = ({ message, context, onChangeMenuVisibility }: MessageActionMenuProps) => { + const starsAction = useMessageActionAppsActionButtons(message, context, 'ai'); const { t } = useTranslation(); const id = useUniqueId(); - const groupOptions = options.reduce((acc, option) => { + if (!starsAction.data?.length) { + return null; + } + + const isMessageEncrypted = isE2EEMessage(message); + + const groupOptions = starsAction.data.reduce((acc, option) => { const transformedOption = { variant: option.color === 'alert' ? 'danger' : '', id: option.id, @@ -40,9 +38,9 @@ const MessageToolbarStarsActionMenu = ({ 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) }) }), + ...(typeof option.disabled === 'boolean' && { disabled: option.disabled }), + ...(typeof option.disabled === 'boolean' && + option.disabled && { tooltip: t('Action_not_available_encrypted_content', { action: t(option.label) }) }), }; const group = option.type || ''; @@ -74,14 +72,14 @@ const MessageToolbarStarsActionMenu = ({ return ( ); }; diff --git a/apps/meteor/client/components/message/toolbar/items/DefaultItems.tsx b/apps/meteor/client/components/message/toolbar/items/DefaultItems.tsx new file mode 100644 index 000000000000..d7c4a9f9baa2 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/DefaultItems.tsx @@ -0,0 +1,26 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import ForwardMessageAction from './actions/ForwardMessageAction'; +import QuoteMessageAction from './actions/QuoteMessageAction'; +import ReactionMessageAction from './actions/ReactionMessageAction'; +import ReplyInThreadMessageAction from './actions/ReplyInThreadMessageAction'; + +type DefaultItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const DefaultItems = ({ message, room, subscription }: DefaultItemsProps) => { + return ( + <> + + + + + + ); +}; + +export default DefaultItems; diff --git a/apps/meteor/client/components/message/toolbar/items/DirectItems.tsx b/apps/meteor/client/components/message/toolbar/items/DirectItems.tsx new file mode 100644 index 000000000000..0b729e5ac28e --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/DirectItems.tsx @@ -0,0 +1,16 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import JumpToMessageAction from './actions/JumpToMessageAction'; + +type DirectItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const DirectItems = ({ message, subscription }: DirectItemsProps) => { + return <>{!!subscription && }; +}; + +export default DirectItems; diff --git a/apps/meteor/client/components/message/toolbar/items/FederatedItems.tsx b/apps/meteor/client/components/message/toolbar/items/FederatedItems.tsx new file mode 100644 index 000000000000..536dc114b698 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/FederatedItems.tsx @@ -0,0 +1,24 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import QuoteMessageAction from './actions/QuoteMessageAction'; +import ReactionMessageAction from './actions/ReactionMessageAction'; +import ReplyInThreadMessageAction from './actions/ReplyInThreadMessageAction'; + +type FederatedItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const FederatedItems = ({ message, room, subscription }: FederatedItemsProps) => { + return ( + <> + + + + + ); +}; + +export default FederatedItems; diff --git a/apps/meteor/client/components/message/toolbar/items/MentionsItems.tsx b/apps/meteor/client/components/message/toolbar/items/MentionsItems.tsx new file mode 100644 index 000000000000..03e363787d58 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/MentionsItems.tsx @@ -0,0 +1,20 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import JumpToMessageAction from './actions/JumpToMessageAction'; + +type MentionsItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const MentionsItems = ({ message }: MentionsItemsProps) => { + return ( + <> + + + ); +}; + +export default MentionsItems; diff --git a/apps/meteor/client/components/message/toolbar/items/MobileItems.tsx b/apps/meteor/client/components/message/toolbar/items/MobileItems.tsx new file mode 100644 index 000000000000..fde71116b3b5 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/MobileItems.tsx @@ -0,0 +1,28 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import ForwardMessageAction from './actions/ForwardMessageAction'; +import JumpToMessageAction from './actions/JumpToMessageAction'; +import QuoteMessageAction from './actions/QuoteMessageAction'; +import ReactionMessageAction from './actions/ReactionMessageAction'; +import ReplyInThreadMessageAction from './actions/ReplyInThreadMessageAction'; + +type MobileItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const MobileItems = ({ message, room, subscription }: MobileItemsProps) => { + return ( + <> + + + + + + + ); +}; + +export default MobileItems; diff --git a/apps/meteor/client/components/message/toolbar/items/PinnedItems.tsx b/apps/meteor/client/components/message/toolbar/items/PinnedItems.tsx new file mode 100644 index 000000000000..f854952add04 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/PinnedItems.tsx @@ -0,0 +1,20 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import JumpToMessageAction from './actions/JumpToMessageAction'; + +type PinnedItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const PinnedItems = ({ message }: PinnedItemsProps) => { + return ( + <> + + + ); +}; + +export default PinnedItems; diff --git a/apps/meteor/client/components/message/toolbar/items/SearchItems.tsx b/apps/meteor/client/components/message/toolbar/items/SearchItems.tsx new file mode 100644 index 000000000000..d3bf6cfca4f1 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/SearchItems.tsx @@ -0,0 +1,20 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import JumpToMessageAction from './actions/JumpToMessageAction'; + +type SearchItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const SearchItems = ({ message }: SearchItemsProps) => { + return ( + <> + + + ); +}; + +export default SearchItems; diff --git a/apps/meteor/client/components/message/toolbar/items/StarredItems.tsx b/apps/meteor/client/components/message/toolbar/items/StarredItems.tsx new file mode 100644 index 000000000000..35f56aba73bc --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/StarredItems.tsx @@ -0,0 +1,20 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import JumpToMessageAction from './actions/JumpToMessageAction'; + +type StarredItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const StarredItems = ({ message }: StarredItemsProps) => { + return ( + <> + + + ); +}; + +export default StarredItems; diff --git a/apps/meteor/client/components/message/toolbar/items/ThreadsItems.tsx b/apps/meteor/client/components/message/toolbar/items/ThreadsItems.tsx new file mode 100644 index 000000000000..43dff5224a7e --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/ThreadsItems.tsx @@ -0,0 +1,26 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import ForwardMessageAction from './actions/ForwardMessageAction'; +import JumpToMessageAction from './actions/JumpToMessageAction'; +import QuoteMessageAction from './actions/QuoteMessageAction'; +import ReactionMessageAction from './actions/ReactionMessageAction'; + +type ThreadsItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const ThreadsItems = ({ message, room, subscription }: ThreadsItemsProps) => { + return ( + <> + + + + + + ); +}; + +export default ThreadsItems; diff --git a/apps/meteor/client/components/message/toolbar/items/VideoconfItems.tsx b/apps/meteor/client/components/message/toolbar/items/VideoconfItems.tsx new file mode 100644 index 000000000000..392d78820f2c --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/VideoconfItems.tsx @@ -0,0 +1,22 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import ReactionMessageAction from './actions/ReactionMessageAction'; +import ReplyInThreadMessageAction from './actions/ReplyInThreadMessageAction'; + +type VideoconfItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const VideoconfItems = ({ message, room, subscription }: VideoconfItemsProps) => { + return ( + <> + + + + ); +}; + +export default VideoconfItems; diff --git a/apps/meteor/client/components/message/toolbar/items/VideoconfThreadsItems.tsx b/apps/meteor/client/components/message/toolbar/items/VideoconfThreadsItems.tsx new file mode 100644 index 000000000000..819bc2f04705 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/VideoconfThreadsItems.tsx @@ -0,0 +1,22 @@ +import type { IRoom, ISubscription, IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; + +import JumpToMessageAction from './actions/JumpToMessageAction'; +import ReactionMessageAction from './actions/ReactionMessageAction'; + +type VideoconfThreadsItemsProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const VideoconfThreadsItems = ({ message, room, subscription }: VideoconfThreadsItemsProps) => { + return ( + <> + + + + ); +}; + +export default VideoconfThreadsItems; diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx new file mode 100644 index 000000000000..88bb222d0641 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx @@ -0,0 +1,43 @@ +import { type IMessage, isE2EEMessage } from '@rocket.chat/core-typings'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { getPermaLink } from '../../../../../lib/getPermaLink'; +import ForwardMessageModal from '../../../../../views/room/modals/ForwardMessageModal'; +import MessageToolbarItem from '../../MessageToolbarItem'; + +type ForwardMessageActionProps = { + message: IMessage; +}; + +const ForwardMessageAction = ({ message }: ForwardMessageActionProps) => { + const setModal = useSetModal(); + const { t } = useTranslation(); + + const encrypted = isE2EEMessage(message); + + return ( + { + const permalink = await getPermaLink(message._id); + setModal( + { + setModal(null); + }} + />, + ); + }} + /> + ); +}; + +export default ForwardMessageAction; diff --git a/apps/meteor/client/components/message/toolbar/items/actions/JumpToMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/JumpToMessageAction.tsx new file mode 100644 index 000000000000..f8c72add56a6 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/actions/JumpToMessageAction.tsx @@ -0,0 +1,29 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { setMessageJumpQueryStringParameter } from '../../../../../lib/utils/setMessageJumpQueryStringParameter'; +import MessageToolbarItem from '../../MessageToolbarItem'; + +type JumpToMessageActionProps = { + id: 'jump-to-message' | 'jump-to-pin-message' | 'jump-to-star-message'; + message: IMessage; +}; + +const JumpToMessageAction = ({ id, message }: JumpToMessageActionProps) => { + const { t } = useTranslation(); + + return ( + { + setMessageJumpQueryStringParameter(message._id); + }} + /> + ); +}; + +export default JumpToMessageAction; diff --git a/apps/meteor/client/components/message/toolbar/items/actions/QuoteMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/QuoteMessageAction.tsx new file mode 100644 index 000000000000..f7a3d1d8cd99 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/actions/QuoteMessageAction.tsx @@ -0,0 +1,43 @@ +import type { ITranslatedMessage, IMessage, ISubscription } from '@rocket.chat/core-typings'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useAutoTranslate } from '../../../../../views/room/MessageList/hooks/useAutoTranslate'; +import { useChat } from '../../../../../views/room/contexts/ChatContext'; +import MessageToolbarItem from '../../MessageToolbarItem'; + +type QuoteMessageActionProps = { + message: IMessage & Partial; + subscription: ISubscription | undefined; +}; + +const QuoteMessageAction = ({ message, subscription }: QuoteMessageActionProps) => { + const chat = useChat(); + const autoTranslateOptions = useAutoTranslate(subscription); + const { t } = useTranslation(); + + if (!chat || !subscription) { + return null; + } + + return ( + { + if (message && autoTranslateOptions?.autoTranslateEnabled && autoTranslateOptions.showAutoTranslate(message)) { + message.msg = + message.translations && autoTranslateOptions.autoTranslateLanguage + ? message.translations[autoTranslateOptions.autoTranslateLanguage] + : message.msg; + } + + chat?.composer?.quoteMessage(message); + }} + /> + ); +}; + +export default QuoteMessageAction; diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx new file mode 100644 index 000000000000..5099cb13c969 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx @@ -0,0 +1,62 @@ +import { isOmnichannelRoom, type IMessage, type IRoom, type ISubscription } from '@rocket.chat/core-typings'; +import { useFeaturePreview } from '@rocket.chat/ui-client'; +import { useUser, useMethod } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useEmojiPickerData } from '../../../../../contexts/EmojiPickerContext'; +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; +import EmojiElement from '../../../../../views/composer/EmojiPicker/EmojiElement'; +import { useChat } from '../../../../../views/room/contexts/ChatContext'; +import MessageToolbarItem from '../../MessageToolbarItem'; + +type ReactionMessageActionProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const ReactionMessageAction = ({ message, room, subscription }: ReactionMessageActionProps) => { + const chat = useChat(); + const user = useUser(); + const setReaction = useMethod('setReaction'); + const quickReactionsEnabled = useFeaturePreview('quickReactions'); + const { quickReactions, addRecentEmoji } = useEmojiPickerData(); + const { t } = useTranslation(); + + if (!chat || !room || isOmnichannelRoom(room) || !subscription || message.private || !user) { + return null; + } + + if (roomCoordinator.readOnly(room._id, user) && !room.reactWhenReadOnly) { + return null; + } + + const toggleReaction = (emoji: string) => { + setReaction(`:${emoji}:`, message._id); + addRecentEmoji(emoji); + }; + + return ( + <> + {quickReactionsEnabled && + quickReactions.slice(0, 3).map(({ emoji, image }) => { + return toggleReaction(emoji)} />; + })} + { + event.stopPropagation(); + chat.emojiPicker.open(event.currentTarget, (emoji) => { + toggleReaction(emoji); + }); + }} + /> + + ); +}; + +export default ReactionMessageAction; diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ReplyInThreadMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ReplyInThreadMessageAction.tsx new file mode 100644 index 000000000000..55f4967625ad --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/actions/ReplyInThreadMessageAction.tsx @@ -0,0 +1,48 @@ +import { type IMessage, type ISubscription, type IRoom, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { useRouter, useSetting } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import MessageToolbarItem from '../../MessageToolbarItem'; + +type ReplyInThreadMessageActionProps = { + message: IMessage; + room: IRoom; + subscription: ISubscription | undefined; +}; + +const ReplyInThreadMessageAction = ({ message, room, subscription }: ReplyInThreadMessageActionProps) => { + const router = useRouter(); + const threadsEnabled = useSetting('Threads_enabled', true); + const { t } = useTranslation(); + + if (!threadsEnabled || isOmnichannelRoom(room) || !subscription) { + return null; + } + + return ( + { + event.stopPropagation(); + const routeName = router.getRouteName(); + + if (routeName) { + router.navigate({ + name: routeName, + params: { + ...router.getRouteParameters(), + tab: 'thread', + context: message.tmid || message._id, + }, + }); + } + }} + /> + ); +}; + +export default ReplyInThreadMessageAction; diff --git a/apps/meteor/client/components/message/toolbar/useCopyAction.ts b/apps/meteor/client/components/message/toolbar/useCopyAction.ts new file mode 100644 index 000000000000..1a03dac99936 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useCopyAction.ts @@ -0,0 +1,39 @@ +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; + +const getMainMessageText = (message: IMessage): IMessage => { + const newMessage = { ...message }; + newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.description || newMessage.attachments?.[0]?.title || ''; + newMessage.md = newMessage.md || newMessage.attachments?.[0]?.descriptionMd || undefined; + return { ...newMessage }; +}; + +export const useCopyAction = ( + message: IMessage, + { subscription }: { subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const { t } = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + if (!subscription) { + return null; + } + + return { + id: 'copy', + icon: 'copy', + label: 'Copy_text', + context: ['message', 'message-mobile', 'threads', 'federated'], + type: 'duplication', + async action() { + const msgText = getMainMessageText(message).msg; + await navigator.clipboard.writeText(msgText); + dispatchToastMessage({ type: 'success', message: t('Copied') }); + }, + order: 6, + group: 'menu', + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useDeleteMessageAction.ts b/apps/meteor/client/components/message/toolbar/useDeleteMessageAction.ts new file mode 100644 index 000000000000..ff8a24d12b84 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useDeleteMessageAction.ts @@ -0,0 +1,54 @@ +import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { ISubscription, IRoom, IMessage } from '@rocket.chat/core-typings'; +import { useUser } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import { useChat } from '../../../views/room/contexts/ChatContext'; + +export const useDeleteMessageAction = ( + message: IMessage, + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); + const chat = useChat(); + + const { data: condition = false } = useQuery({ + queryKey: ['delete-message', message] as const, + queryFn: async () => { + if (!subscription) { + return false; + } + + if (isRoomFederated(room)) { + return message.u._id === user?._id; + } + + const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); + if (isLivechatRoom) { + return false; + } + + return chat?.data.canDeleteMessage(message) ?? false; + }, + }); + + if (!condition) { + return null; + } + + return { + id: 'delete-message', + icon: 'trash', + label: 'Delete', + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + color: 'alert', + type: 'management', + async action() { + await chat?.flows.requestMessageDeletion(message); + }, + order: 10, + group: 'menu', + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useEditMessageAction.ts b/apps/meteor/client/components/message/toolbar/useEditMessageAction.ts new file mode 100644 index 000000000000..92df4f7cea35 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useEditMessageAction.ts @@ -0,0 +1,59 @@ +import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage, ISubscription } from '@rocket.chat/core-typings'; +import { usePermission, useSetting, useUser } from '@rocket.chat/ui-contexts'; +import moment from 'moment'; + +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { useChat } from '../../../views/room/contexts/ChatContext'; + +export const useEditMessageAction = ( + message: IMessage, + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); + const chat = useChat(); + const isEditAllowed = useSetting('Message_AllowEditing', true); + const canEditMessage = usePermission('edit-message', message.rid); + const blockEditInMinutes = useSetting('Message_AllowEditing_BlockEditInMinutes', 0); + const canBypassBlockTimeLimit = usePermission('bypass-time-limit-edit-and-delete', message.rid); + + if (!subscription) { + return null; + } + + const condition = (() => { + if (isRoomFederated(room)) { + return message.u._id === user?._id; + } + + const editOwn = message.u && message.u._id === user?._id; + if (!canEditMessage && (!isEditAllowed || !editOwn)) { + return false; + } + + if (!canBypassBlockTimeLimit && blockEditInMinutes) { + const msgTs = message.ts ? moment(message.ts) : undefined; + const currentTsDiff = msgTs ? moment().diff(msgTs, 'minutes') : undefined; + return typeof currentTsDiff === 'number' && currentTsDiff < blockEditInMinutes; + } + + return true; + })(); + + if (!condition) { + return null; + } + + return { + id: 'edit-message', + icon: 'edit', + label: 'Edit', + context: ['message', 'message-mobile', 'threads', 'federated'], + type: 'management', + async action() { + await chat?.messageEditing.editMessage(message); + }, + order: 8, + group: 'menu', + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts b/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts index 7fdde7c97b5e..3a9409260035 100644 --- a/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts +++ b/apps/meteor/client/components/message/toolbar/useFollowMessageAction.ts @@ -1,12 +1,9 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useSetting, useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; import { Messages } from '../../../../app/models/client'; -import { MessageAction } from '../../../../app/ui-utils/client'; -import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionContext, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { t } from '../../../../app/utils/lib/i18n'; import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; import { roomsQueryKeys } from '../../../lib/queryKeys'; @@ -14,14 +11,13 @@ import { useToggleFollowingThreadMutation } from '../../../views/room/contextual export const useFollowMessageAction = ( message: IMessage, - { room, user, context }: { room: IRoom; user: IUser | undefined; context: MessageActionContext }, -) => { + { room, context }: { room: IRoom; context: MessageActionContext }, +): MessageActionConfig | null => { + const user = useUser(); const threadsEnabled = useSetting('Threads_enabled'); const dispatchToastMessage = useToastMessageDispatch(); - const queryClient = useQueryClient(); - const { mutate: toggleFollowingThread } = useToggleFollowingThreadMutation({ onSuccess: () => { dispatchToastMessage({ @@ -36,40 +32,36 @@ export const useFollowMessageAction = ( Messages.findOne({ _id: tmid || _id }, { fields: { replies: 1 } }), ); - useEffect(() => { - if (!message || !threadsEnabled || isOmnichannelRoom(room)) { - return; - } - - let { replies = [] } = message; - if (tmid || context) { - const parentMessage = messageQuery.data; - if (parentMessage) { - replies = parentMessage.replies || []; - } - } + if (!message || !threadsEnabled || isOmnichannelRoom(room)) { + return null; + } - if (!user?._id) { - return; + let { replies = [] } = message; + if (tmid || context) { + const parentMessage = messageQuery.data; + if (parentMessage) { + replies = parentMessage.replies || []; } + } - if ((replies as string[]).includes(user._id)) { - return; - } + if (!user?._id) { + return null; + } - MessageAction.addButton({ - id: 'follow-message', - icon: 'bell', - label: 'Follow_message', - type: 'interaction', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - action() { - toggleFollowingThread({ tmid: tmid || _id, follow: true, rid: room._id }); - }, - order: 1, - group: 'menu', - }); + if (replies.includes(user._id)) { + return null; + } - return () => MessageAction.removeButton('follow-message'); - }, [_id, context, message, messageQuery, messageQuery.data, queryClient, room, threadsEnabled, tmid, toggleFollowingThread, user]); + return { + id: 'follow-message', + icon: 'bell', + label: 'Follow_message', + type: 'interaction', + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + action() { + toggleFollowingThread({ tmid: tmid || _id, follow: true, rid: room._id }); + }, + order: 1, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/useJumpToMessageContextAction.tsx b/apps/meteor/client/components/message/toolbar/useJumpToMessageContextAction.tsx deleted file mode 100644 index f225e8fc81f7..000000000000 --- a/apps/meteor/client/components/message/toolbar/useJumpToMessageContextAction.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import { useEffect } from 'react'; - -import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; -import { setMessageJumpQueryStringParameter } from '../../../lib/utils/setMessageJumpQueryStringParameter'; - -export const useJumpToMessageContextAction = ( - message: IMessage, - { id, order, hidden, context }: { id: string; order: number; hidden?: boolean; context: MessageActionContext[] }, -) => { - useEffect(() => { - if (hidden) { - return; - } - - MessageAction.addButton({ - id, - icon: 'jump', - label: 'Jump_to_message', - context, - async action() { - setMessageJumpQueryStringParameter(message._id); - }, - order, - group: 'message', - }); - - return () => { - MessageAction.removeButton(id); - }; - }, [hidden, context, id, message._id, order]); -}; diff --git a/apps/meteor/client/components/message/toolbar/useMarkAsUnreadMessageAction.ts b/apps/meteor/client/components/message/toolbar/useMarkAsUnreadMessageAction.ts index 208a83679f7e..54f70cb08d41 100644 --- a/apps/meteor/client/components/message/toolbar/useMarkAsUnreadMessageAction.ts +++ b/apps/meteor/client/components/message/toolbar/useMarkAsUnreadMessageAction.ts @@ -1,47 +1,42 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import type { ISubscription, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; -import { useRouter } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; +import type { ISubscription, IMessage, IRoom } from '@rocket.chat/core-typings'; +import { useRouter, useUser } from '@rocket.chat/ui-contexts'; -import { MessageAction } from '../../../../app/ui-utils/client'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useMarkAsUnreadMutation } from '../hooks/useMarkAsUnreadMutation'; export const useMarkAsUnreadMessageAction = ( message: IMessage, - { user, room, subscription }: { user: IUser | undefined; room: IRoom; subscription: ISubscription | undefined }, -) => { + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); const { mutateAsync: markAsUnread } = useMarkAsUnreadMutation(); const router = useRouter(); - useEffect(() => { - if (isOmnichannelRoom(room) || !user) { - return; - } + if (isOmnichannelRoom(room) || !user) { + return null; + } - if (!subscription) { - return; - } + if (!subscription) { + return null; + } - if (message.u._id === user._id) { - return; - } + if (message.u._id === user._id) { + return null; + } - MessageAction.addButton({ - id: 'mark-message-as-unread', - icon: 'flag', - label: 'Mark_unread', - context: ['message', 'message-mobile', 'threads'], - type: 'interaction', - async action() { - router.navigate('/home'); - await markAsUnread({ message, subscription }); - }, - order: 4, - group: 'menu', - }); - return () => { - MessageAction.removeButton('mark-message-as-unread'); - }; - }, [markAsUnread, message, room, router, subscription, user]); + return { + id: 'mark-message-as-unread', + icon: 'flag', + label: 'Mark_unread', + context: ['message', 'message-mobile', 'threads'], + type: 'interaction', + async action() { + router.navigate('/home'); + await markAsUnread({ message, subscription }); + }, + order: 4, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/useMessageActionAppsActionButtons.ts b/apps/meteor/client/components/message/toolbar/useMessageActionAppsActionButtons.ts new file mode 100644 index 000000000000..cecb79c183af --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useMessageActionAppsActionButtons.ts @@ -0,0 +1,78 @@ +import { type IUIActionButton, MessageActionContext as AppsEngineMessageActionContext } from '@rocket.chat/apps-engine/definition/ui'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { UiKitTriggerTimeoutError } from '../../../../app/ui-message/client/UiKitTriggerTimeoutError'; +import type { MessageActionContext, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { Utilities } from '../../../../ee/lib/misc/Utilities'; +import { useAppActionButtons, getIdForActionButton } from '../../../hooks/useAppActionButtons'; +import { useApplyButtonFilters } from '../../../hooks/useApplyButtonFilters'; +import { useUiKitActionManager } from '../../../uikit/hooks/useUiKitActionManager'; + +const filterActionsByContext = (context: string | undefined, action: IUIActionButton) => { + if (!context) { + return true; + } + + const messageActionContext = action.when?.messageActionContext || Object.values(AppsEngineMessageActionContext); + const isContextMatch = messageActionContext.includes(context as AppsEngineMessageActionContext); + + return isContextMatch; +}; + +export const useMessageActionAppsActionButtons = (message: IMessage, context?: MessageActionContext, category?: string) => { + const result = useAppActionButtons('messageAction'); + const actionManager = useUiKitActionManager(); + const applyButtonFilters = useApplyButtonFilters(category); + const dispatchToastMessage = useToastMessageDispatch(); + const { t } = useTranslation(); + const data = useMemo( + () => + result.data + ?.filter((action) => filterActionsByContext(context, action)) + .filter((action) => applyButtonFilters(action)) + .map((action) => { + const item: MessageActionConfig = { + icon: undefined as any, + id: getIdForActionButton(action), + label: Utilities.getI18nKeyForApp(action.labelI18n, action.appId), + order: 7, + type: 'apps', + variant: action.variant, + group: 'menu', + action: () => { + void actionManager + .emitInteraction(action.appId, { + type: 'actionButton', + rid: message.rid, + tmid: message.tmid, + mid: message._id, + actionId: action.actionId, + payload: { context: action.context }, + }) + .catch(async (reason) => { + if (reason instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + return; + } + + return reason; + }); + }, + }; + + return item; + }), + [actionManager, applyButtonFilters, context, dispatchToastMessage, message._id, message.rid, message.tmid, result.data, t], + ); + return { + ...result, + data, + } as UseQueryResult; +}; diff --git a/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx index 2812e1c06ba8..18a6c267da4e 100644 --- a/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx @@ -1,68 +1,70 @@ -import { useSetModal, useSetting } from '@rocket.chat/ui-contexts'; -import React, { useEffect } from 'react'; +import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { usePermission, useSetModal, useSetting, useUser } from '@rocket.chat/ui-contexts'; +import React from 'react'; -import { hasPermission } from '../../../../app/authorization/client'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import CreateDiscussion from '../../CreateDiscussion'; -export const useNewDiscussionMessageAction = () => { +export const useNewDiscussionMessageAction = ( + message: IMessage, + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); const enabled = useSetting('Discussion_enabled', false); const setModal = useSetModal(); - useEffect(() => { - if (!enabled) { - return MessageAction.removeButton('start-discussion'); - } - MessageAction.addButton({ - id: 'start-discussion', - icon: 'discussion', - label: 'Discussion_start', - type: 'communication', - context: ['message', 'message-mobile', 'videoconf'], - async action(_, { message, room }) { - setModal( - setModal(undefined)} - parentMessageId={message._id} - nameSuggestion={message?.msg?.substr(0, 140)} - />, - ); - }, - condition({ - message: { - u: { _id: uid }, - drid, - dcount, - }, - room, - subscription, - user, - }) { - if (drid || !Number.isNaN(Number(dcount))) { - return false; - } - if (!subscription) { - return false; - } - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { - return false; - } + const canStartDiscussion = usePermission('start-discussion', room._id); + const canStartDiscussionOtherUser = usePermission('start-discussion-other-user', room._id); - if (!user) { - return false; - } + if (!enabled) { + return null; + } - return uid !== user._id ? hasPermission('start-discussion-other-user', room._id) : hasPermission('start-discussion', room._id); - }, - order: 1, - group: 'menu', - }); - return () => { - MessageAction.removeButton('start-discussion'); - }; - }, [enabled, setModal]); + const { + u: { _id: uid }, + drid, + dcount, + } = message; + if (drid || !Number.isNaN(Number(dcount))) { + return null; + } + + if (!subscription) { + return null; + } + + const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); + if (isLivechatRoom) { + return null; + } + + if (!user) { + return null; + } + + if (!(uid !== user._id ? canStartDiscussionOtherUser : canStartDiscussion)) { + return null; + } + + return { + id: 'start-discussion', + icon: 'discussion', + label: 'Discussion_start', + type: 'communication', + context: ['message', 'message-mobile', 'videoconf'], + async action() { + setModal( + setModal(undefined)} + parentMessageId={message._id} + nameSuggestion={message?.msg?.substr(0, 140)} + />, + ); + }, + order: 1, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts b/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts index 78a197d5c5d7..d3d0ea975dc2 100644 --- a/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts +++ b/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts @@ -1,52 +1,38 @@ -import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import { isE2EEMessage } from '@rocket.chat/core-typings'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import type { MessageActionConfig, MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; import { getPermaLink } from '../../../lib/getPermaLink'; export const usePermalinkAction = ( message: IMessage, - { - subscription, - id, - context, - type, - order, - }: { subscription: ISubscription | undefined; context: MessageActionContext[]; order: number } & Pick, -) => { + { id, context, type, order }: { context: MessageActionContext[]; order: number } & Pick, +): MessageActionConfig | null => { const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const encrypted = isE2EEMessage(message); - useEffect(() => { - MessageAction.addButton({ - id, - icon: 'permalink', - label: 'Copy_link', - context, - type, - async action() { - try { - const permalink = await getPermaLink(message._id); - navigator.clipboard.writeText(permalink); - dispatchToastMessage({ type: 'success', message: t('Copied') }); - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - }, - order, - group: 'menu', - disabled: () => encrypted, - }); - - return () => { - MessageAction.removeButton(id); - }; - }, [context, dispatchToastMessage, encrypted, id, message._id, order, subscription, t, type]); + return { + id, + icon: 'permalink', + label: 'Copy_link', + context, + type, + async action() { + try { + const permalink = await getPermaLink(message._id); + navigator.clipboard.writeText(permalink); + dispatchToastMessage({ type: 'success', message: t('Copied') }); + } catch (e) { + dispatchToastMessage({ type: 'error', message: e }); + } + }, + order, + group: 'menu', + disabled: encrypted, + }; }; diff --git a/apps/meteor/client/components/message/toolbar/usePinMessageAction.tsx b/apps/meteor/client/components/message/toolbar/usePinMessageAction.tsx index 53b5764de475..7012d1c7e4a3 100644 --- a/apps/meteor/client/components/message/toolbar/usePinMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/usePinMessageAction.tsx @@ -1,47 +1,41 @@ import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useSetting, useSetModal, usePermission } from '@rocket.chat/ui-contexts'; -import React, { useEffect } from 'react'; +import React from 'react'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import PinMessageModal from '../../../views/room/modals/PinMessageModal'; import { usePinMessageMutation } from '../hooks/usePinMessageMutation'; export const usePinMessageAction = ( message: IMessage, { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, -) => { +): MessageActionConfig | null => { const setModal = useSetModal(); const allowPinning = useSetting('Message_AllowPinning'); const hasPermission = usePermission('pin-message', room._id); const { mutateAsync: pinMessage } = usePinMessageMutation(); - useEffect(() => { - if (!allowPinning || isOmnichannelRoom(room) || !hasPermission || message.pinned || !subscription) { - return; - } + if (!allowPinning || isOmnichannelRoom(room) || !hasPermission || message.pinned || !subscription) { + return null; + } - const onConfirm = async () => { - pinMessage(message); - setModal(null); - }; + const onConfirm = async () => { + pinMessage(message); + setModal(null); + }; - MessageAction.addButton({ - id: 'pin-message', - icon: 'pin', - label: 'Pin', - type: 'interaction', - context: ['pinned', 'message', 'message-mobile', 'threads', 'direct', 'videoconf', 'videoconf-threads'], - async action() { - setModal( setModal(null)} />); - }, - order: 2, - group: 'menu', - }); - - return () => { - MessageAction.removeButton('pin-message'); - }; - }, [allowPinning, hasPermission, message, pinMessage, room, setModal, subscription]); + return { + id: 'pin-message', + icon: 'pin', + label: 'Pin', + type: 'interaction', + context: ['pinned', 'message', 'message-mobile', 'threads', 'direct', 'videoconf', 'videoconf-threads'], + async action() { + setModal( setModal(null)} />); + }, + order: 2, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/useReactionMessageAction.ts b/apps/meteor/client/components/message/toolbar/useReactionMessageAction.ts deleted file mode 100644 index 3456a01204a4..000000000000 --- a/apps/meteor/client/components/message/toolbar/useReactionMessageAction.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import type { IRoom, ISubscription, IUser, IMessage } from '@rocket.chat/core-typings'; -import { useEffect } from 'react'; - -import { MessageAction } from '../../../../app/ui-utils/client'; -import { sdk } from '../../../../app/utils/client/lib/SDKClient'; -import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; - -export const useReactionMessageAction = ( - message: IMessage, - { user, room, subscription }: { user: IUser | undefined; room: IRoom; subscription: ISubscription | undefined }, -) => { - useEffect(() => { - if (!room || isOmnichannelRoom(room) || !subscription || message.private || !user) { - return; - } - - if (roomCoordinator.readOnly(room._id, user) && !room.reactWhenReadOnly) { - return; - } - - MessageAction.addButton({ - id: 'reaction-message', - icon: 'add-reaction', - label: 'Add_Reaction', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - action(event, { message, chat }) { - event?.stopPropagation(); - chat?.emojiPicker.open(event?.currentTarget as Element, (emoji) => sdk.call('setReaction', `:${emoji}:`, message._id)); - }, - order: -3, - group: 'message', - }); - - return () => { - MessageAction.removeButton('reaction-message'); - }; - }, [message.private, room, subscription, user]); -}; diff --git a/apps/meteor/client/components/message/toolbar/useReadReceiptsDetailsAction.tsx b/apps/meteor/client/components/message/toolbar/useReadReceiptsDetailsAction.tsx new file mode 100644 index 000000000000..7cd87b2fd564 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useReadReceiptsDetailsAction.tsx @@ -0,0 +1,37 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useSetModal, useSetting } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import ReadReceiptsModal from '../../../views/room/modals/ReadReceiptsModal'; + +export const useReadReceiptsDetailsAction = (message: IMessage): MessageActionConfig | null => { + const setModal = useSetModal(); + + const readReceiptsEnabled = useSetting('Message_Read_Receipt_Enabled', false); + const readReceiptsStoreUsers = useSetting('Message_Read_Receipt_Store_Users', false); + + if (!readReceiptsEnabled || !readReceiptsStoreUsers) { + return null; + } + + return { + id: 'receipt-detail', + icon: 'check-double', + label: 'Read_Receipts', + context: ['starred', 'message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], + type: 'duplication', + action() { + setModal( + { + setModal(null); + }} + />, + ); + }, + order: 10, + group: 'menu', + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts b/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts new file mode 100644 index 000000000000..abf4babf1577 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useReplyInDMAction.ts @@ -0,0 +1,63 @@ +import { type IMessage, type ISubscription, type IRoom, isE2EEMessage } from '@rocket.chat/core-typings'; +import { usePermission, useRouter, useUser } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +import { Rooms, Subscriptions } from '../../../../app/models/client'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; +import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; + +export const useReplyInDMAction = ( + message: IMessage, + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); + const router = useRouter(); + const encrypted = isE2EEMessage(message); + const canCreateDM = usePermission('create-d'); + const isLayoutEmbedded = useEmbeddedLayout(); + + const condition = useReactiveValue( + useCallback(() => { + if (!subscription || room.t === 'd' || room.t === 'l' || isLayoutEmbedded) { + return false; + } + + // Check if we already have a DM started with the message user (not ourselves) or we can start one + if (!!user && user._id !== message.u._id && !canCreateDM) { + const dmRoom = Rooms.findOne({ _id: [user._id, message.u._id].sort().join('') }); + if (!dmRoom || !Subscriptions.findOne({ 'rid': dmRoom._id, 'u._id': user._id })) { + return false; + } + } + + return true; + }, [canCreateDM, isLayoutEmbedded, message.u._id, room.t, subscription, user]), + ); + + if (!condition) { + return null; + } + + return { + id: 'reply-directly', + icon: 'reply-directly', + label: 'Reply_in_direct_message', + context: ['message', 'message-mobile', 'threads', 'federated'], + type: 'communication', + action() { + roomCoordinator.openRouteLink( + 'd', + { name: message.u.username }, + { + ...router.getSearchParameters(), + reply: message._id, + }, + ); + }, + order: 0, + group: 'menu', + disabled: encrypted, + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useReplyInThreadMessageAction.ts b/apps/meteor/client/components/message/toolbar/useReplyInThreadMessageAction.ts deleted file mode 100644 index 1920ae68dd36..000000000000 --- a/apps/meteor/client/components/message/toolbar/useReplyInThreadMessageAction.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { useSetting, useRouter } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; - -import { MessageAction } from '../../../../app/ui-utils/client'; - -export const useReplyInThreadMessageAction = ( - message: IMessage, - { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, -) => { - const threadsEnabled = useSetting('Threads_enabled'); - - const route = useRouter(); - - useEffect(() => { - if (!threadsEnabled || isOmnichannelRoom(room) || !subscription) { - return; - } - - MessageAction.addButton({ - id: 'reply-in-thread', - icon: 'thread', - label: 'Reply_in_thread', - context: ['message', 'message-mobile', 'federated', 'videoconf'], - action(e) { - e?.stopPropagation(); - const routeName = route.getRouteName(); - if (routeName) { - route.navigate({ - name: routeName, - params: { - ...route.getRouteParameters(), - tab: 'thread', - context: message.tmid || message._id, - }, - }); - } - }, - order: -1, - group: 'message', - }); - - return () => MessageAction.removeButton('unfollow-message'); - }, [message._id, message.tmid, room, route, subscription, threadsEnabled]); -}; diff --git a/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx new file mode 100644 index 000000000000..0ba5d653743b --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx @@ -0,0 +1,53 @@ +import type { ISubscription, IRoom, IMessage } from '@rocket.chat/core-typings'; +import { useSetModal, useUser } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import ReportMessageModal from '../../../views/room/modals/ReportMessageModal'; + +const getMainMessageText = (message: IMessage): IMessage => { + const newMessage = { ...message }; + newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.description || newMessage.attachments?.[0]?.title || ''; + newMessage.md = newMessage.md || newMessage.attachments?.[0]?.descriptionMd || undefined; + return { ...newMessage }; +}; + +export const useReportMessageAction = ( + message: IMessage, + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); + const setModal = useSetModal(); + + const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); + + if (!subscription) { + return null; + } + + if (isLivechatRoom || message.u._id === user?._id) { + return null; + } + + return { + id: 'report-message', + icon: 'report', + label: 'Report', + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + color: 'alert', + type: 'management', + action() { + setModal( + { + setModal(null); + }} + />, + ); + }, + order: 9, + group: 'menu', + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useShowMessageReactionsAction.tsx b/apps/meteor/client/components/message/toolbar/useShowMessageReactionsAction.tsx new file mode 100644 index 000000000000..e2fe4eb4661e --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useShowMessageReactionsAction.tsx @@ -0,0 +1,34 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import ReactionListModal from '../../../views/room/modals/ReactionListModal'; + +export const useShowMessageReactionsAction = (message: IMessage): MessageActionConfig | null => { + const setModal = useSetModal(); + + if (!message.reactions) { + return null; + } + + return { + id: 'reaction-list', + icon: 'emoji', + label: 'Reactions', + context: ['message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], + type: 'interaction', + action() { + setModal( + { + setModal(null); + }} + />, + ); + }, + order: 9, + group: 'menu', + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useStarMessageAction.ts b/apps/meteor/client/components/message/toolbar/useStarMessageAction.ts index 829a94db9aa8..df24b8b49f41 100644 --- a/apps/meteor/client/components/message/toolbar/useStarMessageAction.ts +++ b/apps/meteor/client/components/message/toolbar/useStarMessageAction.ts @@ -1,40 +1,34 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { useSetting } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; +import { useSetting, useUser } from '@rocket.chat/ui-contexts'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useStarMessageMutation } from '../hooks/useStarMessageMutation'; -export const useStarMessageAction = (message: IMessage, { room, user }: { room: IRoom; user: IUser | undefined }) => { +export const useStarMessageAction = (message: IMessage, { room }: { room: IRoom }): MessageActionConfig | null => { + const user = useUser(); const allowStarring = useSetting('Message_AllowStarring', true); const { mutateAsync: starMessage } = useStarMessageMutation(); - useEffect(() => { - if (!allowStarring || isOmnichannelRoom(room)) { - return; - } + if (!allowStarring || isOmnichannelRoom(room)) { + return null; + } - if (Array.isArray(message.starred) && message.starred.some((star) => star._id === user?._id)) { - return; - } + if (Array.isArray(message.starred) && message.starred.some((star) => star._id === user?._id)) { + return null; + } - MessageAction.addButton({ - id: 'star-message', - icon: 'star', - label: 'Star', - type: 'interaction', - context: ['starred', 'message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - async action() { - await starMessage(message); - }, - order: 3, - group: 'menu', - }); - - return () => { - MessageAction.removeButton('star-message'); - }; - }, [allowStarring, message, room, starMessage, user?._id]); + return { + id: 'star-message', + icon: 'star', + label: 'Star', + type: 'interaction', + context: ['starred', 'message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + async action() { + await starMessage(message); + }, + order: 3, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/useTranslateAction.ts b/apps/meteor/client/components/message/toolbar/useTranslateAction.ts new file mode 100644 index 000000000000..5848b172b978 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useTranslateAction.ts @@ -0,0 +1,59 @@ +import type { IMessage, ISubscription, IRoom } from '@rocket.chat/core-typings'; +import { useMethod, usePermission, useSetting, useUser } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { AutoTranslate } from '../../../../app/autotranslate/client'; +import { Messages } from '../../../../app/models/client'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage } from '../../../views/room/MessageList/lib/autoTranslate'; + +export const useTranslateAction = ( + message: IMessage & { autoTranslateShowInverse?: boolean }, + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); + const autoTranslateEnabled = useSetting('AutoTranslate_Enabled', false); + const canAutoTranslate = usePermission('auto-translate'); + const translateMessage = useMethod('autoTranslate.translateMessage'); + + const language = useMemo( + () => subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid), + [message.rid, subscription?.autoTranslateLanguage], + ); + const hasTranslations = useMemo( + () => hasTranslationLanguageInMessage(message, language) || hasTranslationLanguageInAttachments(message.attachments, language), + [message, language], + ); + + if (!autoTranslateEnabled || !canAutoTranslate || !user) { + return null; + } + + const isLivechatRoom = roomCoordinator.isLivechatRoom(room?.t); + const isDifferentUser = message?.u && message.u._id !== user._id; + const autoTranslationActive = subscription?.autoTranslate || isLivechatRoom; + + if (!message.autoTranslateShowInverse && (!isDifferentUser || !autoTranslationActive || hasTranslations)) { + return null; + } + + return { + id: 'translate', + icon: 'language', + label: 'Translate', + context: ['message', 'message-mobile', 'threads'], + type: 'interaction', + group: 'menu', + action() { + if (!hasTranslations) { + AutoTranslate.messageIdsToWait[message._id] = true; + Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); + void translateMessage(message, language); + } + const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; + Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); + }, + order: 90, + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts b/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts index f54f25a1d00b..7ebca9e01180 100644 --- a/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts +++ b/apps/meteor/client/components/message/toolbar/useUnFollowMessageAction.ts @@ -1,12 +1,9 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useSetting, useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; import { Messages } from '../../../../app/models/client'; -import { MessageAction } from '../../../../app/ui-utils/client'; -import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionContext, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { t } from '../../../../app/utils/lib/i18n'; import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; import { roomsQueryKeys } from '../../../lib/queryKeys'; @@ -14,14 +11,13 @@ import { useToggleFollowingThreadMutation } from '../../../views/room/contextual export const useUnFollowMessageAction = ( message: IMessage, - { room, user, context }: { room: IRoom; user: IUser | undefined; context: MessageActionContext }, -) => { + { room, context }: { room: IRoom; context: MessageActionContext }, +): MessageActionConfig | null => { + const user = useUser(); const threadsEnabled = useSetting('Threads_enabled'); const dispatchToastMessage = useToastMessageDispatch(); - const queryClient = useQueryClient(); - const { mutate: toggleFollowingThread } = useToggleFollowingThreadMutation({ onSuccess: () => { dispatchToastMessage({ @@ -37,41 +33,37 @@ export const useUnFollowMessageAction = ( () => Messages.findOne({ _id: tmid || _id }, { fields: { replies: 1 } }) ?? null, ); - useEffect(() => { - if (!message || !threadsEnabled || isOmnichannelRoom(room)) { - return; - } - - let { replies } = message; + if (!message || !threadsEnabled || isOmnichannelRoom(room)) { + return null; + } - if (tmid || context) { - const parentMessage = messageQuery.data; - if (parentMessage) { - replies = parentMessage.replies || []; - } - } + let { replies } = message; - if (!user?._id) { - return; + if (tmid || context) { + const parentMessage = messageQuery.data; + if (parentMessage) { + replies = parentMessage.replies || []; } + } - if (!replies?.includes(user._id)) { - return; - } + if (!user?._id) { + return null; + } - MessageAction.addButton({ - id: 'unfollow-message', - icon: 'bell-off', - label: 'Unfollow_message', - type: 'interaction', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - action() { - toggleFollowingThread({ tmid: tmid || _id, follow: false, rid: room._id }); - }, - order: 1, - group: 'menu', - }); + if (!replies?.includes(user._id)) { + return null; + } - return () => MessageAction.removeButton('unfollow-message'); - }, [_id, context, message, messageQuery.data, queryClient, room, threadsEnabled, tmid, toggleFollowingThread, user]); + return { + id: 'unfollow-message', + icon: 'bell-off', + label: 'Unfollow_message', + type: 'interaction', + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + action() { + toggleFollowingThread({ tmid: tmid || _id, follow: false, rid: room._id }); + }, + order: 1, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/useUnpinMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useUnpinMessageAction.tsx index 06daaaa45dce..038dd4a662d9 100644 --- a/apps/meteor/client/components/message/toolbar/useUnpinMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/useUnpinMessageAction.tsx @@ -1,40 +1,33 @@ import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useSetting, usePermission } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useUnpinMessageMutation } from '../hooks/useUnpinMessageMutation'; export const useUnpinMessageAction = ( message: IMessage, { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, -) => { +): MessageActionConfig | null => { const allowPinning = useSetting('Message_AllowPinning'); const hasPermission = usePermission('pin-message', room._id); const { mutate: unpinMessage } = useUnpinMessageMutation(); - useEffect(() => { - if (!allowPinning || isOmnichannelRoom(room) || !hasPermission || !message.pinned || !subscription) { - return; - } + if (!allowPinning || isOmnichannelRoom(room) || !hasPermission || !message.pinned || !subscription) { + return null; + } - MessageAction.addButton({ - id: 'unpin-message', - icon: 'pin', - label: 'Unpin', - type: 'interaction', - context: ['pinned', 'message', 'message-mobile', 'threads', 'direct', 'videoconf', 'videoconf-threads'], - action() { - unpinMessage(message); - }, - order: 2, - group: 'menu', - }); - - return () => { - MessageAction.removeButton('unpin-message'); - }; - }, [allowPinning, hasPermission, message, room, subscription, unpinMessage]); + return { + id: 'unpin-message', + icon: 'pin', + label: 'Unpin', + type: 'interaction', + context: ['pinned', 'message', 'message-mobile', 'threads', 'direct', 'videoconf', 'videoconf-threads'], + action() { + unpinMessage(message); + }, + order: 2, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/useUnstarMessageAction.ts b/apps/meteor/client/components/message/toolbar/useUnstarMessageAction.ts index 851ce1ae4115..4ffb090dffc5 100644 --- a/apps/meteor/client/components/message/toolbar/useUnstarMessageAction.ts +++ b/apps/meteor/client/components/message/toolbar/useUnstarMessageAction.ts @@ -1,40 +1,34 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { useSetting } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; +import { useSetting, useUser } from '@rocket.chat/ui-contexts'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useUnstarMessageMutation } from '../hooks/useUnstarMessageMutation'; -export const useUnstarMessageAction = (message: IMessage, { room, user }: { room: IRoom; user: IUser | undefined }) => { +export const useUnstarMessageAction = (message: IMessage, { room }: { room: IRoom }): MessageActionConfig | null => { + const user = useUser(); const allowStarring = useSetting('Message_AllowStarring'); const { mutateAsync: unstarMessage } = useUnstarMessageMutation(); - useEffect(() => { - if (!allowStarring || isOmnichannelRoom(room)) { - return; - } + if (!allowStarring || isOmnichannelRoom(room)) { + return null; + } - if (!Array.isArray(message.starred) || message.starred.every((star) => star._id !== user?._id)) { - return; - } + if (!Array.isArray(message.starred) || message.starred.every((star) => star._id !== user?._id)) { + return null; + } - MessageAction.addButton({ - id: 'unstar-message', - icon: 'star', - label: 'Unstar_Message', - type: 'interaction', - context: ['starred', 'message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - async action() { - await unstarMessage(message); - }, - order: 3, - group: 'menu', - }); - - return () => { - MessageAction.removeButton('unstar-message'); - }; - }, [allowStarring, message, room, unstarMessage, user?._id]); + return { + id: 'unstar-message', + icon: 'star', + label: 'Unstar_Message', + type: 'interaction', + context: ['starred', 'message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + async action() { + await unstarMessage(message); + }, + order: 3, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts b/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts new file mode 100644 index 000000000000..3d6e91e5eb54 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts @@ -0,0 +1,59 @@ +import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { useMethod, usePermission, useSetting, useUser } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { AutoTranslate } from '../../../../app/autotranslate/client'; +import { Messages } from '../../../../app/models/client'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage } from '../../../views/room/MessageList/lib/autoTranslate'; + +export const useViewOriginalTranslationAction = ( + message: IMessage & { autoTranslateShowInverse?: boolean }, + { room, subscription }: { room: IRoom; subscription: ISubscription | undefined }, +): MessageActionConfig | null => { + const user = useUser(); + const autoTranslateEnabled = useSetting('AutoTranslate_Enabled', false); + const canAutoTranslate = usePermission('auto-translate'); + const translateMessage = useMethod('autoTranslate.translateMessage'); + + const language = useMemo( + () => subscription?.autoTranslateLanguage || AutoTranslate.getLanguage(message.rid), + [message.rid, subscription?.autoTranslateLanguage], + ); + const hasTranslations = useMemo( + () => hasTranslationLanguageInMessage(message, language) || hasTranslationLanguageInAttachments(message.attachments, language), + [message, language], + ); + + if (!autoTranslateEnabled || !canAutoTranslate || !user) { + return null; + } + + const isLivechatRoom = roomCoordinator.isLivechatRoom(room?.t); + const isDifferentUser = message?.u && message.u._id !== user._id; + const autoTranslationActive = subscription?.autoTranslate || isLivechatRoom; + + if (message.autoTranslateShowInverse || !isDifferentUser || !autoTranslationActive || !hasTranslations) { + return null; + } + + return { + id: 'view-original', + icon: 'language', + label: 'View_original', + context: ['message', 'message-mobile', 'threads'], + type: 'interaction', + group: 'menu', + action() { + if (!hasTranslations) { + AutoTranslate.messageIdsToWait[message._id] = true; + Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); + void translateMessage(message, language); + } + const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; + Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); + }, + order: 90, + }; +}; diff --git a/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx index 166872acaa42..fe2c753a397f 100644 --- a/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/useWebDAVMessageAction.tsx @@ -1,42 +1,44 @@ +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { useSetModal, useSetting } from '@rocket.chat/ui-contexts'; -import React, { useEffect } from 'react'; +import React from 'react'; -import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; +import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; import { getURL } from '../../../../app/utils/client'; import { useWebDAVAccountIntegrationsQuery } from '../../../hooks/webdav/useWebDAVAccountIntegrationsQuery'; import SaveToWebdavModal from '../../../views/room/webdav/SaveToWebdavModal'; -export const useWebDAVMessageAction = () => { +export const useWebDAVMessageAction = ( + message: IMessage, + { subscription }: { subscription: ISubscription | undefined }, +): MessageActionConfig | null => { const enabled = useSetting('Webdav_Integration_Enabled', false); const { data } = useWebDAVAccountIntegrationsQuery({ enabled }); const setModal = useSetModal(); - useEffect(() => { - if (!enabled) { - return; - } - - MessageAction.addButton({ - id: 'webdav-upload', - icon: 'upload', - label: 'Save_To_Webdav', - condition: ({ message, subscription }) => { - return !!subscription && !!data?.length && !!message.file; - }, - action(_, { message }) { - const [attachment] = message.attachments || []; - const url = getURL(attachment.title_link as string, { full: true }); - - setModal( setModal(undefined)} />); - }, - order: 100, - group: 'menu', - }); - - return () => { - MessageAction.removeButton('webdav-upload'); - }; - }, [data?.length, enabled, setModal]); + if (!enabled || !subscription || !data?.length || !message.file) { + return null; + } + + return { + id: 'webdav-upload', + icon: 'upload', + label: 'Save_To_Webdav', + action() { + const [attachment] = message.attachments || []; + const url = getURL(attachment.title_link as string, { full: true }); + + setModal( + { + setModal(null); + }} + />, + ); + }, + order: 100, + group: 'menu', + }; }; diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index bf5c12a9a6dd..8dec6c9abbaa 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -110,7 +110,7 @@ const RoomMessage = ({ )} - {!message.private && message?.e2e !== 'pending' && } + {!message.private && message?.e2e !== 'pending' && !selecting && } ); }; diff --git a/apps/meteor/client/hooks/useAppActionButtons.ts b/apps/meteor/client/hooks/useAppActionButtons.ts index 0dcb2d380d46..b5796965b062 100644 --- a/apps/meteor/client/hooks/useAppActionButtons.ts +++ b/apps/meteor/client/hooks/useAppActionButtons.ts @@ -1,21 +1,10 @@ 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'; -import type { UseQueryResult } from '@tanstack/react-query'; +import { useEndpoint, useStream, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useEffect } from 'react'; -import { useApplyButtonFilters, useApplyButtonAuthFilter } from './useApplyButtonFilters'; -import { useFilterActionsByContext } from './useFilterActions'; -import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTriggerTimeoutError'; -import type { MessageActionConfig, MessageActionContext } from '../../app/ui-utils/client/lib/MessageAction'; -import type { MessageBoxAction } from '../../app/ui-utils/client/lib/messageBox'; -import { Utilities } from '../../ee/lib/misc/Utilities'; -import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; - -const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${appId}/${actionId}`; +export const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${appId}/${actionId}`; export const useAppActionButtons = (context?: TContext) => { const queryClient = useQueryClient(); @@ -61,160 +50,3 @@ export const useAppActionButtons = return result; }; - -export const useMessageboxAppsActionButtons = () => { - const result = useAppActionButtons('messageBoxAction'); - const actionManager = useUiKitActionManager(); - const dispatchToastMessage = useToastMessageDispatch(); - const { t } = useTranslation(); - - const applyButtonFilters = useApplyButtonFilters(); - - const data = useMemo( - () => - result.data - ?.filter((action) => { - return applyButtonFilters(action); - }) - .map((action) => { - const item: Omit = { - id: getIdForActionButton(action), - label: Utilities.getI18nKeyForApp(action.labelI18n, action.appId), - action: (params) => { - void actionManager - .emitInteraction(action.appId, { - type: 'actionButton', - rid: params.rid, - tmid: params.tmid, - actionId: action.actionId, - payload: { context: action.context, message: params.chat.composer?.text ?? '' }, - }) - .catch(async (reason) => { - if (reason instanceof UiKitTriggerTimeoutError) { - dispatchToastMessage({ - type: 'error', - message: t('UIKit_Interaction_Timeout'), - }); - return; - } - - return reason; - }); - }, - }; - - return item; - }), - [actionManager, applyButtonFilters, dispatchToastMessage, result.data, t], - ); - return { - ...result, - data, - } as UseQueryResult; -}; - -export const useUserDropdownAppsActionButtons = () => { - const result = useAppActionButtons('userDropdownAction'); - const actionManager = useUiKitActionManager(); - const dispatchToastMessage = useToastMessageDispatch(); - const { t } = useTranslation(); - - const applyButtonFilters = useApplyButtonAuthFilter(); - - const data = useMemo( - () => - result.data - ?.filter((action) => { - return applyButtonFilters(action); - }) - .map((action) => { - return { - id: `${action.appId}_${action.actionId}`, - // icon: action.icon as GenericMenuItemProps['icon'], - content: action.labelI18n, - onClick: () => { - void actionManager - .emitInteraction(action.appId, { - type: 'actionButton', - actionId: action.actionId, - payload: { context: action.context }, - }) - .catch(async (reason) => { - if (reason instanceof UiKitTriggerTimeoutError) { - dispatchToastMessage({ - type: 'error', - message: t('UIKit_Interaction_Timeout'), - }); - return; - } - - return reason; - }); - }, - }; - }), - [actionManager, applyButtonFilters, dispatchToastMessage, result.data, t], - ); - return { - ...result, - data, - } as UseQueryResult; -}; - -export const useMessageActionAppsActionButtons = (context?: MessageActionContext, category?: string) => { - const result = useAppActionButtons('messageAction'); - const actionManager = useUiKitActionManager(); - const applyButtonFilters = useApplyButtonFilters(category); - const dispatchToastMessage = useToastMessageDispatch(); - const { t } = useTranslation(); - const filterActionsByContext = useFilterActionsByContext(context); - const data = useMemo( - () => - result.data - ?.filter((action) => { - if (!filterActionsByContext(action)) { - return false; - } - return applyButtonFilters(action); - }) - .map((action) => { - const item: MessageActionConfig = { - icon: undefined as any, - id: getIdForActionButton(action), - label: Utilities.getI18nKeyForApp(action.labelI18n, action.appId), - order: 7, - type: 'apps', - variant: action.variant, - action: (_, params) => { - void actionManager - .emitInteraction(action.appId, { - type: 'actionButton', - rid: params.message.rid, - tmid: params.message.tmid, - mid: params.message._id, - actionId: action.actionId, - payload: { context: action.context }, - }) - .catch(async (reason) => { - if (reason instanceof UiKitTriggerTimeoutError) { - dispatchToastMessage({ - type: 'error', - message: t('UIKit_Interaction_Timeout'), - }); - return; - } - - return reason; - }); - }, - }; - - return item; - }), - [actionManager, applyButtonFilters, dispatchToastMessage, filterActionsByContext, result.data, t], - ); - return { - ...result, - data, - } as UseQueryResult; -}; diff --git a/apps/meteor/client/hooks/useFilterActions.ts b/apps/meteor/client/hooks/useFilterActions.ts deleted file mode 100644 index 4dda122c3bb3..000000000000 --- a/apps/meteor/client/hooks/useFilterActions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MessageActionContext } from '@rocket.chat/apps-engine/definition/ui'; -import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; -import { useCallback } from 'react'; - -export const useFilterActionsByContext = (context: string | undefined) => { - return useCallback( - (action: IUIActionButton) => { - if (!context) { - return true; - } - - const messageActionContext = action.when?.messageActionContext || Object.values(MessageActionContext); - const isContextMatch = messageActionContext.includes(context as MessageActionContext); - - return isContextMatch; - }, - [context], - ); -}; diff --git a/apps/meteor/client/hooks/useMessageboxAppsActionButtons.ts b/apps/meteor/client/hooks/useMessageboxAppsActionButtons.ts new file mode 100644 index 000000000000..10c6f4f58ef4 --- /dev/null +++ b/apps/meteor/client/hooks/useMessageboxAppsActionButtons.ts @@ -0,0 +1,62 @@ +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useAppActionButtons, getIdForActionButton } from './useAppActionButtons'; +import { useApplyButtonFilters } from './useApplyButtonFilters'; +import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTriggerTimeoutError'; +import type { MessageBoxAction } from '../../app/ui-utils/client/lib/messageBox'; +import { Utilities } from '../../ee/lib/misc/Utilities'; +import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; + +export const useMessageboxAppsActionButtons = (): UseQueryResult => { + const result = useAppActionButtons('messageBoxAction'); + const actionManager = useUiKitActionManager(); + const dispatchToastMessage = useToastMessageDispatch(); + const { t } = useTranslation(); + + const applyButtonFilters = useApplyButtonFilters(); + + const data = useMemo( + () => + result.data + ?.filter((action) => { + return applyButtonFilters(action); + }) + .map((action) => { + const item: Omit = { + id: getIdForActionButton(action), + label: Utilities.getI18nKeyForApp(action.labelI18n, action.appId), + action: (params) => { + void actionManager + .emitInteraction(action.appId, { + type: 'actionButton', + rid: params.rid, + tmid: params.tmid, + actionId: action.actionId, + payload: { context: action.context, message: params.chat.composer?.text ?? '' }, + }) + .catch(async (reason) => { + if (reason instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + return; + } + + return reason; + }); + }, + }; + + return item; + }), + [actionManager, applyButtonFilters, dispatchToastMessage, result.data, t], + ); + return { + ...result, + data, + } as UseQueryResult; +}; diff --git a/apps/meteor/client/hooks/useUserDropdownAppsActionButtons.ts b/apps/meteor/client/hooks/useUserDropdownAppsActionButtons.ts new file mode 100644 index 000000000000..69355590fe90 --- /dev/null +++ b/apps/meteor/client/hooks/useUserDropdownAppsActionButtons.ts @@ -0,0 +1,56 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useAppActionButtons } from './useAppActionButtons'; +import { useApplyButtonAuthFilter } from './useApplyButtonFilters'; +import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTriggerTimeoutError'; +import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; + +export const useUserDropdownAppsActionButtons = () => { + const result = useAppActionButtons('userDropdownAction'); + const actionManager = useUiKitActionManager(); + const dispatchToastMessage = useToastMessageDispatch(); + const { t } = useTranslation(); + + const applyButtonFilters = useApplyButtonAuthFilter(); + + const data = useMemo( + () => + result.data + ?.filter((action) => applyButtonFilters(action)) + .map((action) => { + return { + id: `${action.appId}_${action.actionId}`, + // icon: action.icon as GenericMenuItemProps['icon'], + content: action.labelI18n, + onClick: () => { + void actionManager + .emitInteraction(action.appId, { + type: 'actionButton', + actionId: action.actionId, + payload: { context: action.context }, + }) + .catch(async (reason) => { + if (reason instanceof UiKitTriggerTimeoutError) { + dispatchToastMessage({ + type: 'error', + message: t('UIKit_Interaction_Timeout'), + }); + return; + } + + return reason; + }); + }, + }; + }), + [actionManager, applyButtonFilters, dispatchToastMessage, result.data, t], + ); + return { + ...result, + data, + } as UseQueryResult; +}; diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index 40382fcd6bef..a9217935b821 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, Serialized } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; export const roomsQueryKeys = { all: ['rooms'] as const, @@ -7,9 +7,6 @@ export const roomsQueryKeys = { pinnedMessages: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'pinned-messages'] as const, messages: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'messages'] as const, message: (rid: IRoom['_id'], mid: IMessage['_id']) => [...roomsQueryKeys.messages(rid), mid] as const, - messageActions: (rid: IRoom['_id'], mid: IMessage['_id']) => [...roomsQueryKeys.message(rid, mid), 'actions'] as const, - messageActionsWithParameters: (rid: IRoom['_id'], message: IMessage | Serialized) => - [...roomsQueryKeys.messageActions(rid, message._id), message] as const, threads: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'threads'] as const, }; diff --git a/apps/meteor/client/providers/RouterProvider.tsx b/apps/meteor/client/providers/RouterProvider.tsx index 590c5f20da57..1fab7713a5b2 100644 --- a/apps/meteor/client/providers/RouterProvider.tsx +++ b/apps/meteor/client/providers/RouterProvider.tsx @@ -46,7 +46,7 @@ const subscribeToRouteChange = (onRouteChange: () => void): (() => void) => { }; }; -const getLocationPathname = () => FlowRouter.current().path as LocationPathname; +const getLocationPathname = () => FlowRouter.current().path.replace(/\?.*/, '') as LocationPathname; const getLocationSearch = () => location.search as LocationSearch; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx index f7b9cea0d56a..2fa95d1606e1 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx @@ -3,7 +3,7 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useRoute, usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; -import { useUserDropdownAppsActionButtons } from '../../../../hooks/useAppActionButtons'; +import { useUserDropdownAppsActionButtons } from '../../../../hooks/useUserDropdownAppsActionButtons'; import { useAppRequestStats } from '../../../../views/marketplace/hooks/useAppRequestStats'; /** diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 3edff17dc427..7810fc23fc5f 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -16,7 +16,6 @@ import './messageObserve'; import './messageTypes'; import './notifications'; import './otr'; -import './readReceipt'; import './reloadRoomAfterLogin'; import './roles'; import './rootUrlChange'; diff --git a/apps/meteor/client/startup/readReceipt.ts b/apps/meteor/client/startup/readReceipt.ts deleted file mode 100644 index 5998ea3e6c52..000000000000 --- a/apps/meteor/client/startup/readReceipt.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { settings } from '../../app/settings/client'; -import { MessageAction } from '../../app/ui-utils/client'; -import { imperativeModal } from '../lib/imperativeModal'; -import ReadReceiptsModal from '../views/room/modals/ReadReceiptsModal'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const enabled = settings.get('Message_Read_Receipt_Enabled') && settings.get('Message_Read_Receipt_Store_Users'); - - if (!enabled) { - return MessageAction.removeButton('receipt-detail'); - } - - MessageAction.addButton({ - id: 'receipt-detail', - icon: 'check-double', - label: 'Read_Receipts', - context: ['starred', 'message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], - type: 'duplication', - action(_, { message }) { - imperativeModal.open({ - component: ReadReceiptsModal, - props: { messageId: message._id, onClose: imperativeModal.close }, - }); - }, - order: 10, - group: 'menu', - }); - }); -}); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index b4ec012f3337..64eed975e9cc 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -16,7 +16,7 @@ import { useVideoMessageAction } from './hooks/useVideoMessageAction'; import { useWebdavActions } from './hooks/useWebdavActions'; import { messageBox } from '../../../../../../app/ui-utils/client'; import { isTruthy } from '../../../../../../lib/isTruthy'; -import { useMessageboxAppsActionButtons } from '../../../../../hooks/useAppActionButtons'; +import { useMessageboxAppsActionButtons } from '../../../../../hooks/useMessageboxAppsActionButtons'; import { useChat } from '../../../contexts/ChatContext'; import { useRoom } from '../../../contexts/RoomContext';