diff --git a/.changeset/shaggy-bulldogs-beg.md b/.changeset/shaggy-bulldogs-beg.md new file mode 100644 index 000000000000..211d11d7b67c --- /dev/null +++ b/.changeset/shaggy-bulldogs-beg.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-composer': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduces a new option when exporting messages, allowing users to select and download a JSON file directly from client diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index f7fff0b2a2aa..3745864061f4 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -31,7 +31,7 @@ export class ChatMessages implements ChatAPI { public composer: ComposerAPI | undefined; - public setComposerAPI = (composer: ComposerAPI): void => { + public setComposerAPI = (composer?: ComposerAPI): void => { this.composer?.release(); this.composer = composer; }; diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 8dec6c9abbaa..90bc2236e837 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -67,7 +67,7 @@ const RoomMessage = ({ ref={messageRef} id={message._id} role='listitem' - aria-roledescription={sequential ? t('sequential_message') : t('message')} + aria-roledescription={t('message')} tabIndex={0} aria-labelledby={`${message._id}-displayName ${message._id}-time ${message._id}-content ${message._id}-read-status`} onClick={selecting ? toggleSelected : undefined} diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 6a782faafa1f..dbdaa1b04ac7 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -111,7 +111,7 @@ export type UploadsAPI = { export type ChatAPI = { readonly uid: string | null; readonly composer?: ComposerAPI; - readonly setComposerAPI: (composer: ComposerAPI) => void; + readonly setComposerAPI: (composer?: ComposerAPI) => void; readonly data: DataAPI; readonly uploads: UploadsAPI; readonly readStateManager: ReadStateManager; diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index ca21153126fd..8af62f3fdd15 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -9,7 +9,7 @@ import { HeaderState } from '../../../../components/Header'; const Encrypted = ({ room }: { room: IRoom }) => { const { t } = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); - return e2eEnabled && room?.encrypted ? : null; + return e2eEnabled && room?.encrypted ? : null; }; export default memo(Encrypted); diff --git a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx index f9fda919d88c..800f7155082e 100644 --- a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx +++ b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useContext } from 'react'; +import { createContext, useCallback, useContext, useEffect } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { selectedMessageStore } from '../../providers/SelectedMessagesProvider'; @@ -21,7 +21,19 @@ export const useIsSelectedMessage = (mid: string): boolean => { const getSnapshot = (): boolean => selectedMessageStore.isSelected(mid); - return useSyncExternalStore(subscribe, getSnapshot); + const isSelected = useSyncExternalStore(subscribe, getSnapshot); + + useEffect(() => { + if (isSelected) { + return; + } + + selectedMessageStore.addAvailableMessage(mid); + + return () => selectedMessageStore.removeAvailableMessage(mid); + }, [mid, selectedMessageStore, isSelected]); + + return isSelected; }; export const useIsSelecting = (): boolean => { @@ -44,6 +56,20 @@ export const useToggleSelect = (mid: string): (() => void) => { }, [mid, selectedMessageStore]); }; +export const useToggleSelectAll = (): (() => void) => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + return useCallback(() => { + selectedMessageStore.toggleAll(Array.from(selectedMessageStore.availableMessages)); + }, [selectedMessageStore]); +}; + +export const useClearSelection = (): (() => void) => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + return useCallback(() => { + selectedMessageStore.clearStore(); + }, [selectedMessageStore]); +}; + export const useCountSelected = (): number => { const { selectedMessageStore } = useContext(SelectedMessageContext); @@ -56,3 +82,16 @@ export const useCountSelected = (): number => { return useSyncExternalStore(subscribe, getSnapshot); }; + +export const useAvailableMessagesCount = () => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + + const subscribe = useCallback( + (callback: () => void): (() => void) => selectedMessageStore.on('change', callback), + [selectedMessageStore], + ); + + const getSnapshot = () => selectedMessageStore.availableMessagesCount(); + + return useSyncExternalStore(subscribe, getSnapshot); +}; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 13c111592c39..b78ed29ad4d1 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -40,6 +40,7 @@ import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; +import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; const RoomBody = (): ReactElement => { @@ -116,6 +117,7 @@ const RoomBody = (): ReactElement => { const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); const { messageListRef } = useMessageListNavigation(); + const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); const { handleNewMessageButtonClick, handleJumpToRecentButtonClick, handleComposerResize, hasNewMessages, newMessagesScrollRef } = useHasNewMessages(room._id, user?._id, atBottomRef, { @@ -133,7 +135,7 @@ const RoomBody = (): ReactElement => { leaderBannerInnerRef, unreadBarInnerRef, getMoreInnerRef, - + selectAndScrollRef, messageListRef, ); @@ -313,6 +315,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 3f1ecb0fd2bc..3b92e9b7910d 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -37,6 +37,7 @@ import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; +import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; const RoomBody = (): ReactElement => { @@ -111,6 +112,7 @@ const RoomBody = (): ReactElement => { const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); const { messageListRef } = useMessageListNavigation(); + const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); const { handleNewMessageButtonClick, handleJumpToRecentButtonClick, handleComposerResize, hasNewMessages, newMessagesScrollRef } = useHasNewMessages(room._id, user?._id, atBottomRef, { @@ -128,7 +130,7 @@ const RoomBody = (): ReactElement => { sectionScrollRef, unreadBarInnerRef, getMoreInnerRef, - + selectAndScrollRef, messageListRef, ); @@ -285,6 +287,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts b/apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts new file mode 100644 index 000000000000..bf53178fa67e --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts @@ -0,0 +1,15 @@ +import { useRef } from 'react'; + +import { useToggleSelectAll } from '../../MessageList/contexts/SelectedMessagesContext'; + +export const useSelectAllAndScrollToTop = () => { + const ref = useRef(null); + const handleToggleAll = useToggleSelectAll(); + + const selectAllAndScrollToTop = () => { + ref.current?.scrollTo({ top: 0, behavior: 'smooth' }); + handleToggleAll(); + }; + + return { innerRef: ref, selectAllAndScrollToTop }; +}; diff --git a/apps/meteor/client/views/room/composer/ComposerContainer.tsx b/apps/meteor/client/views/room/composer/ComposerContainer.tsx index e027ff1f5b87..77001827032b 100644 --- a/apps/meteor/client/views/room/composer/ComposerContainer.tsx +++ b/apps/meteor/client/views/room/composer/ComposerContainer.tsx @@ -13,6 +13,7 @@ import type { ComposerMessageProps } from './ComposerMessage'; import ComposerMessage from './ComposerMessage'; import ComposerOmnichannel from './ComposerOmnichannel'; import ComposerReadOnly from './ComposerReadOnly'; +import ComposerSelectMessages from './ComposerSelectMessages'; import ComposerVoIP from './ComposerVoIP'; import { useRoom } from '../contexts/RoomContext'; import { useMessageComposerIsAnonymous } from './hooks/useMessageComposerIsAnonymous'; @@ -20,6 +21,7 @@ import { useMessageComposerIsArchived } from './hooks/useMessageComposerIsArchiv import { useMessageComposerIsBlocked } from './hooks/useMessageComposerIsBlocked'; import { useMessageComposerIsReadOnly } from './hooks/useMessageComposerIsReadOnly'; import { useAirGappedRestriction } from '../../../hooks/useAirGappedRestriction'; +import { useIsSelecting } from '../MessageList/contexts/SelectedMessagesContext'; const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactElement => { const room = useRoom(); @@ -28,6 +30,7 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE const mustJoinWithCode = !props.subscription && room.joinCodeRequired && !canJoinWithoutCode; const isAnonymous = useMessageComposerIsAnonymous(); + const isSelectingMessages = useIsSelecting(); const isBlockedOrBlocker = useMessageComposerIsBlocked({ subscription: props.subscription }); const isArchived = useMessageComposerIsArchived(room._id, props.subscription); const isReadOnly = useMessageComposerIsReadOnly(room._id); @@ -74,6 +77,10 @@ const ComposerContainer = ({ children, ...props }: ComposerMessageProps): ReactE return ; } + if (isSelectingMessages) { + return ; + } + return ( <> {children} diff --git a/apps/meteor/client/views/room/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 9148185da50c..a5fd473f788b 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -22,6 +22,7 @@ export type ComposerMessageProps = { onNavigateToNextMessage?: () => void; onNavigateToPreviousMessage?: () => void; onUploadFiles?: (files: readonly File[]) => void; + onClickSelectAll?: () => void; }; const ComposerMessage = ({ tmid, onSend, ...props }: ComposerMessageProps): ReactElement => { diff --git a/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx new file mode 100644 index 000000000000..2a77e250e5ba --- /dev/null +++ b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx @@ -0,0 +1,34 @@ +import { Button, ButtonGroup } from '@rocket.chat/fuselage'; +import { MessageFooterCallout, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; +import type { ReactElement } from 'react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { ComposerMessageProps } from './ComposerMessage'; +import { useCountSelected, useClearSelection, useAvailableMessagesCount } from '../MessageList/contexts/SelectedMessagesContext'; + +const ComposerSelectMessages = ({ onClickSelectAll }: ComposerMessageProps): ReactElement => { + const { t } = useTranslation(); + + const clearSelection = useClearSelection(); + const countSelected = useCountSelected(); + const countAvailable = useAvailableMessagesCount(); + + return ( + + + {t('__count__messages_selected', { count: countSelected })} + + + + {t('Clear_selection')} + + + {t('Select__count__messages', { count: countAvailable })} + + + + ); +}; + +export default ComposerSelectMessages; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 6546a6be9245..0a52355ac58a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -131,7 +131,11 @@ const MessageBox = ({ const callbackRef = useCallback( (node: HTMLTextAreaElement) => { - if (node === null || chat.composer) { + if (node === null && chat.composer) { + return chat.setComposerAPI(); + } + + if (chat.composer) { return; } chat.setComposerAPI(createComposerAPI(node, storageID)); diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index f6a621200119..8736c8a1bec3 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -1,18 +1,43 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { + FieldError, + Field, + FieldLabel, + FieldRow, + TextAreaInput, + TextInput, + ButtonGroup, + Button, + Icon, + FieldGroup, + Select, + InputBox, + Callout, +} from '@rocket.chat/fuselage'; +import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import React, { useContext, useEffect, useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import FileExport from './FileExport'; -import MailExportForm from './MailExportForm'; -import { ContextualbarHeader, ContextualbarIcon, ContextualbarTitle, ContextualbarClose } from '../../../../components/Contextualbar'; +import { useDownloadExportMutation } from './useDownloadExportMutation'; +import { useRoomExportMutation } from './useRoomExportMutation'; +import { validateEmail } from '../../../../../lib/emailValidator'; +import { + ContextualbarHeader, + ContextualbarScrollableContent, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarFooter, +} from '../../../../components/Contextualbar'; +import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; import { useRoom } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; -export type MailExportFormValues = { - type: 'email' | 'file'; +export type ExportMessagesFormValues = { + type: 'email' | 'file' | 'download'; dateFrom: string; dateTo: string; format: 'html' | 'json'; @@ -24,16 +49,25 @@ export type MailExportFormValues = { const ExportMessages = () => { const { t } = useTranslation(); - const room = useRoom(); - const { closeTab } = useRoomToolbox(); + const formFocus = useAutoFocus(); + const room = useRoom(); + const isE2ERoom = room.encrypted; const roomName = room?.t && roomCoordinator.getRoomName(room.t, room); - const methods = useForm({ + const { + control, + formState: { errors, isSubmitting }, + watch, + register, + setValue, + handleSubmit, + clearErrors, + } = useForm({ mode: 'onBlur', defaultValues: { - type: 'email', + type: isE2ERoom ? 'download' : 'email', dateFrom: '', dateTo: '', toUsers: [], @@ -43,18 +77,94 @@ const ExportMessages = () => { postProcess: 'sprintf', sprintf: [roomName], }), - format: 'html', + format: isE2ERoom ? 'json' : 'html', }, }); + const exportOptions = useMemo( () => [ - ['email', t('Send_via_email')], - ['file', t('Export_as_file')], + ['email', t('Send_email')], + ['file', t('Send_file_via_email')], + ['download', t('Download_file')], ], [t], ); + const outputOptions = useMemo( + () => [ + ['html', t('HTML')], + ['json', t('JSON')], + ], + [t], + ); + + const roomExportMutation = useRoomExportMutation(); + const downloadExportMutation = useDownloadExportMutation(); + + const { selectedMessageStore } = useContext(SelectedMessageContext); + const messageCount = useCountSelected(); + + const { type, toUsers } = watch(); + + useEffect(() => { + if (type !== 'file') { + selectedMessageStore.setIsSelecting(true); + } + + return (): void => { + selectedMessageStore.reset(); + }; + }, [type, selectedMessageStore]); + + useEffect(() => { + if (type === 'email') { + setValue('format', 'html'); + } + + if (type === 'download') { + setValue('format', 'json'); + } + + setValue('messagesCount', messageCount); + }, [type, setValue, messageCount]); + + const handleExport = async ({ type, toUsers, dateFrom, dateTo, format, subject, additionalEmails }: ExportMessagesFormValues) => { + const messages = selectedMessageStore.getSelectedMessages(); + + if (type === 'download') { + return downloadExportMutation.mutateAsync({ + mids: messages, + }); + } + + if (type === 'file') { + return roomExportMutation.mutateAsync({ + rid: room._id, + type: 'file', + ...(dateFrom && { dateFrom }), + ...(dateTo && { dateTo }), + format, + }); + } + + roomExportMutation.mutateAsync({ + rid: room._id, + type: 'email', + toUsers, + toEmails: additionalEmails?.split(','), + subject, + messages, + }); + }; + const formId = useUniqueId(); + const methodField = useUniqueId(); + const formatField = useUniqueId(); + const toUsersField = useUniqueId(); + const dateFromField = useUniqueId(); + const dateToField = useUniqueId(); + const additionalEmailsField = useUniqueId(); + const subjectField = useUniqueId(); return ( <> @@ -63,14 +173,176 @@ const ExportMessages = () => { {t('Export_Messages')} - - {methods.watch('type') === 'email' && ( - - )} - {methods.watch('type') === 'file' && ( - - )} - + + + + + {t('Method')} + + ( + + )} + /> + + + + {t('Output_format')} + + ( + + )} + /> + + + {type === 'file' && ( + <> + + {t('Date_From')} + + } + /> + + + + {t('Date_to')} + + } + /> + + + > + )} + {type === 'email' && ( + <> + + {t('To_users')} + + ( + { + onChange(value); + clearErrors('additionalEmails'); + }} + onBlur={onBlur} + name={name} + /> + )} + /> + + + + {t('To_additional_emails')} + + { + if (additionalEmails === '') { + return undefined; + } + + const emails = additionalEmails?.split(',').map((email) => email.trim()); + if (Array.isArray(emails) && emails.every((email) => validateEmail(email.trim()))) { + return undefined; + } + + return t('Mail_Message_Invalid_emails', { postProcess: 'sprintf', sprintf: [additionalEmails] }); + }, + validateToUsers: (additionalEmails) => { + if (additionalEmails !== '' || toUsers?.length > 0) { + return undefined; + } + + return t('Mail_Message_Missing_to'); + }, + }, + }} + render={({ field }) => ( + } + aria-describedby={`${additionalEmailsField}-error`} + aria-invalid={Boolean(errors?.additionalEmails?.message)} + error={errors?.additionalEmails?.message} + /> + )} + /> + + {errors?.additionalEmails && ( + + {errors.additionalEmails.message} + + )} + + + {t('Subject')} + + ( + } /> + )} + /> + + + > + )} + {type !== 'file' && ( + <> + (messagesCount > 0 ? undefined : t('Mail_Message_No_messages_selected_select_all')), + })} + /> + {errors.messagesCount && ( + + + {errors.messagesCount.message} + + + )} + > + )} + + + + + + {t('Cancel')} + + {type === 'download' ? t('Download') : t('Send')} + + + > ); }; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx deleted file mode 100644 index 2d4a3bf0030c..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, FieldLabel, FieldRow, Select, ButtonGroup, Button, FieldGroup, InputBox } from '@rocket.chat/fuselage'; -import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import type { MailExportFormValues } from './ExportMessages'; -import { useRoomExportMutation } from './useRoomExportMutation'; -import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../components/Contextualbar'; - -type FileExportProps = { - formId: string; - rid: IRoom['_id']; - onCancel: () => void; - exportOptions: SelectOption[]; -}; - -const FileExport = ({ formId, rid, exportOptions, onCancel }: FileExportProps) => { - const { t } = useTranslation(); - const { control, handleSubmit } = useFormContext(); - const roomExportMutation = useRoomExportMutation(); - const formFocus = useAutoFocus(); - - const outputOptions = useMemo( - () => [ - ['html', t('HTML')], - ['json', t('JSON')], - ], - [t], - ); - - const handleExport = ({ dateFrom, dateTo, format }: MailExportFormValues) => { - roomExportMutation.mutateAsync({ - rid, - type: 'file', - ...(dateFrom && { dateFrom }), - ...(dateTo && { dateTo }), - format, - }); - }; - - const typeField = useUniqueId(); - const dateFromField = useUniqueId(); - const dateToField = useUniqueId(); - const formatField = useUniqueId(); - - return ( - <> - - - - - {t('Method')} - - } - /> - - - - {t('Date_From')} - - } - /> - - - - {t('Date_to')} - - } /> - - - - {t('Output_format')} - - } - /> - - - - - - - - {t('Cancel')} - - {t('Export')} - - - - > - ); -}; - -export default FileExport; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx deleted file mode 100644 index b6f0e4b88bf2..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { css } from '@rocket.chat/css-in-js'; -import type { SelectOption } from '@rocket.chat/fuselage'; -import { - FieldError, - Field, - FieldLabel, - FieldRow, - TextAreaInput, - TextInput, - ButtonGroup, - Button, - Box, - Icon, - Callout, - FieldGroup, - Select, -} from '@rocket.chat/fuselage'; -import { useAutoFocus, useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import React, { useEffect, useContext } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import type { MailExportFormValues } from './ExportMessages'; -import { useRoomExportMutation } from './useRoomExportMutation'; -import { validateEmail } from '../../../../../lib/emailValidator'; -import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../components/Contextualbar'; -import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; -import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; - -type MailExportFormProps = { - formId: string; - rid: IRoom['_id']; - onCancel: () => void; - exportOptions: SelectOption[]; -}; - -const MailExportForm = ({ formId, rid, onCancel, exportOptions }: MailExportFormProps) => { - const { t } = useTranslation(); - const formFocus = useAutoFocus(); - - const { - watch, - setValue, - control, - register, - formState: { errors, isDirty, isSubmitting }, - handleSubmit, - clearErrors, - } = useFormContext(); - const roomExportMutation = useRoomExportMutation(); - - const { selectedMessageStore } = useContext(SelectedMessageContext); - const messages = selectedMessageStore.getSelectedMessages(); - - const count = useCountSelected(); - - const clearSelection = useMutableCallback(() => { - selectedMessageStore.clearStore(); - }); - - useEffect(() => { - selectedMessageStore.setIsSelecting(true); - return (): void => { - selectedMessageStore.reset(); - }; - }, [selectedMessageStore]); - - const { toUsers } = watch(); - - useEffect(() => { - setValue('messagesCount', messages.length); - }, [setValue, messages.length]); - - const handleExport = async ({ toUsers, subject, additionalEmails }: MailExportFormValues) => { - roomExportMutation.mutateAsync({ - rid, - type: 'email', - toUsers, - toEmails: additionalEmails?.split(','), - subject, - messages, - }); - }; - - const clickable = css` - cursor: pointer; - `; - - const methodField = useUniqueId(); - const toUsersField = useUniqueId(); - const additionalEmailsField = useUniqueId(); - const subjectField = useUniqueId(); - - return ( - <> - - - - - {t('Method')} - - } - /> - - - - 0 ? 'success' : 'info'}> - {`${count} Messages selected`} - {count > 0 && ( - - {t('Click_here_to_clear_the_selection')} - - )} - {count === 0 && {t('Click_the_messages_you_would_like_to_send_by_email')}} - - (messagesCount > 0 ? undefined : t('Mail_Message_No_messages_selected_select_all')), - })} - /> - {errors.messagesCount && {errors.messagesCount.message}} - - - {t('To_users')} - - ( - { - onChange(value); - clearErrors('additionalEmails'); - }} - onBlur={onBlur} - name={name} - /> - )} - /> - - - - {t('To_additional_emails')} - - { - const emails = additionalEmails?.split(',').map((email) => email.trim()); - if (Array.isArray(emails) && emails.every((email) => validateEmail(email.trim()))) { - return undefined; - } - - return t('Mail_Message_Invalid_emails', { postProcess: 'sprintf', sprintf: [additionalEmails] }); - }, - validateToUsers: (additionalEmails) => { - if (additionalEmails !== '' || toUsers?.length > 0) { - return undefined; - } - - return t('Mail_Message_Missing_to'); - }, - }, - }} - render={({ field }) => ( - } - aria-describedby={`${additionalEmailsField}-error`} - aria-invalid={Boolean(errors?.additionalEmails?.message)} - error={errors?.additionalEmails?.message} - /> - )} - /> - - {errors?.additionalEmails && ( - - {errors.additionalEmails.message} - - )} - - - {t('Subject')} - - } />} - /> - - - - - - - - {t('Cancel')} - - {t('Send')} - - - - > - ); -}; - -export default MailExportForm; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts new file mode 100644 index 000000000000..f35f153da8bb --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -0,0 +1,50 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import type { FindOptions } from 'mongodb'; +import { useTranslation } from 'react-i18next'; + +import { Messages } from '../../../../../app/models/client'; +import { downloadJsonAs } from '../../../../lib/download'; +import { useRoom } from '../../contexts/RoomContext'; + +const messagesFields: FindOptions = { projection: { _id: 1, ts: 1, u: 1, msg: 1, _updatedAt: 1, tlm: 1, replies: 1, tmid: 1 } }; + +export const useDownloadExportMutation = () => { + const { t } = useTranslation(); + const room = useRoom(); + const user = useUser(); + const dispatchToastMessage = useToastMessageDispatch(); + + return useMutation({ + mutationFn: async ({ mids }: { mids: IMessage['_id'][] }) => { + const messages = Messages.find( + { + $or: [{ _id: { $in: mids } }, { tmid: { $in: mids } }], + }, + messagesFields, + ).fetch(); + + const fileData = { + roomId: room._id, + roomName: room.fname || room.name, + userExport: { + id: user?._id, + username: user?.username, + name: user?.name, + roles: user?.roles, + }, + exportDate: new Date().toISOString(), + messages, + }; + + return downloadJsonAs(fileData, `exportedMessages-${new Date().toISOString()}`); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Messages_exported_successfully') }); + }, + }); +}; diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index 4100751037ff..e70156126df0 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -4,8 +4,6 @@ import React, { useMemo } from 'react'; import { SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; -// data-qa-select - export const selectedMessageStore = new (class SelectMessageStore extends Emitter< { change: undefined; @@ -14,8 +12,20 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte > { store = new Set(); + availableMessages = new Set(); + isSelecting = false; + addAvailableMessage(mid: string): void { + this.availableMessages.add(mid); + this.emit('change'); + } + + removeAvailableMessage(mid: string): void { + this.availableMessages.delete(mid); + this.emit('change'); + } + setIsSelecting(isSelecting: boolean): void { this.isSelecting = isSelecting; this.emit('toggleIsSelecting', isSelecting); @@ -49,6 +59,10 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte return this.store.size; } + availableMessagesCount(): number { + return this.availableMessages.size; + } + clearStore(): void { const selectedMessages = this.getSelectedMessages(); this.store.clear(); @@ -61,6 +75,11 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte this.isSelecting = false; this.emit('toggleIsSelecting', false); } + + toggleAll(mids: string[]): void { + this.store = new Set([...this.store, ...mids]); + this.emit('change'); + } })(); type SelectedMessagesProviderProps = { diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 04d01d5a7c71..cecb6fd525d4 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -231,6 +231,20 @@ test.describe.serial('e2e-encryption initial setup', () => { ); await expect(poHomeChannel.content.nthMessage(0).locator('.rcx-icon--name-key')).toBeVisible(); }); + + test('should display only the download file method when exporting messages in an e2ee room', async ({ page }) => { + await page.goto('/home'); + const channelName = faker.string.uuid(); + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + await expect(poHomeChannel.tabs.exportMessages.downloadFileMethod).toBeVisible(); + }); }); test.describe.serial('e2e-encryption', () => { diff --git a/apps/meteor/tests/e2e/export-messages.spec.ts b/apps/meteor/tests/e2e/export-messages.spec.ts new file mode 100644 index 000000000000..dffb6b4d5edb --- /dev/null +++ b/apps/meteor/tests/e2e/export-messages.spec.ts @@ -0,0 +1,72 @@ +import { Users } from './fixtures/userStates'; +import { HomeChannel, Utils } from './page-objects'; +import { createTargetChannel } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('export-messages', () => { + let poHomeChannel: HomeChannel; + let poUtils: Utils; + let targetChannel: string; + + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + poUtils = new Utils(page); + + await page.goto('/home'); + }); + + test('should all export methods be available in targetChannel', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + await expect(poHomeChannel.tabs.exportMessages.sendEmailMethod).not.toBeDisabled(); + + await poHomeChannel.tabs.exportMessages.sendEmailMethod.click(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Send email')).toBeVisible(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Send file via email')).toBeVisible(); + await expect(poHomeChannel.tabs.exportMessages.getMethodByName('Download file')).toBeVisible(); + }); + + test('should display an error when trying to send email without filling to users or to additional emails', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello world'); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.content.getMessageByText('hello world').click(); + await poHomeChannel.tabs.exportMessages.btnSend.click(); + + await expect( + poUtils.getAlertByText('You must select one or more users or provide one or more email addresses, separated by commas'), + ).toBeVisible(); + }); + + test('should display an error when trying to send email without selecting any message', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.tabs.exportMessages.textboxAdditionalEmails.fill('mail@mail.com'); + await poHomeChannel.tabs.exportMessages.btnSend.click(); + + await expect(poUtils.getAlertByText(`You haven't selected any messages`)).toBeVisible(); + }); + + test('should be able to send messages after closing export messages', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.kebab.click({ force: true }); + await poHomeChannel.tabs.btnExportMessages.click(); + + await poHomeChannel.content.getMessageByText('hello world').click(); + await poHomeChannel.tabs.exportMessages.btnCancel.click(); + await poHomeChannel.content.sendMessage('hello export'); + + await expect(poHomeChannel.content.getMessageByText('hello export')).toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 59df066d8163..088d8dd3d647 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -391,7 +391,7 @@ export class HomeContent { } getSystemMessageByText(text: string): Locator { - return this.page.locator('[aria-roledescription="system message"]', { hasText: text }); + return this.page.locator('[role="listitem"][aria-roledescription="system message"]', { hasText: text }); } getMessageByText(text: string): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts new file mode 100644 index 000000000000..ddf78b7f4388 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts @@ -0,0 +1,33 @@ +import type { Page } from '@playwright/test'; + +export class HomeFlextabExportMessages { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get sendEmailMethod() { + return this.page.getByLabel('Send email'); + } + + get downloadFileMethod() { + return this.page.getByLabel('Download file'); + } + + getMethodByName(name: string) { + return this.page.getByRole('option', { name }); + } + + get textboxAdditionalEmails() { + return this.page.getByRole('textbox', { name: 'To additional emails' }); + } + + get btnSend() { + return this.page.locator('role=button[name="Send"]'); + } + + get btnCancel() { + return this.page.locator('role=button[name="Cancel"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts index 6e22bea99faf..a19cf72d5172 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts @@ -1,6 +1,7 @@ import type { Locator, Page } from '@playwright/test'; import { HomeFlextabChannels } from './home-flextab-channels'; +import { HomeFlextabExportMessages } from './home-flextab-exportMessages'; import { HomeFlextabMembers } from './home-flextab-members'; import { HomeFlextabNotificationPreferences } from './home-flextab-notificationPreferences'; import { HomeFlextabRoom } from './home-flextab-room'; @@ -16,12 +17,15 @@ export class HomeFlextab { readonly notificationPreferences: HomeFlextabNotificationPreferences; + readonly exportMessages: HomeFlextabExportMessages; + constructor(page: Page) { this.page = page; this.members = new HomeFlextabMembers(page); this.room = new HomeFlextabRoom(page); this.channels = new HomeFlextabChannels(page); this.notificationPreferences = new HomeFlextabNotificationPreferences(page); + this.exportMessages = new HomeFlextabExportMessages(page); } get btnTabMembers(): Locator { @@ -48,6 +52,10 @@ export class HomeFlextab { return this.page.locator('role=menuitem[name="Notifications Preferences"]'); } + get btnExportMessages(): Locator { + return this.page.locator('role=menuitem[name="Export messages"]'); + } + get btnE2EERoomSetupDisableE2E(): Locator { return this.page.locator('[data-qa-id=ToolBoxAction-key]'); } diff --git a/apps/meteor/tests/e2e/page-objects/utils.ts b/apps/meteor/tests/e2e/page-objects/utils.ts index 066c5eac153f..15fb0b88b986 100644 --- a/apps/meteor/tests/e2e/page-objects/utils.ts +++ b/apps/meteor/tests/e2e/page-objects/utils.ts @@ -26,4 +26,10 @@ export class Utils { get btnModalConfirmDelete() { return this.page.locator('.rcx-modal >> button >> text="Delete"'); } + + getAlertByText(text: string): Locator { + return this.page.locator('[role="alert"]', { + hasText: text, + }); + } } diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 0d8218149ad6..d3c5cb1eae9d 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -3272,7 +3272,6 @@ "Message_VideoRecorderEnabledDescription": "Erfordert, dass der Medientyp 'video/webm' in den \"Datei-Upload\"-Einstellungen als Medientyp akzeptiert wird", "messages": "Nachrichten", "Messages": "Nachrichten", - "Messages_selected": "Ausgewählte Nachrichten", "Messages_sent": "Nachrichten versandt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht", "Meta": "Metadaten", @@ -5506,4 +5505,4 @@ "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8b8a0b5e4b1b..e9c78cc644f9 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -26,7 +26,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} encryption keys need to be updated to give you access. Another room member needs to be online for this to happen.", "removed__username__as__role_": "removed {{username}} as {{role}}", "set__username__as__role_": "set {{username}} as {{role}}", - "sequential_message": "sequential message", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by {{username}}", "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", "Third_party_login": "Third-party login", @@ -1067,6 +1066,7 @@ "clean-channel-history_description": "Permission to Clear the history from channels", "clear": "Clear", "Clear_all_unreads_question": "Clear all unreads?", + "Clear_selection": "Clear selection", "clear_cache_now": "Clear Cache Now", "Clear_filters": "Clear filters", "clear_history": "Clear History", @@ -1813,6 +1813,7 @@ "Download": "Download", "Download_Destkop_App": "Download Desktop App", "Download_Disabled": "Download disabled", + "Download_file": "Download file", "Download_Info": "Download info", "Download_My_Data": "Download My Data (HTML)", "Download_Pending_Avatars": "Download Pending Avatars", @@ -2319,7 +2320,7 @@ "Expiration": "Expiration", "Expiration_(Days)": "Expiration (Days)", "Export_as_file": "Export as file", - "Export_Messages": "Export Messages", + "Export_Messages": "Export messages", "Export_My_Data": "Export My Data (JSON)", "expression": "Expression", "Extended": "Extended", @@ -3774,7 +3775,8 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "Messages_selected": "Messages selected", + "__count__messages_selected": "{{count}} messages selected", + "Messages_exported_successfully": "Messages exported successfully", "Messages_sent": "Messages sent", "Message_sent": "Message sent", "Message_viewed": "Message viewed", @@ -4975,8 +4977,9 @@ "Send_a_test_push_to_my_user": "Send a test push to my user", "Send_confirmation_email": "Send confirmation email", "Send_data_into_RocketChat_in_realtime": "Send data into Rocket.Chat in real-time.", - "Send_email": "Send Email", + "Send_email": "Send email", "Send_Email_SMTP_Warning": "Set up the SMTP server in email settings to enable.", + "Send_file_via_email": "Send file via email", "Send_invitation_email": "Send invitation email", "Send_invitation_email_error": "You haven't provided any valid email address.", "Send_invitation_email_info": "You can send multiple email invitations at once.", @@ -6685,6 +6688,7 @@ "Go_to_href": "Go to: {{href}}", "Anyone_can_send_new_messages": "Anyone can send new messages", "Select_messages_to_hide": "Select messages to hide", + "Select__count__messages": "Select {{count}} messages", "Name_cannot_have_special_characters": "Name cannot have spaces or special characters", "Resize": "Resize", "Zoom_out": "Zoom out", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 0f7abeb50c71..6365656bf6cc 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -3319,7 +3319,6 @@ "Message_VideoRecorderEnabledDescription": "Vaatii 'video/webm'-tiedostot hyväksytyksi mediatyypiksi 'Tiedoston lataus'-asetuksissa.", "messages": "viestit", "Messages": "Viestit", - "Messages_selected": "Valitut viestit", "Messages_sent": "Lähetetyt viestit", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Saapuvaan WebHookiin lähetetyt viestit julkaistaan tässä.", "Meta": "Meta", @@ -5719,4 +5718,4 @@ "Theme_Appearence": "Teeman ulkoasu", "Enterprise": "Yritys", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index cbf6c1a11f13..1a6a29a71cd6 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -3454,7 +3454,6 @@ "Message_VideoRecorderEnabledDescription": "'फ़ाइल अपलोड' सेटिंग्स के अंतर्गत 'वीडियो/वेबएम' फ़ाइलों को एक स्वीकृत मीडिया प्रकार होना आवश्यक है।", "messages": "संदेशों", "Messages": "संदेशों", - "Messages_selected": "संदेश चयनित", "Messages_sent": "संदेश भेजे गए", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "इनकमिंग वेबहुक पर भेजे गए संदेश यहां पोस्ट किए जाएंगे।", "Meta": "मेटा", @@ -6103,4 +6102,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index e5b68aeddbee..90b0c1d8ef82 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -3199,7 +3199,6 @@ "Message_VideoRecorderEnabledDescription": "Azt igényli, hogy a „video/webm” fájlok elfogadott médiatípus legyen a „Fájlfeltöltés” beállításaiban.", "messages": "üzenetek", "Messages": "Üzenetek", - "Messages_selected": "Üzenetek kijelölve", "Messages_sent": "Üzenetek elküldve", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "A bejövő webhorogra küldött üzenetek itt lesznek beküldve.", "Meta": "Meta", @@ -5407,4 +5406,4 @@ "Enterprise": "Vállalati", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "UpgradeToGetMore_auditing_Title": "Üzenet ellenőrzés" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index d81230c554d1..9b9f6935a254 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -2855,7 +2855,6 @@ "Message_VideoRecorderEnabledDescription": "Krever at video / webm-filer skal være en akseptert medietype i \"Filopplastings\" -innstillinger.", "messages": "meldinger", "Messages": "meldinger", - "Messages_selected": "Meldinger er valgt", "Messages_sent": "Meldinger sendt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meldinger som sendes til Incoming WebHook vil bli lagt ut her.", "Meta": "Meta", @@ -4559,4 +4558,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/no.i18n.json b/packages/i18n/src/locales/no.i18n.json index 3baf4ebc7215..952b0ca9b711 100644 --- a/packages/i18n/src/locales/no.i18n.json +++ b/packages/i18n/src/locales/no.i18n.json @@ -2855,7 +2855,6 @@ "Message_VideoRecorderEnabledDescription": "Krever at video / webm-filer skal være en akseptert medietype i \"Filopplastings\" -innstillinger.", "messages": "meldinger", "Messages": "meldinger", - "Messages_selected": "Meldinger er valgt", "Messages_sent": "Meldinger sendt", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meldinger som sendes til Incoming WebHook vil bli lagt ut her.", "Meta": "Meta", @@ -4561,4 +4560,4 @@ "free_per_month_user": "$0 per måned per bruker", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "Buy_more": "Kjøp mer" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 3a58a1a4c4eb..f43c37775fa3 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -26,7 +26,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} klucze szyfrowania muszą zostać zaktualizowane, aby umożliwić dostęp. Aby tak się stało, inny członek pokoju musi być online.", "removed__username__as__role_": "usunięto {{username}} jako {{role}}", "set__username__as__role_": "ustaw {{username}} jako {{role}}", - "sequential_message": "komunikat sekwencyjny", "This_room_encryption_has_been_enabled_by__username_": "Użytkownik {{username}} włączył szyfrowanie w tym pokoju", "This_room_encryption_has_been_disabled_by__username_": "Użytkownik {{username}} wyłączył szyfrowanie w tym pokoju", "Third_party_login": "Logowanie przez stronę trzecią", @@ -3204,7 +3203,6 @@ "Message_VideoRecorderEnabledDescription": "Wymaga plików \"wideo / webm\", aby były akceptowanym typem mediów w ustawieniach \"Przesyłanie pliku\".", "messages": "Wiadomości", "Messages": "Wiadomości", - "Messages_selected": "Wybrane wiadomości", "Messages_sent": "Wiadomości wysłane", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Wiadomości, które zostaną przesłane przez WebHook będą publikowane tutaj.", "Meta": "Meta", @@ -5405,4 +5403,4 @@ "Broadcast_hint_enabled": "Tylko właściciele {{roomType}} mogą pisać nowe wiadomości, ale każdy może odpowiadać w wątku", "Anyone_can_send_new_messages": "Każdy może wysyłać nowe wiadomości", "Select_messages_to_hide": "Wybierz wiadomości do ukrycia" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json index 39a5595b0525..3ac677c4dcbd 100644 --- a/packages/i18n/src/locales/se.i18n.json +++ b/packages/i18n/src/locales/se.i18n.json @@ -24,7 +24,6 @@ "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} encryption keys need to be updated to give you access. Another room member needs to be online for this to happen.", "removed__username__as__role_": "removed {{username}} as {{role}}", "set__username__as__role_": "set {{username}} as {{role}}", - "sequential_message": "sequential message", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by {{username}}", "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", "Third_party_login": "Third-party login", @@ -3679,7 +3678,6 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "Messages_selected": "Messages selected", "Messages_sent": "Messages sent", "Message_sent": "Message sent", "Message_viewed": "Message viewed", @@ -6576,4 +6574,4 @@ "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", "Show_discussions_description": "Show team discussions in second sidebar" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 8615418f947c..69b59f03c321 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -3324,7 +3324,6 @@ "Message_VideoRecorderEnabledDescription": "Kräver \"video/webm\"-filer för att vara en accepterad medietyp inom inställningarna \"Filuppladdning\".", "messages": "Meddelanden", "Messages": "Meddelanden", - "Messages_selected": "Valda meddelanden", "Messages_sent": "Skickade meddelanden", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Meddelanden som skickas till inkommande WebHook kommer att publiceras här.", "Meta": "Meta", @@ -5721,4 +5720,4 @@ "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file diff --git a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx index deab14822f83..2d5c87b57d34 100644 --- a/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx +++ b/packages/ui-composer/src/MessageFooterCallout/MessageFooterCalloutContent.tsx @@ -1,14 +1,13 @@ import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement, ReactNode } from 'react'; +import type { ComponentProps } from 'react'; import { forwardRef } from 'react'; -const MessageFooterCalloutContent = forwardRef< - HTMLButtonElement, - { - children: ReactNode; - } ->(function MessageFooterCalloutContent(props, ref): ReactElement { - return ; -}); +type MessageFooterCalloutContentProps = ComponentProps; + +const MessageFooterCalloutContent = forwardRef( + function MessageFooterCalloutContent(props, ref) { + return ; + }, +); export default MessageFooterCalloutContent;
{`${count} Messages selected`}