Keep a feed, an OPML or a Patreon creator out of All Subscriptions (#288)

A switch in the feed's settings, In All Subscriptions, on its
subscription as hide_from_all. All Subscriptions' list and count leave
out a feed hidden on its own or through the OPML it is in, and the page's
unread count does the same. Signals read every feed as before, and
Currently Listening, which is All Subscriptions' in-progress filter, keeps
an episode under way whatever the feed's setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 14:19:54 +00:00
parent 6daab6989e
commit b89c371662
10 changed files with 114 additions and 2 deletions

View File

@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- A feed's, an OPML subscription's or a Patreon creator's settings have In All Subscriptions: off keeps its items out of All Subscriptions and its unread count, still there on its own page, in Signals and in Currently Listening (#288).
- An item sent as both HTML and Markdown, as Scripting News's are, has a choice beside its buttons to show it as the publisher's HTML or as the Markdown, kept from one item to the next (#285).
- 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).

View File

@@ -578,6 +578,8 @@ 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;
// Hidden from All Subscriptions with the OPML it is in, whatever its own setting (#288).
const byList=f.group?S.feeds.find(g=>g.id===f.group&&g.hide_from_all):undefined;
const admin=!!(S.me&&S.me.admin);
const name=f.title||f.id;
// Blank follows the server's default; 0 is every item (#166).
@@ -620,6 +622,10 @@ function settingsModal(f: Feed, newUrl?: string){
<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
with what Settings hides everywhere.</p></div>`}
<div class="ffield">${sw(`<input type="checkbox" role="switch" id="sinall"${inAll(f)?' checked':''}${byList?' disabled':''}>`,'In All Subscriptions',' lead')}
<p class="cap">${byList
?`Left out with the rest of ${esc(byList.title||byList.id)}. Its own settings bring them back.`
:`Off keeps ${isGroup?'these shows':'this show'} out of All Subscriptions and its unread count. ${isGroup?'They are':'It is'} still here, in Signals and in Currently Listening.`}</p></div>
<div class="ffield">${sw(`<input type="checkbox" role="switch" id="sprivate"${f.private||f.private_by_address?' checked':''}${
f.private_by_address||(f.private&&!admin)?' disabled':''}>`,'Private',' lead')}
<p class="cap">${f.private_by_address
@@ -716,6 +722,8 @@ function settingsModal(f: Feed, newUrl?: string){
// Sent only when it changed: anyone may mark a feed private, but only an admin unmark it.
const priv=$<HTMLInputElement>('#sprivate');
if(!priv.disabled&&priv.checked!==!!f.private) patch.private=priv.checked;
const inall=$<HTMLInputElement>('#sinall');
if(!inall.disabled&&inall.checked===!!f.hide_from_all) patch.hide_from_all=!inall.checked;
// The address is the same for everyone: an admin's to change, for a feed that moved, and the
// API refuses it from anyone else. The folder and category are not this dialog's at all.
if(admin) patch.url=$<HTMLInputElement>('#surl').value.trim();

View File

@@ -14,7 +14,7 @@ function renderFeed(){
const name=f?(f.title||f.id):sg?sg.name:v.title;
$<HTMLInputElement>('#epSearch').placeholder=`Search ${name}…`;
const kids=f?S.feeds.filter(c=>c.group===f.id):[];
const unreadAll=S.feeds.reduce((n,x)=>n+(x.unread||0),0);
const unreadAll=unreadInAll();
box.innerHTML = (f&&kids.length ? groupHead(f,kids) : f ? `
<div class="fhead slim">
${artHTML(f.image,name)}

View File

@@ -35,6 +35,11 @@ const VIEWS: Record<string,{title: string, icon: string, url?: string, blurb?: s
blurb:'Episodes you started and have not finished, across every feed you subscribe to. Pick one up where you left off.'},
':all':{title:'All Subscriptions',icon:ICON.all},
};
/// Whether a feed's items are in All Subscriptions: not when it, or the OPML or creator it came
/// from, is hidden from it (#288). The server leaves out the same.
const inAll=(f: Feed)=>!f.hide_from_all&&!(f.group&&S.feeds.some(g=>g.id===f.group&&g.hide_from_all));
/// All Subscriptions' unread count.
const unreadInAll=()=>S.feeds.reduce((n,f)=>n+(inAll(f)?f.unread||0:0),0);
/// Feeds being checked right now, from the event stream, and since when: their rows, and the row
/// of a folder holding one, carry a spinner.
const scanning=new Map<string, number>();
@@ -90,7 +95,7 @@ function renderFeeds(){
const drawn=new Map<string,HTMLImageElement[]>();
for(const img of $$<HTMLImageElement>('img',list)){ const k=img.outerHTML; if(!drawn.has(k)) drawn.set(k,[]); drawn.get(k)!.push(img); }
list.innerHTML='';
const unreadAll=S.feeds.reduce((n,f)=>n+(f.unread||0),0);
const unreadAll=unreadInAll();
const places=document.createElement('div');
places.className='places';
for(const [id,v] of Object.entries(VIEWS)){

View File

@@ -37,6 +37,8 @@ interface Feed {
pinned: boolean;
/** From an OPML, and yours to keep should it drop the feed (#280). */
kept: boolean;
/** Left out of All Subscriptions by its own setting; a show is also when its OPML is (#288). */
hide_from_all: boolean;
private: boolean;
private_by_address: boolean;
}