Mail opens faster: the page draws at once from a no-network status, the list, folders and unread counts load side by side, unread counts are cached and time-limited per account, IMAP keeps connections warmer and limits folder counting, messages are prefetched on hover
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
833342fced
commit
fb7aabff61
4 files changed
+56
-36
No files matched your search
+25
-10
@@ -24,7 +24,7 @@ const mailHasRemote = html => /<img[^>]+src=["']?https?:|url\(\s*["']?https?:/i.
|
||||
// ---- unread badge and Home card
|
||||
async function mailPoll() {
|
||||
if (!(allowed('mail') && cfg.mail)) return;
|
||||
try { const s = await jfetch('/api/mail/status'); mail.st = s; const n = s.connected && s.unread ? s.unread : 0; if (n !== mailUnread) { mailUnread = n; renderNav(); } } catch {}
|
||||
try { const s = await jfetch('/api/mail/status'); mail.st = s; mail.accts = s.accounts || mail.accts; const n = s.connected && s.unread ? s.unread : 0; if (n !== mailUnread) { mailUnread = n; renderNav(); } } catch {}
|
||||
}
|
||||
async function renderMailCard(el) {
|
||||
if (!el) return;
|
||||
@@ -125,23 +125,33 @@ async function mailAdminCard() {
|
||||
async function viewMail(args = []) {
|
||||
const v = $('#view');
|
||||
if (!cfg.mail) { v.innerHTML = '<div class="wrap"><h1>Mail</h1><p class="empty">Mail is not available to you.</p></div>'; return; }
|
||||
v.innerHTML = '<div class="wrap"><h1>Mail</h1><p class="empty">Loading…</p></div>';
|
||||
let st; try { st = await jfetch('/api/mail/status'); } catch (e) { v.innerHTML = `<div class="wrap"><h1>Mail</h1><p class="err">${esc(e.message)}</p></div>`; return; }
|
||||
// The accounts come from the server without any network call to the mail providers ("light"), so the page can draw at once; the folders, the message
|
||||
// list and the real unread counts then load side by side instead of one after another.
|
||||
let st = mail.st;
|
||||
if (!st) { v.innerHTML = '<div class="wrap"><h1>Mail</h1><p class="empty">Loading…</p></div>'; try { st = await jfetch('/api/mail/status?light=1'); } catch (e) { v.innerHTML = `<div class="wrap"><h1>Mail</h1><p class="err">${esc(e.message)}</p></div>`; return; } }
|
||||
if (!location.hash.startsWith('#/mail')) return;
|
||||
mail.st = st; mail.accts = st.accounts || [];
|
||||
if (!st.connected) return mailConnectScreen(st);
|
||||
if (typeof st.unread === 'number' && st.unread !== mailUnread) { mailUnread = st.unread; renderNav(); }
|
||||
if (!st.connected) { mail.st = null; return mailConnectScreen(st); }
|
||||
const [kind, key] = args;
|
||||
const idAcct = x => String(x || '').includes('~') ? String(x).split('~')[0] : '';
|
||||
const want = idAcct(kind === 'f' ? key : kind === 'm' ? key : '') || (mail.accts.some(a => a.id === mail.acct) ? mail.acct : mail.accts[0].id);
|
||||
if (want !== mail.acct) { mail.acct = want; mail.folders = []; mail.items = []; if (!(kind === 'f' && idAcct(key))) mail.folder = want + '~inbox'; }
|
||||
if (kind === 'f' && key) mail.folder = idAcct(key) ? key : mail.acct + '~' + key;
|
||||
else if (!String(mail.folder).startsWith(mail.acct + '~')) mail.folder = mail.acct + '~inbox';
|
||||
if (!mail.folders.length) { try { mail.folders = await jfetch('/api/mail/folders?acct=' + encodeURIComponent(mail.acct)); } catch {} }
|
||||
mailShell(kind === 'm' && key ? 'read' : 'list');
|
||||
if (!mail.items.length || kind === 'f' || kind === undefined) mailLoadList(true);
|
||||
else mailDrawList();
|
||||
const acct = mail.acct;
|
||||
if (!mail.items.length || kind === 'f' || kind === undefined) mailLoadList(true); else mailDrawList();
|
||||
if (kind === 'm' && key) mailOpen(key); else { mail.msg = null; mailDrawReader(); }
|
||||
// folders (with counts) and the real unread numbers arrive when they arrive
|
||||
jfetch('/api/mail/folders?acct=' + encodeURIComponent(acct)).then(f => { if (mail.acct !== acct) return; mail.folders = f; mailRefreshFolders(); }).catch(() => {});
|
||||
mailPoll().then(() => { if (mail.st && location.hash.startsWith('#/mail')) mailRefreshFolders(); });
|
||||
}
|
||||
/** Redraw the folder pickers (names and unread counts) without touching the list or the open message. */
|
||||
function mailRefreshFolders() {
|
||||
const fl = mail.folders; if (!fl.length) return;
|
||||
const f = $('#mfsel'); if (f) f.innerHTML = fl.map(x => `<option value="${esc(x.id)}" ${x.id === mail.folder ? 'selected' : ''}>${esc(x.name)}${x.unread ? ' (' + x.unread + ')' : ''}</option>`).join('');
|
||||
const side = document.querySelector('.mfold'); if (side) { const sel = side.querySelector('select'); side.innerHTML = (sel ? sel.outerHTML : '') + fl.map(x => `<a href="#/mail/f/${encodeURIComponent(x.id)}" class="${x.id === mail.folder ? 'on' : ''}"><span class="grow">${esc(x.name)}</span>${x.unread ? `<i class="nbadge" style="position:static">${x.unread}</i>` : ''}</a>`).join(''); const s2 = side.querySelector('select'); if (s2) s2.onchange = e => { mail.q = ''; location.hash = '#/mail/f/' + encodeURIComponent(e.target.value + '~inbox'); }; }
|
||||
const acc = $('#macct'); if (acc && mail.accts.length) acc.innerHTML = mail.accts.map(a => `<option value="${esc(a.id)}" ${a.id === mail.acct ? 'selected' : ''}>${esc(a.address)}${a.unread ? ' (' + a.unread + ')' : ''}</option>`).join('');
|
||||
}
|
||||
|
||||
function mailConnectScreen(st) {
|
||||
@@ -197,11 +207,16 @@ function mailDrawList() {
|
||||
if ($('#mmb')) $('#mmb').onclick = () => mailLoadList(false);
|
||||
}
|
||||
|
||||
const mailCache = new Map(); // message id -> { t, p: promise }: a message you point at or tap is fetched once and kept for a minute
|
||||
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 });
|
||||
async function mailOpen(id) {
|
||||
const rd = $('#mread'); if (rd) rd.innerHTML = '<p class="empty" style="padding:16px">Opening…</p>';
|
||||
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 jfetch('/api/mail/message?id=' + encodeURIComponent(id));
|
||||
const m = await mailFetch(id);
|
||||
if (!(location.hash.startsWith('#/mail/m/'))) return;
|
||||
mail.msg = m; mailDrawReader(); mailDrawList();
|
||||
if (!m.read) { // opening a message marks it read
|
||||
|
||||
Reference in new issue
Block a user