snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import { Contextualbar } from '@rocket.chat/ui-client';
import type { StoryObj, Meta } from '@storybook/react';
import ExportMessages from './index';
export default {
component: ExportMessages,
parameters: {
layout: 'fullscreen',
},
decorators: [(fn) => <Contextualbar height='100vh'>{fn()}</Contextualbar>],
} satisfies Meta<typeof ExportMessages>;
export const Default: StoryObj<typeof ExportMessages> = {
render: () => <ExportMessages />,
name: 'ExportMessages',
};

View File

@@ -0,0 +1,397 @@
import type { SelectOption } from '@rocket.chat/fuselage';
import { ButtonGroup, Button, Icon, InputBox, Callout } from '@rocket.chat/fuselage';
import { FieldError, Field, FieldLabel, FieldRow, TextAreaInput, FieldGroup, Select, TextInput } from '@rocket.chat/fuselage-forms';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { validateEmail } from '@rocket.chat/tools';
import {
ContextualbarHeader,
ContextualbarScrollableContent,
ContextualbarIcon,
ContextualbarTitle,
ContextualbarClose,
ContextualbarFooter,
ContextualbarDialog,
} from '@rocket.chat/ui-client';
import { usePermission, useRoomToolbox } 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 UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
import { SelectedMessageContext, useCountSelected } from '../../MessageList/contexts/SelectedMessagesContext';
import { useRoom } from '../../contexts/RoomContext';
export type ExportMessagesFormValues = {
type: 'email' | 'file' | 'download';
dateFrom: string;
dateTo: string;
format: 'html' | 'json' | 'pdf';
toUsers: string[];
additionalEmails: string;
messagesCount: number;
subject: string;
};
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;
const roomName = room?.t && roomCoordinator.getRoomName(room.t, room);
const {
control,
formState: { errors, isSubmitting, isDirty, isSubmitted },
watch,
register,
setValue,
handleSubmit,
clearErrors,
reset,
} = useForm<ExportMessagesFormValues>({
defaultValues: {
type: isE2ERoom ? 'download' : 'email',
dateFrom: '',
dateTo: '',
toUsers: [],
additionalEmails: '',
messagesCount: 0,
subject: t('Mail_Messages_Subject', { roomName }),
format: isE2ERoom ? 'json' : 'html',
},
});
const exportOptions = useMemo<SelectOption[]>(
() => [
['email', t('Send_email')],
['file', t('Send_file_via_email')],
['download', t('Download_file')],
],
[t],
);
const outputOptions = useMemo<SelectOption[]>(() => {
const options: SelectOption[] = [
['html', t('HTML')],
['json', t('JSON')],
];
if (pfdExportPermission) {
options.push(['pdf', t('PDF')]);
}
return options;
}, [t, pfdExportPermission]);
// Remove HTML from download options
const downloadOutputOptions = useMemo<SelectOption[]>(() => {
return outputOptions.filter((option) => option[0] !== 'html');
}, [outputOptions]);
// Remove PDF from file options
const fileOutputOptions = useMemo<SelectOption[]>(() => {
return outputOptions.filter((option) => option[0] !== 'pdf');
}, [outputOptions]);
const { mutateAsync: exportRoom } = useRoomExportMutation();
const { mutateAsync: exportAndDownload } = useDownloadExportMutation();
const { selectedMessageStore } = useContext(SelectedMessageContext);
const messageCount = useCountSelected();
const { type, toUsers } = watch();
useEffect(() => {
if (type === 'email') {
setValue('format', 'html');
}
if (type === 'download') {
setValue('format', 'json');
}
}, [type, setValue]);
useEffect(() => {
if (type !== 'file') {
selectedMessageStore.setIsSelecting(true);
}
return (): void => {
selectedMessageStore.reset();
};
}, [type, selectedMessageStore]);
useEffect(() => {
setValue('messagesCount', messageCount, { shouldDirty: true, shouldValidate: isSubmitted });
}, [messageCount, setValue, isSubmitted]);
const { mutateAsync: exportAsPDF } = useExportMessagesAsPDFMutation();
const handleExport = async ({
type,
toUsers,
dateFrom,
dateTo,
format,
subject,
additionalEmails,
}: ExportMessagesFormValues): Promise<void> => {
const messages = selectedMessageStore.getSelectedMessages();
if (type === 'download') {
if (format === 'pdf') {
await exportAsPDF(messages);
return;
}
if (format === 'json') {
await exportAndDownload({
mids: messages,
});
return;
}
}
if (type === 'file') {
await exportRoom({
rid: room._id,
type: 'file',
...(dateFrom && { dateFrom }),
...(dateTo && { dateTo }),
format: format as 'html' | 'json',
});
return;
}
await exportRoom({
rid: room._id,
type: 'email',
toUsers,
toEmails: additionalEmails?.split(','),
subject,
messages,
});
};
const formId = useId();
const dateFromField = useId();
const dateToField = useId();
return (
<ContextualbarDialog aria-labelledby={`${formId}-title`}>
<ContextualbarHeader>
<ContextualbarIcon name='mail' />
<ContextualbarTitle id={`${formId}-title`}>{t('Export_Messages')}</ContextualbarTitle>
<ContextualbarClose onClick={closeTab} />
</ContextualbarHeader>
<ContextualbarScrollableContent>
<form ref={formFocus} tabIndex={-1} aria-labelledby={`${formId}-title`} id={formId} onSubmit={handleSubmit(handleExport)}>
<FieldGroup>
<Field>
<FieldLabel>{t('Method')}</FieldLabel>
<FieldRow>
<Controller
name='type'
control={control}
render={({ field }) => (
<Select
data-testid='export-messages-method'
{...field}
placeholder={t('Type')}
disabled={isE2ERoom}
options={exportOptions}
/>
)}
/>
</FieldRow>
</Field>
<Field>
<FieldLabel>{t('Output_format')}</FieldLabel>
<FieldRow>
<Controller
name='format'
control={control}
render={({ field }) => {
let options: SelectOption[];
if (type === 'download') {
options = downloadOutputOptions;
} else if (type === 'file') {
options = fileOutputOptions;
} else {
options = outputOptions;
}
return (
<Select
{...field}
data-testid='export-messages-output-format'
disabled={type === 'email'}
placeholder={t('Format')}
options={options}
/>
);
}}
/>
</FieldRow>
</Field>
{type === 'file' && (
<>
<Field>
<FieldLabel htmlFor={dateFromField}>{t('Date_From')}</FieldLabel>
<FieldRow>
<Controller
name='dateFrom'
control={control}
render={({ field }) => <InputBox id={dateFromField} type='date' {...field} />}
/>
</FieldRow>
</Field>
<Field>
<FieldLabel htmlFor={dateToField}>{t('Date_to')}</FieldLabel>
<FieldRow>
<Controller
name='dateTo'
control={control}
render={({ field }) => <InputBox id={dateToField} {...field} type='date' />}
/>
</FieldRow>
</Field>
</>
)}
{type === 'email' && (
<>
<Field>
<FieldLabel>{t('To_users')}</FieldLabel>
<FieldRow>
<Controller
name='toUsers'
control={control}
rules={{
validate: {
validateRecipient: (toUsers) => {
const additionalEmails = watch('additionalEmails');
if (toUsers?.length > 0 || additionalEmails !== '') {
return undefined;
}
return t('Mail_Message_Missing_to');
},
},
}}
render={({ field: { value, onChange, onBlur, name, ...field } }) => (
<UserAutoCompleteMultiple
{...field}
value={value}
onChange={(value) => {
onChange(value);
clearErrors('additionalEmails');
}}
onBlur={onBlur}
name={name}
error={errors?.toUsers?.message}
/>
)}
/>
</FieldRow>
{errors?.toUsers && <FieldError>{errors.toUsers.message}</FieldError>}
</Field>
<Field>
<FieldLabel>{t('To_additional_emails')}</FieldLabel>
<FieldRow>
<Controller
name='additionalEmails'
control={control}
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;
}
return t('Mail_Message_Invalid_emails', { emails: additionalEmails });
},
validateRecipient: (additionalEmails) => {
if (additionalEmails !== '' || toUsers?.length > 0) {
return undefined;
}
return t('Mail_Message_Missing_to');
},
},
}}
render={({ field: { onChange, onBlur, ...field } }) => (
<TextInput
{...field}
onChange={(e) => {
onChange(e);
clearErrors('toUsers');
}}
onBlur={onBlur}
placeholder={t('Email_Placeholder_any')}
endAddon={<Icon name='mail' size='x20' />}
error={errors?.additionalEmails?.message}
/>
)}
/>
</FieldRow>
{errors?.additionalEmails && <FieldError>{errors.additionalEmails.message}</FieldError>}
</Field>
<Field>
<FieldLabel>{t('Subject')}</FieldLabel>
<FieldRow>
<Controller
name='subject'
control={control}
render={({ field }) => <TextAreaInput rows={3} {...field} endAddon={<Icon name='edit' size='x20' />} />}
/>
</FieldRow>
</Field>
</>
)}
{type !== 'file' && (
<>
<input
type='hidden'
{...register('messagesCount', {
validate: (messagesCount) => (messagesCount > 0 ? undefined : t('Mail_Message_No_messages_selected_select_all')),
})}
/>
{errors.messagesCount && (
<Field>
<Callout role='alert' type='danger'>
{errors.messagesCount.message}
</Callout>
</Field>
)}
</>
)}
</FieldGroup>
</form>
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button type='reset' disabled={!isDirty || isSubmitting} onClick={() => reset()}>
{t('Reset')}
</Button>
<Button loading={isSubmitting} form={formId} primary type='submit'>
{type === 'download' ? t('Download') : t('Send')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</ContextualbarDialog>
);
};
export default ExportMessages;

View File

@@ -0,0 +1 @@
export { default } from './ExportMessages';

View File

@@ -0,0 +1,69 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { downloadJsonAs } from '../../../../lib/download';
import { Messages } from '../../../../stores';
import { useRoom } from '../../contexts/RoomContext';
export const useDownloadExportMutation = () => {
const { t } = useTranslation();
const room = useRoom();
const user = useUser();
const dispatchToastMessage = useToastMessageDispatch();
const filterMessages = Messages.use((state) => state.filter);
return useMutation({
mutationFn: async ({ mids }: { mids: IMessage['_id'][] }) => {
const messages = filterMessages((record) => mids.includes(record._id) || (!!record.tmid && mids.includes(record.tmid))).map(
({ _id, ts, u, msg, _updatedAt, tlm, replies, tmid, attachments }) => ({
_id,
ts,
u,
msg,
_updatedAt,
tlm,
replies,
tmid,
attachments:
attachments?.map((attachment) => ({
ts: attachment.ts,
title: attachment.title,
title_link: attachment.title_link,
title_link_download: attachment.title_link_download,
...('image_dimensions' in attachment && { image_dimensions: attachment.image_dimensions }),
...('image_preview' in attachment && { image_preview: attachment.image_preview }),
...('image_url' in attachment && { image_url: attachment.image_url }),
...('image_type' in attachment && { image_type: attachment.image_type }),
...('image_size' in attachment && { image_size: attachment.image_size }),
...('type' in attachment && { type: attachment.type }),
description: attachment.description,
})) ?? [],
}),
);
const fileData = {
roomId: room._id,
roomName: room.fname || room.name,
userExport: {
id: user?._id,
username: user?.username,
name: user?.name,
roles: user?.roles,
},
exportDate: new Date().toISOString(),
messages,
};
return downloadJsonAs(fileData, `exportedMessages-${new Date().toISOString()}`);
},
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
onSuccess: () => {
dispatchToastMessage({ type: 'success', message: t('Messages_exported_successfully') });
},
});
};

View File

@@ -0,0 +1,196 @@
import { Document, Font, Image, Page, pdf, StyleSheet, Text, View } from '@react-pdf/renderer';
import type { IMessage, MessageAttachmentDefault } from '@rocket.chat/core-typings';
import { MessageTypes } from '@rocket.chat/message-types';
import { escapeHTML } from '@rocket.chat/tools';
import { useSetting, useToastMessageDispatch, useAbsoluteUrl } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime';
import { Messages } from '../../../../stores';
const leftTab = {
marginLeft: 20,
};
const NOTO_SANS_FONTS: { name: string; fontSrc: string }[] = [
{ name: 'Noto Sans Hebrew', fontSrc: '/fonts/NotoSansHebrew-Regular.ttf' },
{ name: 'Noto Sans', fontSrc: '/fonts/NotoSans-Regular.ttf' },
{ name: 'Noto Sans Arabic', fontSrc: '/fonts/NotoSansArabic-Regular.ttf' },
{ name: 'Noto Sans Devanagari', fontSrc: '/fonts/NotoSansDevanagari-Regular.ttf' },
{ name: 'Noto Sans Bengali', fontSrc: '/fonts/NotoSansBengali-Regular.ttf' },
{ name: 'Noto Sans Tamil', fontSrc: '/fonts/NotoSansTamil-Regular.ttf' },
{ name: 'Noto Sans Sinhala', fontSrc: '/fonts/NotoSansSinhala-Regular.ttf' },
{ name: 'Noto Sans Thai', fontSrc: '/fonts/NotoSansThai-Regular.ttf' },
{ name: 'Noto Sans Lao', fontSrc: '/fonts/NotoSansLao-Regular.ttf' },
{ name: 'Noto Sans Georgian', fontSrc: '/fonts/NotoSansGeorgian-Regular.ttf' },
{ name: 'Noto Sans JP', fontSrc: '/fonts/NotoSansJP-Regular.ttf' },
{ name: 'Noto Sans KR', fontSrc: '/fonts/NotoSansKR-Regular.ttf' },
{ name: 'Noto Sans SC', fontSrc: '/fonts/NotoSansSC-Regular.ttf' },
{ name: 'Noto Sans TC', fontSrc: '/fonts/NotoSansTC-Regular.ttf' },
{ name: 'Noto Sans HK', fontSrc: '/fonts/NotoSansHK-Regular.ttf' },
];
const EMOJI_RENDER_SIZE = 72;
const renderEmojiToDataURL = (codePoints: string): string => {
const points = codePoints.split('-').map((codePoint) => parseInt(codePoint, 16));
if (points.some((point) => !Number.isInteger(point) || point < 0 || point > 0x10ffff)) {
return '';
}
const emoji = String.fromCodePoint(...points);
const canvas = document.createElement('canvas');
canvas.width = EMOJI_RENDER_SIZE;
canvas.height = EMOJI_RENDER_SIZE;
const ctx = canvas.getContext('2d');
if (!ctx) {
return '';
}
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.font = `${Math.floor(EMOJI_RENDER_SIZE * 0.8)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Noto Emoji", sans-serif`;
ctx.fillText(emoji, EMOJI_RENDER_SIZE / 2, EMOJI_RENDER_SIZE / 2);
return canvas.toDataURL('image/png');
};
const pdfStyles = StyleSheet.create({
page: {
fontFamily: NOTO_SANS_FONTS.map((font) => font.name),
},
messageHeader: {
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-end',
gap: 10,
},
username: {
color: '#000',
fontSize: 14,
},
dateTime: {
color: '#aaa',
fontSize: 12,
},
threadMessagesCount: {
color: '#000',
fontSize: 14,
},
threadMessage: {
color: '#555',
fontSize: 12,
...leftTab,
},
message: {
color: '#555',
fontSize: 14,
},
});
export const useExportMessagesAsPDFMutation = () => {
const { t } = useTranslation();
const chatopsUsername = useSetting('Chatops_Username');
const formatDateAndTime = useFormatDateAndTime();
const dispatchToastMessage = useToastMessageDispatch();
const absoluteUrl = useAbsoluteUrl();
useEffect(() => {
for (const font of NOTO_SANS_FONTS) {
Font.register({
family: font.name,
fonts: [{ src: absoluteUrl(font.fontSrc) }],
});
}
// Manually build the emoji data to avoid relying on a cdn
// https://react-pdf.org/fonts#registeremojisource
Font.registerEmojiSource({
withVariationSelectors: true,
builder: renderEmojiToDataURL,
});
}, []);
return useMutation({
mutationFn: async (messageIds: IMessage['_id'][]) => {
const parseMessage = (msg: IMessage) => {
const messageType = MessageTypes.getType(msg);
if (messageType) {
return messageType.text(t, msg);
}
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={{ ...pdfStyles.page, margin: 10 }}>
{messages.map((message) => {
const dateTime = formatDateAndTime(message.ts);
return (
<View key={message._id} style={{ marginBottom: 10 }}>
<View style={pdfStyles.messageHeader}>
<Text style={{ ...(message.tmid ? leftTab : {}), ...pdfStyles.username }}>{message.u.username}</Text>
<Text style={pdfStyles.dateTime}>{dateTime}</Text>
{message.tcount && <Text style={pdfStyles.threadMessagesCount}>{`${message.tcount} ${t('thread_messages')}`}</Text>}
</View>
<Text style={message.tmid ? pdfStyles.threadMessage : pdfStyles.message}>{parseMessage(message)}</Text>
{message.attachments?.map((attachment: MessageAttachmentDefault, index) => (
<View key={index}>
{attachment.description && <Text style={pdfStyles.message}>{attachment.description}</Text>}
{attachment.image_url && <Image src={attachment.title_link} style={attachment.image_dimensions} />}
<Text style={pdfStyles.message}>{attachment.title}</Text>
</View>
))}
</View>
);
})}
</View>
</Page>
</Document>
);
const instance = pdf();
await new Promise<void>((resolve, reject) => {
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);
resolve();
};
try {
instance.on('change', callback);
instance.updateContainer(jsx);
} catch (error) {
reject(error);
} finally {
instance.removeListener('change', callback);
}
});
},
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
onSuccess: () => {
dispatchToastMessage({ type: 'success', message: t('Messages_exported_successfully') });
},
});
};

View File

@@ -0,0 +1,25 @@
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
export const useRoomExportMutation = () => {
const { t } = useTranslation();
const roomsExport = useEndpoint('POST', '/v1/rooms.export');
const dispatchToastMessage = useToastMessageDispatch();
return useMutation({
mutationFn: roomsExport,
onSuccess: () => {
dispatchToastMessage({
type: 'success',
message: t('Your_email_has_been_queued_for_sending'),
});
},
onError: (error) => {
dispatchToastMessage({
type: 'error',
message: error,
});
},
});
};