grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: respect label negation when filtering hidden content

The hide filter was ignoring neg=1 (resolved) labels, so content
stayed hidden even after a moderator resolved the label. Now the
latest entry per label value wins — a negation unhides, and a
subsequent re-application hides again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+19 -4
+12 -2
server/labels/_hidden.ts
··· 10 10 "doxxing", 11 11 ]); 12 12 13 - /** SQL fragment: NOT EXISTS subquery filtering rows with hide-severity labels. `uriExpr` is the column to match against (e.g. "t.uri"). */ 13 + /** SQL fragment: NOT EXISTS subquery filtering rows with hide-severity labels. 14 + * A label is only active if its latest row (by cts) has neg = 0. 15 + * `uriExpr` is the column to match against (e.g. "t.uri"). */ 14 16 export function hideLabelsFilter(uriExpr: string): string { 15 17 const inList = [...HIDE_LABELS].map((v) => `'${v}'`).join(","); 16 - return `NOT EXISTS (SELECT 1 FROM _labels l WHERE l.uri = ${uriExpr} AND l.val IN (${inList}) AND l.neg = 0)`; 18 + return `NOT EXISTS ( 19 + SELECT 1 FROM _labels l 20 + WHERE l.uri = ${uriExpr} AND l.val IN (${inList}) 21 + AND l.neg = 0 22 + AND NOT EXISTS ( 23 + SELECT 1 FROM _labels l2 24 + WHERE l2.uri = l.uri AND l2.val = l.val AND l2.neg = 1 AND l2.cts > l.cts 25 + ) 26 + )`; 17 27 }
+7 -2
server/xrpc/getStories.ts
··· 59 59 ? ((await ctx.labels(storyUris)) as Map<string, Label[]>) 60 60 : new Map<string, Label[]>(); 61 61 62 - // Filter out stories with hide-severity labels 62 + // Filter out stories with active hide-severity labels (latest entry per val wins) 63 63 const visibleRows = rows.filter((row) => { 64 64 const labels = labelsByUri.get(row.uri); 65 65 if (!labels) return true; 66 - return !labels.some((l) => HIDE_LABELS.has(l.val) && !l.neg); 66 + const latestByVal = new Map<string, Label>(); 67 + for (const l of labels) { 68 + const prev = latestByVal.get(l.val); 69 + if (!prev || l.cts > prev.cts) latestByVal.set(l.val, l); 70 + } 71 + return ![...latestByVal.values()].some((l) => HIDE_LABELS.has(l.val) && !l.neg); 67 72 }); 68 73 69 74 // Cross-post lookup