You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TL;DR Grow this from a tag-only deny list into a private, viewer-only content filter: mute a tagor a keyword/phrase (with * wildcards), and hide matching posts from your feed, your recommendations, and (later) search. Silent and one-directional like per-follow mute, but topic-level instead of person-level.
Motivation
Muting a whole tag is blunt. Someone who never wants crypto spam should not have to hunt down every tag it hides behind (#crypto, #bitcoin, #web3), and the stuff they want gone is often in the post text, not a tag at all. So the filter should also match words in the post body, with wildcards, and treat a muted tag as just one kind of entry in the same list.
Where
Feed:Vutuv.Posts.feed_page/2 (+ feed_post_items/3, feed_repost_items/3). Posts already carry tags via post_tags (Vutuv.Posts.PostTag, has_many :tags, through:), and the feed already drops muted follows (follows.muted) and blocks. This is the same shape one level up.
Recommendations: "Who to follow" (Vutuv.Social.most_followed_users/1 / Vutuv.Social.PopularUsers) and tag suggestions (Vutuv.Tags).
Search:VutuvWeb.SearchLive + Vutuv.Search (today has tag: / city: / @handle operators, no negative ones yet).
Siblings to reuse, not reinvent: per-follow mute (follows.muted, <.mute_button>, the post ⋯-menu "Mute @handle"), block (Vutuv.Social), #hashtag / honor tags (Vutuv.Tags), and the inverse Follow tags (Feature Request: Follow tags #872).
What: one list, two kinds of entry
Each entry is either:
Tag — an exact tag (crypto hides the tag and #crypto in a body).
Keyword/phrase — free text matched against the post's rendered plain text, with * wildcards.
Match rules (this is what makes it useful):
Case-insensitive.
Default whole-word, so cess does not hide "success" (the classic false-positive trap).
* = any run of characters, so the user opts into prefix/suffix/substring on purpose: crypto* → cryptocurrency, cryptobro; *coin → bitcoin, altcoin; *maxi* → anywhere.
A phrase ("machine learning") matches those words adjacent, in order.
Keyword entries match the post body and its tags/hashtags (muting the word crypto also catches #crypto); a Tag entry matches the tag only.
On a match, hide (do not vanish): collapse the post to a one-line placeholder ("Hidden: matches your filter crypto* · Show anyway") that reveals in place. A silently shorter feed confuses and breaks reply threads; a per-entry "remove completely" option can come later.
Never filter your own posts.
Private and one-directional: the list is yours alone. It never touches the author or anyone else, never notifies, never appears in the agent formats (.md/.txt/.json/.xml). It is sensitive (it reveals what a member dislikes: politics, health, identity), so it is owner-only, belongs in the GDPR export, and is never public.
UI sketch (a /settings page)
Muted words & tags [ + Add ]
─────────────────────────────────────────────────────────────
#crypto tag Unmute
crypto* word · whole-word off Unmute
"machine learning" phrase Unmute
politics word · until 12 Aug (snoozed) Unmute
─────────────────────────────────────────────────────────────
A hidden post shows a "Show anyway" line, never a gap.
Data model (new table)
Do not reuse the existing search_terms table: that is the fuzzy name-search index, unrelated. New table:
content_filters
id uuid v7 pk
user_id uuid v7 -> users (owner; private)
kind enum tag | keyword
pattern string tag slug, or keyword/phrase with * wildcards
whole_word boolean keyword only (default true; * overrides locally)
expires_at utc_datetime null = permanent (optional "snooze")
timestamps
Entry points
Settings: a new /settings/filters row ("Muted words & tags"), or folded into Privacy.
Post ⋯ menu: "Mute #tag" per tag on the post plus a quick "Mute a word…", right beside the existing "Mute @handle".
Engineering notes (the sharp edges)
Tags filter cheaply in the feed query (exclude posts whose post_tags intersect the muted set), exactly like the existing block/mute subqueries.
Keywords cannot be a join: compile the viewer's patterns once per request into one regex (glob * → .*, word boundaries unless *), match the post's plain text (strip markdown and mentions so **crypto** still matches), filter in Elixir after hydrate_posts.
Pagination hazard: filtering after the DB page yields short pages and a wrong "Load more" count. Push tag exclusion into the query and top up keyword-filtered pages inside the cursor loop (or over-fetch), do not just Enum.reject/2 the finished page.
Cap the list size and pattern length, and validate the glob, so a pathological pattern cannot build a catastrophic regex.
Open questions / extensions
Search: add negative operators (-word, -tag:x) to Vutuv.Search, and let the filter list pre-apply on /search. (This is the original "tag exclusion on the search page" idea, generalised.)
Notifications about a muted-topic post: filter them too, or leave? (Lean: leave for v1.)
A muted tag should also drop out of your own suggested-tags list. (Yes.)
Related
#872 (Follow tags, the inverse), per-follow mute (follows.muted), block (Vutuv.Social). Follow-up to #872.
Written with this GitHub account, but not necessarily by the human behind it — this may just be a note-taking issue that gets deleted later without ever being tackled.
TL;DR Grow this from a tag-only deny list into a private, viewer-only content filter: mute a tag or a keyword/phrase (with
*wildcards), and hide matching posts from your feed, your recommendations, and (later) search. Silent and one-directional like per-follow mute, but topic-level instead of person-level.Motivation
Muting a whole tag is blunt. Someone who never wants crypto spam should not have to hunt down every tag it hides behind (
#crypto,#bitcoin,#web3), and the stuff they want gone is often in the post text, not a tag at all. So the filter should also match words in the post body, with wildcards, and treat a muted tag as just one kind of entry in the same list.Where
Vutuv.Posts.feed_page/2(+feed_post_items/3,feed_repost_items/3). Posts already carry tags viapost_tags(Vutuv.Posts.PostTag,has_many :tags, through:), and the feed already drops muted follows (follows.muted) and blocks. This is the same shape one level up.Vutuv.Social.most_followed_users/1/Vutuv.Social.PopularUsers) and tag suggestions (Vutuv.Tags).VutuvWeb.SearchLive+Vutuv.Search(today hastag:/city:/@handleoperators, no negative ones yet).follows.muted,<.mute_button>, the post ⋯-menu "Mute @handle"), block (Vutuv.Social), #hashtag / honor tags (Vutuv.Tags), and the inverse Follow tags (Feature Request: Follow tags #872).What: one list, two kinds of entry
Each entry is either:
cryptohides the tag and#cryptoin a body).*wildcards.Match rules (this is what makes it useful):
cessdoes not hide "success" (the classic false-positive trap).*= any run of characters, so the user opts into prefix/suffix/substring on purpose:crypto*→ cryptocurrency, cryptobro;*coin→ bitcoin, altcoin;*maxi*→ anywhere."machine learning") matches those words adjacent, in order.cryptoalso catches#crypto); a Tag entry matches the tag only.On a match, hide (do not vanish): collapse the post to a one-line placeholder ("Hidden: matches your filter
crypto*· Show anyway") that reveals in place. A silently shorter feed confuses and breaks reply threads; a per-entry "remove completely" option can come later.Never filter your own posts.
Private and one-directional: the list is yours alone. It never touches the author or anyone else, never notifies, never appears in the agent formats (
.md/.txt/.json/.xml). It is sensitive (it reveals what a member dislikes: politics, health, identity), so it is owner-only, belongs in the GDPR export, and is never public.UI sketch (a /settings page)
Data model (new table)
Do not reuse the existing
search_termstable: that is the fuzzy name-search index, unrelated. New table:Entry points
/settings/filtersrow ("Muted words & tags"), or folded into Privacy./tags/:slug): a "Mute this tag" button (the mirror of Feature Request: Follow tags #872's follow).Engineering notes (the sharp edges)
post_tagsintersect the muted set), exactly like the existing block/mute subqueries.*→.*, word boundaries unless*), match the post's plain text (strip markdown and mentions so**crypto**still matches), filter in Elixir afterhydrate_posts.Enum.reject/2the finished page.Open questions / extensions
-word,-tag:x) toVutuv.Search, and let the filter list pre-apply on/search. (This is the original "tag exclusion on the search page" idea, generalised.)Related
#872 (Follow tags, the inverse), per-follow mute (
follows.muted), block (Vutuv.Social). Follow-up to #872.Written with this GitHub account, but not necessarily by the human behind it — this may just be a note-taking issue that gets deleted later without ever being tackled.