diff --git a/static/js/emailInbox.js b/static/js/emailInbox.js index b64b7f12c..7edb59df4 100644 --- a/static/js/emailInbox.js +++ b/static/js/emailInbox.js @@ -1183,15 +1183,42 @@ async function _deleteEmail(em) { const { styledConfirm } = await import('./ui.js'); const ok = await styledConfirm(`Delete "${subject}"?`, { confirmText: 'Delete', cancelText: 'Cancel', danger: true }); if (!ok) return; + const row = document.querySelector(`.email-item[data-uid="${CSS.escape(String(em.uid))}"]`); + const busy = _showEmailDeleteOverlay(row); try { await fetch(`${API_BASE}/api/email/delete/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}`, { method: 'DELETE' }); + busy?.remove?.(); _emails = _emails.filter(e => e.uid !== em.uid); _renderList(); } catch (e) { + busy?.remove?.(); console.error('Failed to delete:', e); } } +function _showEmailDeleteOverlay(target) { + if (!target) return null; + const wp = spinnerModule.createWhirlpool(16); + const overlay = document.createElement('div'); + overlay.className = 'email-delete-overlay'; + overlay.appendChild(wp.element); + const prevPos = target.style.position; + const prevPointerEvents = target.style.pointerEvents; + if (getComputedStyle(target).position === 'static') target.style.position = 'relative'; + target.style.pointerEvents = 'none'; + target.classList.add('email-delete-busy'); + target.appendChild(overlay); + return { + remove() { + try { wp.destroy?.(); } catch (_) {} + overlay.remove(); + target.classList.remove('email-delete-busy'); + target.style.pointerEvents = prevPointerEvents; + target.style.position = prevPos; + } + }; +} + async function _toggleDone(em, itemEl) { const newState = !em.is_answered; em.is_answered = newState; diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index 21c4db9dd..be61b4f6c 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -721,6 +721,7 @@ async function _deleteEmailAndAdvance(em, card, opts = {}) { const ok = await styledConfirm(`Delete "${subject}"?`, { confirmText: 'Delete', cancelText: 'Cancel', danger: true }); if (!ok) return; } + const busy = _showEmailDeleteOverlay(card); const wasExpanded = !!card?.classList?.contains('doclib-card-expanded'); const sibling = wasExpanded ? (_findSiblingEmailCard(card, +1) || _findSiblingEmailCard(card, -1)) @@ -730,9 +731,11 @@ async function _deleteEmailAndAdvance(em, card, opts = {}) { await fetch(`${API_BASE}/api/email/delete/${em.uid}?folder=${encodeURIComponent(state._libFolder)}${_acct()}`, { method: 'DELETE' }); } catch (err) { console.error('Failed to delete email:', err); + busy?.remove?.(); showToast('Failed to delete email'); return; } + busy?.remove?.(); await _animateEmailCardRemoval([em.uid]); state._libEmails = state._libEmails.filter(e => String(e.uid) !== String(em.uid)); state._selectedUids.delete(em.uid); @@ -752,6 +755,29 @@ async function _deleteEmailAndAdvance(em, card, opts = {}) { } } +function _showEmailDeleteOverlay(target) { + if (!target) return null; + const wp = spinnerModule.createWhirlpool(18); + const overlay = document.createElement('div'); + overlay.className = 'email-delete-overlay'; + overlay.appendChild(wp.element); + const prevPos = target.style.position; + const prevPointerEvents = target.style.pointerEvents; + if (getComputedStyle(target).position === 'static') target.style.position = 'relative'; + target.style.pointerEvents = 'none'; + target.classList.add('email-delete-busy'); + target.appendChild(overlay); + return { + remove() { + try { wp.destroy?.(); } catch (_) {} + overlay.remove(); + target.classList.remove('email-delete-busy'); + target.style.pointerEvents = prevPointerEvents; + target.style.position = prevPos; + } + }; +} + function _animateEmailCardRemoval(uids, opts = {}) { const uidSet = new Set((uids || []).map(uid => String(uid))); if (!uidSet.size) return Promise.resolve(); @@ -6555,9 +6581,16 @@ function _showReaderMoreMenu(em, card, reader, anchor) { label: 'Move to Trash', icon: _trashIcon, action: async () => { + const busy = _showEmailDeleteOverlay(card); try { await fetch(`${API_BASE}/api/email/delete/${em.uid}?folder=${encodeURIComponent(state._libFolder)}${_acct()}`, { method: 'DELETE' }); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + busy?.remove?.(); + showToast('Failed to delete email'); + return; + } + busy?.remove?.(); await closeAndRemove(); }, }, @@ -6572,9 +6605,16 @@ function _showReaderMoreMenu(em, card, reader, anchor) { { confirmText: 'Delete', cancelText: 'Cancel', danger: true } ); if (!ok) return; + const busy = _showEmailDeleteOverlay(card); try { await fetch(`${API_BASE}/api/email/delete-permanent/${em.uid}?folder=${encodeURIComponent(state._libFolder)}${_acct()}`, { method: 'DELETE' }); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + busy?.remove?.(); + showToast('Failed to delete email'); + return; + } + busy?.remove?.(); await closeAndRemove(); }, }, diff --git a/static/style.css b/static/style.css index 2e1293f8e..b116d8717 100644 --- a/static/style.css +++ b/static/style.css @@ -29296,6 +29296,27 @@ button .spinner-whirlpool { 0 0 12px -3px color-mix(in srgb, var(--accent, var(--red)) 65%, transparent); } +.email-delete-busy { + opacity: 0.72; +} + +.email-delete-overlay { + position: absolute; + inset: 0; + z-index: 8; + display: flex; + align-items: center; + justify-content: center; + border-radius: inherit; + background: color-mix(in srgb, var(--panel, var(--bg)) 82%, transparent); + pointer-events: none; +} + +.email-delete-overlay .spinner-whirlpool, +.email-delete-overlay .ai-spinner-whirlpool { + margin: 0 !important; +} + .doclib-card.email-card-removing { pointer-events: none !important; max-height: 0 !important;