feat: Discovery alert filters + notification reasons (v0.9.6)

- Add 4th "Discovery" tab in Alert Filters for member/topic follow notifications,
  with per-source enable toggle, independent event-type filters, and per-entity
  mute chips (mute specific members/topics without unfollowing)
- Enrich notification event payloads with follow_mode, matched_member_name,
  matched_member_id, and matched_topic so each event knows why it was created
- Dispatcher branches on payload.source for member_follow/topic_follow events,
  checking source-level enabled toggle, per-event-type filters, and muted_ids/muted_tags
- Add _build_reason helper; ntfy messages append a "why" line (📌/👤/🏷)
- EventRow in notification history shows a small italic reason line
- Update How It Works: fix stale member/topic paragraph, add Discovery alerts item

Authored-by: Jack Levy
This commit is contained in:
Jack Levy
2026-03-14 13:21:22 -04:00
parent 91473e6464
commit 247a874c8d
5 changed files with 281 additions and 15 deletions

View File

@@ -75,7 +75,7 @@ def emit_bill_notification(
user_id=follow.user_id,
bill_id=bill.bill_id,
event_type=event_type,
payload=payload,
payload={**payload, "follow_mode": follow.follow_mode},
))
count += 1
if count:
@@ -97,7 +97,11 @@ def emit_member_follow_notifications(
if not followers:
return 0
from app.models.member import Member
member = db.get(Member, bill.sponsor_id)
payload = _build_payload(bill, action_summary, action_category, source="member_follow")
payload["matched_member_name"] = member.name if member else None
payload["matched_member_id"] = bill.sponsor_id
count = 0
for follow in followers:
if _is_duplicate(db, follow.user_id, bill.bill_id, event_type):
@@ -125,14 +129,16 @@ def emit_topic_follow_notifications(
from app.models.follow import Follow
from app.models.notification import NotificationEvent
# Collect unique followers across all matching tags
# Collect unique followers across all matching tags, recording the first matching tag per user
seen_user_ids: set[int] = set()
followers = []
follower_topic: dict[int, str] = {}
for tag in topic_tags:
for follow in db.query(Follow).filter_by(follow_type="topic", follow_value=tag).all():
if follow.user_id not in seen_user_ids:
seen_user_ids.add(follow.user_id)
followers.append(follow)
follower_topic[follow.user_id] = tag
if not followers:
return 0
@@ -146,7 +152,7 @@ def emit_topic_follow_notifications(
user_id=follow.user_id,
bill_id=bill.bill_id,
event_type=event_type,
payload=payload,
payload={**payload, "matched_topic": follower_topic.get(follow.user_id)},
))
count += 1
if count: