Mail: folder unread counts refresh at once after a change, read state survives a late list, no wrong message after quick clicks, mark unread keeps the message in the list
build-container / build (push) Successful in 21m39s
build-container / build (push) Successful in 21m39s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
89fd595809
commit
a3010299d0
4 files changed
+29
-9
No files matched your search
+22
-7
@@ -232,7 +232,7 @@ async function mailLoadList(reset) {
|
||||
try {
|
||||
const r = await jfetch(`/api/mail/messages?folder=${encodeURIComponent(folder)}&skip=${showing ? 0 : mail.next}${mail.q ? '&q=' + encodeURIComponent(mail.q) : ''}`);
|
||||
if (!$('#mrows') || mail.folder !== folder) return;
|
||||
mail.items = showing ? r.items : mail.items.concat(r.items); mail.more = r.more; mail.next = r.next;
|
||||
mail.items = showing ? mailApplyMine(r.items) : mail.items.concat(mailApplyMine(r.items)); mail.more = r.more; mail.next = r.next;
|
||||
if (first) mcPut('l:' + folder, { items: r.items, more: r.more, next: r.next });
|
||||
mailDrawList();
|
||||
} catch (e) { if (showing) return; const el = $('#mrows'); if (el) el.innerHTML = `<p class="err" style="padding:12px">${esc(e.message)}</p>`; if (/Connect again/.test(e.message)) { mail.st = null; mcClear(); setTimeout(route, 1200); } }
|
||||
@@ -252,19 +252,34 @@ const mailCache = new Map(); // message id -> { t, p: promise }: a message you p
|
||||
const mailFetch = id => { const c = mailCache.get(id); if (c && Date.now() - c.t < 60e3) return c.p; const p = jfetch('/api/mail/message?id=' + encodeURIComponent(id)); mailCache.set(id, { t: Date.now(), p }); p.catch(() => mailCache.delete(id)); if (mailCache.size > 30) mailCache.delete(mailCache.keys().next().value); return p; };
|
||||
document.addEventListener('pointerover', e => { const a = e.target.closest && e.target.closest('a.mrow'); if (a && a.getAttribute('href').startsWith('#/mail/m/')) mailFetch(decodeURIComponent(a.getAttribute('href').slice(9))).catch(() => {}); }, { passive: true });
|
||||
document.addEventListener('touchstart', e => { const a = e.target.closest && e.target.closest('a.mrow'); if (a && a.getAttribute('href').startsWith('#/mail/m/')) mailFetch(decodeURIComponent(a.getAttribute('href').slice(9))).catch(() => {}); }, { passive: true });
|
||||
// What this device has just done to a message (read, unread), kept for a minute: a list that was already on its way from the server
|
||||
// (it started before the change) must not put the old state back.
|
||||
const mailMine = new Map();
|
||||
const mailInInbox = () => /(^|~)inbox$/.test(mail.folder);
|
||||
function mailSetRead(id, read) {
|
||||
mailMine.set(id, { read, t: Date.now() });
|
||||
const it = mail.items.find(x => x.id === id); if (it) it.read = read;
|
||||
const c = mcGet('l:' + mail.folder); // the saved copy of this folder shows it too the next time Mail opens
|
||||
if (c && c.items) { const x = c.items.find(y => y.id === id); if (x) { x.read = read; mcPut('l:' + mail.folder, { items: c.items, more: c.more, next: c.next }); } }
|
||||
}
|
||||
const mailApplyMine = items => { const now = Date.now(); for (const [k, v] of mailMine) if (now - v.t > 60e3) mailMine.delete(k); for (const m of items) { const o = mailMine.get(m.id); if (o) m.read = o.read; } return items; };
|
||||
|
||||
async function mailOpen(id) {
|
||||
const rd = $('#mread'), it = mail.items.find(x => x.id === id);
|
||||
if (rd) rd.innerHTML = `<div class="mrhead"><a class="btn ghost mback" href="#/mail/f/${encodeURIComponent(mail.folder)}">${icon('back')}${esc(mailFolderName(mail.folder))}</a>${it ? `<h2 class="msubj">${esc(it.subject)}</h2><p class="mfrom"><b>${esc(mailFrom(it.from))}</b> <small>${esc(it.from.address)}</small></p>` : ''}</div><p class="empty" style="padding:16px">Opening…</p>`; // (what the list already knows, at once)
|
||||
mail.images = false;
|
||||
try {
|
||||
const m = await mailFetch(id);
|
||||
if (!(location.hash.startsWith('#/mail/m/'))) return;
|
||||
if (decodeURIComponent((location.hash.match(/^#\/mail\/m\/([^/]+)/) || [])[1] || '') !== id) return; // you moved on to another message while this one loaded
|
||||
mail.msg = m; mailDrawReader(); mailDrawList();
|
||||
if (!m.read) { // opening a message marks it read
|
||||
const inbox = mailInInbox();
|
||||
jfetch('/api/mail/update', { method: 'POST', body: JSON.stringify({ id, read: true }) }).then(() => {
|
||||
const it = mail.items.find(x => x.id === id); if (it) it.read = true; m.read = true; mailDrawList();
|
||||
mailUnread = Math.max(0, mailUnread - 1); renderNav(); jfetch('/api/mail/folders?acct=' + encodeURIComponent(mail.acct)).then(f => { mail.folders = f; }).catch(() => {});
|
||||
}).catch(() => {});
|
||||
m.read = true; mailSetRead(id, true); mailDrawList();
|
||||
if (inbox) { mailUnread = Math.max(0, mailUnread - 1); renderNav(); }
|
||||
mailPoll(); // the real count (the server drops its saved one when a message changes)
|
||||
jfetch('/api/mail/folders?acct=' + encodeURIComponent(mail.acct)).then(f => { mail.folders = f; mailRefreshFolders(); }).catch(() => {});
|
||||
}).catch(e => toast('Could not mark it as read: ' + e.message));
|
||||
}
|
||||
} catch (e) { if ($('#mread')) $('#mread').innerHTML = `<p class="err" style="padding:16px">${esc(e.message)}</p>`; }
|
||||
}
|
||||
@@ -292,11 +307,11 @@ function mailDrawReader() {
|
||||
async function mailAction(a) {
|
||||
const m = mail.msg; if (!m) return;
|
||||
const post = (p, body) => jfetch('/api/mail/' + p, { method: 'POST', body: JSON.stringify({ id: m.id, ...body }) });
|
||||
const back = () => { mail.items = mail.items.filter(x => x.id !== m.id); mail.msg = null; location.hash = '#/mail/f/' + encodeURIComponent(mail.folder); mailPoll(); jfetch('/api/mail/folders?acct=' + encodeURIComponent(mail.acct)).then(f => { mail.folders = f; }).catch(() => {}); };
|
||||
const back = keep => { if (!keep) { mail.items = mail.items.filter(x => x.id !== m.id); const sv = mcGet('l:' + mail.folder); if (sv && sv.items) mcPut('l:' + mail.folder, { items: sv.items.filter(x => x.id !== m.id), more: sv.more, next: sv.next }); } mail.msg = null; location.hash = '#/mail/f/' + encodeURIComponent(mail.folder); mailPoll(); jfetch('/api/mail/folders?acct=' + encodeURIComponent(mail.acct)).then(f => { mail.folders = f; }).catch(() => {}); };
|
||||
try {
|
||||
if (a === 'reply' || a === 'replyAll' || a === 'forward') return mailCompose({ mode: a, msg: m });
|
||||
if (a === 'archive') { await post('move', { to: 'archive' }); toast('Archived'); return back(); }
|
||||
if (a === 'unread') { await post('update', { read: false }); const it = mail.items.find(x => x.id === m.id); if (it) it.read = false; toast('Marked unread'); mailUnread++; renderNav(); return back(); }
|
||||
if (a === 'unread') { await post('update', { read: false }); m.read = false; mailSetRead(m.id, false); toast('Marked unread'); if (mailInInbox()) { mailUnread++; renderNav(); } return back(true); }
|
||||
if (a === 'flag') { await post('update', { flagged: !m.flagged }); m.flagged = !m.flagged; const it = mail.items.find(x => x.id === m.id); if (it) it.flagged = m.flagged; mailDrawReader(); return mailDrawList(); }
|
||||
if (a === 'delete') {
|
||||
if (/(^|~)deleteditems$/.test(mail.folder)) return confirmSheet('Delete for good?', m.subject, 'Delete', async () => { try { await post('delete', { permanent: true }); toast('Deleted'); back(); } catch (e) { toast(e.message); } });
|
||||
|
||||
Reference in new issue
Block a user