Mark all read on an OPML subscription
Marking the subscription read did nothing: its own row holds no entries. read-all now resolves the feeds grouped under the id -- via subscriptions(), so a child promoted to config is included -- and marks those. Button sits before Unsubscribe, where every other feed keeps it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
@@ -56,6 +56,15 @@ and until now nothing set them.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-10 — Marking a subscription read
|
||||
|
||||
An OPML subscription's page now has **Mark all read**, sitting where every other feed keeps it --
|
||||
before Unsubscribe. The fix is in the handler rather than the button: `read-all` resolves the feeds
|
||||
whose group is the given id (through `subscriptions()`, so a child promoted to config counts too)
|
||||
and marks those, since the subscription's own row holds no entries and marking it did nothing.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-10 — A folder counts what it holds
|
||||
|
||||
An OPML subscription has no entries of its own, so its row always read `0 unread` no matter how much
|
||||
|
||||
10
src/db.rs
10
src/db.rs
@@ -681,9 +681,15 @@ impl Db {
|
||||
}
|
||||
|
||||
/// Marks every entry in a feed read, for the "mark all read" button.
|
||||
pub fn mark_all_read(&self, feed_id: &str) -> Result<usize> {
|
||||
/// Marks every entry of the given feeds read. Takes a list because an OPML subscription
|
||||
/// holds no entries itself -- marking it read means the feeds inside it.
|
||||
pub fn mark_all_read(&self, feed_ids: &[String]) -> Result<usize> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
Ok(conn.execute("UPDATE entries SET read = 1 WHERE feed_id = ?1 AND read = 0", [feed_id])?)
|
||||
let mut n = 0;
|
||||
for id in feed_ids {
|
||||
n += conn.execute("UPDATE entries SET read = 1 WHERE feed_id = ?1 AND read = 0", [id])?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// The next N enclosures with no file, newest entry first -- what "download latest"
|
||||
|
||||
10
src/web.rs
10
src/web.rs
@@ -660,7 +660,15 @@ async fn read_all(
|
||||
State(state): State<WebState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let n = state.ctx.db.mark_all_read(&id)?;
|
||||
// A subscription's own row has no entries, so marking it read means everything under it.
|
||||
let mut ids = vec![id.clone()];
|
||||
ids.extend(
|
||||
crate::subscriptions(&state.ctx)?
|
||||
.into_iter()
|
||||
.filter(|s| s.cfg.group.as_deref() == Some(id.as_str()))
|
||||
.map(|s| s.id),
|
||||
);
|
||||
let n = state.ctx.db.mark_all_read(&ids)?;
|
||||
Ok(Json(serde_json::json!({ "marked": n })))
|
||||
}
|
||||
|
||||
|
||||
@@ -189,3 +189,21 @@ test('an OPML subscription is a collapsible folder', async ({ page }) => {
|
||||
await page.locator('.feed', { hasText: 'Test Subscriptions' }).first().click();
|
||||
await expect(page.locator('.childrow')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('marking an OPML subscription read covers the feeds inside it', async ({ page }) => {
|
||||
await page.evaluate(() =>
|
||||
fetch('/api/fetch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ feed: 'test-subscriptions', force: true }),
|
||||
}));
|
||||
|
||||
// The folder's own row has no entries, so anything it marks read came from its child.
|
||||
const folder = page.locator('.feed', { hasText: 'Test Subscriptions' }).first();
|
||||
await expect(folder).toBeVisible({ timeout: 20_000 });
|
||||
await expect(folder.locator('.badge')).not.toHaveText('0');
|
||||
await folder.click();
|
||||
|
||||
await page.locator('#content .acts button', { hasText: 'Mark all read' }).click();
|
||||
await expect(folder.locator('.badge')).toHaveText('0');
|
||||
});
|
||||
|
||||
@@ -556,6 +556,7 @@ function renderGroup(f,kids){
|
||||
<div class="acts">
|
||||
<button class="btn primary" data-a="scan">Re-read OPML</button>
|
||||
<button class="btn" data-a="settings">Settings</button>
|
||||
<button class="btn" data-a="read">Mark all read</button>
|
||||
<button class="btn danger" data-a="rm">Unsubscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,7 +591,7 @@ function renderGroup(f,kids){
|
||||
|
||||
async function feedAction(a,f){
|
||||
if(a==='scan'){ toast('Scanning '+(f.title||f.id)+'…'); await api('/api/fetch',{method:'POST',body:JSON.stringify({feed:f.id,force:true})}); }
|
||||
if(a==='read'){ const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/read-all`,{method:'POST'}); toast(`Marked ${r.marked} read`); await loadFeeds(true); loadEntries(); }
|
||||
if(a==='read'){ const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/read-all`,{method:'POST'}); toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries(); }
|
||||
if(a==='rm') removeFeed(f);
|
||||
if(a==='settings') settingsModal(f);
|
||||
if(a==='dl') downloadLatestModal(f);
|
||||
|
||||
Reference in New Issue
Block a user