diff --git a/apps/meteor/client/hooks/roomActions/useExportMessagesRoomAction.ts b/apps/meteor/client/hooks/roomActions/useExportMessagesRoomAction.ts index 21d1da3a6843..9996f0da8769 100644 --- a/apps/meteor/client/hooks/roomActions/useExportMessagesRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useExportMessagesRoomAction.ts @@ -5,14 +5,29 @@ import { useRoom } from '../../views/room/contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; const ExportMessages = lazy(() => import('../../views/room/contextualBar/ExportMessages')); +const ExportE2EEMessages = lazy(() => import('../../views/room/contextualBar/ExportMessages/ExportE2EEMessages')); export const useExportMessagesRoomAction = () => { const room = useRoom(); - const permitted = usePermission('mail-messages', room._id); + const hasPermission = usePermission('mail-messages', room._id); - return useMemo((): RoomToolboxActionConfig | undefined => { - if (!permitted) { - return undefined; + return useMemo((): RoomToolboxActionConfig | null => { + if (!hasPermission) { + return null; + } + + if (room.encrypted) { + return { + id: 'export-encrypted-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + anonymous: true, + title: 'Export_Encrypted_Messages', + icon: 'mail', + tabComponent: ExportE2EEMessages, + full: true, + order: 12, + type: 'communication', + }; } return { @@ -26,5 +41,5 @@ export const useExportMessagesRoomAction = () => { order: 12, type: 'communication', }; - }, [permitted]); + }, [hasPermission, room.encrypted]); }; 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..93e806d5f562 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Box } from '@rocket.chat/fuselage'; +import { Box, Button } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEventHandler, ReactElement, UIEvent } from 'react'; @@ -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(); @@ -307,6 +307,9 @@ const RoomBody = (): ReactElement => { + ) => { + const ref: MutableRefObject = useRef(null); + const messages = useMessages({ rid: 'GENERAL' }); + const handleToggleAll = useToggleSelectAll(messages.map((message) => message._id)); + + 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 = {