Block lists: words that hide items and keep them from downloading (#47)
Each person has a list for every feed they read and one per feed. An item whose title or text holds one of the words, matched as whole words so "ai" does not hide everything that "said" anything, is hidden from them and, since the scanner now keeps each subscriber's filters separate, is fetched only if someone else still wants it. Whole-word matching is not something LIKE can do on both SQLite and Postgres, so the matches are worked out in Rust into a `hidden` table whenever a list changes, someone subscribes, or a scan brings in new items, and the queries only look that table up. Both new tables are tables rather than columns because create_missing adds tables but never columns. Hidden counts as read for the reaper and for "others still want this file", since whoever it is hidden from is as done with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,11 @@ async function prefsModal(){
|
||||
</div>
|
||||
<span class="hint">Export saves your subscriptions as OPML for another podcast app. Import
|
||||
subscribes you to every feed in one.</span></div>
|
||||
<div class="field"><label for="sblock">Hide items mentioning</label>
|
||||
<input type="text" id="sblock" value="${esc((S.me?.blocked||[]).join(', '))}">
|
||||
<span class="hint">Comma separated words or phrases, in every feed you read. An item with
|
||||
one in its title or text is hidden from you and not downloaded for you. Each feed's
|
||||
settings can add more.</span></div>
|
||||
<div class="field"><label>Feeds are checked every</label>
|
||||
<span class="hint">${everyText(g.every_mins)}, for every feed that does not set its own.
|
||||
${admin?'This and the rest of the server\'s settings are on the <a href="/admin">admin page</a>.':'Only an admin changes this.'}</span></div>
|
||||
@@ -242,8 +247,18 @@ async function prefsModal(){
|
||||
$('#stheme').onchange=e=>setTheme(e.target.value,undefined,true);
|
||||
$('#smode').onchange=e=>setTheme(undefined,e.target.value,true);
|
||||
$('#gopml').onclick=opmlModal;
|
||||
$('#sblock').onchange=async e=>{
|
||||
const blocked=splitWords(e.target.value);
|
||||
try{
|
||||
await api('/api/me',{method:'PATCH',body:JSON.stringify({blocked})});
|
||||
if(S.me) S.me.blocked=blocked;
|
||||
toast('Saved'); await loadFeeds(true); loadEntries();
|
||||
}catch(err){ toast(err.message,true); }
|
||||
};
|
||||
}
|
||||
|
||||
const splitWords=(s: string)=>s.split(',').map(w=>w.trim()).filter(Boolean);
|
||||
|
||||
function settingsModal(f, newUrl?: string){
|
||||
const isGroup = S.feeds.some(c=>c.group===f.id);
|
||||
openModal(`<h3>${esc(f.title||f.id)}</h3>
|
||||
@@ -257,6 +272,11 @@ function settingsModal(f, newUrl?: string){
|
||||
<div class="field"><label>Keywords</label>
|
||||
<input type="text" id="skw" value="${esc(f.keywords.join(', '))}">
|
||||
<span class="hint">Comma separated. Empty takes everything.</span></div>
|
||||
${isGroup?'':`<div class="field"><label for="sblock">Hide items mentioning</label>
|
||||
<input type="text" id="sblock" value="${esc((f.blocked||[]).join(', '))}">
|
||||
<span class="hint">Comma separated words or phrases. An item with one in its title or text
|
||||
is hidden from you and not downloaded for you, as well as those your Settings hide
|
||||
everywhere.</span></div>`}
|
||||
<div class="field"><label>Max new downloads per scan</label>
|
||||
<input type="number" id="smax" min="0" value="${f.max_new_per_check??''}">
|
||||
<span class="hint">Blank follows the global default (${globalMax}). The rest wait for
|
||||
@@ -294,9 +314,10 @@ function settingsModal(f, newUrl?: string){
|
||||
const max=$('#smax').value;
|
||||
try{
|
||||
const patch: Record<string, unknown>={
|
||||
keywords:$('#skw').value.split(',').map(s=>s.trim()).filter(Boolean),
|
||||
keywords:splitWords($('#skw').value),
|
||||
max_new_per_check:max===''?null:Number(max),
|
||||
auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked};
|
||||
if($('#sblock')) patch.blocked=splitWords($('#sblock').value);
|
||||
// The shared half is an admin's to change, and the API refuses it from anyone else.
|
||||
if(S.me&&S.me.admin){
|
||||
patch.url=$('#surl').value.trim();
|
||||
|
||||
Reference in New Issue
Block a user