diff --git a/docs/configuration.md b/docs/configuration.md index 721e986..e972a10 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -166,7 +166,7 @@ new in a year), with nobody subscribed and no file on disk, is removed from the Feeds derived from a subscribed OPML are **not** in the catalogue: the OPML is the source of truth and they are re-derived on every scan. Editing one in the UI promotes it to a catalogue entry. So -does Keep, in a show's settings, for whoever turns it on (`kept` on their subscription, #280): when +does Keep, the plus on a show's page, for whoever turns it on (`kept` on their subscription, #280): when the OPML stops listing the show, or nobody subscribes to the OPML any more, it becomes a catalogue entry outside the OPML instead of going, and stays on the lists of those who kept it. Anyone who had it only through the OPML loses it as before. Leaving the OPML yourself keeps what you kept. diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 90946be..1c1bcf0 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -497,30 +497,22 @@ test('a show inside an OPML can be kept, and only it', async ({ page }) => { await expect(chev).toBeVisible({ timeout: 20_000 }); if (await chev.getAttribute('aria-expanded') === 'false') await chev.click(); await page.locator('#feedlist').getByText('Aardvark Radio').click(); - await page.locator('#content .acts [data-a="settings"]').click(); - // Saved settings on a show would stop it following the OPML's for later tests, so the save is - // caught here and only what it would send is checked (#280). - let sent = null; - await page.route('**/api/feeds/*', (route) => { - if (route.request().method() !== 'PATCH') return route.continue(); - sent = route.request().postDataJSON(); - return route.fulfill({ status: 204 }); - }); - const keep = page.locator('#skept'); - await expect(keep).not.toBeChecked(); - await expect(page.locator('#modalCard')).toContainText('Test Subscriptions stops listing it'); - await keep.check({ force: true }); - await page.locator('#ssave').click(); - await expect.poll(() => sent && sent.kept).toBe(true); - await page.unroute('**/api/feeds/*'); + // A plus on the show's page; pressed, a check (#280). + const keep = page.locator('#content .acts [data-a="keep"]'); + await expect(keep).toHaveAttribute('aria-pressed', 'false'); + await expect(keep).toHaveAttribute('title', /Test Subscriptions drops it/); + await keep.click(); + await expect(keep).toHaveAttribute('aria-pressed', 'true'); + const kept = await page.evaluate(() => fetch('/api/feeds').then((r) => r.json())); + expect((kept.feeds || kept).find((f) => f.title === 'Aardvark Radio')?.kept).toBe(true); + await keep.click(); + await expect(keep).toHaveAttribute('aria-pressed', 'false'); - // The OPML's own settings have no Keep. - await page.keyboard.press('Escape'); + // The OPML itself has no Keep. await page.locator('.feed', { hasText: 'Test Subscriptions' }).first().click(); - await page.locator('#content .acts [data-a="settings"]').click(); - await expect(page.locator('#modalCard')).toBeVisible(); - await expect(page.locator('#skept')).toHaveCount(0); + await expect(page.locator('.childrow').first()).toBeVisible(); + await expect(page.locator('#content [data-a="keep"]')).toHaveCount(0); }); test('marking an OPML subscription read covers the feeds inside it', async ({ page }) => { diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index 71e73bf..f779214 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- A show inside an OPML subscription has Keep in its settings, so it stays on your list as a show of its own if the OPML drops it or you unsubscribe from the OPML (#280). +- A show inside an OPML subscription has a Keep button, a plus, on its page, so it stays on your list as a show of its own if the OPML drops it or you unsubscribe from the OPML (#280). - Admin, Server: a setting to give everyone Signals on the server's TypeSafe key (#271). - Admin, Server: how many days back a new Signal reads (#276). diff --git a/web/src/dialogs.ts b/web/src/dialogs.ts index 4564bb1..473f90d 100644 --- a/web/src/dialogs.ts +++ b/web/src/dialogs.ts @@ -578,9 +578,6 @@ function settingsModal(f: Feed, newUrl?: string){ const kids=S.feeds.filter(c=>c.group===f.id).length; const isGroup=kids>0; const parent=f.managed?S.feeds.find(p=>p.id===f.group):null; - // A show from an OPML or a Patreon creator, which you may keep should the list drop it (#280). - const list=f.managed&&!!f.group; - const listName=parent?(parent.title||parent.id):f.group||''; const admin=!!(S.me&&S.me.admin); const name=f.title||f.id; // Blank follows the server's default; 0 is every item (#166). @@ -619,9 +616,6 @@ function settingsModal(f: Feed, newUrl?: string){

The silence kept where a long one is cut. More is easier on the ear; less saves more time.

- ${list?`
${sw(``,'Keep',' lead')} -

Stays on your list as a show of its own if ${esc(listName)} stops listing it, or if you - unsubscribe from ${esc(listName)}.

`:''} ${isGroup?'':`
${chipsHTML(f.blocked||[],'sblockin')}

Whole words or phrases. Hidden from your list and not downloaded for you, along @@ -719,8 +713,6 @@ function settingsModal(f: Feed, newUrl?: string){ auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked, deadair:$('#sdead').checked, deadair_pause:Number($('#sdpause').value)}; if($('#sblock')) patch.blocked=chipWords($('#sblock')); - const kept=$('#skept'); - if(kept&&kept.checked!==!!f.kept) patch.kept=kept.checked; // Sent only when it changed: anyone may mark a feed private, but only an admin unmark it. const priv=$('#sprivate'); if(!priv.disabled&&priv.checked!==!!f.private) patch.private=priv.checked; diff --git a/web/src/feedpage.ts b/web/src/feedpage.ts index 149b592..afe5d46 100644 --- a/web/src/feedpage.ts +++ b/web/src/feedpage.ts @@ -34,6 +34,7 @@ function renderFeed(){ + ${keepHTML(f)} @@ -161,6 +162,15 @@ function renderGroup(f: Feed,kids: Feed[]){ draw(); } +/// Keep, for a show from an OPML or a Patreon creator: it stays yours, a show of its own, should +/// the list drop it or you leave the list (#280). A plus to keep it, a check once kept. +function keepHTML(f: Feed){ + if(!f.managed||!f.group) return ''; + const list=S.feeds.find(p=>p.id===f.group); + const name=esc(list?(list.title||list.id):f.group); + const title=f.kept?`Kept: stays on your list if ${name} drops it. Stop keeping it`:`Keep it on your list if ${name} drops it`; + return ``; +} async function feedAction(a: string,f: Feed){ if(a==='scan'){ spinStart(f.id); @@ -175,6 +185,12 @@ async function feedAction(a: string,f: Feed){ await loadFeeds(true); renderFeed(); }catch(e){ toast(why(e),true); } } + if(a==='keep'){ + try{ + await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({kept:!f.kept})}); + await loadFeeds(true); renderFeed(); + }catch(e){ toast(why(e),true); } + } if(a==='settings') settingsModal(f); if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]')); if(a==='dl') downloadLatestModal(f);