diff --git a/src/frontend/public/locales/common/en-US.json b/src/frontend/public/locales/common/en-US.json index 8e958c67..c75d76eb 100644 --- a/src/frontend/public/locales/common/en-US.json +++ b/src/frontend/public/locales/common/en-US.json @@ -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", diff --git a/src/frontend/public/locales/common/fr-FR.json b/src/frontend/public/locales/common/fr-FR.json index 974d29f1..02c3a927 100644 --- a/src/frontend/public/locales/common/fr-FR.json +++ b/src/frontend/public/locales/common/fr-FR.json @@ -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", diff --git a/src/frontend/public/locales/common/nl-NL.json b/src/frontend/public/locales/common/nl-NL.json index 72f0b667..2a21acdc 100644 --- a/src/frontend/public/locales/common/nl-NL.json +++ b/src/frontend/public/locales/common/nl-NL.json @@ -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", diff --git a/src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-actions.tsx b/src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-actions.tsx index dad6ac61..3424935e 100644 --- a/src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-actions.tsx +++ b/src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-actions.tsx @@ -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"; @@ -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; @@ -80,6 +82,11 @@ const ThreadMessageActions = ({ icon: , callback: () => toggleReadStateFrom(true) }]), + { + label: t('Print'), + icon: , + callback: () => print(message) + }, { label: t('Download raw email'), icon: , diff --git a/src/frontend/src/features/message/use-print.tsx b/src/frontend/src/features/message/use-print.tsx new file mode 100644 index 00000000..0d6c2a6e --- /dev/null +++ b/src/frontend/src/features/message/use-print.tsx @@ -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 = '' + renderToStaticMarkup( + + + + {message.subject ?? ''} + + + + + {headers.map((h, i) => ( + + + + + ))} +
{h.label}{h.value}
+
+
+ + + ); + + 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;