Keep is a button on the show's page, a plus, not a switch in its settings (#280)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
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
|
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
|
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
|
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.
|
had it only through the OPML loses it as before. Leaving the OPML yourself keeps what you kept.
|
||||||
|
|||||||
@@ -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 });
|
await expect(chev).toBeVisible({ timeout: 20_000 });
|
||||||
if (await chev.getAttribute('aria-expanded') === 'false') await chev.click();
|
if (await chev.getAttribute('aria-expanded') === 'false') await chev.click();
|
||||||
await page.locator('#feedlist').getByText('Aardvark Radio').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
|
// A plus on the show's page; pressed, a check (#280).
|
||||||
// caught here and only what it would send is checked (#280).
|
const keep = page.locator('#content .acts [data-a="keep"]');
|
||||||
let sent = null;
|
await expect(keep).toHaveAttribute('aria-pressed', 'false');
|
||||||
await page.route('**/api/feeds/*', (route) => {
|
await expect(keep).toHaveAttribute('title', /Test Subscriptions drops it/);
|
||||||
if (route.request().method() !== 'PATCH') return route.continue();
|
await keep.click();
|
||||||
sent = route.request().postDataJSON();
|
await expect(keep).toHaveAttribute('aria-pressed', 'true');
|
||||||
return route.fulfill({ status: 204 });
|
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);
|
||||||
const keep = page.locator('#skept');
|
await keep.click();
|
||||||
await expect(keep).not.toBeChecked();
|
await expect(keep).toHaveAttribute('aria-pressed', 'false');
|
||||||
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/*');
|
|
||||||
|
|
||||||
// The OPML's own settings have no Keep.
|
// The OPML itself has no Keep.
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.locator('.feed', { hasText: 'Test Subscriptions' }).first().click();
|
await page.locator('.feed', { hasText: 'Test Subscriptions' }).first().click();
|
||||||
await page.locator('#content .acts [data-a="settings"]').click();
|
await expect(page.locator('.childrow').first()).toBeVisible();
|
||||||
await expect(page.locator('#modalCard')).toBeVisible();
|
await expect(page.locator('#content [data-a="keep"]')).toHaveCount(0);
|
||||||
await expect(page.locator('#skept')).toHaveCount(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('marking an OPML subscription read covers the feeds inside it', async ({ page }) => {
|
test('marking an OPML subscription read covers the feeds inside it', async ({ page }) => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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: a setting to give everyone Signals on the server's TypeSafe key (#271).
|
||||||
- Admin, Server: how many days back a new Signal reads (#276).
|
- Admin, Server: how many days back a new Signal reads (#276).
|
||||||
|
|
||||||
|
|||||||
@@ -578,9 +578,6 @@ function settingsModal(f: Feed, newUrl?: string){
|
|||||||
const kids=S.feeds.filter(c=>c.group===f.id).length;
|
const kids=S.feeds.filter(c=>c.group===f.id).length;
|
||||||
const isGroup=kids>0;
|
const isGroup=kids>0;
|
||||||
const parent=f.managed?S.feeds.find(p=>p.id===f.group):null;
|
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 admin=!!(S.me&&S.me.admin);
|
||||||
const name=f.title||f.id;
|
const name=f.title||f.id;
|
||||||
// Blank follows the server's default; 0 is every item (#166).
|
// Blank follows the server's default; 0 is every item (#166).
|
||||||
@@ -619,9 +616,6 @@ function settingsModal(f: Feed, newUrl?: string){
|
|||||||
<div class="dpslide"><input type="range" id="sdpause" min="200" max="500" step="100" value="${f.deadair_pause||300}"${f.deadair?'':' disabled'}></div>
|
<div class="dpslide"><input type="range" id="sdpause" min="200" max="500" step="100" value="${f.deadair_pause||300}"${f.deadair?'':' disabled'}></div>
|
||||||
<div class="dpends" aria-hidden="true"><span>Brisk</span><span>Relaxed</span></div>
|
<div class="dpends" aria-hidden="true"><span>Brisk</span><span>Relaxed</span></div>
|
||||||
<p class="cap">The silence kept where a long one is cut. More is easier on the ear; less saves more time.</p></div></div></div>
|
<p class="cap">The silence kept where a long one is cut. More is easier on the ear; less saves more time.</p></div></div></div>
|
||||||
${list?`<div class="ffield">${sw(`<input type="checkbox" role="switch" id="skept"${f.kept?' checked':''}>`,'Keep',' lead')}
|
|
||||||
<p class="cap">Stays on your list as a show of its own if ${esc(listName)} stops listing it, or if you
|
|
||||||
unsubscribe from ${esc(listName)}.</p></div>`:''}
|
|
||||||
${isGroup?'':`<div class="ffield"><label class="flabel" for="sblockin">Hide items that mention</label>
|
${isGroup?'':`<div class="ffield"><label class="flabel" for="sblockin">Hide items that mention</label>
|
||||||
<div class="chips" id="sblock">${chipsHTML(f.blocked||[],'sblockin')}</div>
|
<div class="chips" id="sblock">${chipsHTML(f.blocked||[],'sblockin')}</div>
|
||||||
<p class="cap">Whole words or phrases. Hidden from your list and not downloaded for you, along
|
<p class="cap">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:$<HTMLInputElement>('#sauto').checked, allow_explicit:$<HTMLInputElement>('#sexp').checked, deadair:$<HTMLInputElement>('#sdead').checked,
|
auto_download:$<HTMLInputElement>('#sauto').checked, allow_explicit:$<HTMLInputElement>('#sexp').checked, deadair:$<HTMLInputElement>('#sdead').checked,
|
||||||
deadair_pause:Number($<HTMLInputElement>('#sdpause').value)};
|
deadair_pause:Number($<HTMLInputElement>('#sdpause').value)};
|
||||||
if($('#sblock')) patch.blocked=chipWords($('#sblock'));
|
if($('#sblock')) patch.blocked=chipWords($('#sblock'));
|
||||||
const kept=$<HTMLInputElement>('#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.
|
// Sent only when it changed: anyone may mark a feed private, but only an admin unmark it.
|
||||||
const priv=$<HTMLInputElement>('#sprivate');
|
const priv=$<HTMLInputElement>('#sprivate');
|
||||||
if(!priv.disabled&&priv.checked!==!!f.private) patch.private=priv.checked;
|
if(!priv.disabled&&priv.checked!==!!f.private) patch.private=priv.checked;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function renderFeed(){
|
|||||||
<button class="btn ico" data-a="dl" title="Download latest…" aria-label="Download latest">${ICON.download}</button>
|
<button class="btn ico" data-a="dl" title="Download latest…" aria-label="Download latest">${ICON.download}</button>
|
||||||
<button class="btn ico" data-a="read" title="Mark all read" aria-label="Mark all read">${ICON.checks}</button>
|
<button class="btn ico" data-a="read" title="Mark all read" aria-label="Mark all read">${ICON.checks}</button>
|
||||||
<button class="btn ico" data-a="pin" title="${f.pinned?'Unpin from the top of the feed list':'Pin to the top of the feed list'}" aria-label="${f.pinned?'Unpin':'Pin'}" aria-pressed="${!!f.pinned}">${f.pinned?ICON.pinOn:ICON.pin}</button>
|
<button class="btn ico" data-a="pin" title="${f.pinned?'Unpin from the top of the feed list':'Pin to the top of the feed list'}" aria-label="${f.pinned?'Unpin':'Pin'}" aria-pressed="${!!f.pinned}">${f.pinned?ICON.pinOn:ICON.pin}</button>
|
||||||
|
${keepHTML(f)}
|
||||||
<button class="btn ico" data-a="share" title="Share" aria-label="Share">${ICON.share}</button>
|
<button class="btn ico" data-a="share" title="Share" aria-label="Share">${ICON.share}</button>
|
||||||
<button class="btn ico" data-a="settings" title="Settings" aria-label="Settings">${ICON.settings}</button>
|
<button class="btn ico" data-a="settings" title="Settings" aria-label="Settings">${ICON.settings}</button>
|
||||||
<button class="btn ico danger" data-a="rm" title="Unsubscribe" aria-label="Unsubscribe">${ICON.circleMinus}</button>
|
<button class="btn ico danger" data-a="rm" title="Unsubscribe" aria-label="Unsubscribe">${ICON.circleMinus}</button>
|
||||||
@@ -161,6 +162,15 @@ function renderGroup(f: Feed,kids: Feed[]){
|
|||||||
draw();
|
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 `<button class="btn ico" data-a="keep" title="${title}" aria-label="${f.kept?'Kept':'Keep'}" aria-pressed="${!!f.kept}">${f.kept?ICON.check:ICON.plus}</button>`;
|
||||||
|
}
|
||||||
async function feedAction(a: string,f: Feed){
|
async function feedAction(a: string,f: Feed){
|
||||||
if(a==='scan'){
|
if(a==='scan'){
|
||||||
spinStart(f.id);
|
spinStart(f.id);
|
||||||
@@ -175,6 +185,12 @@ async function feedAction(a: string,f: Feed){
|
|||||||
await loadFeeds(true); renderFeed();
|
await loadFeeds(true); renderFeed();
|
||||||
}catch(e){ toast(why(e),true); }
|
}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==='settings') settingsModal(f);
|
||||||
if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]'));
|
if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]'));
|
||||||
if(a==='dl') downloadLatestModal(f);
|
if(a==='dl') downloadLatestModal(f);
|
||||||
|
|||||||
Reference in New Issue
Block a user