function ComposeModal({ open, onClose, onSend, addresses, defaultFrom, busy, token }) { const { t } = useI18n(); const [from, setFrom] = React.useState(defaultFrom || ""); const [to, setTo] = React.useState(""); const [cc, setCc] = React.useState(""); const [bcc, setBcc] = React.useState(""); const [showCc, setShowCc] = React.useState(false); const [subject, setSubject] = React.useState(""); const [text, setText] = React.useState(""); // Plain stays the default: it is what most replies are, and it cannot be // mangled by a converter. const [format, setFormat] = React.useState("text"); const [preview, setPreview] = React.useState(false); const [previewHtml, setPreviewHtml] = React.useState(""); const [files, setFiles] = React.useState([]); const fileInput = React.useRef(null); const [error, setError] = React.useState(""); const [sent, setSent] = React.useState(""); React.useEffect(() => { if (!open) return; setError(""); setSent(""); setFrom((prev) => prev || defaultFrom || (addresses || [])[0] || ""); }, [open, defaultFrom, addresses]); if (!open) return null; const field = "w-full bg-paper border border-paper-line rounded px-3 py-2 text-sm text-ink placeholder:text-ink-dim focus:outline-none focus:border-accent"; const btn = "text-xs px-2.5 py-1 rounded border border-paper-line text-ink-muted hover:border-accent hover:text-accent"; const reset = () => { setTo(""); setCc(""); setBcc(""); setShowCc(false); setSubject(""); setText(""); setPreview(false); setFiles([]); if (fileInput.current) fileInput.current.value = ""; }; const addFiles = (list) => { const chosen = Array.from(list || []); if (!chosen.length) return; setError(""); Promise.all( chosen.map( (f) => new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve({ filename: f.name, content_type: f.type || "application/octet-stream", bytes: f.size, // strip the "data:...;base64," prefix the reader adds content: String(r.result).split(",", 2)[1] || "", }); r.onerror = () => reject(new Error(f.name)); r.readAsDataURL(f); }) ) ) .then((read) => setFiles((prev) => prev.concat(read))) .catch((err) => setError(String(err.message || err))); }; const totalBytes = files.reduce((n, f) => n + (f.bytes || 0), 0); const submit = (e) => { e.preventDefault(); if (busy) return; setError(""); setSent(""); if (!to.trim()) return setError(t("err_recipient")); if (!subject.trim()) return setError(t("err_subject")); if (!text.trim()) return setError(t("err_body")); onSend({ from, to: to.trim(), cc: cc.trim(), bcc: bcc.trim(), subject: subject.trim(), text, format, attachments: files.map((f) => ({ filename: f.filename, content_type: f.content_type, content: f.content, })), }) .then((res) => { setSent(t("sent_to", Array.isArray(res?.to) ? res.to.join(", ") : to.trim())); reset(); }) .catch((err) => setError(String(err.message || err))); }; return (
{ if (e.target === e.currentTarget) onClose(); }} >
{t("new_message")}
{showCc ? (
) : ( )}
{[ ["text", t("fmt_plain")], ["markdown", t("fmt_markdown")], ["html", t("fmt_html")], ].map(([value, name]) => ( ))}
{format !== "text" && ( )}
{preview && format !== "text" ? (
…

" }} /> ) : (