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
1 change: 1 addition & 0 deletions src/frontend/public/locales/common/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@
"Please enter a valid email address.": "Please enter a valid email address.",
"Prefix can only contain letters, numbers, dots, underscores and hyphens.": "Prefix can only contain letters, numbers, dots, underscores and hyphens.",
"Prefix is required.": "Prefix is required.",
"Print": "Print",
"Read": "Read",
"Read state": "Read state",
"Recurring": "Recurring",
Expand Down
1 change: 1 addition & 0 deletions src/frontend/public/locales/common/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@
"Please enter a valid email address.": "Veuillez saisir une adresse email valide.",
"Prefix can only contain letters, numbers, dots, underscores and hyphens.": "Le préfixe ne peut contenir que des lettres, chiffres, points, tirets bas et tirets.",
"Prefix is required.": "Le préfixe est requis.",
"Print": "Imprimer",
"Read": "Lu",
"Read state": "État de lecture",
"Recurring": "Récurrent",
Expand Down
1 change: 1 addition & 0 deletions src/frontend/public/locales/common/nl-NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
"Please enter a valid email address.": "Vul een correct email-adres in.",
"Prefix can only contain letters, numbers, dots, underscores and hyphens.": "Voorvoegsel kan alleen letters, cijfers, punten, onderstrepingstekens en koppeltekens bevatten.",
"Prefix is required.": "Voorvoegsel is vereist.",
"Print": "Afdrukken",
"Read": "Lees",
"Read state": "Lees status",
"Redirection": "Omleiding",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DropdownMenu, Icon, IconType } from "@gouvfr-lasuite/ui-kit";
import { getMessagesEmlRetrieveUrl } from "@/features/api/gen/messages/messages";
import { getRequestUrl } from "@/features/api/utils";
import { useMailboxContext } from "@/features/providers/mailbox";
import usePrint from "@/features/message/use-print";
import useRead from "@/features/message/use-read";
import useTrash from "@/features/message/use-trash";
import { ThreadMessageActionsProps } from "./types";
Expand All @@ -25,6 +26,7 @@ const ThreadMessageActions = ({
const { unselectThread, selectedThread, messages } = useMailboxContext();
const { markAsUnread, markAsRead } = useRead();
const { markAsTrashed } = useTrash();
const { print } = usePrint();

const hasSiblingMessages = useMemo(() => {
if (!selectedThread) return false;
Expand Down Expand Up @@ -80,6 +82,11 @@ const ThreadMessageActions = ({
icon: <Icon type={IconType.FILLED} name="mark_email_unread" />,
callback: () => toggleReadStateFrom(true)
}]),
{
label: t('Print'),
icon: <Icon type={IconType.FILLED} name="print" />,
callback: () => print(message)
},
{
label: t('Download raw email'),
icon: <Icon type={IconType.FILLED} name="download" />,
Expand Down
77 changes: 77 additions & 0 deletions src/frontend/src/features/message/use-print.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useCallback } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { useTranslation } from "react-i18next";
import { Message } from "@/features/api/gen/models";

const formatContact = (c: { name?: string | null; email: string }) =>
c.name ? `${c.name} <${c.email}>` : c.email;

/**
* Hook to print a message in a new browser window
*/
const usePrint = () => {
const { t, i18n } = useTranslation();

const print = useCallback((message: Message) => {
const iframe = document
.querySelector(`#thread-message-${message.id} .thread-message__body`) as HTMLIFrameElement | null;
const bodyHtml = iframe?.contentDocument?.body?.innerHTML;
if (!bodyHtml) return;

const date = new Date(message.sent_at ?? message.created_at);
const formattedDate = date.toLocaleString(i18n.resolvedLanguage, {
dateStyle: 'full',
timeStyle: 'short',
});

const headers = [
{ label: t('From'), value: formatContact(message.sender) },
{ label: t('To'), value: message.to.map((r) => formatContact(r.contact)).join(', ') },
...(message.cc.length ? [{ label: t('Cc'), value: message.cc.map((r) => formatContact(r.contact)).join(', ') }] : []),
...(message.bcc.length ? [{ label: t('Bcc'), value: message.bcc.map((r) => formatContact(r.contact)).join(', ') }] : []),
{ label: t('Date'), value: formattedDate },
{ label: t('Subject'), value: message.subject ?? '' },
];

const html = '<!DOCTYPE html>' + renderToStaticMarkup(
<html>
<head>
<meta charSet="utf-8" />
<title>{message.subject ?? ''}</title>
<style>{`
body { font-family: system-ui, -apple-system, sans-serif; margin: 2em; color: #000; }
table { font-size: 14px; border-collapse: collapse; }
hr { border: none; border-top: 1px solid #ccc; margin: 16px 0; }
.body { font-size: 14px; }
.body img { max-width: 100%; }
@media print { body { margin: 0; } }
`}</style>
</head>
<body>
<table>
{headers.map((h, i) => (
<tr key={i}>
<td style={{ fontWeight: 'bold', padding: '2px 12px 2px 0', whiteSpace: 'nowrap', verticalAlign: 'top' }}>{h.label}</td>
<td style={{ padding: '2px 0' }}>{h.value}</td>
</tr>
))}
</table>
<hr />
<div className="body" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
</body>
</html>
);

const printWindow = window.open('');
if (!printWindow) return;

printWindow.document.write(html);
printWindow.document.close();
printWindow.addEventListener('afterprint', () => printWindow.close());
printWindow.onload = () => printWindow.print();
}, [t, i18n.resolvedLanguage]);

return { print };
};

export default usePrint;