Email: persist bulk read state to provider

Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
This commit is contained in:
ghreprimand
2026-06-02 06:28:01 -05:00
committed by GitHub
parent 6c654fb0ef
commit 431b98525b
2 changed files with 53 additions and 6 deletions

View File

@@ -4640,6 +4640,7 @@ function _updateBulkBar() {
async function _bulkAction(action) { async function _bulkAction(action) {
const uids = Array.from(state._selectedUids); const uids = Array.from(state._selectedUids);
if (uids.length === 0) return; if (uids.length === 0) return;
let failedReadSync = 0;
if (action === 'delete') { if (action === 'delete') {
const ok = await styledConfirm( const ok = await styledConfirm(
`Delete ${uids.length} selected email${uids.length === 1 ? '' : 's'}?`, `Delete ${uids.length} selected email${uids.length === 1 ? '' : 's'}?`,
@@ -4655,11 +4656,19 @@ async function _bulkAction(action) {
} else if (action === 'delete') { } else if (action === 'delete') {
await fetch(`${API_BASE}/api/email/delete/${uid}?folder=${encodeURIComponent(state._libFolder)}${_acct()}`, { method: 'DELETE' }); await fetch(`${API_BASE}/api/email/delete/${uid}?folder=${encodeURIComponent(state._libFolder)}${_acct()}`, { method: 'DELETE' });
} else if (action === 'read' || action === 'unread') { } else if (action === 'read' || action === 'unread') {
// Local toggle for now (no backend endpoint yet) const endpoint = action === 'read' ? 'mark-read' : 'mark-unread';
const em = state._libEmails.find(e => e.uid === uid); const res = await fetch(`${API_BASE}/api/email/${endpoint}/${uid}?folder=${encodeURIComponent(state._libFolder)}${_acct()}`, { method: 'POST' });
if (em) em.is_read = (action === 'read'); let data = null;
try { data = await res.json(); } catch (_) {}
if (!res.ok || data?.success === false) {
throw new Error(data?.error || `HTTP ${res.status}`);
}
_syncEmailReadState(uid, action === 'read');
} }
} catch (e) { console.error(`Failed to ${action} ${uid}:`, e); } } catch (e) {
if (action === 'read' || action === 'unread') failedReadSync += 1;
console.error(`Failed to ${action} ${uid}:`, e);
}
} }
if (action === 'archive' || action === 'delete') { if (action === 'archive' || action === 'delete') {
@@ -4671,8 +4680,10 @@ async function _bulkAction(action) {
state._selectMode = false; state._selectMode = false;
_updateBulkBar(); _updateBulkBar();
_renderGrid(); _renderGrid();
// Sync the local mutation (delete/archive, or in-place read/unread if (failedReadSync > 0) {
// flag flips on email objects) into the SWR cache so reopen doesn't showToast(`Failed to update ${failedReadSync} email${failedReadSync === 1 ? '' : 's'}`);
}
// Sync successful local mutations into the SWR cache so reopen doesn't
// briefly show the pre-bulk state. // briefly show the pre-bulk state.
_libCacheWriteBack(); _libCacheWriteBack();
} }

View File

@@ -0,0 +1,36 @@
from pathlib import Path
_REPO = Path(__file__).resolve().parents[1]
_EMAIL_LIBRARY = _REPO / "static" / "js" / "emailLibrary.js"
def _bulk_action_source() -> str:
text = _EMAIL_LIBRARY.read_text(encoding="utf-8")
start = text.index("async function _bulkAction(action)")
end = text.index("\n}\n\n// _extractName", start) + 3
return text[start:end]
def test_email_bulk_read_unread_calls_provider_write_routes():
"""Bulk read/unread must persist to IMAP/provider, not only mutate UI state.
Regression for issue #800's email follow-up: list select -> Actions ->
Mark Read used to update `em.is_read` locally and cache that fake state,
then refresh from the provider made the message unread again.
"""
src = _bulk_action_source()
assert "Local toggle for now" not in src
assert "mark-read" in src
assert "mark-unread" in src
assert "method: 'POST'" in src
assert "_syncEmailReadState(uid, action === 'read')" in src
def test_email_bulk_read_unread_checks_backend_success_before_syncing_cache():
src = _bulk_action_source()
assert "data?.success === false" in src
assert "throw new Error(data?.error" in src
assert "_libCacheWriteBack()" in src