From e2ec0bfc0050f4c9663eae713184924b322c7565 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 3 Dec 2024 16:53:30 -0300 Subject: [PATCH 01/14] feat: add handleGetMore --- .../views/room/Header/icons/Encrypted.tsx | 2 +- .../contexts/SelectedMessagesContext.tsx | 7 + .../client/views/room/body/RoomBody.tsx | 3 +- .../client/views/room/body/RoomBodyV2.tsx | 3 +- .../views/room/body/hooks/useGetMore.ts | 13 +- .../ExportMessages/ExportE2EEMessages.tsx | 219 ++++++++++++++++++ .../providers/SelectedMessagesProvider.tsx | 14 ++ 7 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx 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..8a47dfaf6a4b 100644 --- a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx +++ b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx @@ -44,6 +44,13 @@ export const useToggleSelect = (mid: string): (() => void) => { }, [mid, selectedMessageStore]); }; +export const useToggleSelectAll = (mids: string[]): (() => void) => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + return useCallback(() => { + selectedMessageStore.toggleAll(mids); + }, [mids, selectedMessageStore]); +}; + export const useCountSelected = (): number => { const { selectedMessageStore } = useContext(SelectedMessageContext); diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 13c111592c39..e8352c93783c 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -102,7 +102,7 @@ const RoomBody = (): ReactElement => { const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef); const { wrapperRef: leaderBannerWrapperRef, hideLeaderHeader, innerRef: leaderBannerInnerRef } = useLeaderBanner(); @@ -313,6 +313,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + onGetMore={handleGetMore} // 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..7de9eb92113a 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -97,7 +97,7 @@ const RoomBody = (): ReactElement => { const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef); const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection(); @@ -285,6 +285,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} + onGetMore={handleGetMore} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index c2f182e5131f..5fd4775603e6 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,17 +1,28 @@ import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; +import { useToggleSelectAll } from '../../MessageList/contexts/SelectedMessagesContext'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { + const ref: MutableRefObject = useRef(null); + const handleToggleAll = useToggleSelectAll(); + + const handleGetMore = () => { + ref.current?.scrollTo({ top: 0, behavior: 'smooth' }); + handleToggleAll(); + }; + return { + handleGetMore, innerRef: useCallback( (wrapper: HTMLElement | null) => { if (!wrapper) { return; } + ref.current = wrapper; let lastScrollTopRef = 0; wrapper.addEventListener( diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx new file mode 100644 index 000000000000..1733edb32a87 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx @@ -0,0 +1,219 @@ +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Field, + FieldLabel, + FieldRow, + Select, + ButtonGroup, + Button, + FieldGroup, + InputBox, + Margins, + Box, + FieldHint, + Callout, +} from '@rocket.chat/fuselage'; +import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React, { useCallback, useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +// import type { MailExportFormValues } from './ExportMessages'; +// import { useRoomExportMutation } from './useRoomExportMutation'; +import { Messages } from '../../../../../app/models/client'; +import { + ContextualbarScrollableContent, + ContextualbarFooter, + ContextualbarClose, + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, +} from '../../../../components/Contextualbar'; +import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import { downloadJsonAs } from '../../../../lib/download'; +import { useRoom } from '../../contexts/RoomContext'; +import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; + +export const useMessages = ({ rid }: { rid: IRoom['_id'] }): IMessage[] => { + const showThreadsInMainChannel = useUserPreference('showThreadsInMainChannel', false); + // const hideSysMesSetting = useSetting('Hide_System_Messages', []); + const room = useRoom(); + // const hideRoomSysMes: Array = Array.isArray(room.sysMes) ? room.sysMes : []; + + // const hideSysMessages = useStableArray(mergeHideSysMessages(hideSysMesSetting, hideRoomSysMes)); + + const query: Mongo.Selector = useMemo( + () => ({ + rid, + _hidden: { $ne: true }, + // t: { $nin: hideSysMessages }, + ...(!showThreadsInMainChannel && { + $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], + }), + ts: { $gte: new Date('2024-11-30'), $lt: new Date() }, + }), + [rid, showThreadsInMainChannel], + ); + + return useReactiveValue( + useCallback( + () => + Messages.find(query, { + sort: { + ts: 1, + }, + }).fetch(), + [query], + ), + ); +}; + +const useExportE2EEMessages = ({ rid }: { rid: string }) => { + const showThreadsInMainChannel = useUserPreference('showThreadsInMainChannel', false); + + // const messages = useMessages({ rid: room._id }); + + // const query: Mongo.Selector = useMemo( + // () => ({ + // rid, + // _hidden: { $ne: true }, + // // t: { $nin: hideSysMessages }, + // ...(!showThreadsInMainChannel && { + // $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], + // }), + // ts: { $gte: new Date('2024-11-15'), $lt: new Date() }, + // }), + // [rid, showThreadsInMainChannel], + // ); + + return useMutation({ + mutationFn: ({ from, until }: FormValues) => { + return Messages.find({ + rid, + _hidden: { $ne: true }, + // t: { $nin: hideSysMessages }, + ...(!showThreadsInMainChannel && { + $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], + }), + ...((from.date || until.date) && { ts: { $gte: new Date(from.date), $lt: new Date(until.date) } }), + }).fetch(); + }, + onSuccess: (data) => { + console.log(data); + downloadJsonAs(data, 'exportedMessages'); + }, + }); +}; + +type FormValues = { + type: 'file'; + format: 'html' | 'json'; + from: { date: string; time: string }; + until: { date: string; time: string }; +}; + +const ExportE2EEMessages = () => { + const { t } = useTranslation(); + const room = useRoom(); + const { closeTab } = useRoomToolbox(); + + // console.log(messages); + const exportE2EEMessages = useExportE2EEMessages({ rid: room._id }); + + const { control, register, handleSubmit } = useForm({ + defaultValues: { type: 'file', format: 'html', from: { date: '', time: '' }, until: { date: '', time: '' } }, + }); + + const outputOptions = useMemo( + () => [ + ['html', t('HTML')], + ['json', t('JSON')], + ], + [t], + ); + + const handleExport = async (data: FormValues) => { + console.log(data); + + exportE2EEMessages.mutate(data); + // return downloadJsonAs(statisticsQuery.data, 'statistics'); + }; + + const formId = useUniqueId(); + const typeField = useUniqueId(); + const formatField = useUniqueId(); + const formFocus = useAutoFocus(); + + return ( + <> + + + {t('Export_Encrypted_Messages')} + + + <> + +
+ + + {t('Method')} + + ( + } + /> + + + A maximum of 500 messages can be exported at a time from encrypted `room type`. + +
+
+ + + + + + + + + ); +}; + +export default ExportE2EEMessages; diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index 4100751037ff..f9b133b1c039 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -45,6 +45,16 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte this.emit('change'); } + select(mid: string): void { + if (this.store.has(mid)) { + return; + } + + this.store.add(mid); + this.emit(mid, true); + this.emit('change'); + } + count(): number { return this.store.size; } @@ -61,6 +71,10 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte this.isSelecting = false; this.emit('toggleIsSelecting', false); } + + toggleAll(mids: string[]): void { + mids.forEach((mid) => this.select(mid)); + } })(); type SelectedMessagesProviderProps = { From eedbc37a53c062d6b52ba5188aada323e444e06f Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 3 Dec 2024 16:27:01 -0300 Subject: [PATCH 02/14] feat: `ComposerSelectMessages` --- .../contexts/SelectedMessagesContext.tsx | 13 +++++-- .../views/room/composer/ComposerContainer.tsx | 7 ++++ .../views/room/composer/ComposerMessage.tsx | 1 + .../room/composer/ComposerSelectMessages.tsx | 36 +++++++++++++++++++ .../providers/SelectedMessagesProvider.tsx | 13 ++++++- 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx diff --git a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx index 8a47dfaf6a4b..0e127009023d 100644 --- a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx +++ b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx @@ -44,11 +44,18 @@ export const useToggleSelect = (mid: string): (() => void) => { }, [mid, selectedMessageStore]); }; -export const useToggleSelectAll = (mids: string[]): (() => void) => { +export const useToggleSelectAll = (): (() => void) => { const { selectedMessageStore } = useContext(SelectedMessageContext); return useCallback(() => { - selectedMessageStore.toggleAll(mids); - }, [mids, selectedMessageStore]); + selectedMessageStore.toggleAll(Array.from(selectedMessageStore.availableMessages)); + }, [selectedMessageStore]); +}; + +export const useClearSelection = (): (() => void) => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + return useCallback(() => { + selectedMessageStore.clearStore(); + }, [selectedMessageStore]); }; export const useCountSelected = (): number => { 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..47f9f21bf67e 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; + onGetMore?: () => 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..2c65f254e13c --- /dev/null +++ b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx @@ -0,0 +1,36 @@ +import { Button, ButtonGroup } from '@rocket.chat/fuselage'; +import { MessageFooterCallout, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; +import type { ReactElement } from 'react'; +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { ComposerMessageProps } from './ComposerMessage'; +import { useCountSelected, useClearSelection, SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; + +const ComposerSelectMessages = ({ onGetMore }: ComposerMessageProps): ReactElement => { + const { t } = useTranslation(); + + const { selectedMessageStore } = useContext(SelectedMessageContext); + const clearSelection = useClearSelection(); + const countSelected = useCountSelected(); + const countAvailable = selectedMessageStore.availableMessages.size; + const noMessagesAvailable = countAvailable <= countSelected; + + return ( + + +
{countSelected} messages selected
+
+ + + + +
+ ); +}; + +export default ComposerSelectMessages; diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index f9b133b1c039..267ebf1777ae 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -1,8 +1,10 @@ import { Emitter } from '@rocket.chat/emitter'; import type { ReactNode } from 'react'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; +import { useMessages } from '../MessageList/hooks/useMessages'; +import { useRoom } from '../contexts/RoomContext'; // data-qa-select @@ -14,6 +16,8 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte > { store = new Set(); + availableMessages = new Set(); + isSelecting = false; setIsSelecting(isSelecting: boolean): void { @@ -82,6 +86,13 @@ type SelectedMessagesProviderProps = { }; export const SelectedMessagesProvider = ({ children }: SelectedMessagesProviderProps) => { + const room = useRoom(); + const messages = useMessages({ rid: room._id }); + + useEffect(() => { + selectedMessageStore.availableMessages = new Set(messages.map((message) => message._id)); + }, [messages]); + const value = useMemo( () => ({ selectedMessageStore, From 2f3f9384a5bb813117c58b803ec32d40f52278cf Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 3 Dec 2024 16:55:02 -0300 Subject: [PATCH 03/14] feat: merge export forms --- .../ExportMessages/ExportE2EEMessages.tsx | 219 ------------ .../ExportMessages/ExportMessages.tsx | 315 ++++++++++++++++-- .../ExportMessages/FileExport.tsx | 105 ------ .../ExportMessages/MailExportForm.tsx | 221 ------------ .../useDownloadExportMutation.ts | 24 ++ packages/i18n/src/locales/en.i18n.json | 7 +- 6 files changed, 321 insertions(+), 570 deletions(-) delete mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/FileExport.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx deleted file mode 100644 index 1733edb32a87..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportE2EEMessages.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; -import type { SelectOption } from '@rocket.chat/fuselage'; -import { - Field, - FieldLabel, - FieldRow, - Select, - ButtonGroup, - Button, - FieldGroup, - InputBox, - Margins, - Box, - FieldHint, - Callout, -} from '@rocket.chat/fuselage'; -import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useUserPreference } from '@rocket.chat/ui-contexts'; -import { useMutation } from '@tanstack/react-query'; -import React, { useCallback, useMemo } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -// import type { MailExportFormValues } from './ExportMessages'; -// import { useRoomExportMutation } from './useRoomExportMutation'; -import { Messages } from '../../../../../app/models/client'; -import { - ContextualbarScrollableContent, - ContextualbarFooter, - ContextualbarClose, - ContextualbarHeader, - ContextualbarIcon, - ContextualbarTitle, -} from '../../../../components/Contextualbar'; -import { useReactiveValue } from '../../../../hooks/useReactiveValue'; -import { downloadJsonAs } from '../../../../lib/download'; -import { useRoom } from '../../contexts/RoomContext'; -import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; - -export const useMessages = ({ rid }: { rid: IRoom['_id'] }): IMessage[] => { - const showThreadsInMainChannel = useUserPreference('showThreadsInMainChannel', false); - // const hideSysMesSetting = useSetting('Hide_System_Messages', []); - const room = useRoom(); - // const hideRoomSysMes: Array = Array.isArray(room.sysMes) ? room.sysMes : []; - - // const hideSysMessages = useStableArray(mergeHideSysMessages(hideSysMesSetting, hideRoomSysMes)); - - const query: Mongo.Selector = useMemo( - () => ({ - rid, - _hidden: { $ne: true }, - // t: { $nin: hideSysMessages }, - ...(!showThreadsInMainChannel && { - $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], - }), - ts: { $gte: new Date('2024-11-30'), $lt: new Date() }, - }), - [rid, showThreadsInMainChannel], - ); - - return useReactiveValue( - useCallback( - () => - Messages.find(query, { - sort: { - ts: 1, - }, - }).fetch(), - [query], - ), - ); -}; - -const useExportE2EEMessages = ({ rid }: { rid: string }) => { - const showThreadsInMainChannel = useUserPreference('showThreadsInMainChannel', false); - - // const messages = useMessages({ rid: room._id }); - - // const query: Mongo.Selector = useMemo( - // () => ({ - // rid, - // _hidden: { $ne: true }, - // // t: { $nin: hideSysMessages }, - // ...(!showThreadsInMainChannel && { - // $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], - // }), - // ts: { $gte: new Date('2024-11-15'), $lt: new Date() }, - // }), - // [rid, showThreadsInMainChannel], - // ); - - return useMutation({ - mutationFn: ({ from, until }: FormValues) => { - return Messages.find({ - rid, - _hidden: { $ne: true }, - // t: { $nin: hideSysMessages }, - ...(!showThreadsInMainChannel && { - $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], - }), - ...((from.date || until.date) && { ts: { $gte: new Date(from.date), $lt: new Date(until.date) } }), - }).fetch(); - }, - onSuccess: (data) => { - console.log(data); - downloadJsonAs(data, 'exportedMessages'); - }, - }); -}; - -type FormValues = { - type: 'file'; - format: 'html' | 'json'; - from: { date: string; time: string }; - until: { date: string; time: string }; -}; - -const ExportE2EEMessages = () => { - const { t } = useTranslation(); - const room = useRoom(); - const { closeTab } = useRoomToolbox(); - - // console.log(messages); - const exportE2EEMessages = useExportE2EEMessages({ rid: room._id }); - - const { control, register, handleSubmit } = useForm({ - defaultValues: { type: 'file', format: 'html', from: { date: '', time: '' }, until: { date: '', time: '' } }, - }); - - const outputOptions = useMemo( - () => [ - ['html', t('HTML')], - ['json', t('JSON')], - ], - [t], - ); - - const handleExport = async (data: FormValues) => { - console.log(data); - - exportE2EEMessages.mutate(data); - // return downloadJsonAs(statisticsQuery.data, 'statistics'); - }; - - const formId = useUniqueId(); - const typeField = useUniqueId(); - const formatField = useUniqueId(); - const formFocus = useAutoFocus(); - - return ( - <> - - - {t('Export_Encrypted_Messages')} - - - <> - -
- - - {t('Method')} - - ( - } - /> - - - A maximum of 500 messages can be exported at a time from encrypted `room type`. - -
-
- - - - - - - - - ); -}; - -export default ExportE2EEMessages; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index f6a621200119..17411840d7ea 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 } 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,92 @@ 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 messages = selectedMessageStore.getSelectedMessages(); + + 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', messages.length); + }, [type, setValue, messages.length]); + + const handleExport = async ({ type, toUsers, dateFrom, dateTo, format, subject, additionalEmails }: ExportMessagesFormValues) => { + 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 +171,175 @@ const ExportMessages = () => { {t('Export_Messages')} - - {methods.watch('type') === 'email' && ( - - )} - {methods.watch('type') === 'file' && ( - - )} - + +
+ + + {t('Method')} + + ( + + )} + /> + + + {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; + } + + if (additionalEmails !== '' && validateEmail(additionalEmails)) { + 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} + + + )} + + )} + +
+
+ + + + + + ); }; 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')} - - } - /> - - - -
-
- - - - - - - - ); -}; - -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 c7a3276ac8e1..000000000000 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/MailExportForm.tsx +++ /dev/null @@ -1,221 +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')} - - (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')} - - { - if (additionalEmails === '') { - return undefined; - } - - if (additionalEmails !== '' && validateEmail(additionalEmails)) { - 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')} - - } />} - /> - - - -
-
- - - - - - - - ); -}; - -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..aacdc39b8365 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -0,0 +1,24 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; + +import { Messages } from '../../../../../app/models/client'; +import { downloadJsonAs } from '../../../../lib/download'; + +export const useDownloadExportMutation = () => { + const dispatchToastMessage = useToastMessageDispatch(); + + return useMutation({ + mutationFn: async ({ mids }: { mids: IMessage['_id'][] }) => { + return Messages.find({ + _id: { $in: mids }, + }).fetch(); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSuccess: (data) => { + downloadJsonAs(data, 'exportedMessages'); + }, + }); +}; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index f6a6aea00eb6..be2fa9c10cae 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1067,6 +1067,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 +1814,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 +2321,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", @@ -4972,8 +4974,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.", From 028f89eba31ed21969fc669feca4ef9aa57c3599 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 5 Dec 2024 16:34:36 -0300 Subject: [PATCH 04/14] chore: composer tweaks --- .../contexts/SelectedMessagesContext.tsx | 13 +++++++++++++ .../room/composer/ComposerSelectMessages.tsx | 8 +++----- .../ExportMessages/ExportMessages.tsx | 4 ++-- .../room/providers/SelectedMessagesProvider.tsx | 11 ++++++++++- .../MessageFooterCalloutContent.tsx | 17 ++++++++--------- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx index 0e127009023d..5cfbc3352274 100644 --- a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx +++ b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx @@ -70,3 +70,16 @@ export const useCountSelected = (): number => { return useSyncExternalStore(subscribe, getSnapshot); }; + +export const useSelectedMessages = (): string[] => { + const { selectedMessageStore } = useContext(SelectedMessageContext); + + const subscribe = useCallback( + (callback: () => void): (() => void) => selectedMessageStore.on('change', callback), + [selectedMessageStore], + ); + + const getSnapshot = () => selectedMessageStore.getSelectedMessages(); + + return useSyncExternalStore(subscribe, getSnapshot); +}; diff --git a/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx index 2c65f254e13c..1c108df6119f 100644 --- a/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx +++ b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx @@ -18,15 +18,13 @@ const ComposerSelectMessages = ({ onGetMore }: ComposerMessageProps): ReactEleme return ( - -
{countSelected} messages selected
-
+ {t('number_messages_selected', { count: countSelected })} -
diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index 6d9d1beafa94..be72ec6b2aee 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -32,7 +32,7 @@ import { } from '../../../../components/Contextualbar'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import { SelectedMessageContext } from '../../MessageList/contexts/SelectedMessagesContext'; +import { SelectedMessageContext, useSelectedMessages } from '../../MessageList/contexts/SelectedMessagesContext'; import { useRoom } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; @@ -102,7 +102,7 @@ const ExportMessages = () => { const downloadExportMutation = useDownloadExportMutation(); const { selectedMessageStore } = useContext(SelectedMessageContext); - const messages = selectedMessageStore.getSelectedMessages(); + const messages = useSelectedMessages(); const { type, toUsers } = watch(); diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index 267ebf1777ae..5687f83e5e3b 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -16,10 +16,19 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte > { store = new Set(); + private storeArray = Array.from(this.store); + availableMessages = new Set(); isSelecting = false; + constructor() { + super(); + this.on('change', () => { + this.storeArray = Array.from(this.store); + }); + } + setIsSelecting(isSelecting: boolean): void { this.isSelecting = isSelecting; this.emit('toggleIsSelecting', isSelecting); @@ -34,7 +43,7 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte } getSelectedMessages(): string[] { - return Array.from(this.store); + return this.storeArray; } toggle(mid: string): void { 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; From c3d93c4456a84d3c83be7e10e270b15958057051 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 5 Dec 2024 16:52:01 -0300 Subject: [PATCH 05/14] chore: format file --- .../useDownloadExportMutation.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts index aacdc39b8365..0c80fd2141a9 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -1,24 +1,49 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import { Messages } from '../../../../../app/models/client'; import { downloadJsonAs } from '../../../../lib/download'; +import { useRoom } from '../../contexts/RoomContext'; + +const messagesFields = { _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'][] }) => { - return Messages.find({ - _id: { $in: mids }, - }).fetch(); + const messages = Messages.find( + { + _id: { $in: mids }, + }, + { projection: messagesFields }, + ).fetch(); + + const fileData = { + roomId: room._id, + roomName: room.fname || room.name, + messages, + exportDate: new Date().toISOString(), + userExport: { + id: user?._id, + username: user?.username, + name: user?.name, + roles: user?.roles, + }, + }; + + return downloadJsonAs(fileData, `exportedMessages-${new Date().toISOString()}`); }, onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); }, - onSuccess: (data) => { - downloadJsonAs(data, 'exportedMessages'); + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Messages_exported_successfully') }); }, }); }; From da3d0fc197cd1926b2a996929496eafdfaf6be36 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 5 Dec 2024 16:52:17 -0300 Subject: [PATCH 06/14] chore: translations --- packages/i18n/src/locales/de.i18n.json | 3 +-- packages/i18n/src/locales/en.i18n.json | 4 +++- packages/i18n/src/locales/fi.i18n.json | 3 +-- packages/i18n/src/locales/hi-IN.i18n.json | 3 +-- packages/i18n/src/locales/hu.i18n.json | 3 +-- packages/i18n/src/locales/nn.i18n.json | 3 +-- packages/i18n/src/locales/no.i18n.json | 3 +-- packages/i18n/src/locales/pl.i18n.json | 3 +-- packages/i18n/src/locales/se.i18n.json | 3 +-- packages/i18n/src/locales/sv.i18n.json | 3 +-- 10 files changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index f39ec20f1622..7017b17aba97 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -3273,7 +3273,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", @@ -5509,4 +5508,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 bb6fdfd5d1b1..ec8deee584b3 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3775,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", + "number_messages_selected": "{{count}} messages selected", + "Messages_exported_successfully": "Messages exported successfully", "Messages_sent": "Messages sent", "Message_sent": "Message sent", "Message_viewed": "Message viewed", @@ -6687,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_number_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 497484914396..2949b01d65b6 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -3320,7 +3320,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", @@ -5722,4 +5721,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 46d5a2325fc2..76ce8e8950d8 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -3455,7 +3455,6 @@ "Message_VideoRecorderEnabledDescription": "'फ़ाइल अपलोड' सेटिंग्स के अंतर्गत 'वीडियो/वेबएम' फ़ाइलों को एक स्वीकृत मीडिया प्रकार होना आवश्यक है।", "messages": "संदेशों", "Messages": "संदेशों", - "Messages_selected": "संदेश चयनित", "Messages_sent": "संदेश भेजे गए", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "इनकमिंग वेबहुक पर भेजे गए संदेश यहां पोस्ट किए जाएंगे।", "Meta": "मेटा", @@ -6106,4 +6105,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 27df7821fae0..4154259f84aa 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", @@ -4560,4 +4559,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 a053a75912d3..daf800fc75f1 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", @@ -4562,4 +4561,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..54f230f686e2 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -3204,7 +3204,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 +5404,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 cec29a4655e7..9e0c37b29a81 100644 --- a/packages/i18n/src/locales/se.i18n.json +++ b/packages/i18n/src/locales/se.i18n.json @@ -3681,7 +3681,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", @@ -6584,4 +6583,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 5bcb4e9af48f..6ba08156de6d 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -3325,7 +3325,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", @@ -5724,4 +5723,4 @@ "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} +} \ No newline at end of file From 676a6f611517f20c9577618ca31e43a904e17815 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 5 Dec 2024 17:21:40 -0300 Subject: [PATCH 07/14] chore: typecheck --- .../ExportMessages/useDownloadExportMutation.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts index 0c80fd2141a9..ba5dbdf02567 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -1,13 +1,14 @@ 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 = { _id: 1, ts: 1, u: 1, msg: 1, _updatedAt: 1, tlm: 1, replies: 1, tmid: 1 }; +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(); @@ -21,7 +22,7 @@ export const useDownloadExportMutation = () => { { _id: { $in: mids }, }, - { projection: messagesFields }, + messagesFields, ).fetch(); const fileData = { From 845f2f00d27520cd86c5c5eedce7707403bc9626 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 5 Dec 2024 17:25:16 -0300 Subject: [PATCH 08/14] chore: changeset --- .changeset/shaggy-bulldogs-beg.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/shaggy-bulldogs-beg.md 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 From ced342f0a4e8eeef4dd2537764661a09233a66b7 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 9 Dec 2024 14:44:50 -0300 Subject: [PATCH 09/14] chore: add some e2e tests --- .../ExportMessages/ExportMessages.tsx | 6 +- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 14 +++++ apps/meteor/tests/e2e/export-messages.spec.ts | 61 +++++++++++++++++++ .../fragments/home-flextab-exportMessages.ts | 29 +++++++++ .../page-objects/fragments/home-flextab.ts | 8 +++ apps/meteor/tests/e2e/page-objects/utils.ts | 6 ++ 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/tests/e2e/export-messages.spec.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index be72ec6b2aee..96a53675d78a 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -260,6 +260,10 @@ const ExportMessages = () => { rules={{ validate: { validateEmail: (additionalEmails) => { + if (additionalEmails === '') { + return undefined; + } + const emails = additionalEmails?.split(',').map((email) => email.trim()); if (Array.isArray(emails) && emails.every((email) => validateEmail(email.trim()))) { return undefined; @@ -290,7 +294,7 @@ const ExportMessages = () => { /> {errors?.additionalEmails && ( - + {errors.additionalEmails.message} )} 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..952e83af8139 --- /dev/null +++ b/apps/meteor/tests/e2e/export-messages.spec.ts @@ -0,0 +1,61 @@ +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 email', 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.tabs.exportMessages.textboxAdditionalEmails.fill('mail@mail.com'); + await poHomeChannel.tabs.exportMessages.btnSend.click(); + + await expect(poUtils.getAlertByText(`You haven't selected any messages`)).toBeVisible(); + }); +}); 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..58ff0a47fba8 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts @@ -0,0 +1,29 @@ +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"]'); + } +} 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, + }); + } } From 4e000f128f9d07aa57329c256edc2c3b0edd56d5 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 11 Dec 2024 18:10:08 -0300 Subject: [PATCH 10/14] fix: review Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .../contexts/SelectedMessagesContext.tsx | 20 ++++++-- .../client/views/room/body/RoomBody.tsx | 8 ++-- .../client/views/room/body/RoomBodyV2.tsx | 8 ++-- .../views/room/body/hooks/useGetMore.ts | 13 +---- .../body/hooks/useSelectAllAndScrollToTop.ts | 15 ++++++ .../views/room/composer/ComposerMessage.tsx | 2 +- .../room/composer/ComposerSelectMessages.tsx | 18 +++---- .../ExportMessages/ExportMessages.tsx | 10 ++-- .../providers/SelectedMessagesProvider.tsx | 47 +++++++------------ packages/i18n/src/locales/en.i18n.json | 4 +- 10 files changed, 76 insertions(+), 69 deletions(-) create mode 100644 apps/meteor/client/views/room/body/hooks/useSelectAllAndScrollToTop.ts diff --git a/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx b/apps/meteor/client/views/room/MessageList/contexts/SelectedMessagesContext.tsx index 5cfbc3352274..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 => { @@ -71,7 +83,7 @@ export const useCountSelected = (): number => { return useSyncExternalStore(subscribe, getSnapshot); }; -export const useSelectedMessages = (): string[] => { +export const useAvailableMessagesCount = () => { const { selectedMessageStore } = useContext(SelectedMessageContext); const subscribe = useCallback( @@ -79,7 +91,7 @@ export const useSelectedMessages = (): string[] => { [selectedMessageStore], ); - const getSnapshot = () => selectedMessageStore.getSelectedMessages(); + 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 e8352c93783c..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 => { @@ -102,7 +103,7 @@ const RoomBody = (): ReactElement => { const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef); + const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); const { wrapperRef: leaderBannerWrapperRef, hideLeaderHeader, innerRef: leaderBannerInnerRef } = useLeaderBanner(); @@ -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,7 +315,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} - onGetMore={handleGetMore} + 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 7de9eb92113a..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 => { @@ -97,7 +98,7 @@ const RoomBody = (): ReactElement => { const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); - const { innerRef: getMoreInnerRef, handleGetMore } = useGetMore(room._id, atBottomRef); + const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection(); @@ -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,7 +287,7 @@ const RoomBody = (): ReactElement => { onNavigateToPreviousMessage={handleNavigateToPreviousMessage} onNavigateToNextMessage={handleNavigateToNextMessage} onUploadFiles={handleUploadFiles} - onGetMore={handleGetMore} + onClickSelectAll={selectAllAndScrollToTop} // TODO: send previewUrls param // previewUrls={} /> diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index 5fd4775603e6..c2f182e5131f 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,28 +1,17 @@ import type { MutableRefObject } from 'react'; -import { useCallback, useRef } from 'react'; +import { useCallback } from 'react'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; -import { useToggleSelectAll } from '../../MessageList/contexts/SelectedMessagesContext'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { - const ref: MutableRefObject = useRef(null); - const handleToggleAll = useToggleSelectAll(); - - const handleGetMore = () => { - ref.current?.scrollTo({ top: 0, behavior: 'smooth' }); - handleToggleAll(); - }; - return { - handleGetMore, innerRef: useCallback( (wrapper: HTMLElement | null) => { if (!wrapper) { return; } - ref.current = wrapper; let lastScrollTopRef = 0; wrapper.addEventListener( 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/ComposerMessage.tsx b/apps/meteor/client/views/room/composer/ComposerMessage.tsx index 47f9f21bf67e..a5fd473f788b 100644 --- a/apps/meteor/client/views/room/composer/ComposerMessage.tsx +++ b/apps/meteor/client/views/room/composer/ComposerMessage.tsx @@ -22,7 +22,7 @@ export type ComposerMessageProps = { onNavigateToNextMessage?: () => void; onNavigateToPreviousMessage?: () => void; onUploadFiles?: (files: readonly File[]) => void; - onGetMore?: () => 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 index 1c108df6119f..2a77e250e5ba 100644 --- a/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx +++ b/apps/meteor/client/views/room/composer/ComposerSelectMessages.tsx @@ -1,30 +1,30 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { MessageFooterCallout, MessageFooterCalloutContent } from '@rocket.chat/ui-composer'; import type { ReactElement } from 'react'; -import React, { useContext } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import type { ComposerMessageProps } from './ComposerMessage'; -import { useCountSelected, useClearSelection, SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; +import { useCountSelected, useClearSelection, useAvailableMessagesCount } from '../MessageList/contexts/SelectedMessagesContext'; -const ComposerSelectMessages = ({ onGetMore }: ComposerMessageProps): ReactElement => { +const ComposerSelectMessages = ({ onClickSelectAll }: ComposerMessageProps): ReactElement => { const { t } = useTranslation(); - const { selectedMessageStore } = useContext(SelectedMessageContext); const clearSelection = useClearSelection(); const countSelected = useCountSelected(); - const countAvailable = selectedMessageStore.availableMessages.size; - const noMessagesAvailable = countAvailable <= countSelected; + const countAvailable = useAvailableMessagesCount(); return ( - {t('number_messages_selected', { count: countSelected })} + + {t('__count__messages_selected', { count: countSelected })} + - diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index 96a53675d78a..8736c8a1bec3 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -32,7 +32,7 @@ import { } from '../../../../components/Contextualbar'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import { SelectedMessageContext, useSelectedMessages } from '../../MessageList/contexts/SelectedMessagesContext'; +import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext'; import { useRoom } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; @@ -102,7 +102,7 @@ const ExportMessages = () => { const downloadExportMutation = useDownloadExportMutation(); const { selectedMessageStore } = useContext(SelectedMessageContext); - const messages = useSelectedMessages(); + const messageCount = useCountSelected(); const { type, toUsers } = watch(); @@ -125,10 +125,12 @@ const ExportMessages = () => { setValue('format', 'json'); } - setValue('messagesCount', messages.length); - }, [type, setValue, messages.length]); + 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, diff --git a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx index 5687f83e5e3b..e70156126df0 100644 --- a/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx +++ b/apps/meteor/client/views/room/providers/SelectedMessagesProvider.tsx @@ -1,12 +1,8 @@ import { Emitter } from '@rocket.chat/emitter'; import type { ReactNode } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { SelectedMessageContext } from '../MessageList/contexts/SelectedMessagesContext'; -import { useMessages } from '../MessageList/hooks/useMessages'; -import { useRoom } from '../contexts/RoomContext'; - -// data-qa-select export const selectedMessageStore = new (class SelectMessageStore extends Emitter< { @@ -16,17 +12,18 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte > { store = new Set(); - private storeArray = Array.from(this.store); - availableMessages = new Set(); isSelecting = false; - constructor() { - super(); - this.on('change', () => { - this.storeArray = Array.from(this.store); - }); + 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 { @@ -43,7 +40,7 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte } getSelectedMessages(): string[] { - return this.storeArray; + return Array.from(this.store); } toggle(mid: string): void { @@ -58,20 +55,14 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte this.emit('change'); } - select(mid: string): void { - if (this.store.has(mid)) { - return; - } - - this.store.add(mid); - this.emit(mid, true); - this.emit('change'); - } - count(): number { return this.store.size; } + availableMessagesCount(): number { + return this.availableMessages.size; + } + clearStore(): void { const selectedMessages = this.getSelectedMessages(); this.store.clear(); @@ -86,7 +77,8 @@ export const selectedMessageStore = new (class SelectMessageStore extends Emitte } toggleAll(mids: string[]): void { - mids.forEach((mid) => this.select(mid)); + this.store = new Set([...this.store, ...mids]); + this.emit('change'); } })(); @@ -95,13 +87,6 @@ type SelectedMessagesProviderProps = { }; export const SelectedMessagesProvider = ({ children }: SelectedMessagesProviderProps) => { - const room = useRoom(); - const messages = useMessages({ rid: room._id }); - - useEffect(() => { - selectedMessageStore.availableMessages = new Set(messages.map((message) => message._id)); - }, [messages]); - const value = useMemo( () => ({ selectedMessageStore, diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index ec8deee584b3..09286ad00c02 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3775,7 +3775,7 @@ "Message_VideoRecorderEnabledDescription": "Requires 'video/webm' files to be an accepted media type within 'File Upload' settings.", "messages": "messages", "Messages": "Messages", - "number_messages_selected": "{{count}} messages selected", + "__count__messages_selected": "{{count}} messages selected", "Messages_exported_successfully": "Messages exported successfully", "Messages_sent": "Messages sent", "Message_sent": "Message sent", @@ -6688,7 +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_number_messages": "Select {{count}} messages", + "Select__count__messages": "Select {{count}} messages", "Name_cannot_have_special_characters": "Name cannot have spaces or special characters", "Resize": "Resize", "Zoom_out": "Zoom out", From abf9858d416c60c57590d94fc9ffde7120cddf58 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 17 Dec 2024 20:06:38 -0300 Subject: [PATCH 11/14] fix: reset ComposerAPI when has `chat.composer` and node is null [CORE-889] --- apps/meteor/app/ui/client/lib/ChatMessages.ts | 2 +- apps/meteor/client/lib/chats/ChatAPI.ts | 2 +- .../client/views/room/composer/messageBox/MessageBox.tsx | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) 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/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/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)); From c45a8af0b11ca8e268eaaee908c3caae64965b42 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 17 Dec 2024 20:07:07 -0300 Subject: [PATCH 12/14] fix: add thread messages to file when thread is selected [CORE-858] --- .../ExportMessages/useDownloadExportMutation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts index ba5dbdf02567..f35f153da8bb 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -20,7 +20,7 @@ export const useDownloadExportMutation = () => { mutationFn: async ({ mids }: { mids: IMessage['_id'][] }) => { const messages = Messages.find( { - _id: { $in: mids }, + $or: [{ _id: { $in: mids } }, { tmid: { $in: mids } }], }, messagesFields, ).fetch(); @@ -28,14 +28,14 @@ export const useDownloadExportMutation = () => { const fileData = { roomId: room._id, roomName: room.fname || room.name, - messages, - exportDate: new Date().toISOString(), 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()}`); From 63695289d8a2f789a001f4ee55f4de1329d10e48 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 17 Dec 2024 20:24:23 -0300 Subject: [PATCH 13/14] test: add new case --- .../components/message/variants/RoomMessage.tsx | 2 +- apps/meteor/tests/e2e/export-messages.spec.ts | 13 ++++++++++++- .../e2e/page-objects/fragments/home-content.ts | 2 +- .../fragments/home-flextab-exportMessages.ts | 4 ++++ packages/i18n/src/locales/en.i18n.json | 1 - packages/i18n/src/locales/pl.i18n.json | 1 - packages/i18n/src/locales/se.i18n.json | 1 - 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index bf5c12a9a6dd..72b52cf031f9 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/tests/e2e/export-messages.spec.ts b/apps/meteor/tests/e2e/export-messages.spec.ts index 952e83af8139..7fe2f0b2e555 100644 --- a/apps/meteor/tests/e2e/export-messages.spec.ts +++ b/apps/meteor/tests/e2e/export-messages.spec.ts @@ -49,7 +49,6 @@ test.describe.serial('export-messages', () => { test('should display an error when trying to send email without selecting any email', async () => { await poHomeChannel.sidenav.openChat(targetChannel); - await poHomeChannel.content.sendMessage('hello world'); await poHomeChannel.tabs.kebab.click({ force: true }); await poHomeChannel.tabs.btnExportMessages.click(); @@ -58,4 +57,16 @@ test.describe.serial('export-messages', () => { 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 0c77364ed57a..e1186ec06b82 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -388,7 +388,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 index 58ff0a47fba8..ddf78b7f4388 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-exportMessages.ts @@ -26,4 +26,8 @@ export class HomeFlextabExportMessages { get btnSend() { return this.page.locator('role=button[name="Send"]'); } + + get btnCancel() { + return this.page.locator('role=button[name="Cancel"]'); + } } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 09286ad00c02..163e1f27ce16 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", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 54f230f686e2..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ą", diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json index 9e0c37b29a81..e73c4897457a 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", From fdc5310b1f08313f5bcb69fe3be340dcc19853fe Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 19 Dec 2024 10:44:16 -0300 Subject: [PATCH 14/14] chore: fix typo Co-authored-by: Marcos Spessatto Defendi --- apps/meteor/tests/e2e/export-messages.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/export-messages.spec.ts b/apps/meteor/tests/e2e/export-messages.spec.ts index 7fe2f0b2e555..dffb6b4d5edb 100644 --- a/apps/meteor/tests/e2e/export-messages.spec.ts +++ b/apps/meteor/tests/e2e/export-messages.spec.ts @@ -47,7 +47,7 @@ test.describe.serial('export-messages', () => { ).toBeVisible(); }); - test('should display an error when trying to send email without selecting any email', async () => { + 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();