From 99660e1c6d8fa0403d1cfcc93435a1386c8d3309 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Thu, 11 Jun 2026 19:11:07 +0900 Subject: [PATCH] Email library chip-bar: smaller pills, persist across refresh, Esc + sender click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four fixes from the first round of usage: 1. Pill height was larger than the chip-bar's row — shrink to a fixed 18px-tall pill (line-height + height pinned) so it sits inside the input row. 2. List refresh wiped pill state — when _loadEmails replaces state._libEmails (refresh, folder switch, etc.), refresh the snapshot to the new list and re-apply the pill filter so pills persist instead of resetting to 'show all emails'. 3. Click-to-add only worked inside the open email reader. Extend the capture-phase handler to ALSO catch clicks on .email-meta-sender inside the library grid — the list card's sender name is the most natural place to want to pivot from. 4. Esc inside the chip-input didn't close the modal. New behaviour: if the autocomplete dropdown is open, Esc closes only the dropdown (and swallows the event); otherwise Esc blurs the input and bubbles so the existing modal Esc handler can close the library. Also wires data-email + data-name on .email-meta-sender so the click handler has reliable targeting. --- static/js/emailLibrary.js | 79 +++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index bed503194..31f052f05 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -1802,9 +1802,9 @@ function _renderSearchPills() { const esc = s => String(s || '').replace(/&/g, '&').replace(/ { const label = p.type === 'contact' ? (p.name || p.email || '?') : (p.text || ''); - return ` + return ``; }).join(''); wrap.querySelectorAll('.email-lib-pill-x').forEach(btn => { @@ -1962,27 +1962,51 @@ async function _initEmailSearchChipBar() { return; } if (e.key === 'Escape') { - _hideSearchSuggestions(); + if (menuOpen) { + // Just close the dropdown — let the modal Esc handler run on the + // next Esc to actually dismiss the library. + e.preventDefault(); + e.stopPropagation(); + _hideSearchSuggestions(); + } else { + // Blur first so the modal Esc handler doesn't get suppressed by + // any IME / typing-target check, and let the event propagate. + try { input.blur(); } catch (_) {} + } } }); } -// Click-to-add: if a recipient chip in the email reader is clicked, drop -// the person into the library search as a contact pill so the user can -// pivot to "everything from / to this person" in one tap. +// Click-to-add: clicking a recipient-chip in the email reader OR a +// .email-meta-sender in the library list drops the person into the +// library search as a contact pill so the user can pivot to "everything +// from / to this person" in one tap. window.addEventListener('click', (e) => { + const lib = document.getElementById('email-lib-modal'); + // 1) Recipient chips inside the email reader area const chip = e.target.closest && e.target.closest('.recipient-chip'); - if (!chip) return; - // Only hijack inside the email reader area; ignore composer / forms etc. - if (!chip.closest('.email-reader-header, .email-card-reader, .email-reader-tab-modal')) return; - const email = (chip.dataset && chip.dataset.email) || ''; - const name = (chip.dataset && chip.dataset.name) || (chip.textContent || '').trim(); - if (!email) return; - e.preventDefault(); - e.stopPropagation(); - // Surface the library window if it's hidden, then drop a pill in. - try { window.openEmailLibrary && window.openEmailLibrary(); } catch (_) {} - _addSearchPill({ type: 'contact', name, email }); + if (chip && chip.closest('.email-reader-header, .email-card-reader, .email-reader-tab-modal')) { + const email = (chip.dataset && chip.dataset.email) || ''; + const name = (chip.dataset && chip.dataset.name) || (chip.textContent || '').trim(); + if (!email) return; + e.preventDefault(); + e.stopPropagation(); + try { window.openEmailLibrary && window.openEmailLibrary(); } catch (_) {} + _addSearchPill({ type: 'contact', name, email }); + return; + } + // 2) Sender name in a library list card row (only when the library is open) + if (lib && !lib.classList.contains('hidden')) { + const senderEl = e.target.closest && e.target.closest('.email-meta-sender'); + if (senderEl && senderEl.closest('#email-lib-grid')) { + const email = (senderEl.dataset && senderEl.dataset.email) || ''; + const name = (senderEl.dataset && senderEl.dataset.name) || (senderEl.textContent || '').trim(); + if (!email) return; + e.preventDefault(); + e.stopPropagation(); + _addSearchPill({ type: 'contact', name, email }); + } + } }, true); async function _doSearch() { @@ -2258,7 +2282,18 @@ async function _loadEmails({ force = false, useCache = true } = {}) { state._libEmails = data.emails || []; state._libTotal = data.total || 0; if (sp) sp.destroy(); - _renderGrid(); + // If chip-bar pills are active, swap the snapshot to the freshly + // loaded list and re-apply the filter so pills persist across + // refreshes / folder switches instead of getting wiped. + const _activePills = (state._libSearchPills || []).length > 0 + || (state._libSearchDraft || '').length > 0; + if (_activePills) { + _libPreSearchEmails = state._libEmails.slice(); + _libPreSearchTotal = state._libTotal; + _applyPillFilter(); + } else { + _renderGrid(); + } const stats = document.getElementById('email-lib-stats'); if (stats) stats.textContent = `${state._libTotal} emails`; _refreshUnreadBadge(); @@ -2453,10 +2488,16 @@ function _createCard(em) { // hides the actually useful info. Outside Sent, show the sender as before. const isSentFolderEarly = /sent/i.test(state._libFolder); let senderName; + let senderAddress; if (isSentFolderEarly) { senderName = _formatRecipients(em.to) || em.to || '(no recipient)'; + // First address out of em.to for click-to-pill targeting. + const _firstTo = String(em.to || '').split(',')[0] || ''; + const _m = _firstTo.match(/<([^>]+)>/); + senderAddress = (_m ? _m[1] : _firstTo).trim(); } else { senderName = em.from_name || em.from_address; + senderAddress = em.from_address || ''; } const color = _senderColor(senderName); @@ -2588,7 +2629,7 @@ function _createCard(em) { meta.className = 'memory-item-meta'; meta.style.cssText = 'font-size:10px;opacity:0.7;margin-top:2px;'; const senderPrefix = isSentFolderEarly ? 'to ' : ''; - meta.innerHTML = ``; + meta.innerHTML = ``; content.appendChild(meta); card.appendChild(content);