The Directory comes from the directory service (#205)
ipx asks ipx-directory on its people's behalf, with the token an admin set in the new Admin, Directory tab, so the page never talks to it and never sees the token or a feed's address. /api/directory, /api/popular and a feed's Directory page answer from it; subscribing takes the address from the directory and goes through Add feed's steps. With no directory set up, the Directory says so. The local directory code is gone; its privacy checks are the snapshot builder's now, and their tests moved with them. With sharing on, ipx sends its snapshot after every scan, whenever the config changes (set_cfg) and every half hour: each public feed and how many here subscribe, never an OPML or Patreon creator itself or anything private. A feed can be marked private in its settings, for a paid feed whose address does not give it away; anyone subscribed may mark it, only an admin unmark it. The admin's category for a feed that names none goes with it, which the directory uses only while the feed names none, so the category an admin gives a blog still reaches the Directory. The directory lists only feeds it has read as a feed: the first snapshot after a start went before the first scan, when an OPML was not yet known to be one, and the directory listed the OPML. It now fetches what a snapshot brings in at once instead of within the minute. The browser suite runs its own ipx-directory on SQLite; the Directory's tests match feeds by title, since the directory names them by address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,11 @@ $('#addFeed').onclick=()=>{
|
||||
};
|
||||
};
|
||||
|
||||
// The rows carry an id, never a URL, so a key in someone's feed address never reaches this page.
|
||||
const NONE_LISTED='<p class="hint">Nothing yet. Feeds people here subscribe to show up here.</p>';
|
||||
// The rows come from the directory through iPX (#205), and carry its id, never a URL, so a key in
|
||||
// someone's feed address never reaches this page.
|
||||
const NONE_LISTED='<p class="hint">Nothing in the directory yet. Feeds people subscribe to show up here once their servers share them.</p>';
|
||||
// Why the directory could not be shown: none set up, or it did not answer. In iPX's words.
|
||||
let dirError='';
|
||||
|
||||
/// One listed feed: a tile in the Directory's grid or, given its place, a row in its chart. The
|
||||
/// parts are the same either way; the class lays them out. Yours opens its feed; any other, its
|
||||
@@ -57,7 +60,8 @@ function listedFeed(p,cls,rank?: number){
|
||||
// Green, as a downloaded file is: it is already yours. Plus, beside it, is the way to get one.
|
||||
(p.subscribed?`<span class="subbed" title="Subscribed: click to open it" aria-label="Subscribed">${ICON.subbed}</span>`
|
||||
:`<button class="btn ico" data-a="sub" title="Subscribe" aria-label="Subscribe">${ICON.plus}</button>`);
|
||||
if(p.subscribed){ el.onclick=()=>selectFeed(p.id); return el; }
|
||||
// Yours has an id here as well as the directory's, and opens by it.
|
||||
if(p.subscribed){ el.onclick=()=>selectFeed(p.feed||p.id); return el; }
|
||||
el.onclick=()=>{ dirShow=p.id; $('#content').scrollTop=0; drawDirectory(undefined,'.dback'); };
|
||||
// Its own click, not the tile's as well.
|
||||
$('[data-a="sub"]',el).onclick=ev=>{ ev.stopPropagation(); subscribeListed(p); };
|
||||
@@ -66,9 +70,9 @@ function listedFeed(p,cls,rank?: number){
|
||||
|
||||
async function subscribeListed(p){
|
||||
try{
|
||||
await api(`/api/popular/${encodeURIComponent(p.id)}`,{method:'POST'});
|
||||
const r=await api(`/api/popular/${encodeURIComponent(p.id)}`,{method:'POST'});
|
||||
toast(`Subscribed to ${NAME(p)}`);
|
||||
await loadFeeds(true); selectFeed(p.id);
|
||||
await loadFeeds(true); selectFeed(r.id);
|
||||
}catch(e){ toast(e.message,true); }
|
||||
}
|
||||
|
||||
@@ -99,7 +103,8 @@ const KIND_WORD={All:'feed',Podcasts:'podcast',Blogs:'blog'};
|
||||
async function renderDirectory(url,box){
|
||||
// Each visit starts at the front, as going back to a tab does.
|
||||
dirCat=null; dirSub=null; dirShow=null;
|
||||
try{ dirRows=await api(url)||[]; }catch{ dirRows=[]; }
|
||||
dirError='';
|
||||
try{ dirRows=await api(url)||[]; }catch(e){ dirRows=[]; dirError=e.message; }
|
||||
drawDirectory(box);
|
||||
return dirRows.length;
|
||||
}
|
||||
@@ -107,7 +112,7 @@ async function renderDirectory(url,box){
|
||||
/// `focus` is the control to put the keyboard back on, since the whole page is redrawn.
|
||||
function drawDirectory(box=$('#directory'), focus?: string){
|
||||
if(!box) return;
|
||||
if(!dirRows.length){ box.innerHTML=NONE_LISTED; return; }
|
||||
if(!dirRows.length){ box.innerHTML=dirError?`<p class="hint">${esc(dirError)}</p>`:NONE_LISTED; return; }
|
||||
// Only a choice when the server has both kinds.
|
||||
const both=dirRows.some(KINDS.Podcasts)&&dirRows.some(KINDS.Blogs);
|
||||
if(!both) dirKind='All';
|
||||
@@ -137,7 +142,7 @@ function drawDirectory(box=$('#directory'), focus?: string){
|
||||
<div class="show">${artHTML(show.image,NAME(show))}<div class="meta"><h2>${esc(NAME(show))}</h2>${
|
||||
// Its category's page is one click away from here.
|
||||
show.category?`<button type="button" class="dlink" data-go="${esc(show.category)}">${esc(show.subcategory||show.category)}</button>`:''}
|
||||
${n?`<p class="sub">${n} ${n===1?'person here subscribes':'people here subscribe'}</p>`:''}
|
||||
${n?`<p class="sub">${n} ${n===1?'person subscribes':'people subscribe'}</p>`:''}
|
||||
<p class="sub dfail" id="dfail" hidden></p>
|
||||
<div class="acts"><button class="btn ico primary" data-a="sub" title="Subscribe" aria-label="Subscribe">${ICON.plus}</button></div></div></div>
|
||||
<div class="about" id="dabout" hidden><p></p><button type="button" class="dlink" aria-expanded="false" hidden>More</button></div>`
|
||||
@@ -153,7 +158,7 @@ function drawDirectory(box=$('#directory'), focus?: string){
|
||||
for(const p of rows){ const c=p.category||OTHER; if(!cats.has(c)) cats.set(c,[]); cats.get(c).push(p); }
|
||||
// Other, the feeds with no category, goes last.
|
||||
const order=[...cats.keys()].sort((a,b)=>Number(a===OTHER)-Number(b===OTHER)||a.localeCompare(b));
|
||||
html=head('Directory',`${count(rows.length)} on this server. Private feeds are never listed.`)
|
||||
html=head('Directory',`${count(rows.length)} shared by iPX servers. Private feeds are never listed.`)
|
||||
+(chart.length?sec('Most subscribed',board):'')
|
||||
+sec('Categories',`<div class="cats">${order.map(c=>catTile(c,catName(c),count(cats.get(c).length),cats.get(c))).join('')}</div>`,
|
||||
`<button type="button" class="dall" data-go="${EVERY}">See all ${rows.length.toLocaleString()}</button>`);
|
||||
@@ -227,8 +232,7 @@ async function fillShow(box,p){
|
||||
return `<article class="lep"><b>${e.link?`<a href="${esc(e.link)}" target="_blank" rel="noopener">${esc(name.text)}</a>`:esc(name.text)}</b>`
|
||||
+(text?`<p>${esc(text)}</p>`:'')
|
||||
+(when.length?`<small>${when.map(s=>`<span>${s}</span>`).join('<span class="dot"></span>')}</small>`:'')+'</article>';
|
||||
}).join(''):`<p class="hint">Nothing read from it yet.${p.subscribers||got.failing?''
|
||||
:' A feed nobody here subscribes to is checked once a day.'}</p>`;
|
||||
}).join(''):'<p class="hint">Nothing read from it yet. The directory checks each feed every few hours.</p>';
|
||||
}
|
||||
|
||||
/// A category as a tile: its name, how many feeds, and the covers of its three most subscribed
|
||||
@@ -539,6 +543,13 @@ function settingsModal(f, 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="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
|
||||
?'Its address or login shows it is someone’s own, so it is never shared with the directory.'
|
||||
:f.private&&!admin
|
||||
?'Never shared with the directory. Only an admin can share it again.'
|
||||
:`Never shared with the directory, for ${isGroup?'shows':'a show'} you pay for. iPX knows most paid feeds by their address; this is for the ones it does not.`}</p></div>
|
||||
</div>
|
||||
<div class="cardacts fsetfoot"><button class="btn ico" onclick="closeModal()" title="Cancel" aria-label="Cancel">${ICON.close}</button>
|
||||
<button class="btn ico primary" id="ssave" title="Save" aria-label="Save">${ICON.check}</button></div>`, false, 'fset');
|
||||
@@ -625,6 +636,9 @@ function settingsModal(f, newUrl?: string){
|
||||
auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked, deadair:$('#sdead').checked,
|
||||
deadair_pause:Number($('#sdpause').value)};
|
||||
if($('#sblock')) patch.blocked=chipWords($('#sblock'));
|
||||
// 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;
|
||||
// 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=$('#surl').value.trim();
|
||||
|
||||
Reference in New Issue
Block a user