Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tidy-colts-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/i18n": patch
---

Introduces PDF file as an export type for room messages
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,5 @@ export const permissions = [
{ _id: 'view-moderation-console', roles: ['admin'] },
{ _id: 'manage-moderation-actions', roles: ['admin'] },
{ _id: 'bypass-time-limit-edit-and-delete', roles: ['bot', 'app'] },
{ _id: 'export-messages-as-pdf', roles: ['admin', 'user'] },
];
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import {
Callout,
} from '@rocket.chat/fuselage';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { usePermission } from '@rocket.chat/ui-contexts';
import { useContext, useEffect, useId, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import { useDownloadExportMutation } from './useDownloadExportMutation';
import { useExportMessagesAsPDFMutation } from './useExportMessagesAsPDFMutation';
import { useRoomExportMutation } from './useRoomExportMutation';
import { validateEmail } from '../../../../../lib/emailValidator';
import {
Expand All @@ -41,7 +43,7 @@ export type ExportMessagesFormValues = {
type: 'email' | 'file' | 'download';
dateFrom: string;
dateTo: string;
format: 'html' | 'json';
format: 'html' | 'json' | 'pdf';
toUsers: string[];
additionalEmails: string;
messagesCount: number;
Expand All @@ -51,6 +53,7 @@ export type ExportMessagesFormValues = {
const ExportMessages = () => {
const { t } = useTranslation();
const { closeTab } = useRoomToolbox();
const pfdExportPermission = usePermission('export-messages-as-pdf');
const formFocus = useAutoFocus<HTMLFormElement>();
const room = useRoom();
const isE2ERoom = room.encrypted;
Expand Down Expand Up @@ -92,13 +95,21 @@ const ExportMessages = () => {
[t],
);

const outputOptions = useMemo<SelectOption[]>(
() => [
const outputOptions = useMemo<SelectOption[]>(() => {
const options: SelectOption[] = [
['html', t('HTML')],
['json', t('JSON')],
],
[t],
);
];

if (pfdExportPermission) {
options.push(['pdf', t('PDF')]);
}

return options;
}, [t, pfdExportPermission]);

// Remove HTML from download options
const downloadOutputOptions = outputOptions.slice(1);

const roomExportMutation = useRoomExportMutation();
const downloadExportMutation = useDownloadExportMutation();
Expand All @@ -108,6 +119,12 @@ const ExportMessages = () => {

const { type, toUsers } = watch();

useEffect(() => {
if (type === 'email') {
setValue('format', 'html');
}
}, [type, setValue]);

useEffect(() => {
if (type !== 'file') {
selectedMessageStore.setIsSelecting(true);
Expand All @@ -119,24 +136,24 @@ const ExportMessages = () => {
}, [type, selectedMessageStore]);

useEffect(() => {
if (type === 'email') {
setValue('format', 'html');
}

if (type === 'download') {
setValue('format', 'json');
}

setValue('messagesCount', messageCount, { shouldDirty: true });
}, [type, setValue, messageCount]);
}, [messageCount, setValue]);

const { mutate: exportAsPDF } = useExportMessagesAsPDFMutation();

const handleExport = async ({ type, toUsers, dateFrom, dateTo, format, subject, additionalEmails }: ExportMessagesFormValues) => {
const messages = selectedMessageStore.getSelectedMessages();

if (type === 'download') {
return downloadExportMutation.mutateAsync({
mids: messages,
});
if (format === 'pdf') {
return exportAsPDF(messages);
}

if (format === 'json') {
return downloadExportMutation.mutateAsync({
mids: messages,
});
}
}

if (type === 'file') {
Expand All @@ -145,7 +162,7 @@ const ExportMessages = () => {
type: 'file',
...(dateFrom && { dateFrom }),
...(dateTo && { dateTo }),
format,
format: format as 'html' | 'json',
});
}

Expand Down Expand Up @@ -207,9 +224,9 @@ const ExportMessages = () => {
<Select
{...field}
id={formatField}
disabled={type === 'email' || type === 'download'}
disabled={type === 'email'}
placeholder={t('Format')}
options={outputOptions}
options={type === 'download' ? downloadOutputOptions : outputOptions}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Document, Page, pdf, Text, View } from '@react-pdf/renderer';
import type { IMessage } from '@rocket.chat/core-typings';
import { escapeHTML } from '@rocket.chat/string-helpers';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';

import { Messages } from '../../../../../app/models/client';
import { MessageTypes } from '../../../../../app/ui-utils/lib/MessageTypes';
import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime';

export const useExportMessagesAsPDFMutation = () => {
const { t } = useTranslation();
const chatopsUsername = useSetting('Chatops_Username');
const formatDateAndTime = useFormatDateAndTime();

return useMutation({
mutationFn: async (messageIds: IMessage['_id'][]) => {
const parseMessage = (msg: IMessage) => {
const messageType = MessageTypes.getType(msg);
if (messageType) {
if (messageType.template) {
// Render message
return;
}
if (messageType.message) {
const data = (typeof messageType.data === 'function' && messageType.data(msg)) || {};
return t(messageType.message, data);
}
}
if (msg.u && msg.u.username === chatopsUsername) {
msg.html = msg.msg;
return msg.html;
}
msg.html = msg.msg;
if (msg.html.trim() !== '') {
msg.html = escapeHTML(msg.html);
}
return msg.html;
};

const messages = Messages.state.filter((record) => messageIds.includes(record._id)).sort((a, b) => a.ts.getTime() - b.ts.getTime());

const jsx = (
<Document>
<Page size='A4'>
<View style={{ margin: 10 }}>
{messages.map((message) => {
const dateTime = formatDateAndTime(message.ts);
return (
<Text key={message._id} style={{ marginBottom: 5 }}>
<Text style={{ color: '#555', fontSize: 14 }}>{message.u.username}</Text>{' '}
<Text style={{ color: '#aaa', fontSize: 12 }}>{dateTime}</Text>
<Text>{'\n'}</Text>
{parseMessage(message)}
</Text>
);
})}
</View>
</Page>
</Document>
);

const instance = pdf();

const callback = async () => {
const link = document.createElement('a');
link.href = URL.createObjectURL(await instance.toBlob());
link.download = `exportedMessages-${new Date().toISOString()}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
};

try {
instance.on('change', callback);
instance.updateContainer(jsx);
} finally {
instance.removeListener('change', callback);
}
},
});
};
1 change: 1 addition & 0 deletions apps/meteor/server/startup/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ import './v315';
import './v316';
import './v317';
import './v318';
import './v319';

export * from './xrun';
9 changes: 9 additions & 0 deletions apps/meteor/server/startup/migrations/v319.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions';
import { addMigration } from '../../lib/migrations';

addMigration({
version: 319,
async up() {
await upsertPermissions();
},
});
2 changes: 2 additions & 0 deletions apps/meteor/tests/e2e/export-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ test.describe.serial('export-messages', () => {
await poHomeChannel.tabs.kebab.click({ force: true });
await poHomeChannel.tabs.btnExportMessages.click();

await expect(poHomeChannel.btnContextualbarClose).toBeVisible();

await poHomeChannel.content.getMessageByText('hello world').click();
await poHomeChannel.tabs.exportMessages.btnSend.click();

Expand Down
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@
"yarn": "1.22.18"
},
"resolutions": {
"@react-pdf/fns": "2.0.1",
"@react-pdf/font": "2.3.7",
"@react-pdf/image": "2.2.2",
"@react-pdf/layout": "3.6.3",
"@react-pdf/pdfkit": "3.0.2",
"@react-pdf/png-js": "2.2.0",
"@react-pdf/primitives": "3.0.1",
"@react-pdf/render": "3.2.7",
"@react-pdf/renderer": "3.1.14",
"@react-pdf/stylesheet": "4.1.8",
"@react-pdf/textkit": "4.2.0",
"@react-pdf/types": "2.3.4",
"@react-pdf/yoga": "4.1.2",
"minimist": "1.2.6",
"adm-zip": "0.5.9",
"underscore": "1.13.7",
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,7 @@
"Export": "Export",
"Export_Messages": "Export messages",
"Export_My_Data": "Export My Data (JSON)",
"export-messages-as-pdf": "Export messages as PDF",
"Export_as_PDF": "Export as PDF",
"Export_as_file": "Export as file",
"Export_conversation_transcript_as_PDF": "Export conversation transcript as PDF",
Expand Down
Loading
Loading