Files
platform/src/apps/officer-web/Screens/Dashboard/Email/EmailReader.tsx
T

162 lines
5.5 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Mail, Reply } from 'lucide-react';
import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'officerdev';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useClient } from 'hooks/useClient';
import { useGlobal } from 'hooks/useGlobal';
import type { EmailMessage } from 'types';
import { useComposer, replyDraft } from './Compose';
type OpenAttachment = {
filePath: string;
fileName: string;
root: string;
};
const HtmlBody = ({ html }: { html: string }) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument;
if (!doc) return;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; margin: 0; padding: 16px; color: #1a1a1a; background: #ffffff; }
a { color: #1a73e8; }
img { max-width: 100%; height: auto; }
</style>
</head>
<body>${html}</body>
</html>
`);
doc.close();
}, [html]);
return <iframe ref={iframeRef} className="h-full w-full border-0" sandbox="allow-same-origin" title="Email body" />;
};
export const EmailReader = () => {
const client = useClient();
const queryClient = useQueryClient();
const [, openCompose] = useComposer();
const [selectedId] = useGlobal<string | null>('EMAIL_SELECTED', null);
const [openAttachment, setOpenAttachment] = useState<OpenAttachment | null>(null);
const { data: message, isLoading } = useQuery({
queryKey: ['email-message', selectedId],
queryFn: () => client.get<EmailMessage>(`/email/messages/${selectedId}`),
enabled: !!selectedId,
});
// Mark as read when message loads
useEffect(() => {
if (!message || message.read) return;
client.patch(`/email/messages/${message.id}/read`).then(() => {
queryClient.invalidateQueries({ queryKey: ['email-messages'] });
}).catch(() => {});
}, [message?.id]);
const extractAttachment = async (index: number) => {
if (!selectedId) return;
const result = await client.post<OpenAttachment>(`/email/messages/${selectedId}/attachments/${index}/extract`);
setOpenAttachment(result);
};
if (!selectedId) {
return (
<div className="flex h-full flex-col items-center justify-center gap-2 text-sm opacity-50">
<Mail className="h-10 w-10" />
Select an email to read
</div>
);
}
if (isLoading) {
return (
<div className="flex h-full items-center justify-center text-sm opacity-50">
Loading...
</div>
);
}
if (!message) return null;
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="flex flex-col gap-1 border-b px-4 py-3">
<div className="flex items-start justify-between gap-3">
<h2 className="text-lg font-semibold">{message.subject}</h2>
<button
onClick={() => openCompose(replyDraft(message))}
className="shrink-0 flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs hover:bg-accent cursor-pointer"
title="Reply"
>
<Reply className="h-3.5 w-3.5" /> Reply
</button>
</div>
<div className="flex flex-col gap-0.5 text-sm opacity-70">
<div>
<span className="font-medium">From:</span> {message.from}
</div>
<div>
<span className="font-medium">To:</span> {message.to}
</div>
{message.cc && (
<div>
<span className="font-medium">Cc:</span> {message.cc}
</div>
)}
<div className="text-xs opacity-60">{new Date(message.date).toLocaleString()}</div>
</div>
{message.attachments.length > 0 && (
<div className="flex flex-wrap gap-1.5 pt-1">
{message.attachments.map((att: { filename: string; size: number; contentType: string }, i: number) => (
<button
key={i}
className="cursor-pointer rounded bg-accent px-2 py-0.5 text-xs hover:bg-accent/80"
onClick={() => extractAttachment(i)}
>
{att.filename} ({Math.round(att.size / 1024)}KB)
</button>
))}
</div>
)}
</div>
<div className="flex-1 overflow-auto">
{message.html ? (
<HtmlBody html={message.html} />
) : (
<pre className="whitespace-pre-wrap p-4 text-sm">{message.text}</pre>
)}
</div>
<Dialog open={!!openAttachment} onOpenChange={(open) => !open && setOpenAttachment(null)}>
<DialogContent className="flex h-[80vh] max-w-4xl flex-col gap-0 p-0">
<DialogTitle className="sr-only">{openAttachment?.fileName}</DialogTitle>
{openAttachment && (
<FileViewerProvider
filePath={openAttachment.filePath}
fileName={openAttachment.fileName}
root={openAttachment.root}
>
<div className="flex shrink-0 items-center gap-2 border-b px-4 pr-12 py-1.5">
<FileViewerHeader />
</div>
<div className="min-h-0 flex-1">
<FileViewerBody />
</div>
</FileViewerProvider>
)}
</DialogContent>
</Dialog>
</div>
);
};