Signals in a folder of their own in the feed list, as an OPML's feeds are (#255)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -797,7 +797,7 @@ $('#feedlist').onkeydown=(ev: KeyboardEvent)=>{
|
||||
const row=(ev.target as Element).closest<HTMLElement>('[data-id]'); if(!row) return;
|
||||
const id=row.dataset.id!;
|
||||
if((ev.key==='Enter'||ev.key===' ')&&ev.target===row) row.click();
|
||||
else if(row.classList.contains('group')&&
|
||||
else if(row.matches('.group,.sigs')&&
|
||||
(ev.key==='ArrowRight'&&!expanded.has(id)||ev.key==='ArrowLeft'&&expanded.has(id))) toggleGroup(id);
|
||||
else return;
|
||||
ev.preventDefault(); ev.stopPropagation();
|
||||
|
||||
@@ -5,6 +5,7 @@ function renderFeed(){
|
||||
box.classList.remove('plain');
|
||||
const v=VIEWS[S.feed ?? ''];
|
||||
if(v&&v.url) return renderListed(v);
|
||||
if(S.feed===SIGNALS) return renderSignals();
|
||||
const f=v?null:S.feeds.find(x=>x.id===S.feed);
|
||||
const sg=signalOf(S.feed);
|
||||
$<HTMLButtonElement>('#tbRemove').disabled=!f;
|
||||
|
||||
@@ -19,7 +19,7 @@ async function loadFeeds(keepSel?: boolean){
|
||||
// All Subscriptions rather than picking one alphabetically. Nothing to land on at all (a
|
||||
// brand new account) leaves S.feed alone, so the empty state's own message shows instead.
|
||||
if(!keepSel && S.feeds.length){
|
||||
const known = S.feed && (VIEWS[S.feed] || signalOf(S.feed) || S.feeds.some(f=>f.id===S.feed));
|
||||
const known = S.feed && (VIEWS[S.feed] || S.feed===SIGNALS || signalOf(S.feed) || S.feeds.some(f=>f.id===S.feed));
|
||||
selectFeed(known ? S.feed! : ':all');
|
||||
}
|
||||
}
|
||||
@@ -103,10 +103,9 @@ function renderFeeds(){
|
||||
el.onpointerenter=el.ontouchstart=()=>prefetch(id);
|
||||
places.appendChild(el);
|
||||
}
|
||||
signalRows(places);
|
||||
list.appendChild(places);
|
||||
const shown=S.feeds.filter(f=>!q||(f.title||f.id).toLowerCase().includes(q));
|
||||
if(!shown.length){ list.insertAdjacentHTML('beforeend','<p class="empty" style="padding:20px 8px">No feeds.</p>'); return done(); }
|
||||
if(!shown.length){ signalFolder(list,q); list.insertAdjacentHTML('beforeend','<p class="empty" style="padding:20px 8px">No feeds.</p>'); return done(); }
|
||||
|
||||
// Feeds from a subscribed OPML sit under it, so the group reads as one thing. Pinned feeds
|
||||
// go first: a folder with its feeds under it, a feed from inside one lifted out of it.
|
||||
@@ -125,7 +124,10 @@ function renderFeeds(){
|
||||
// it is open.
|
||||
const lastTop=order.findIndex(([f,d])=>!d&&!f.pinned);
|
||||
const lastPin=(lastTop<0?order.filter(([f])=>f.pinned):order.slice(0,lastTop)).pop()?.[0];
|
||||
// The Signals folder goes under the pinned feeds, which are first because you put them there.
|
||||
let folder=false;
|
||||
for(const [f,depth] of order){
|
||||
if(!folder&&!depth&&!f.pinned){ signalFolder(list,q); folder=true; }
|
||||
const kids=inside(f).length;
|
||||
// A subscription holds no entries itself, so its counts are the sum of what is inside --
|
||||
// taken from every feed it holds, not just the ones a filter left showing.
|
||||
@@ -169,6 +171,7 @@ function renderFeeds(){
|
||||
for(const img of $$('img',el)){ const was=drawn.get(img.outerHTML)?.shift(); if(was) img.replaceWith(was); }
|
||||
list.appendChild(el);
|
||||
}
|
||||
if(!folder) signalFolder(list,q);
|
||||
paintScanning();
|
||||
done();
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function preloadLists(){
|
||||
/// `quiet` for a refresh nobody asked for (a live update): not a wait anyone sees, so not timed.
|
||||
async function loadEntries(append?: boolean, quiet?: boolean){
|
||||
// The Directory lists feeds, and Currently Listening draws its own episodes.
|
||||
if(!S.feed || VIEWS[S.feed]?.url) return;
|
||||
if(!S.feed || VIEWS[S.feed]?.url || S.feed===SIGNALS) return;
|
||||
const feed=S.feed, key=listKey(feed), asked=performance.now(), seq=++loadSeq;
|
||||
// From asking to the list drawn: what someone waits for after a click, under that click's trace
|
||||
// (#246). Once, when the list first shows: from what is kept, at once.
|
||||
|
||||
@@ -12,23 +12,75 @@ function signalOf(view: string|null){
|
||||
return id===null ? undefined : S.signals.find(s=>s.id===id);
|
||||
}
|
||||
|
||||
/// Your Signals under the places, and the way to make one.
|
||||
function signalRows(places: HTMLElement){
|
||||
for(const sg of S.signals){
|
||||
const id=':signal:'+sg.id, el=document.createElement('div');
|
||||
el.className='place'+(S.feed===id?' sel':'');
|
||||
el.tabIndex=0; el.dataset.id=id;
|
||||
el.innerHTML=`<span class="ico">${ICON.signal}</span><b>${esc(sg.name)}</b>`+
|
||||
`<span class="badge${sg.unread?'':' zero'}" title="${sg.unread} unread">${sg.unread>999?'999+':sg.unread}</span>`;
|
||||
el.onclick=()=>{ selectFeed(id); nav(false); };
|
||||
el.onpointerenter=el.ontouchstart=()=>prefetch(id);
|
||||
places.appendChild(el);
|
||||
/// The Signals folder's place: a folder of their own at the top of the feed list, open or closed as
|
||||
/// an OPML's is (#255). Under the places, a long list of them pushed the feeds down.
|
||||
const SIGNALS=':signals';
|
||||
// Tinted as a feed's initials are: a plain tile vanished into a selected row (see .art.ini).
|
||||
const sigArt=(icon=ICON.signal)=>`<span class="fart"><div class="art ini sig" style="--tint:var(--accent)">${icon}</div></span>`;
|
||||
function feedRow(id: string, cls: string, html: string, open: ()=>void){
|
||||
const el=document.createElement('div');
|
||||
el.className='feed '+cls+(S.feed===id?' sel':'');
|
||||
el.tabIndex=0; el.dataset.id=id; el.innerHTML=html;
|
||||
el.onclick=()=>{ open(); nav(false); };
|
||||
return el;
|
||||
}
|
||||
const badgeHTML=(n: number)=>`<span class="badge${n?'':' zero'}" title="${n} unread">${n>999?'999+':n}</span>`;
|
||||
function signalFolder(list: HTMLElement, q: string){
|
||||
const mine=S.signals.filter(s=>!q||s.name.toLowerCase().includes(q));
|
||||
if(q&&!mine.length) return;
|
||||
const open=expanded.has(SIGNALS)||!!q;
|
||||
const n=S.signals.length;
|
||||
list.appendChild(feedRow(SIGNALS,'sigs',
|
||||
`<button class="chev" aria-expanded="${open}" title="Show or hide your signals" aria-label="Show or hide your signals">${ICON.caret}</button>`+
|
||||
sigArt()+`<div class="txt"><b>Signals</b><small>${n?plural(n,'signal'):'What matters, out of the noise'}</small></div>`+
|
||||
badgeHTML(S.signals.reduce((k,s)=>k+s.unread,0)),
|
||||
()=>selectFeed(SIGNALS)));
|
||||
$('.chev',list.lastElementChild!).onclick=(ev: Event)=>{ ev.stopPropagation(); toggleGroup(SIGNALS); };
|
||||
if(!open) return;
|
||||
for(const sg of mine){
|
||||
const el=feedRow(':signal:'+sg.id,'child',
|
||||
`<span class="fart">${tileHTML(sg.name)}</span><div class="txt"><b>${esc(sg.name)}</b><small>${esc(sg.keywords.join(', '))}</small></div>`+badgeHTML(sg.unread),
|
||||
()=>selectFeed(':signal:'+sg.id));
|
||||
el.onpointerenter=el.ontouchstart=()=>prefetch(':signal:'+sg.id);
|
||||
list.appendChild(el);
|
||||
}
|
||||
list.appendChild(feedRow(':signal:new','child',sigArt(ICON.plus)+'<div class="txt"><b>New signal</b></div>',()=>signalModal()));
|
||||
}
|
||||
|
||||
/// The folder's page: every Signal, as an OPML's page lists its feeds.
|
||||
function renderSignals(){
|
||||
const unread=S.signals.reduce((k,s)=>k+s.unread,0);
|
||||
$('#count').textContent=`Signals: ${plural(S.signals.length,'signal')}, ${unread} unread`;
|
||||
$<HTMLButtonElement>('#tbRemove').disabled=true;
|
||||
syncTools(null);
|
||||
const box=$('#content');
|
||||
box.classList.add('plain');
|
||||
box.innerHTML=`
|
||||
<div class="fhead slim">
|
||||
<div class="art">${ICON.signal}</div>
|
||||
<div class="meta">
|
||||
<h2>Signals</h2>
|
||||
<div class="sub">Each one picks what matters out of the noise of your ${plural(S.feeds.length,'feed')} · ${unread} unread</div>
|
||||
</div>
|
||||
<div class="acts">
|
||||
<button class="btn ico" data-a="new" title="New signal" aria-label="New signal">${ICON.plus}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="childlist" id="siglist"></div>`;
|
||||
$<HTMLButtonElement>('#content .acts [data-a="new"]').onclick=()=>signalModal();
|
||||
const list=$('#siglist');
|
||||
if(!S.signals.length){
|
||||
list.innerHTML='<p class="empty">No signals yet. Make one to pull a subject out of every feed you subscribe to.</p>';
|
||||
return;
|
||||
}
|
||||
for(const sg of S.signals){
|
||||
const el=document.createElement('div');
|
||||
el.className='childrow';
|
||||
el.innerHTML=tileHTML(sg.name)+`<div class="txt"><b>${esc(sg.name)}</b>`+
|
||||
`<small class="meta">${esc(sg.keywords.join(', '))}</small></div><span class="badge${sg.unread?'':' zero'}">${sg.unread}</span>`;
|
||||
el.onclick=()=>selectFeed(':signal:'+sg.id);
|
||||
list.appendChild(el);
|
||||
}
|
||||
const add=document.createElement('div');
|
||||
add.className='place'; add.tabIndex=0; add.dataset.id=':signal:new';
|
||||
add.innerHTML=`<span class="ico">${ICON.plus}</span><b>New signal</b>`;
|
||||
add.onclick=()=>{ signalModal(); nav(false); };
|
||||
places.appendChild(add);
|
||||
}
|
||||
|
||||
function signalHead(sg: Signal){
|
||||
|
||||
Reference in New Issue
Block a user