Extend dismissed-exclusion to mood feeds for consistency
Mood feeds now honor the same dismissed list as the brief: /api/feed accepts an exclude param (over-fetching to stay full), and the client passes the persisted dismissed set. Swapping a story away now keeps it gone everywhere — brief and browse — not just on the home view. Also simplified the feed filter path to the shared _prefs_sql_kw helper. Tests: feed exclude (91 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -83,7 +83,8 @@
|
|||||||
} else {
|
} else {
|
||||||
const mood = moods.find((m) => m.key === key);
|
const mood = moods.find((m) => m.key === key);
|
||||||
const q = P.param(P.merge(userPrefs, mood?.filter ?? {}));
|
const q = P.param(P.merge(userPrefs, mood?.filter ?? {}));
|
||||||
feed = (await getJSON(`/api/feed?limit=24${q ? '&' + q : ''}`)).items;
|
const ex = Array.from(dismissed).join(',');
|
||||||
|
feed = (await getJSON(`/api/feed?limit=24${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`)).items;
|
||||||
remember(feed);
|
remember(feed);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+11
-20
@@ -261,6 +261,7 @@ def create_app() -> FastAPI:
|
|||||||
limit: int = Query(30, ge=1, le=100),
|
limit: int = Query(30, ge=1, le=100),
|
||||||
offset: int = Query(0, ge=0),
|
offset: int = Query(0, ge=0),
|
||||||
prefs: str | None = Query(None),
|
prefs: str | None = Query(None),
|
||||||
|
exclude: str = Query("", description="comma-separated article ids the reader has dismissed"),
|
||||||
) -> FeedResponse:
|
) -> FeedResponse:
|
||||||
if topic and topic.lower() not in TOPICS:
|
if topic and topic.lower() not in TOPICS:
|
||||||
raise HTTPException(400, f"unknown topic: {topic}")
|
raise HTTPException(400, f"unknown topic: {topic}")
|
||||||
@@ -268,30 +269,20 @@ def create_app() -> FastAPI:
|
|||||||
raise HTTPException(400, f"unknown flavor: {flavor}")
|
raise HTTPException(400, f"unknown flavor: {flavor}")
|
||||||
fp = prefs_from_json(prefs)
|
fp = prefs_from_json(prefs)
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
excl = {int(x) for x in exclude.split(",") if x.strip().lstrip("-").isdigit()}
|
||||||
|
# Categorical filters (include/mute topics+flavors incl. active pauses,
|
||||||
|
# cortisol ceiling) go to SQL so nothing is truncated by ranking. Only
|
||||||
|
# word-boundary avoid-terms and dismissals need a Python pass.
|
||||||
|
kw = _prefs_sql_kw(fp, now)
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
if fp.is_empty():
|
if fp.avoid_terms or excl:
|
||||||
rows = queries.feed(
|
# Over-fetch enough to cover what the Python pass might remove.
|
||||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only, limit=limit, offset=offset
|
fetch_n = min(2000, (offset + limit) * 4 + 50 + len(excl))
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Categorical filters (include/mute topics+flavors incl. active
|
|
||||||
# pauses, cortisol ceiling) go to SQL so nothing is truncated by
|
|
||||||
# ranking. Only word-boundary avoid-terms need a Python pass, so
|
|
||||||
# over-fetch just enough to cover what they might remove.
|
|
||||||
kw = dict(
|
|
||||||
include_topics=fp.include_topics or None,
|
|
||||||
include_flavors=fp.include_flavors or None,
|
|
||||||
mute_topics=list(fp.muted_topics(now)) or None,
|
|
||||||
mute_flavors=list(fp.muted_flavors(now)) or None,
|
|
||||||
max_cortisol=fp.max_cortisol,
|
|
||||||
max_ragebait=fp.max_ragebait,
|
|
||||||
)
|
|
||||||
if fp.avoid_terms:
|
|
||||||
raw = queries.feed(
|
raw = queries.feed(
|
||||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||||
limit=min(2000, (offset + limit) * 4 + 50), offset=0, **kw,
|
limit=fetch_n, offset=0, **kw,
|
||||||
)
|
)
|
||||||
kept = filter_articles(raw, fp, now) # drops avoid-term matches
|
kept = [a for a in filter_articles(raw, fp, now) if a["id"] not in excl]
|
||||||
rows = kept[offset : offset + limit]
|
rows = kept[offset : offset + limit]
|
||||||
else:
|
else:
|
||||||
rows = queries.feed(
|
rows = queries.feed(
|
||||||
|
|||||||
@@ -74,3 +74,9 @@ def test_brief_filters_down_without_refill(client):
|
|||||||
def test_category_counts_match_filtered_feed(client):
|
def test_category_counts_match_filtered_feed(client):
|
||||||
counts = client.get("/api/category-counts", params={"prefs": json.dumps({"mute_topics": ["health"]})}).json()
|
counts = client.get("/api/category-counts", params={"prefs": json.dumps({"mute_topics": ["health"]})}).json()
|
||||||
assert all(c["topic"] != "health" for c in counts)
|
assert all(c["topic"] != "health" for c in counts)
|
||||||
|
|
||||||
|
|
||||||
|
def test_feed_excludes_dismissed(client):
|
||||||
|
r = client.get("/api/feed", params={"exclude": "1"})
|
||||||
|
ids = [i["id"] for i in r.json()["items"]]
|
||||||
|
assert 1 not in ids
|
||||||
|
|||||||
Reference in New Issue
Block a user