refactor: Message actions (1st iteration) (#34133)
parent
f11efb4011
commit
3d41ae2455
@ -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'); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
@ -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', |
||||
}); |
||||
}); |
||||
@ -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<MessageActionConfig, 'condition' | 'context' | 'order' | 'action'> & { |
||||
action: (e?: MouseEvent<HTMLElement>) => 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 ( |
||||
<GenericMenu |
||||
onOpenChange={onChangeMenuVisibility} |
||||
detached |
||||
title={t('More')} |
||||
data-qa-id='menu' |
||||
data-qa-type='message-action-menu' |
||||
sections={groupOptions} |
||||
placement='bottom-end' |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default MessageActionMenu; |
||||
@ -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 ( |
||||
<GenericMenu |
||||
onOpenChange={onChangeMenuVisibility} |
||||
detached |
||||
title={t('More')} |
||||
data-qa-id='menu' |
||||
data-qa-type='message-action-menu-options' |
||||
sections={groupOptions} |
||||
placement='bottom-end' |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default MessageToolbarActionMenu; |
||||
@ -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 ( |
||||
<FuselageMessageToolbarItem |
||||
icon={icon} |
||||
title={title} |
||||
disabled={disabled} |
||||
data-qa-id={qa} |
||||
data-qa-type='message-action-menu' |
||||
onClick={onClick} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default MessageToolbarItem; |
||||
@ -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 ( |
||||
<> |
||||
<ReactionMessageAction message={message} room={room} subscription={subscription} /> |
||||
<QuoteMessageAction message={message} subscription={subscription} /> |
||||
<ReplyInThreadMessageAction message={message} room={room} subscription={subscription} /> |
||||
<ForwardMessageAction message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default DefaultItems; |
||||
@ -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 && <JumpToMessageAction id='jump-to-pin-message' message={message} />}</>; |
||||
}; |
||||
|
||||
export default DirectItems; |
||||
@ -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 ( |
||||
<> |
||||
<ReactionMessageAction message={message} room={room} subscription={subscription} /> |
||||
<QuoteMessageAction message={message} subscription={subscription} /> |
||||
<ReplyInThreadMessageAction message={message} room={room} subscription={subscription} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default FederatedItems; |
||||
@ -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 ( |
||||
<> |
||||
<JumpToMessageAction id='jump-to-message' message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default MentionsItems; |
||||
@ -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 ( |
||||
<> |
||||
<ReactionMessageAction message={message} room={room} subscription={subscription} /> |
||||
<QuoteMessageAction message={message} subscription={subscription} /> |
||||
<ReplyInThreadMessageAction message={message} room={room} subscription={subscription} /> |
||||
<ForwardMessageAction message={message} /> |
||||
<JumpToMessageAction id='jump-to-message' message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default MobileItems; |
||||
@ -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 ( |
||||
<> |
||||
<JumpToMessageAction id='jump-to-pin-message' message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default PinnedItems; |
||||
@ -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 ( |
||||
<> |
||||
<JumpToMessageAction id='jump-to-message' message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default SearchItems; |
||||
@ -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 ( |
||||
<> |
||||
<JumpToMessageAction id='jump-to-star-message' message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default StarredItems; |
||||
@ -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 ( |
||||
<> |
||||
<ReactionMessageAction message={message} room={room} subscription={subscription} /> |
||||
<QuoteMessageAction message={message} subscription={subscription} /> |
||||
<ForwardMessageAction message={message} /> |
||||
<JumpToMessageAction id='jump-to-message' message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ThreadsItems; |
||||
@ -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 ( |
||||
<> |
||||
<ReactionMessageAction message={message} room={room} subscription={subscription} /> |
||||
<ReplyInThreadMessageAction message={message} room={room} subscription={subscription} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default VideoconfItems; |
||||
@ -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 ( |
||||
<> |
||||
<ReactionMessageAction message={message} room={room} subscription={subscription} /> |
||||
<JumpToMessageAction id='jump-to-message' message={message} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default VideoconfThreadsItems; |
||||
@ -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 ( |
||||
<MessageToolbarItem |
||||
id='forward-message' |
||||
icon='arrow-forward' |
||||
title={encrypted ? t('Action_not_available_encrypted_content', { action: t('Forward_message') }) : t('Forward_message')} |
||||
qa='Forward_message' |
||||
disabled={encrypted} |
||||
onClick={async () => { |
||||
const permalink = await getPermaLink(message._id); |
||||
setModal( |
||||
<ForwardMessageModal |
||||
message={message} |
||||
permalink={permalink} |
||||
onClose={() => { |
||||
setModal(null); |
||||
}} |
||||
/>, |
||||
); |
||||
}} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default ForwardMessageAction; |
||||
@ -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 ( |
||||
<MessageToolbarItem |
||||
id={id} |
||||
icon='jump' |
||||
title={t('Jump_to_message')} |
||||
qa='Jump_to_message' |
||||
onClick={() => { |
||||
setMessageJumpQueryStringParameter(message._id); |
||||
}} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default JumpToMessageAction; |
||||
@ -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<ITranslatedMessage>; |
||||
subscription: ISubscription | undefined; |
||||
}; |
||||
|
||||
const QuoteMessageAction = ({ message, subscription }: QuoteMessageActionProps) => { |
||||
const chat = useChat(); |
||||
const autoTranslateOptions = useAutoTranslate(subscription); |
||||
const { t } = useTranslation(); |
||||
|
||||
if (!chat || !subscription) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<MessageToolbarItem |
||||
id='quote-message' |
||||
icon='quote' |
||||
title={t('Quote')} |
||||
qa='Quote' |
||||
onClick={() => { |
||||
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; |
||||
@ -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 <EmojiElement key={emoji} small title={emoji} emoji={emoji} image={image} onClick={() => toggleReaction(emoji)} />; |
||||
})} |
||||
<MessageToolbarItem |
||||
id='reaction-message' |
||||
icon='add-reaction' |
||||
title={t('Add_Reaction')} |
||||
qa='Add_Reaction' |
||||
onClick={(event) => { |
||||
event.stopPropagation(); |
||||
chat.emojiPicker.open(event.currentTarget, (emoji) => { |
||||
toggleReaction(emoji); |
||||
}); |
||||
}} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ReactionMessageAction; |
||||
@ -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 ( |
||||
<MessageToolbarItem |
||||
id='reply-in-thread' |
||||
icon='thread' |
||||
title={t('Reply_in_thread')} |
||||
qa='Reply_in_thread' |
||||
onClick={(event) => { |
||||
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; |
||||
@ -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', |
||||
}; |
||||
}; |
||||
@ -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', |
||||
}; |
||||
}; |
||||
@ -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', |
||||
}; |
||||
}; |
||||
@ -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]); |
||||
}; |
||||
@ -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', |
||||
}; |
||||
}; |
||||
|
||||
@ -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<MessageActionConfig[]>; |
||||
}; |
||||
@ -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( |
||||
<CreateDiscussion |
||||
defaultParentRoom={room?.prid || room?._id} |
||||
onClose={() => 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( |
||||
<CreateDiscussion |
||||
defaultParentRoom={room?.prid || room?._id} |
||||
onClose={() => setModal(undefined)} |
||||
parentMessageId={message._id} |
||||
nameSuggestion={message?.msg?.substr(0, 140)} |
||||
/>, |
||||
); |
||||
}, |
||||
order: 1, |
||||
group: 'menu', |
||||
}; |
||||
}; |
||||
|
||||
@ -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<MessageActionConfig, 'id' | 'type'>, |
||||
) => { |
||||
{ id, context, type, order }: { context: MessageActionContext[]; order: number } & Pick<MessageActionConfig, 'id' | 'type'>, |
||||
): 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, |
||||
}; |
||||
}; |
||||
|
||||
@ -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(<PinMessageModal message={message} onConfirm={onConfirm} onCancel={() => 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(<PinMessageModal message={message} onConfirm={onConfirm} onCancel={() => setModal(null)} />); |
||||
}, |
||||
order: 2, |
||||
group: 'menu', |
||||
}; |
||||
}; |
||||
|
||||
@ -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]); |
||||
}; |
||||
@ -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( |
||||
<ReadReceiptsModal |
||||
messageId={message._id} |
||||
onClose={() => { |
||||
setModal(null); |
||||
}} |
||||
/>, |
||||
); |
||||
}, |
||||
order: 10, |
||||
group: 'menu', |
||||
}; |
||||
}; |
||||
@ -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, |
||||
}; |
||||
}; |
||||
@ -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]); |
||||
}; |
||||
@ -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( |
||||
<ReportMessageModal |
||||
message={getMainMessageText(message)} |
||||
onClose={() => { |
||||
setModal(null); |
||||
}} |
||||
/>, |
||||
); |
||||
}, |
||||
order: 9, |
||||
group: 'menu', |
||||
}; |
||||
}; |
||||
@ -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( |
||||
<ReactionListModal |
||||
reactions={message.reactions ?? {}} |
||||
onClose={() => { |
||||
setModal(null); |
||||
}} |
||||
/>, |
||||
); |
||||
}, |
||||
order: 9, |
||||
group: 'menu', |
||||
}; |
||||
}; |
||||
@ -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', |
||||
}; |
||||
}; |
||||
|
||||
@ -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, |
||||
}; |
||||
}; |
||||
@ -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', |
||||
}; |
||||
}; |
||||
|
||||
@ -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', |
||||
}; |
||||
}; |
||||
|
||||
@ -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, |
||||
}; |
||||
}; |
||||
@ -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(<SaveToWebdavModal data={{ attachment, url }} onClose={() => 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( |
||||
<SaveToWebdavModal |
||||
data={{ attachment, url }} |
||||
onClose={() => { |
||||
setModal(null); |
||||
}} |
||||
/>, |
||||
); |
||||
}, |
||||
order: 100, |
||||
group: 'menu', |
||||
}; |
||||
}; |
||||
|
||||
@ -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], |
||||
); |
||||
}; |
||||
@ -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<MessageBoxAction[]> => { |
||||
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<MessageBoxAction, 'icon'> = { |
||||
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<MessageBoxAction[]>; |
||||
}; |
||||
@ -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<GenericMenuItemProps[]>; |
||||
}; |
||||
@ -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', |
||||
}); |
||||
}); |
||||
}); |
||||
Loading…
Reference in new issue