From 5b1325609f24338bdb2fb467eaa502e6001d2258 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sun, 25 Oct 2026 15:33:26 +0100 Subject: [PATCH] Nachrichten/Blogs: Gelesen-Markieren geht nicht mehr verloren, wenn der Server nicht antwortet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Aufruf an den Server war "abschicken und vergessen": scheiterte er (kein Netz, 5xx, ein Proxy dazwischen), wurde er still verworfen und der Artikel war nach dem nächsten Laden wieder ungelesen. Jetzt wird auf dem Gerät markiert und der Aufruf gemerkt (localStorage diora_feeds_read_pending) - wie die Hefte es mit diora_issue_pending schon tun. Er geht nach, sobald das Gerät wieder online ist oder die Liste neu geladen wird. Eine 4xx-Antwort (der Artikel ist beim Server weg) wird nicht wiederholt, sonst würde die Warteschlange nur wachsen. sw v154. --- static/js/feeds.js | 75 ++++++++++++++++++++++++++++++++++++++++++---- static/js/sw.js | 2 +- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/static/js/feeds.js b/static/js/feeds.js index 1c787a0..baca094 100644 --- a/static/js/feeds.js +++ b/static/js/feeds.js @@ -5,6 +5,9 @@ * the "Seit letztem Mal" briefing on top, old news simply goes away; * Blogs: a plain list of unread articles, per blog if wanted. * Articles open in the page (full text from the server), the browser only on request. + * Marking read happens on the device first and is queued in localStorage when the server + * can't be reached (`diora_feeds_read_pending`, sent again on 'online' and on the next load), + * the same way as the issues' `diora_issue_pending` (static/js/app/54-issues.js). * Runs in the web page, the desktop app and both Android apps (in diora Reader, * which has no tab bar, it brings a small Bücher/Feeds bar). Chrome-61-safe: * the e-reader app transpiles it (compat61), so no ?. / ?? here. @@ -58,6 +61,69 @@ return data; } + // ---- offline: marking read stays on the device and goes later ------------------------ + // + // Telling the server was fire-and-forget before: a failure was swallowed, and offline + // (phone, train) the article came back unread after the next load. A failed call is now + // kept in localStorage and sent again when the connection is back — the same pattern as + // the issues' diora_issue_pending (static/js/app/54-issues.js). Chrome-61-safe. + + const PENDING_READS = 'diora_feeds_read_pending'; + + function pendingReads() { + try { return JSON.parse(localStorage.getItem(PENDING_READS) || '{}') || {}; } catch (e) { return {}; } + } + function setPendingReads(all) { + try { + if (Object.keys(all).length) localStorage.setItem(PENDING_READS, JSON.stringify(all)); + else localStorage.removeItem(PENDING_READS); + } catch (e) {} + } + + // Send what is queued, drop only what the server took. false = still offline. + async function flushPendingReads() { + const all = pendingReads(); + const keys = Object.keys(all); + if (!keys.length) return true; + const yes = keys.filter(k => all[k]).map(Number); + const no = keys.filter(k => !all[k]).map(Number); + try { + if (yes.length) await api('/feeds/items/read/', {ids: yes, read: true}); + if (no.length) await api('/feeds/items/read/', {ids: no, read: false}); + } catch (e) { + if (keepForRetry(e)) return false; + setPendingReads({}); // the server refused them: they would never go through + return true; + } + const now = pendingReads(); + keys.forEach(k => { if (now[k] === all[k]) delete now[k]; }); + setPendingReads(now); + return true; + } + + // Mark on the device at once, then tell the server — and keep it when that fails. + // A 4xx is the server's answer, not a lost connection: the item may have gone away + // (old news is dropped), and repeating it would only fill the queue. Everything else + // — no connection, 5xx, a proxy in between — waits. + function keepForRetry(e) { + return !/^Fehler 4\d\d$/.test((e && e.message) || ''); + } + + async function markRead(ids, read) { + if (!ids || !ids.length) return; + markLocal(ids, read); + try { + await api('/feeds/items/read/', {ids: ids, read: read}); + } catch (e) { + if (!keepForRetry(e)) return; + const all = pendingReads(); + ids.forEach(id => { all[id] = read ? 1 : 0; }); + setPendingReads(all); + } + } + + window.addEventListener('online', () => { flushPendingReads(); }); + // ---- time function when(iso) { @@ -120,6 +186,7 @@ st.loading = true; st.error = ''; render(); + if (!more) await flushPendingReads(); // what offline marking left behind try { if (!more) await loadState(); if (st.view === 'news') await loadNews(more); @@ -619,8 +686,7 @@ // News is read once opened; a blog post once read to 90 % (the server keeps where). if (!readerItem.read) { readerItem.read = true; - markLocal([id], true); - api('/feeds/items/read/', {ids: [id], read: true}).catch(() => {}); + markRead([id], true); } } else { if (progressSent > 0.02 && progressSent < 0.9) { @@ -786,9 +852,8 @@ // Opening a story reads it: all its reports count as read. const unread = story.items.filter(i => !i.read).map(i => i.id); if (unread.length) { - markLocal(unread, true); + markRead(unread, true); story.unread = 0; - api('/feeds/items/read/', {ids: unread, read: true}).catch(() => {}); } openItem(lead.id, story); } @@ -1087,7 +1152,7 @@ und Ordner. Schlage nur Änderungen vor, die wirklich helfen; eine Quelle ohne s if (act === 'blogs-read') { const ids = st.items.filter(i => !i.read).map(i => i.id); if (!ids.length) return; - await api('/feeds/items/read/', {ids, read: true}).catch(() => {}); + await markRead(ids, true); return load(); } if (act === 'folder-rename' || act === 'folder-dissolve') return folderAction(act, t.dataset.name); diff --git a/static/js/sw.js b/static/js/sw.js index df95340..a125705 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v153'; +const CACHE = 'diora-v154'; const SHELL = [ '/static/css/app.css', '/static/js/app/01-core.js',