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',