From e984f11a949858f61bb21380422f8028468e42fd Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:31:58 -0400 Subject: [PATCH] Mail: a button per account with its own unread count (kept fresh), instead of a dropdown Co-Authored-By: Claude Sonnet 5.5 --- public/mail.js | 17 ++++++++++------- public/style.css | 11 +++++++++++ public/sw.js | 2 +- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/public/mail.js b/public/mail.js index e704150..8d5d332 100644 --- a/public/mail.js +++ b/public/mail.js @@ -38,7 +38,7 @@ const mailHasRemote = html => /]+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; mail.accts = s.accounts || mail.accts; 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(); } if (location.hash.startsWith('#/mail') && typeof mailDrawChips === 'function') mailDrawChips(); } catch {} } async function renderMailCard(el) { if (!el) return; @@ -180,12 +180,16 @@ async function viewMail(args = []) { jfetch('/api/mail/folders?acct=' + encodeURIComponent(acct)).then(f => { if (mail.acct !== acct) return; mail.folders = f; mcPut('f:' + acct, { folders: f }); mailRefreshFolders(); }).catch(() => {}); mailPoll().then(() => { if (mail.st && location.hash.startsWith('#/mail')) mailRefreshFolders(); }); } +/** One button per account with its own unread count, so you can see which one has new mail without switching. */ +const mailChips = () => `
${mail.accts.map(a => ``).join('')}
`; +function mailBindChips() { document.querySelectorAll('.maccts button').forEach(b => b.onclick = () => { if (b.dataset.a === mail.acct) return; mail.q = ''; location.hash = '#/mail/f/' + encodeURIComponent(b.dataset.a + '~inbox'); }); } +function mailDrawChips() { if (mail.accts.length < 2) return; document.querySelectorAll('.maccts').forEach(el => el.outerHTML = mailChips()); mailBindChips(); } /** Redraw the folder pickers (names and unread counts) without touching the list or the open message. */ function mailRefreshFolders() { + mailDrawChips(); const fl = mail.folders; if (!fl.length) return; const f = $('#mfsel'); if (f) f.innerHTML = fl.map(x => ``).join(''); - const side = document.querySelector('.mfold'); if (side) { const sel = side.querySelector('select'); side.innerHTML = (sel ? sel.outerHTML : '') + fl.map(x => `${esc(x.name)}${x.unread ? `${x.unread}` : ''}`).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 => ``).join(''); + const side = document.querySelector('.mfold'); if (side) { const sel = side.querySelector('select'); side.innerHTML = (mail.accts.length > 1 ? mailChips() : '') + fl.map(x => `${esc(x.name)}${x.unread ? `${x.unread}` : ''}`).join(''); mailBindChips(); } } function mailConnectScreen(st) { @@ -204,9 +208,9 @@ function mailConnectScreen(st) { function mailShell(mode) { const fl = mail.folders.length ? mail.folders : [{ id: 'inbox', name: 'Inbox', unread: mailUnread }]; $('#view').innerHTML = `
- +
- ${mail.accts.length > 1 ? `
` : ''} + ${mail.accts.length > 1 ? mailChips() : ''}
@@ -214,8 +218,7 @@ function mailShell(mode) {
`; $('#mfsel').onchange = e => { mail.q = ''; location.hash = '#/mail/f/' + encodeURIComponent(e.target.value); }; - if ($('#macct2')) $('#macct2').onchange = e => { mail.q = ''; location.hash = '#/mail/f/' + encodeURIComponent(e.target.value + '~inbox'); }; - if ($('#macct')) $('#macct').onchange = e => { mail.q = ''; location.hash = '#/mail/f/' + encodeURIComponent(e.target.value + '~inbox'); }; + mailBindChips(); $('#mnew').onclick = () => mailCompose(); $('#mrf').onclick = () => { mailLoadList(true); jfetch('/api/mail/folders?acct=' + encodeURIComponent(mail.acct)).then(f => { mail.folders = f; mailPoll(); }).catch(() => {}); }; let t; $('#mq').oninput = e => { clearTimeout(t); t = setTimeout(() => { mail.q = e.target.value.trim(); mailLoadList(true); }, 400); }; diff --git a/public/style.css b/public/style.css index ce2e8ff..bffffe8 100644 --- a/public/style.css +++ b/public/style.css @@ -808,3 +808,14 @@ kbd { display: inline-block; padding: 1px 7px; border: 1px solid var(--line); bo .vsl { position: absolute; top: 28%; z-index: 4; width: 38px; height: 38px; min-height: 0; padding: 0; border-radius: 50%; border: 1px solid var(--line); background: color-mix(in srgb, var(--panel) 92%, transparent); color: var(--text); font-size: 24px; line-height: 1; display: grid; place-items: center; cursor: pointer; box-shadow: 0 4px 14px rgba(0, 0, 0, .3); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); } .vsl[hidden] { display: none; } .vsl.l { left: -6px; } .vsl.r { right: -6px; } @media (hover: none) { .vsl { display: none; } } +/* Mail: one button per account, each with its own unread count */ +.maccts { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; margin-bottom: 6px; padding-bottom: 2px; } +.maccts::-webkit-scrollbar { display: none; } +.maccts button { flex: none; display: flex; align-items: center; gap: 8px; max-width: 220px; min-height: 40px; padding: 4px 12px; border-radius: 12px; border: 1px solid var(--line); background: var(--panel); color: var(--muted); text-align: left; cursor: pointer; } +.maccts button.on { background: var(--accent); color: var(--accent-t); border-color: transparent; } +.maccts button.new:not(.on) { color: var(--text); border-color: var(--accent); } +.maccts .mal { font-weight: 650; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.maccts small { display: none; } +.maccts .nbadge { margin-left: 2px; } +.mfold .maccts { flex-direction: column; overflow: visible; margin-bottom: 10px; } .mfold .maccts button { max-width: none; } .mfold .maccts small { display: block; opacity: .75; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.mfold .maccts .mal, .mfold .maccts small { display: block; } .mfold .maccts button > .mal { max-width: 100%; } diff --git a/public/sw.js b/public/sw.js index 1af1d22..7edc1dc 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v27'; +const CACHE = 'hub-v28'; const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];