Files
gowebmail/web/templates/compose.html
T

449 lines
23 KiB
HTML
Raw Normal View History

2026-03-22 11:28:33 +00:00
{{template "base" .}}
{{define "title"}}Compose — GoWebMail{{end}}
2026-08-30 08:17:03 +01:00
{{define "body_class"}}{{end}}
2026-03-22 11:28:33 +00:00
{{define "body"}}
2026-08-30 08:17:03 +01:00
<div id="compose-page" style="width:100%;box-sizing:border-box;margin:0 auto;padding:20px 32px;min-height:100vh">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid var(--border);flex-wrap:wrap">
<a href="/" id="cp-back-link" style="color:var(--accent);text-decoration:none;font-size:13px;display:flex;align-items:center;gap:4px">
2026-03-22 11:28:33 +00:00
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
Back to GoWebMail
</a>
<span style="color:var(--border);font-size:16px">|</span>
<span id="compose-page-title" style="font-size:14px;color:var(--text2)">New Message</span>
2026-08-30 08:17:03 +01:00
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<button class="btn-secondary" id="discard-draft-btn" style="display:none;font-size:12px;color:var(--danger)" onclick="discardDraftAndReset()">Discard draft</button>
<button type="button" id="cc-toggle" class="btn-secondary" style="font-size:12px" onclick="cpShowCC()">+CC</button>
<button type="button" id="bcc-toggle" class="btn-secondary" style="font-size:12px" onclick="cpShowBCC()">+BCC</button>
<button class="btn-secondary" style="font-size:12px" onclick="triggerAttach()">📎 Attach</button>
<button class="btn-secondary" id="save-draft-btn" onclick="saveDraft()" style="font-size:12px">💾 Save Draft</button>
<button class="btn-secondary" id="sendlater-btn" style="font-size:12px" onclick="toggleSendLaterPanel()">🕐 Send later</button>
2026-03-22 11:28:33 +00:00
<button class="modal-submit" id="send-page-btn" onclick="sendFromPage()" style="font-size:13px;padding:7px 18px">Send</button>
2026-08-30 08:17:03 +01:00
<input type="file" id="cp-file-input" multiple style="display:none" onchange="addPageAttachments(this.files)">
2026-03-22 11:28:33 +00:00
</div>
</div>
2026-08-30 08:17:03 +01:00
<div id="cp-leave-confirm" class="remote-content-banner" style="display:none;margin-bottom:14px">
You have a draft in progress.
<button class="rcb-btn" id="cp-leave-keep">Keep editing</button>
<button class="rcb-btn" id="cp-leave-save">Save &amp; leave</button>
<button class="rcb-btn" id="cp-leave-discard">Discard &amp; leave</button>
</div>
<div id="cp-sendlater-panel" class="remote-content-banner" style="display:none;margin-bottom:14px">
Send at:
<input type="datetime-local" id="cp-sendlater-input" style="background:var(--surface3);border:1px solid var(--border2);border-radius:6px;color:var(--text);padding:4px 8px;font-size:13px">
<button class="rcb-btn" onclick="confirmSendLater()">Schedule</button>
<button class="rcb-btn" onclick="document.getElementById('cp-sendlater-panel').style.display='none'">Cancel</button>
</div>
2026-03-22 11:28:33 +00:00
<div id="compose-page-form">
<!-- From -->
<div style="display:flex;align-items:center;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
2026-08-30 08:17:03 +01:00
<label for="cp-from" style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0">From</label>
2026-03-22 11:28:33 +00:00
<select id="cp-from" style="flex:1;background:transparent;border:none;color:var(--text);font-size:13px;outline:none;cursor:pointer"></select>
</div>
<!-- To -->
<div style="display:flex;align-items:flex-start;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
2026-08-30 08:17:03 +01:00
<span id="cp-to-label" style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0;padding-top:6px">To</span>
<div id="cp-to-tags" class="tag-container" role="group" aria-labelledby="cp-to-label" style="flex:1;min-height:30px"></div>
2026-03-22 11:28:33 +00:00
</div>
<!-- CC -->
2026-08-30 08:17:03 +01:00
<div id="cc-row" style="display:none;align-items:flex-start;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
<span id="cp-cc-label" style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0;padding-top:6px">CC</span>
<div id="cp-cc-tags" class="tag-container" role="group" aria-labelledby="cp-cc-label" style="flex:1;min-height:30px"></div>
</div>
<!-- BCC -->
<div id="bcc-row" style="display:none;align-items:flex-start;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
<span id="cp-bcc-label" style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0;padding-top:6px">BCC</span>
<div id="cp-bcc-tags" class="tag-container" role="group" aria-labelledby="cp-bcc-label" style="flex:1;min-height:30px"></div>
2026-03-22 11:28:33 +00:00
</div>
<!-- Subject -->
<div style="display:flex;align-items:center;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
2026-08-30 08:17:03 +01:00
<label for="cp-subject" style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0">Subject</label>
<input id="cp-subject" type="text" placeholder="Subject" oninput="markDirty()" style="flex:1;background:transparent;border:none;color:var(--text);font-size:14px;outline:none;font-family:'DM Sans',sans-serif">
</div>
<!-- Formatting toolbar -->
<div class="compose-toolbar" role="toolbar" aria-label="Formatting">
<button class="fmt-btn" title="Bold" aria-label="Bold" onclick="execFmt('bold')"><b>B</b></button>
<button class="fmt-btn" title="Italic" aria-label="Italic" onclick="execFmt('italic')"><i>I</i></button>
<button class="fmt-btn" title="Underline" aria-label="Underline" onclick="execFmt('underline')"><u>U</u></button>
<span class="fmt-sep"></span>
<button class="fmt-btn" title="Bullets" aria-label="Bulleted list" onclick="execFmt('insertUnorderedList')">&#8226;&#8212;</button>
<button class="fmt-btn" title="Numbers" aria-label="Numbered list" onclick="execFmt('insertOrderedList')">1&#8212;</button>
<span class="fmt-sep"></span>
<button class="fmt-btn" title="Clear format" aria-label="Clear formatting" onclick="execFmt('removeFormat')">T&#x20D7;</button>
2026-03-22 11:28:33 +00:00
</div>
<!-- Body -->
2026-08-30 08:17:03 +01:00
<div id="cp-editor" contenteditable="true" role="textbox" aria-multiline="true" aria-label="Message body" oninput="markDirty()" style="min-height:400px;padding:16px 0;outline:none;font-size:14px;line-height:1.6;color:var(--text)" data-placeholder="Write your message…"></div>
<!-- Attachments (added via the "Attach" button in the header) -->
<div id="cp-att-list" style="border-top:1px solid var(--border);padding:10px 0;display:flex;flex-wrap:wrap;gap:6px"></div>
2026-03-22 11:28:33 +00:00
</div>
2026-08-30 08:17:03 +01:00
<div id="cp-status" role="status" style="font-size:13px;color:var(--muted);margin-top:8px"></div>
2026-03-22 11:28:33 +00:00
</div>
{{end}}
{{define "scripts"}}
<script>
// Parse URL params
const params = new URLSearchParams(location.search);
const replyId = parseInt(params.get('reply_id') || '0');
const forwardId = parseInt(params.get('forward_id') || '0');
2026-08-30 08:17:03 +01:00
const editDraftId = parseInt(params.get('edit_draft_id') || '0');
2026-03-22 11:28:33 +00:00
const cpAttachments = [];
2026-08-30 08:17:03 +01:00
let draftId = '', dirty = false, draftTimer = null;
let remoteWhitelist = new Set(), remoteImagePolicy = 'manual', contactsCache = null;
2026-03-22 11:28:33 +00:00
async function apiCall(method, path, body) {
const opts = { method, headers: {} };
if (body instanceof FormData) { opts.body = body; }
else if (body) { opts.body = JSON.stringify(body); opts.headers['Content-Type'] = 'application/json'; }
const r = await fetch('/api' + path, opts);
return r.ok ? r.json() : null;
}
2026-08-30 08:17:03 +01:00
function esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function markDirty() { dirty = true; }
function setStatus(msg, isError) {
const el = document.getElementById('cp-status');
el.textContent = msg;
el.style.color = isError ? 'var(--danger)' : 'var(--muted)';
}
2026-03-22 11:28:33 +00:00
2026-08-30 08:17:03 +01:00
// ── Remote-image policy — same rules as the reading pane (app.js / message.html) ──
function stripUnresolvedCID(h){ return h.replace(/src\s*=\s*(['"])cid:[^'"]*\1/gi,'src=""').replace(/src\s*=\s*cid:\S+/gi,'src=""'); }
function stripEmbeddedFrames(h){ return h.replace(/<iframe[\s\S]*?<\/iframe>/gi,'').replace(/<iframe[^>]*>/gi,''); }
function stripRemoteImages(h){
return h.replace(/<img(\s[^>]*?)src\s*=\s*(['"])(https?:\/\/[^'"]+)\2/gi,'<img$1src="" data-blocked-src="$3"')
.replace(/url\s*\(\s*(['"]?)https?:\/\/[^)'"]+\1\s*\)/gi,'url()')
.replace(/<link[^>]*>/gi,'').replace(/<script[\s\S]*?<\/script>/gi,'');
}
function isContactEmail(fromEmail) {
if (!fromEmail || !contactsCache) return false;
const e = fromEmail.toLowerCase();
return contactsCache.some(c => (c.email||'').toLowerCase() === e);
}
function isRemoteContentAllowed(fromEmail) {
if (remoteImagePolicy === 'always') return true;
if (remoteImagePolicy === 'never') return false;
if (remoteImagePolicy === 'contacts') return isContactEmail(fromEmail) || remoteWhitelist.has(fromEmail);
return remoteWhitelist.has(fromEmail); // manual (default)
}
function quotedBodyHTML(msg) {
if (!msg.body_html) return '<pre>'+esc(msg.body_text||'')+'</pre>';
let html = stripUnresolvedCID(stripEmbeddedFrames(msg.body_html));
if (!isRemoteContentAllowed(msg.from_email)) html = stripRemoteImages(html);
return html;
}
function restoreBlockedImages(html) {
return html.replace(/src=""\s+data-blocked-src="([^"]*)"/gi, 'src="$1"');
}
// ── Tag fields (To/Cc/Bcc) — same visual style as the main compose modal ──
2026-03-22 11:28:33 +00:00
function initTagField(id) {
const el = document.getElementById(id);
if (!el) return;
el.innerHTML = '<input class="tag-input" type="email" multiple style="border:none;background:transparent;outline:none;color:var(--text);font-size:13px;min-width:180px;font-family:\'DM Sans\',sans-serif">';
const inp = el.querySelector('input');
inp.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') {
e.preventDefault();
const v = inp.value.trim().replace(/,$/, '');
if (v) addTagTo(id, v);
inp.value = '';
} else if (e.key === 'Backspace' && !inp.value) {
2026-08-30 08:17:03 +01:00
const tags = el.querySelectorAll('.email-tag');
if (tags.length) { tags[tags.length-1].remove(); markDirty(); }
2026-03-22 11:28:33 +00:00
}
});
inp.addEventListener('blur', () => {
const v = inp.value.trim().replace(/,$/, '');
if (v) { addTagTo(id, v); inp.value = ''; }
});
}
function addTagTo(fieldId, email) {
2026-08-30 08:17:03 +01:00
if (!email) return;
2026-03-22 11:28:33 +00:00
const el = document.getElementById(fieldId);
const inp = el.querySelector('input');
2026-08-30 08:17:03 +01:00
const tag = document.createElement('span');
tag.className = 'email-tag';
const label = document.createElement('span');
label.textContent = email;
const remove = document.createElement('button');
remove.innerHTML = '×'; remove.className = 'tag-remove'; remove.type = 'button';
remove.onclick = e => { e.stopPropagation(); tag.remove(); markDirty(); };
tag.appendChild(label); tag.appendChild(remove);
el.insertBefore(tag, inp || null);
markDirty();
2026-03-22 11:28:33 +00:00
}
function getTagValues(fieldId) {
const el = document.getElementById(fieldId);
2026-08-30 08:17:03 +01:00
return Array.from(el.querySelectorAll('.email-tag')).map(c => c.firstChild.textContent.trim()).filter(Boolean);
2026-03-22 11:28:33 +00:00
}
2026-08-30 08:17:03 +01:00
function cpShowCC() { document.getElementById('cc-row').style.display = 'flex'; document.getElementById('cc-toggle').style.display = 'none'; }
function cpShowBCC() { document.getElementById('bcc-row').style.display = 'flex'; document.getElementById('bcc-toggle').style.display = 'none'; }
function execFmt(cmd, val) { document.getElementById('cp-editor').focus(); document.execCommand(cmd, false, val || null); }
function triggerAttach() { document.getElementById('cp-file-input').click(); }
2026-03-22 11:28:33 +00:00
function addPageAttachments(files) {
for (const f of files) {
cpAttachments.push(f);
const chip = document.createElement('span');
chip.style.cssText = 'font-size:11px;padding:3px 8px;background:var(--surface3);border:1px solid var(--border2);border-radius:4px;color:var(--text2)';
chip.textContent = f.name;
document.getElementById('cp-att-list').appendChild(chip);
}
2026-08-30 08:17:03 +01:00
markDirty();
2026-03-22 11:28:33 +00:00
}
async function loadAccounts() {
const accounts = await apiCall('GET', '/accounts') || [];
const sel = document.getElementById('cp-from');
accounts.forEach(a => {
const opt = document.createElement('option');
opt.value = a.id;
opt.textContent = `${a.display_name || a.email_address} <${a.email_address}>`;
sel.appendChild(opt);
});
}
2026-08-30 08:17:03 +01:00
async function loadRemoteImagePrefs() {
const [uiPrefs, wl] = await Promise.all([apiCall('GET', '/ui-prefs'), apiCall('GET', '/remote-content-whitelist')]);
remoteImagePolicy = uiPrefs?.remoteImagePolicy || 'manual';
if (wl?.whitelist) remoteWhitelist = new Set(wl.whitelist);
if (remoteImagePolicy === 'contacts') contactsCache = await apiCall('GET', '/contacts') || [];
}
2026-03-22 11:28:33 +00:00
async function prefillReply() {
if (!replyId) return;
document.getElementById('compose-page-title').textContent = 'Reply';
const msg = await apiCall('GET', '/messages/' + replyId);
if (!msg) return;
document.title = 'Reply: ' + (msg.subject || '') + ' — GoWebMail';
document.getElementById('cp-subject').value = msg.subject?.startsWith('Re:') ? msg.subject : 'Re: ' + (msg.subject || '');
addTagTo('cp-to-tags', msg.from_email || '');
const editor = document.getElementById('cp-editor');
editor.innerHTML = `<br><br><div style="border-left:3px solid #ccc;padding-left:12px;color:#666;margin-top:8px">
<div style="font-size:12px;margin-bottom:4px">On ${msg.date ? new Date(msg.date).toLocaleString() : ''}, ${esc(msg.from_email)} wrote:</div>
2026-08-30 08:17:03 +01:00
<div style="max-width:700px;overflow-x:auto">${quotedBodyHTML(msg)}</div>
2026-03-22 11:28:33 +00:00
</div>`;
// Set from to same account
if (msg.account_id) {
const sel = document.getElementById('cp-from');
for (const opt of sel.options) { if (parseInt(opt.value) === msg.account_id) { opt.selected = true; break; } }
}
2026-08-30 08:17:03 +01:00
dirty = false;
2026-03-22 11:28:33 +00:00
}
async function prefillForward() {
if (!forwardId) return;
document.getElementById('compose-page-title').textContent = 'Forward';
const msg = await apiCall('GET', '/messages/' + forwardId);
if (!msg) return;
document.title = 'Forward: ' + (msg.subject || '') + ' — GoWebMail';
document.getElementById('cp-subject').value = 'Fwd: ' + (msg.subject || '');
const editor = document.getElementById('cp-editor');
editor.innerHTML = `<br><br><div style="border-left:3px solid #ccc;padding-left:12px;color:#666;margin-top:8px">
<div style="font-size:12px;margin-bottom:4px">---------- Forwarded message ----------<br>From: ${esc(msg.from_email)}<br>Subject: ${esc(msg.subject)}</div>
2026-08-30 08:17:03 +01:00
<div style="max-width:700px;overflow-x:auto">${quotedBodyHTML(msg)}</div>
2026-03-22 11:28:33 +00:00
</div>`;
2026-08-30 08:17:03 +01:00
if (msg.account_id) {
const sel = document.getElementById('cp-from');
for (const opt of sel.options) { if (parseInt(opt.value) === msg.account_id) { opt.selected = true; break; } }
}
dirty = false;
}
// Resuming a saved draft: unlike reply/forward, fields are populated directly (no quoting
// wrapper) and draftId is seeded from the draft's own id so the next autosave/send/discard
// replaces this exact draft in place instead of creating a second copy.
async function prefillEditDraft() {
if (!editDraftId) return;
document.getElementById('compose-page-title').textContent = 'Edit Draft';
const msg = await apiCall('GET', '/messages/' + editDraftId);
if (!msg) return;
document.title = 'Edit Draft — GoWebMail';
document.getElementById('cp-subject').value = msg.subject || '';
(msg.to || '').split(',').map(s => s.trim()).filter(Boolean).forEach(a => addTagTo('cp-to-tags', a));
const ccList = (msg.cc || '').split(',').map(s => s.trim()).filter(Boolean);
if (ccList.length) { cpShowCC(); ccList.forEach(a => addTagTo('cp-cc-tags', a)); }
const bccList = (msg.bcc || '').split(',').map(s => s.trim()).filter(Boolean);
if (bccList.length) { cpShowBCC(); bccList.forEach(a => addTagTo('cp-bcc-tags', a)); }
document.getElementById('cp-editor').innerHTML = quotedBodyHTML(msg);
if (msg.account_id) {
const sel = document.getElementById('cp-from');
for (const opt of sel.options) { if (parseInt(opt.value) === msg.account_id) { opt.selected = true; break; } }
}
draftId = msg.remote_uid || '';
updateDraftUI();
dirty = false;
2026-03-22 11:28:33 +00:00
}
async function sendFromPage() {
const btn = document.getElementById('send-page-btn');
const accountId = parseInt(document.getElementById('cp-from').value || '0');
const to = getTagValues('cp-to-tags');
2026-08-30 08:17:03 +01:00
if (!accountId || !to.length) { setStatus('From account and To address required.', true); return; }
2026-03-22 11:28:33 +00:00
btn.disabled = true; btn.textContent = 'Sending…';
const meta = {
account_id: accountId,
to,
cc: getTagValues('cp-cc-tags'),
2026-08-30 08:17:03 +01:00
bcc: getTagValues('cp-bcc-tags'),
2026-03-22 11:28:33 +00:00
subject: document.getElementById('cp-subject').value,
2026-08-30 08:17:03 +01:00
body_html: restoreBlockedImages(document.getElementById('cp-editor').innerHTML.trim()),
2026-03-22 11:28:33 +00:00
body_text: document.getElementById('cp-editor').innerText,
in_reply_to_id: replyId || 0,
};
let r;
const endpoint = replyId ? '/reply' : forwardId ? '/forward' : '/send';
if (cpAttachments.length) {
const fd = new FormData();
fd.append('meta', JSON.stringify(meta));
cpAttachments.forEach(f => fd.append('file', f, f.name));
const resp = await fetch('/api' + endpoint, { method: 'POST', body: fd });
r = await resp.json().catch(() => null);
} else {
r = await apiCall('POST', endpoint, meta);
}
btn.disabled = false; btn.textContent = 'Send';
if (r?.ok) {
2026-08-30 08:17:03 +01:00
stopAutosave();
dirty = false;
await discardDraftReq(); // the autosaved Drafts-folder copy is now redundant — it's been sent
setStatus('✓ Message sent!');
2026-03-22 11:28:33 +00:00
document.getElementById('compose-page-form').style.opacity = '0.5';
document.getElementById('compose-page-form').style.pointerEvents = 'none';
2026-08-30 08:17:03 +01:00
document.getElementById('cp-back-link').innerHTML = '← Back to inbox';
2026-03-22 11:28:33 +00:00
} else {
2026-08-30 08:17:03 +01:00
setStatus(r?.error || 'Send failed.', true);
2026-03-22 11:28:33 +00:00
}
}
2026-08-30 08:17:03 +01:00
// ── Send later ───────────────────────────────────────────────────────────────
function toLocalInput(d) {
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function toggleSendLaterPanel() {
const accountId = parseInt(document.getElementById('cp-from').value || '0');
const to = getTagValues('cp-to-tags');
if (!accountId || !to.length) { setStatus('From account and To address required.', true); return; }
if (cpAttachments.length) { setStatus("Send later doesn't support file attachments yet — forwarded messages are fine", true); return; }
const panel = document.getElementById('cp-sendlater-panel');
const isOpen = panel.style.display !== 'none';
panel.style.display = isOpen ? 'none' : 'flex';
if (!isOpen) {
const input = document.getElementById('cp-sendlater-input');
input.min = toLocalInput(new Date(Date.now() + 60000));
input.value = toLocalInput(new Date(Date.now() + 3600000));
}
}
async function confirmSendLater() {
const input = document.getElementById('cp-sendlater-input');
const d = new Date(input.value);
if (!input.value || isNaN(d.getTime()) || d <= new Date()) { setStatus('Pick a time in the future.', true); return; }
const accountId = parseInt(document.getElementById('cp-from').value || '0');
const to = getTagValues('cp-to-tags');
if (!accountId || !to.length) { setStatus('From account and To address required.', true); return; }
const meta = {
account_id: accountId, to,
cc: getTagValues('cp-cc-tags'), bcc: getTagValues('cp-bcc-tags'),
subject: document.getElementById('cp-subject').value,
body_html: restoreBlockedImages(document.getElementById('cp-editor').innerHTML.trim()),
body_text: document.getElementById('cp-editor').innerText,
send_at: d.toISOString(),
};
const r = await apiCall('POST', '/send-later', meta);
if (r?.ok) {
stopAutosave();
dirty = false;
await discardDraftReq();
setStatus('✓ Message scheduled!');
document.getElementById('cp-sendlater-panel').style.display = 'none';
document.getElementById('compose-page-form').style.opacity = '0.5';
document.getElementById('compose-page-form').style.pointerEvents = 'none';
document.getElementById('cp-back-link').innerHTML = '← Back to inbox';
} else {
setStatus(r?.error || 'Failed to schedule.', true);
}
2026-03-22 11:28:33 +00:00
}
2026-08-30 08:17:03 +01:00
// ── Draft autosave ──────────────────────────────────────────────────────────
function startAutosave() { stopAutosave(); draftTimer = setInterval(() => { if (dirty) saveDraft(true); }, 60000); }
function stopAutosave() { if (draftTimer) { clearInterval(draftTimer); draftTimer = null; } }
function updateDraftUI() {
document.getElementById('discard-draft-btn').style.display = draftId ? 'inline-block' : 'none';
}
async function saveDraft(silent) {
dirty = false;
const accountId = parseInt(document.getElementById('cp-from')?.value || 0);
if (!accountId) { if (!silent) setStatus('Add a From account first.', true); return; }
const editor = document.getElementById('cp-editor');
const meta = {
account_id: accountId,
to: getTagValues('cp-to-tags'),
cc: getTagValues('cp-cc-tags'),
bcc: getTagValues('cp-bcc-tags'),
subject: document.getElementById('cp-subject').value,
body_html: restoreBlockedImages(editor.innerHTML.trim()),
body_text: editor.innerText.trim(),
draft_id: draftId,
};
const r = await apiCall('POST', '/draft', meta);
if (r?.ok) { draftId = r.draft_id || draftId; updateDraftUI(); setStatus(silent ? 'Draft auto-saved' : 'Draft saved'); }
else if (!silent) setStatus(r?.error || 'Draft save failed', true);
}
// Deletes the draft that autosave already wrote to the server for this compose session.
async function discardDraftReq() {
if (!draftId) return;
const accountId = parseInt(document.getElementById('cp-from')?.value || 0);
if (!accountId) return;
await apiCall('POST', '/draft/discard', { account_id: accountId, draft_id: draftId });
draftId = ''; updateDraftUI();
}
async function discardDraftAndReset() {
await discardDraftReq();
setStatus('Draft discarded');
}
// ── Leaving the page with unsent work ───────────────────────────────────────
document.getElementById('cp-back-link').addEventListener('click', e => {
if (dirty || draftId) {
e.preventDefault();
document.getElementById('cp-leave-confirm').style.display = 'flex';
}
});
document.getElementById('cp-leave-keep').onclick = () => { document.getElementById('cp-leave-confirm').style.display = 'none'; };
document.getElementById('cp-leave-discard').onclick = async () => { await discardDraftReq(); location.href = '/'; };
document.getElementById('cp-leave-save').onclick = async () => { await saveDraft(true); location.href = '/'; };
window.addEventListener('beforeunload', e => {
if (dirty || draftId) { e.preventDefault(); e.returnValue = ''; }
});
2026-03-22 11:28:33 +00:00
// Init
2026-08-30 08:17:03 +01:00
async function boot() {
initTagField('cp-to-tags');
initTagField('cp-cc-tags');
initTagField('cp-bcc-tags');
await Promise.all([loadAccounts(), loadRemoteImagePrefs()]);
if (replyId) await prefillReply();
else if (forwardId) await prefillForward();
else if (editDraftId) await prefillEditDraft();
startAutosave();
}
boot();
2026-03-22 11:28:33 +00:00
</script>
{{end}}