diff --git a/.changeset/blue-rats-kick.md b/.changeset/blue-rats-kick.md new file mode 100644 index 000000000000..16b0bbc97e21 --- /dev/null +++ b/.changeset/blue-rats-kick.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Fixes Unit's `numDepartments` property not being updated after a department is removed diff --git a/.changeset/chilled-seas-refuse.md b/.changeset/chilled-seas-refuse.md new file mode 100644 index 000000000000..d4066938513d --- /dev/null +++ b/.changeset/chilled-seas-refuse.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps-engine': patch +--- + +Fixes the subprocess restarting routine failing to correctly restart apps in some cases diff --git a/.changeset/four-cows-sin.md b/.changeset/four-cows-sin.md new file mode 100644 index 000000000000..27f3cd14c0cd --- /dev/null +++ b/.changeset/four-cows-sin.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where room members menu doesn't display properly without enough space diff --git a/.changeset/green-queens-end.md b/.changeset/green-queens-end.md new file mode 100644 index 000000000000..d6cf413bfcf6 --- /dev/null +++ b/.changeset/green-queens-end.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue with Federation startup where the bridge would intermittently fail to start causing error being shown "Matrix Bridge isn't running yet". diff --git a/.changeset/lemon-singers-exercise.md b/.changeset/lemon-singers-exercise.md new file mode 100644 index 000000000000..1e0e77d013c0 --- /dev/null +++ b/.changeset/lemon-singers-exercise.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/i18n": patch +--- + +Changes the wording for voice call permissions, improving consistency and clarity. + +- `Manage Voip Extension` -> `Manage Voice Calls` + > Permission to manage voice calls and assign extensions to users +- `View VoIP extension details` -> `View Voice Call Extensions` + > Permission to view which user is calling and their extension info +- `View User VoIP extension` -> `Allow Voice Calls` + > Permission to allow users to use the voice call feature + diff --git a/.changeset/tidy-boxes-hide.md b/.changeset/tidy-boxes-hide.md new file mode 100644 index 000000000000..33b8f0c812d0 --- /dev/null +++ b/.changeset/tidy-boxes-hide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes special characters not being escaped on sidepanel extended view diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index e8a152db914a..00fa0007df15 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -9,6 +9,7 @@ import { isRoomsExportProps, isRoomsIsMemberProps, isRoomsCleanHistoryProps, + isRoomsOpenProps, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; @@ -16,6 +17,7 @@ import { isTruthy } from '../../../../lib/isTruthy'; import { omit } from '../../../../lib/utils/omit'; import * as dataExport from '../../../../server/lib/dataExport'; import { eraseRoom } from '../../../../server/lib/eraseRoom'; +import { openRoom } from '../../../../server/lib/openRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; @@ -893,3 +895,17 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'rooms.open', + { authRequired: true, validateParams: isRoomsOpenProps }, + { + async post() { + const { roomId } = this.bodyParams; + + await openRoom(this.userId, roomId); + + return API.v1.success(); + }, + }, +); 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/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts index 76c70cba49ba..fdc85105ab07 100644 --- a/apps/meteor/app/livechat/server/lib/departmentsLib.ts +++ b/apps/meteor/app/livechat/server/lib/departmentsLib.ts @@ -231,8 +231,8 @@ export async function setDepartmentForGuest({ token, department }: { token: stri export async function removeDepartment(departmentId: string) { livechatLogger.debug(`Removing department: ${departmentId}`); - const department = await LivechatDepartment.findOneById>(departmentId, { - projection: { _id: 1, businessHourId: 1 }, + const department = await LivechatDepartment.findOneById>(departmentId, { + projection: { _id: 1, businessHourId: 1, parentId: 1 }, }); if (!department) { throw new Error('error-department-not-found'); diff --git a/apps/meteor/app/notification-queue/server/NotificationQueue.ts b/apps/meteor/app/notification-queue/server/NotificationQueue.ts index 78a9e931fcb5..8bfcead0e47f 100644 --- a/apps/meteor/app/notification-queue/server/NotificationQueue.ts +++ b/apps/meteor/app/notification-queue/server/NotificationQueue.ts @@ -102,7 +102,7 @@ class NotificationClass { return true; } - return this.worker(counter++); + return this.worker(++counter); } getNextNotification(): Promise { 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/main.ts b/apps/meteor/client/main.ts index d66b5bcec2de..62da42ae4198 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -13,6 +13,6 @@ import('./polyfills') .then(() => import('./meteorOverrides')) .then(() => import('./ecdh')) .then(() => import('./importPackages')) - .then(() => Promise.all([import('./methods'), import('./startup')])) + .then(() => import('./startup')) .then(() => import('./omnichannel')) .then(() => Promise.all([import('./views/admin'), import('./views/marketplace'), import('./views/account')])); diff --git a/apps/meteor/client/methods/index.ts b/apps/meteor/client/methods/index.ts deleted file mode 100644 index 6e054fd54892..000000000000 --- a/apps/meteor/client/methods/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './openRoom'; diff --git a/apps/meteor/client/methods/openRoom.ts b/apps/meteor/client/methods/openRoom.ts deleted file mode 100644 index afe12060ad50..000000000000 --- a/apps/meteor/client/methods/openRoom.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { Subscriptions } from '../../app/models/client'; - -Meteor.methods({ - async openRoom(rid) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'openRoom', - }); - } - - return Subscriptions.update( - { - rid, - 'u._id': Meteor.userId(), - }, - { - $set: { - open: true, - }, - }, - ); - }, -}); 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/Sidepanel/hooks/useItemData.tsx b/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx index 8e8cb798d2f7..bdf3ceb65ee6 100644 --- a/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx @@ -48,7 +48,7 @@ export const useItemData = ( time, badges, avatar: AvatarTemplate && , - subtitle: message, + subtitle: message ? : null, }), [AvatarTemplate, badges, highlighted, icon, message, openedRoom, room, time], ); 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'; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx index a9ba5dd9dc0c..76185e3b1cbd 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx @@ -25,7 +25,7 @@ const RoomMembersActions = ({ username, _id, name, rid, freeSwitchExtension, rel if (!menuOptions) { return null; } - return ; + return ; }; export default RoomMembersActions; diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.ts index 9937af4ecef7..6c7326f94d76 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.ts @@ -3,6 +3,7 @@ import { useMethod, useRoute, useSetting, useUser } from '@rocket.chat/ui-contex import { useQuery } from '@tanstack/react-query'; import { useRef } from 'react'; +import { useOpenRoomMutation } from './useOpenRoomMutation'; import { roomFields } from '../../../../lib/publishFields'; import { omit } from '../../../../lib/utils/omit'; import { NotAuthorizedError } from '../../../lib/errors/NotAuthorizedError'; @@ -15,8 +16,8 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead', true); const getRoomByTypeAndName = useMethod('getRoomByTypeAndName'); const createDirectMessage = useMethod('createDirectMessage'); - const openRoom = useMethod('openRoom'); const directRoute = useRoute('direct'); + const openRoom = useOpenRoomMutation(); const unsubscribeFromRoomOpenedEvent = useRef<() => void>(() => undefined); @@ -96,8 +97,8 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st // update user's room subscription const sub = Subscriptions.findOne({ rid: room._id }); - if (sub && !sub.open) { - await openRoom(room._id); + if (!!user?._id && sub && !sub.open) { + await openRoom.mutateAsync({ roomId: room._id, userId: user._id }); } return { rid: room._id }; }, diff --git a/apps/meteor/client/views/room/hooks/useOpenRoomMutation.tsx b/apps/meteor/client/views/room/hooks/useOpenRoomMutation.tsx new file mode 100644 index 000000000000..65fbe785bffc --- /dev/null +++ b/apps/meteor/client/views/room/hooks/useOpenRoomMutation.tsx @@ -0,0 +1,32 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; + +import { updateSubscription } from '../../../lib/mutationEffects/updateSubscription'; + +type OpenRoomParams = { + roomId: string; + userId: string; +}; + +export const useOpenRoomMutation = () => { + const openRoom = useEndpoint('POST', '/v1/rooms.open'); + + return useMutation({ + mutationFn: async ({ roomId, userId }: OpenRoomParams) => { + await openRoom({ roomId }); + + return { userId, roomId }; + }, + onMutate: async ({ roomId, userId }) => { + return updateSubscription(roomId, userId, { open: true }); + }, + onError: async (_, { roomId, userId }, rollbackDocument) => { + if (!rollbackDocument) { + return; + } + + const { open } = rollbackDocument; + await updateSubscription(roomId, userId, { open }); + }, + }); +}; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts index 1bab201ab41b..64d92cca5f1e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts @@ -1,11 +1,11 @@ import type { AtLeast, ILivechatAgent, ILivechatDepartment } from '@rocket.chat/core-typings'; -import { LivechatDepartment } from '@rocket.chat/models'; +import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models'; import { callbacks } from '../../../../../lib/callbacks'; import { cbLogger } from '../lib/logger'; const afterRemoveDepartment = async (options: { - department: AtLeast; + department: AtLeast; agentsId: ILivechatAgent['_id'][]; }) => { if (!options?.department) { @@ -15,8 +15,14 @@ const afterRemoveDepartment = async (options: { const { department } = options; - cbLogger.debug(`Removing department from forward list: ${department._id}`); - await LivechatDepartment.removeDepartmentFromForwardListById(department._id); + cbLogger.debug({ + msg: 'Post removal actions on EE code for department', + department, + }); + await Promise.all([ + LivechatDepartment.removeDepartmentFromForwardListById(department._id), + ...(department.parentId ? [LivechatUnit.decrementDepartmentsCount(department.parentId)] : []), + ]); return options; }; diff --git a/apps/meteor/ee/server/local-services/federation/service.ts b/apps/meteor/ee/server/local-services/federation/service.ts index 5e813caf928a..0fc952d2b6f2 100644 --- a/apps/meteor/ee/server/local-services/federation/service.ts +++ b/apps/meteor/ee/server/local-services/federation/service.ts @@ -133,7 +133,7 @@ abstract class AbstractBaseFederationServiceEE extends AbstractFederationService await super.cleanUpHandlers(); } - public async created(): Promise { + public async started(): Promise { await super.setupFederation(); await this.startFederation(); } @@ -213,8 +213,8 @@ export class FederationServiceEE extends AbstractBaseFederationServiceEE impleme return federationService; } - async created(): Promise { - return super.created(); + async started(): Promise { + return super.started(); } async stopped(): Promise { diff --git a/apps/meteor/server/lib/openRoom.ts b/apps/meteor/server/lib/openRoom.ts new file mode 100644 index 000000000000..cf5b615bde09 --- /dev/null +++ b/apps/meteor/server/lib/openRoom.ts @@ -0,0 +1,13 @@ +import { Subscriptions } from '@rocket.chat/models'; + +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; + +export async function openRoom(userId: string, roomId: string) { + const openByRoomResponse = await Subscriptions.openByRoomIdAndUserId(roomId, userId); + + if (openByRoomResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(roomId, userId); + } + + return openByRoomResponse.modifiedCount; +} diff --git a/apps/meteor/server/methods/openRoom.ts b/apps/meteor/server/methods/openRoom.ts index 440de52b87fb..354da36f60eb 100644 --- a/apps/meteor/server/methods/openRoom.ts +++ b/apps/meteor/server/methods/openRoom.ts @@ -1,10 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; +import { openRoom } from '../lib/openRoom'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -25,12 +24,6 @@ Meteor.methods({ }); } - const openByRoomResponse = await Subscriptions.openByRoomIdAndUserId(rid, uid); - - if (openByRoomResponse.modifiedCount) { - void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); - } - - return openByRoomResponse.modifiedCount; + return openRoom(uid, rid); }, }); diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts index 443dfe883a4f..3bbb803efccc 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts @@ -160,6 +160,7 @@ export class RocketChatSettingsAdapter { 'sender_localpart': registrationFile.botName, 'namespaces': registrationFile.listenTo, 'de.sorunome.msc2409.push_ephemeral': registrationFile.enableEphemeralEvents, + 'use_appservice_legacy_authorization': true, }), ); } diff --git a/apps/meteor/server/services/federation/service.ts b/apps/meteor/server/services/federation/service.ts index 904e73913a17..346ca1c4a4e7 100644 --- a/apps/meteor/server/services/federation/service.ts +++ b/apps/meteor/server/services/federation/service.ts @@ -414,7 +414,7 @@ abstract class AbstractBaseFederationService extends AbstractFederationService { await super.cleanUpSettingObserver(); } - public async created(): Promise { + public async started(): Promise { await super.setupFederation(); await this.startFederation(); } @@ -447,8 +447,8 @@ export class FederationService extends AbstractBaseFederationService implements return super.stopped(); } - public async created(): Promise { - return super.created(); + public async started(): Promise { + return super.started(); } public async verifyConfiguration(): Promise { diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 428440a5c523..4e6b62827c85 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -1,6 +1,8 @@ +import { faker } from '@faker-js/faker'; + import { Users } from './fixtures/userStates'; import { AccountProfile, HomeChannel } from './page-objects'; -import { createTargetChannel, setSettingValueById } from './utils'; +import { createTargetChannel, createTargetTeam, deleteChannel, deleteTeam, setSettingValueById } from './utils'; import { setUserPreferences } from './utils/setUserPreferences'; import { test, expect } from './utils/test'; @@ -10,15 +12,17 @@ test.describe.serial('feature preview', () => { let poHomeChannel: HomeChannel; let poAccountProfile: AccountProfile; let targetChannel: string; + let sidepanelTeam: string; + const targetChannelNameInTeam = `channel-from-team-${faker.number.int()}`; test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true); - targetChannel = await createTargetChannel(api); + targetChannel = await createTargetChannel(api, { members: ['user1'] }); }); test.afterAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false); - await api.post('/channels.delete', { roomName: targetChannel }); + await deleteChannel(api, targetChannel); }); test.beforeEach(async ({ page }) => { @@ -155,8 +159,161 @@ test.describe.serial('feature preview', () => { const collapser = poHomeChannel.sidebar.getCollapseGroupByName('Channels'); await collapser.click(); - await expect(poHomeChannel.sidebar.getItemUnreadBadge(collapser)).toBeVisible(); }); }); + + test.describe('Sidepanel', () => { + test.beforeEach(async ({ api }) => { + sidepanelTeam = await createTargetTeam(api, { sidepanel: { items: ['channels', 'discussions'] } }); + + await setUserPreferences(api, { + sidebarViewMode: 'Medium', + featuresPreview: [ + { + name: 'newNavigation', + value: true, + }, + { + name: 'sidepanelNavigation', + value: true, + }, + ], + }); + }); + + test.afterEach(async ({ api }) => { + await deleteTeam(api, sidepanelTeam); + + await setUserPreferences(api, { + sidebarViewMode: 'Medium', + featuresPreview: [ + { + name: 'newNavigation', + value: false, + }, + { + name: 'sidepanelNavigation', + value: false, + }, + ], + }); + }); + test('should be able to toggle "Sidepanel" feature', async ({ page }) => { + await page.goto('/account/feature-preview'); + + await poAccountProfile.getAccordionItemByName('Navigation').click(); + const sidepanelCheckbox = poAccountProfile.getCheckboxByLabelText('Secondary navigation for teams'); + await expect(sidepanelCheckbox).toBeChecked(); + await sidepanelCheckbox.click(); + await expect(sidepanelCheckbox).not.toBeChecked(); + + await poAccountProfile.btnSaveChanges.click(); + + await expect(poAccountProfile.btnSaveChanges).not.toBeVisible(); + await expect(sidepanelCheckbox).not.toBeChecked(); + }); + + test('should display sidepanel on a team and hide it on edit', async ({ page }) => { + await page.goto(`/group/${sidepanelTeam}`); + await poHomeChannel.content.waitForChannel(); + await expect(poHomeChannel.sidepanel.sidepanelList).toBeVisible(); + + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.advancedSettingsAccordion.click(); + await poHomeChannel.tabs.room.toggleSidepanelItems(); + await poHomeChannel.tabs.room.btnSave.click(); + + await expect(poHomeChannel.sidepanel.sidepanelList).not.toBeVisible(); + }); + + test('should display new channel from team on the sidepanel', async ({ page, api }) => { + await page.goto(`/group/${sidepanelTeam}`); + await poHomeChannel.content.waitForChannel(); + + await poHomeChannel.tabs.btnChannels.click(); + await poHomeChannel.tabs.channels.btnCreateNew.click(); + await poHomeChannel.sidenav.inputChannelName.fill(targetChannelNameInTeam); + await poHomeChannel.sidenav.checkboxPrivateChannel.click(); + await poHomeChannel.sidenav.btnCreate.click(); + + await expect(poHomeChannel.sidepanel.sidepanelList).toBeVisible(); + await expect(poHomeChannel.sidepanel.getItemByName(targetChannelNameInTeam)).toBeVisible(); + + await deleteChannel(api, targetChannelNameInTeam); + }); + + test('should display sidepanel item with the same display preference as the sidebar', async ({ page }) => { + await page.goto('/home'); + const message = 'hello world'; + + await poHomeChannel.sidebar.setDisplayMode('Extended'); + await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.content.sendMessage(message); + await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); + }); + + test('should escape special characters on item subtitle', async ({ page }) => { + await page.goto('/home'); + const message = 'hello > world'; + const parsedWrong = 'hello > world'; + + await poHomeChannel.sidebar.setDisplayMode('Extended'); + await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.content.sendMessage(message); + + await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); + await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).not.toHaveText(parsedWrong); + }); + + test('should show channel in sidepanel after adding existing one', async ({ page }) => { + await page.goto(`/group/${sidepanelTeam}`); + + await poHomeChannel.tabs.btnChannels.click(); + await poHomeChannel.tabs.channels.btnAddExisting.click(); + await poHomeChannel.tabs.channels.inputChannels.fill(targetChannel); + await page.getByRole('listbox').getByRole('option', { name: targetChannel }).click(); + await poHomeChannel.tabs.channels.btnAdd.click(); + await poHomeChannel.content.waitForChannel(); + + await expect(poHomeChannel.sidepanel.getItemByName(targetChannel)).toBeVisible(); + }); + + // remove .fail after fix + test.fail('should sort by last message even if unread message is inside thread', async ({ page, browser }) => { + const user1Page = await browser.newPage({ storageState: Users.user1.state }); + const user1Channel = new HomeChannel(user1Page); + + await page.goto(`/group/${sidepanelTeam}`); + + await poHomeChannel.tabs.btnChannels.click(); + await poHomeChannel.tabs.channels.btnAddExisting.click(); + await poHomeChannel.tabs.channels.inputChannels.fill(targetChannel); + await page.getByRole('listbox').getByRole('option', { name: targetChannel }).click(); + await poHomeChannel.tabs.channels.btnAdd.click(); + + const sidepanelTeamItem = poHomeChannel.sidepanel.getItemByName(sidepanelTeam); + const targetChannelItem = poHomeChannel.sidepanel.getItemByName(targetChannel); + + await targetChannelItem.click(); + expect(page.url()).toContain(`/channel/${targetChannel}`); + await poHomeChannel.content.sendMessage('hello channel'); + await sidepanelTeamItem.focus(); + await sidepanelTeamItem.click(); + expect(page.url()).toContain(`/group/${sidepanelTeam}`); + await poHomeChannel.content.sendMessage('hello team'); + + await user1Page.goto(`/channel/${targetChannel}`); + await user1Channel.content.waitForChannel(); + await user1Channel.content.openReplyInThread(); + await user1Channel.content.toggleAlsoSendThreadToChannel(false); + await user1Channel.content.sendMessageInThread('hello thread'); + + const item = poHomeChannel.sidepanel.getItemByName(targetChannel); + await expect(item.locator('..')).toHaveAttribute('data-item-index', '0'); + + await user1Page.close(); + }); + }); }); diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts index f7f41911e6d3..c1493a077fa4 100644 --- a/apps/meteor/tests/e2e/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts @@ -119,4 +119,8 @@ export class AccountProfile { getCheckboxByLabelText(name: string): Locator { return this.page.locator('label', { has: this.page.getByRole('checkbox', { name }) }); } + + get btnSaveChanges(): Locator { + return this.page.getByRole('button', { name: 'Save changes', exact: true }); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 0c77364ed57a..59df066d8163 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -323,7 +323,10 @@ export class HomeContent { } async toggleAlsoSendThreadToChannel(isChecked: boolean): Promise { - await this.page.getByRole('dialog').locator('[name="alsoSendThreadToChannel"]').setChecked(isChecked); + await this.page + .getByRole('dialog') + .locator('label', { has: this.page.getByRole('checkbox', { name: 'Also send to channel' }) }) + .setChecked(isChecked); } get lastSystemMessageBody(): Locator { @@ -398,7 +401,20 @@ export class HomeContent { async waitForChannel(): Promise { await this.page.locator('role=main').waitFor(); await this.page.locator('role=main >> role=heading[level=1]').waitFor(); + const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); + await messageList.waitFor(); - await expect(this.page.locator('role=main >> role=list')).not.toHaveAttribute('aria-busy', 'true'); + await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); + } + + async openReplyInThread(): Promise { + await this.page.locator('[data-qa-type="message"]').last().hover(); + await this.page.locator('[data-qa-type="message"]').last().locator('role=button[name="Reply in thread"]').waitFor(); + await this.page.locator('[data-qa-type="message"]').last().locator('role=button[name="Reply in thread"]').click(); + } + + async sendMessageInThread(text: string): Promise { + await this.page.getByRole('dialog').getByRole('textbox', { name: 'Message' }).fill(text); + await this.page.getByRole('dialog').getByRole('button', { name: 'Send', exact: true }).click(); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts index f0eb7b726d45..61d5ca5945d5 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts @@ -122,4 +122,17 @@ export class HomeFlextabRoom { get checkboxIgnoreThreads(): Locator { return this.page.getByRole('dialog').locator('label', { has: this.page.getByRole('checkbox', { name: 'Do not prune Threads' }) }); } + + get checkboxChannels(): Locator { + return this.page.getByRole('dialog').locator('label', { has: this.page.getByRole('checkbox', { name: 'Channels' }) }); + } + + get checkboxDiscussions(): Locator { + return this.page.getByRole('dialog').locator('label', { has: this.page.getByRole('checkbox', { name: 'Discussions' }) }); + } + + async toggleSidepanelItems() { + await this.checkboxChannels.click(); + await this.checkboxDiscussions.click(); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index fc5ab5e62385..f80290a0d373 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -6,3 +6,4 @@ export * from './omnichannel-sidenav'; export * from './omnichannel-close-chat-modal'; export * from './navbar'; export * from './sidebar'; +export * from './sidepanel'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts index 5e5488c127f8..a34a1af480a5 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -42,6 +42,12 @@ export class Sidebar { return this.sidebarSearchSection.getByRole('searchbox'); } + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { + await this.sidebarSearchSection.getByRole('button', { name: 'Display', exact: true }).click(); + await this.sidebarSearchSection.getByRole('menuitemcheckbox', { name: mode }).click(); + await this.sidebarSearchSection.click(); + } + async escSearch(): Promise { await this.page.keyboard.press('Escape'); } @@ -49,9 +55,10 @@ export class Sidebar { async waitForChannel(): Promise { await this.page.locator('role=main').waitFor(); await this.page.locator('role=main >> role=heading[level=1]').waitFor(); - await this.page.locator('role=main >> role=list').waitFor(); + const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); + await messageList.waitFor(); - await expect(this.page.locator('role=main >> role=list')).not.toHaveAttribute('aria-busy', 'true'); + await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); } async typeSearch(name: string): Promise { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidepanel.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidepanel.ts new file mode 100644 index 000000000000..a7c8b036d70c --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidepanel.ts @@ -0,0 +1,30 @@ +import type { Locator, Page } from '@playwright/test'; + +export class Sidepanel { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get sidepanelList(): Locator { + return this.page.getByRole('main').getByRole('list', { name: 'Channels' }); + } + + get firstChannelFromList(): Locator { + return this.sidepanelList.getByRole('listitem').first(); + } + + getItemByName(name: string): Locator { + return this.sidepanelList.getByRole('link').filter({ hasText: name }); + } + + getExtendedItem(name: string, subtitle?: string): Locator { + const regex = new RegExp(`${name}.*${subtitle}`); + return this.sidepanelList.getByRole('link', { name: regex }); + } + + getItemUnreadBadge(item: Locator): Locator { + return item.getByRole('status', { name: 'unread' }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 76929b6c2dcf..10e28bb23618 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeContent, HomeSidenav, HomeFlextab, Navbar, Sidebar } from './fragments'; +import { HomeContent, HomeSidenav, HomeFlextab, Navbar, Sidebar, Sidepanel } from './fragments'; export class HomeChannel { public readonly page: Page; @@ -11,6 +11,8 @@ export class HomeChannel { readonly sidebar: Sidebar; + readonly sidepanel: Sidepanel; + readonly navbar: Navbar; readonly tabs: HomeFlextab; @@ -20,6 +22,7 @@ export class HomeChannel { this.content = new HomeContent(page); this.sidenav = new HomeSidenav(page); this.sidebar = new Sidebar(page); + this.sidepanel = new Sidepanel(page); this.navbar = new Navbar(page); this.tabs = new HomeFlextab(page); } diff --git a/apps/meteor/tests/e2e/utils/create-target-channel.ts b/apps/meteor/tests/e2e/utils/create-target-channel.ts index 8f7a25aa9718..370f4dc2c8ec 100644 --- a/apps/meteor/tests/e2e/utils/create-target-channel.ts +++ b/apps/meteor/tests/e2e/utils/create-target-channel.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import type { IRoom } from '@rocket.chat/core-typings'; import type { ChannelsCreateProps, GroupsCreateProps } from '@rocket.chat/rest-typings'; import type { BaseTest } from './test'; @@ -25,9 +26,12 @@ export async function createTargetPrivateChannel(api: BaseTest['api'], options?: return name; } -export async function createTargetTeam(api: BaseTest['api']): Promise { +export async function createTargetTeam( + api: BaseTest['api'], + options?: { sidepanel?: IRoom['sidepanel'] } & Omit, +): Promise { const name = faker.string.uuid(); - await api.post('/teams.create', { name, type: 1, members: ['user2', 'user1'] }); + await api.post('/teams.create', { name, type: 1, members: ['user2', 'user1'], ...options }); return name; } diff --git a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts index e0c079ece243..95c24f524486 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts @@ -482,11 +482,10 @@ import { IS_EE } from '../../../e2e/config/constants'; it('should succesfully create a department into an existing unit that a monitor supervises', async () => { const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor1Credentials); - // Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed const updatedUnit = await getUnit(unit._id); expect(updatedUnit).to.have.property('name', unit.name); expect(updatedUnit).to.have.property('numMonitors', 1); - expect(updatedUnit).to.have.property('numDepartments', 2); + expect(updatedUnit).to.have.property('numDepartments', 1); const fullDepartment = await getDepartmentById(department._id); expect(fullDepartment).to.have.property('parentId', unit._id); @@ -495,6 +494,13 @@ import { IS_EE } from '../../../e2e/config/constants'; await deleteDepartment(department._id); }); + + it('unit should end up with 0 departments after removing all of them', async () => { + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 0); + }); }); describe('[PUT] livechat/department', () => { @@ -943,11 +949,10 @@ import { IS_EE } from '../../../e2e/config/constants'; }); testDepartmentId = testDepartment._id; - // Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed const updatedUnit = await getUnit(unit._id); expect(updatedUnit).to.have.property('name', unit.name); expect(updatedUnit).to.have.property('numMonitors', 1); - expect(updatedUnit).to.have.property('numDepartments', 3); + expect(updatedUnit).to.have.property('numDepartments', 2); const fullDepartment = await getDepartmentById(testDepartmentId); expect(fullDepartment).to.have.property('parentId', unit._id); diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index ca033ef88bf8..110eb22a83de 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -3317,6 +3317,7 @@ describe('[Rooms]', () => { }); }); }); + describe('/rooms.isMember', () => { let testChannel: IRoom; let testGroup: IRoom; @@ -3599,4 +3600,51 @@ describe('[Rooms]', () => { }); }); }); + + describe('/rooms.open', () => { + let room: IRoom; + + before(async () => { + room = (await createRoom({ type: 'c', name: `rooms.open.test.${Date.now()}` })).body.channel; + }); + + after(async () => { + await deleteRoom({ type: 'c', roomId: room._id }); + }); + + it('should open the room', (done) => { + void request + .post(api('rooms.open')) + .set(credentials) + .send({ roomId: room._id }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + + void request + .get(api('subscriptions.getOne')) + .set(credentials) + .query({ roomId: room._id }) + .send() + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.subscription).to.have.property('open', true); + }) + .end(done); + }); + + it('should fail if roomId is not provided', async () => { + await request + .post(api('rooms.open')) + .set(credentials) + .send() + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + }); + }); + }); }); diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index 963b90a39a6b..0bb930b723a1 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -1035,6 +1035,10 @@ export class AppManager { result = false; await app.setStatus(status, silenceStatus); + + // If some error has happened in initialization, like license or installations invalidation + // we need to store this on the DB regardless of what the parameter requests + saveToDb = true; } if (saveToDb) { @@ -1113,6 +1117,10 @@ export class AppManager { } console.error(e); + + // If some error has happened during enabling, like license or installations invalidation + // we need to store this on the DB regardless of what the parameter requests + saveToDb = true; } if (enable) { diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts index 458799286c83..22608962bcf7 100644 --- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -9,14 +9,14 @@ import { LivenessManager } from './LivenessManager'; import { ProcessMessenger } from './ProcessMessenger'; import { bundleLegacyApp } from './bundler'; import { decoder } from './codec'; -import { AppStatus } from '../../../definition/AppStatus'; +import { AppStatus, AppStatusUtils } from '../../../definition/AppStatus'; +import type { AppMethod } from '../../../definition/metadata'; import type { AppManager } from '../../AppManager'; import type { AppBridges } from '../../bridges'; import type { IParseAppPackageResult } from '../../compiler'; import { AppConsole, type ILoggerStorageEntry } from '../../logging'; import type { AppAccessorManager, AppApiManager } from '../../managers'; import type { AppLogStorage, IAppStorageItem } from '../../storage'; -import { AppMethod } from '../../../definition/metadata'; const baseDebug = debugFactory('appsEngine:runtime:deno'); @@ -287,6 +287,10 @@ export class DenoRuntimeSubprocessController extends EventEmitter { await this.sendRequest({ method: 'app:initialize' }); await this.sendRequest({ method: 'app:setStatus', params: [this.storageItem.status] }); + if (AppStatusUtils.isEnabled(this.storageItem.status)) { + await this.sendRequest({ method: 'app:onEnable' }); + } + this.state = 'ready'; logger.info('Successfully restarted app subprocess'); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 81c6e0f3715d..3ba0b961b6ee 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3566,8 +3566,8 @@ "manage-user-status_description": "Permission to manage the server custom user statuses", "manage-voip-call-settings": "Manage Voip Call Settings", "manage-voip-call-settings_description": "Permission to manage voip call settings", - "manage-voip-extensions": "Manage Voip Extensions", - "manage-voip-extensions_description": "Permission to manage voip extensions assigned to users", + "manage-voip-extensions": "Manage Voice Calls", + "manage-voip-extensions_description": "Permission to manage voice calls and assign extensions to users", "manage-voip-contact-center-settings": "Manage Voip Contact Center Settings", "manage-voip-contact-center-settings_description": "Permission to manage voip contact center settings", "Manage_Omnichannel": "Manage Omnichannel", @@ -6042,10 +6042,10 @@ "View_thread": "View thread", "view-user-administration": "View User Administration", "view-user-administration_description": "Permission to partial, read-only list view of other user accounts currently logged into the system. No user account information is accessible with this permission", - "view-user-voip-extension": "View User VoIP Extension", - "view-user-voip-extension_description": "Permission to view user's assigned VoIP Extension", - "view-voip-extension-details": "View VoIP Extension Details", - "view-voip-extension-details_description": "Permission to view the details associated with VoIP extensions", + "view-user-voip-extension": "Allow Voice Calls", + "view-user-voip-extension_description": "Permission to allow users to use the voice call feature", + "view-voip-extension-details": "View Voice Call Extensions", + "view-voip-extension-details_description": "Permission to view which user is calling and their extension info", "Viewing_room_administration": "Viewing room administration", "Visibility": "Visibility", "Visible": "Visible", diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json index cec29a4655e7..fac7195afff7 100644 --- a/packages/i18n/src/locales/se.i18n.json +++ b/packages/i18n/src/locales/se.i18n.json @@ -3476,7 +3476,6 @@ "manage-voip-call-settings": "Manage Voip Call Settings", "manage-voip-call-settings_description": "Permission to manage voip call settings", "manage-voip-extensions": "Manage Voip Extensions", - "manage-voip-extensions_description": "Permission to manage voip extensions assigned to users", "manage-voip-contact-center-settings": "Manage Voip Contact Center Settings", "manage-voip-contact-center-settings_description": "Permission to manage voip contact center settings", "Manage_Omnichannel": "Manage Omnichannel", @@ -5923,10 +5922,6 @@ "view-statistics_description": "Permission to view system statistics such as number of users logged in, number of rooms, operating system information", "view-user-administration": "View User Administration", "view-user-administration_description": "Permission to partial, read-only list view of other user accounts currently logged into the system. No user account information is accessible with this permission", - "view-user-voip-extension": "View User VoIP Extension", - "view-user-voip-extension_description": "Permission to view user's assigned VoIP Extension", - "view-voip-extension-details": "View VoIP Extension Details", - "view-voip-extension-details_description": "Permission to view the details associated with VoIP extensions", "Viewing_room_administration": "Viewing room administration", "Visibility": "Visibility", "Visible": "Visible", diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 0f420b486a3b..e9b28a51140c 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -598,6 +598,24 @@ const roomsCleanHistorySchema = { export const isRoomsCleanHistoryProps = ajv.compile(roomsCleanHistorySchema); +type RoomsOpenProps = { + roomId: string; +}; + +const roomsOpenSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + minLength: 1, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isRoomsOpenProps = ajv.compile(roomsOpenSchema); + export type RoomsEndpoints = { '/v1/rooms.autocomplete.channelAndPrivate': { GET: (params: RoomsAutoCompleteChannelAndPrivateProps) => { @@ -764,4 +782,8 @@ export type RoomsEndpoints = { files: IUpload[]; }>; }; + + '/v1/rooms.open': { + POST: (params: RoomsOpenProps) => void; + }; };