148 lines
6.3 KiB
TypeScript
148 lines
6.3 KiB
TypeScript
"use client";
|
||
import type { MailMessage } from "@/app/dashboard/mail/page";
|
||
import { formatBytes } from "@/lib/format";
|
||
|
||
function getFileIcon(contentType: string, filename: string): string {
|
||
if (contentType.startsWith("image/")) return "🖼️";
|
||
if (contentType === "application/pdf") return "📄";
|
||
if (contentType.includes("zip") || contentType.includes("rar") || contentType.includes("tar")) return "📦";
|
||
if (contentType.includes("word") || filename.endsWith(".doc") || filename.endsWith(".docx")) return "📝";
|
||
if (contentType.includes("sheet") || contentType.includes("excel") || filename.endsWith(".xls")) return "📊";
|
||
if (contentType.includes("presentation") || filename.endsWith(".ppt")) return "📑";
|
||
if (contentType.startsWith("video/")) return "🎬";
|
||
if (contentType.startsWith("audio/")) return "🎵";
|
||
return "📎";
|
||
}
|
||
|
||
function canPreview(contentType: string): boolean {
|
||
return contentType.startsWith("image/") || contentType === "application/pdf";
|
||
}
|
||
|
||
export default function MessageView({ message, onReply, onDelete, folder }: {
|
||
message: MailMessage;
|
||
onReply: () => void;
|
||
onDelete: () => void;
|
||
folder: string;
|
||
}) {
|
||
const from = message.from[0];
|
||
const date = new Date(message.date).toLocaleString("tr-TR", {
|
||
day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit",
|
||
});
|
||
|
||
const downloadUrl = (filename: string) =>
|
||
`/api/mail/messages/${message.uid}/attachments?folder=${encodeURIComponent(folder)}&filename=${encodeURIComponent(filename)}`;
|
||
|
||
const handleAttachment = (att: { filename: string; contentType: string }, preview: boolean) => {
|
||
const url = downloadUrl(att.filename);
|
||
if (preview && canPreview(att.contentType)) {
|
||
window.open(url, "_blank");
|
||
} else {
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = att.filename;
|
||
a.click();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="message-view">
|
||
{/* Header */}
|
||
<div className="message-view-header">
|
||
<h2 className="message-view-subject">{message.subject}</h2>
|
||
<div className="message-view-actions">
|
||
<button className="btn btn-ghost btn-sm" onClick={onReply} title="Yanıtla">
|
||
<ReplyIcon /> Yanıtla
|
||
</button>
|
||
<button className="btn btn-danger btn-sm" onClick={onDelete} title="Sil">
|
||
<TrashIcon /> Sil
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Meta */}
|
||
<div className="message-view-meta">
|
||
<div className="message-view-avatar">{(from?.name || from?.address)?.[0]?.toUpperCase() ?? "?"}</div>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ fontWeight: 600, fontSize: 13 }}>{from?.name || from?.address}</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-muted)" }}>
|
||
{from?.address}
|
||
{message.to.length > 0 && <> → {message.to.map((t) => t.address).join(", ")}</>}
|
||
</div>
|
||
</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-muted)", flexShrink: 0 }}>{date}</div>
|
||
</div>
|
||
|
||
{/* Attachments */}
|
||
{message.attachments.length > 0 && (
|
||
<div className="message-attachments">
|
||
<div style={{ width: "100%", fontSize: 12, fontWeight: 600, color: "var(--text-secondary)", marginBottom: 6 }}>
|
||
📎 {message.attachments.length} ek
|
||
</div>
|
||
{message.attachments.map((att, i) => (
|
||
<div key={i} className="attachment-chip" onClick={() => handleAttachment(att, false)}>
|
||
<span>{getFileIcon(att.contentType, att.filename)}</span>
|
||
<span style={{ maxWidth: 160, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||
{att.filename}
|
||
</span>
|
||
<span style={{ color: "var(--text-muted)", fontSize: 11 }}>{formatBytes(att.size)}</span>
|
||
<span className="attachment-actions">
|
||
<button
|
||
className="att-btn"
|
||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, false); }}
|
||
title="İndir"
|
||
>
|
||
⬇
|
||
</button>
|
||
{canPreview(att.contentType) && (
|
||
<button
|
||
className="att-btn"
|
||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, true); }}
|
||
title="Önizle"
|
||
>
|
||
👁
|
||
</button>
|
||
)}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Body */}
|
||
<div className="message-view-body">
|
||
{message.html ? (
|
||
<iframe
|
||
srcDoc={sanitizeHtml(message.html)}
|
||
sandbox="allow-same-origin"
|
||
style={{ width: "100%", border: "none", minHeight: 400 }}
|
||
onLoad={(e) => {
|
||
const iframe = e.target as HTMLIFrameElement;
|
||
if (iframe.contentDocument) {
|
||
iframe.style.height = iframe.contentDocument.body.scrollHeight + 20 + "px";
|
||
}
|
||
}}
|
||
/>
|
||
) : (
|
||
<pre style={{ whiteSpace: "pre-wrap", fontFamily: "inherit", fontSize: 13, color: "var(--text-secondary)" }}>
|
||
{message.text}
|
||
</pre>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function sanitizeHtml(html: string): string {
|
||
const style = `<style>
|
||
body { font-family: Inter, -apple-system, sans-serif; font-size: 14px; color: #c9d1d9; background: transparent; margin: 0; padding: 8px; line-height: 1.6; }
|
||
a { color: #58a6ff; }
|
||
img { max-width: 100%; height: auto; }
|
||
</style>`;
|
||
let clean = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
||
clean = clean.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, "");
|
||
return `<!DOCTYPE html><html><head>${style}</head><body>${clean}</body></html>`;
|
||
}
|
||
|
||
function ReplyIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>; }
|
||
function TrashIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>; }
|