From fff33dfbb2191dfefd1e786eb59529e5315b7d6a Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Wed, 21 Oct 2026 14:21:29 -0500 Subject: [PATCH] Filter feed generators and lists --- package.json | 2 +- src/js/dataLayer/derived.js | 48 +++++++- tests/unit/specs/dataLayer/derived.test.js | 133 +++++++++++++++++++++ 3 files changed, 176 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 05ba14bf..5ca37cbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.20.122", + "version": "0.20.123", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/dataLayer/derived.js b/src/js/dataLayer/derived.js index f49b72b5..47108736 100644 --- a/src/js/dataLayer/derived.js +++ b/src/js/dataLayer/derived.js @@ -146,6 +146,17 @@ function shouldHideTypeaheadProfile(profile, query, preferences) { return preferences.hasHideLabel(accountLabels); } +// Created items == feed generators and lists +function shouldHideCreatedItem(item, preferences) { + const creator = item.creator; + if (creator.viewer?.blocking || creator.viewer?.blockedBy) return true; + if (!preferences) return false; + return preferences.hasHideLabel([ + ...(item.labels ?? []), + ...(creator.labels ?? []).filter(isAccountLabel), + ]); +} + function filterBlockedReactions(reactions, memberProfiles) { return (reactions || []).filter((reaction) => { const profile = memberProfiles.find( @@ -653,17 +664,33 @@ export class Derived extends ReactiveStore { this.$actorFeeds = new ComputedMap((did) => { const data = this.dataStore.$actorFeeds.get(did); if (!data) return data; + const currentUserDid = this.$currentUser.get()?.did; + const preferences = this.$preferences.get(); return { ...data, - feeds: data.feeds.map((feed) => this.hydrateCreator(feed)), + feeds: data.feeds + .map((feed) => this.hydrateCreator(feed)) + .filter( + (feed) => + feed.creator.did === currentUserDid || + !shouldHideCreatedItem(feed, preferences), + ), }; }); this.$actorLists = new ComputedMap((did) => { const data = this.dataStore.$actorLists.get(did); if (!data) return data; + const currentUserDid = this.$currentUser.get()?.did; + const preferences = this.$preferences.get(); return { ...data, - lists: data.lists.map((list) => this.hydrateCreator(list)), + lists: data.lists + .map((list) => this.hydrateCreator(list)) + .filter( + (list) => + list.creator.did === currentUserDid || + !shouldHideCreatedItem(list, preferences), + ), }; }); this.$actorStarterPacks = new ComputedMap((did) => { @@ -679,10 +706,19 @@ export class Derived extends ReactiveStore { this.$popularFeeds = new Signal.Computed(() => { const feeds = this.dataStore.$popularFeeds.get(); if (!feeds) return null; - return feeds.map( - (feed) => - this.$feedGenerators.get(feed.uri) ?? this.hydrateCreator(feed), - ); + const currentUserDid = this.$currentUser.get()?.did; + const preferences = this.$preferences.get(); + return feeds + .map( + (feed) => + this.$feedGenerators.get(feed.uri) ?? this.hydrateCreator(feed), + ) + .filter( + (feed) => + feed.creator.did === currentUserDid || + (!feed.creator.viewer?.muted && + !shouldHideCreatedItem(feed, preferences)), + ); }); this.$listsWithMembershipByActor = new ComputedMap((did) => this.dataStore.$listsWithMembershipByActor.get(did), diff --git a/tests/unit/specs/dataLayer/derived.test.js b/tests/unit/specs/dataLayer/derived.test.js index ce9f825d..1705ac5f 100644 --- a/tests/unit/specs/dataLayer/derived.test.js +++ b/tests/unit/specs/dataLayer/derived.test.js @@ -703,6 +703,139 @@ describe("$actorFeeds", () => { }); }); +describe("feed generator and list moderation", () => { + const ownerDid = "did:plc:owner"; + const labelerDid = "did:plc:labeler"; + + function makeLabelPreferences() { + return new Preferences( + [], + [ + createLabelerView({ + did: labelerDid, + handle: "labeler.test", + labelDefinitions: [ + { identifier: "spam", blurs: "content", defaultSetting: "hide" }, + { identifier: "rude", blurs: "content", defaultSetting: "warn" }, + ], + }), + ], + ); + } + + function label(uri, val) { + return { src: labelerDid, uri, val, cts: "2025-01-01T00:00:00.000Z" }; + } + + const plainCreator = createProfile({ did: ownerDid, handle: "owner.test" }); + const blockingCreator = createProfile({ + did: "did:plc:blocking", + handle: "blocking.test", + viewer: { blocking: "at://did:plc:me/app.bsky.graph.block/1" }, + }); + const blockedByCreator = createProfile({ + did: "did:plc:blockedby", + handle: "blockedby.test", + viewer: { blockedBy: true }, + }); + const mutedCreator = createProfile({ + did: "did:plc:muted", + handle: "muted.test", + viewer: { muted: true }, + }); + const labeledCreator = createProfile({ + did: "did:plc:labeled", + handle: "labeled.test", + labels: [label("did:plc:labeled", "spam")], + }); + + function item(uri, creator, labels = []) { + return { uri, creator, labels }; + } + + const items = [ + item("plain", plainCreator), + item("hide-labeled", plainCreator, [label("hide-labeled", "spam")]), + item("warn-labeled", plainCreator, [label("warn-labeled", "rude")]), + item("blocking", blockingCreator), + item("blocked-by", blockedByCreator), + item("muted", mutedCreator), + item("labeled-creator", labeledCreator), + ]; + + function setup() { + const dataStore = new DataStore(createSessionState(null)); + const { derived } = makeDerived(dataStore, { + preferences: makeLabelPreferences(), + }); + dataStore.setProfiles([ + plainCreator, + blockingCreator, + blockedByCreator, + mutedCreator, + labeledCreator, + ]); + return { dataStore, derived }; + } + + function uris(collection) { + return collection.map((entry) => entry.uri); + } + + it("should hide blocked and hide-labeled feeds on a profile but keep muted creators", () => { + const { dataStore, derived } = setup(); + dataStore.$actorFeeds.set(ownerDid, { feeds: items, cursor: "c" }); + assert.deepEqual(uris(derived.$actorFeeds.get(ownerDid).feeds), [ + "plain", + "warn-labeled", + "muted", + ]); + }); + + it("should hide blocked and hide-labeled lists on a profile but keep muted creators", () => { + const { dataStore, derived } = setup(); + dataStore.$actorLists.set(ownerDid, { lists: items, cursor: "c" }); + assert.deepEqual(uris(derived.$actorLists.get(ownerDid).lists), [ + "plain", + "warn-labeled", + "muted", + ]); + }); + + it("should also hide feeds from muted creators in popular feeds", () => { + const { dataStore, derived } = setup(); + dataStore.$popularFeeds.set(items); + assert.deepEqual(uris(derived.$popularFeeds.get()), [ + "plain", + "warn-labeled", + ]); + }); + + it("should never hide the current user's own feeds and lists", () => { + const { dataStore, derived } = setup(); + dataStore.setCurrentUser(labeledCreator); + const ownItems = [ + item("own-labeled", labeledCreator, [label("own-labeled", "spam")]), + ]; + dataStore.$actorFeeds.set(labeledCreator.did, { + feeds: ownItems, + cursor: "c", + }); + dataStore.$actorLists.set(labeledCreator.did, { + lists: ownItems, + cursor: "c", + }); + dataStore.$popularFeeds.set(ownItems); + assert.deepEqual(uris(derived.$actorFeeds.get(labeledCreator.did).feeds), [ + "own-labeled", + ]); + assert.deepEqual(uris(derived.$actorLists.get(labeledCreator.did).lists), [ + "own-labeled", + ]); + assert.deepEqual(uris(derived.$popularFeeds.get()), ["own-labeled"]); + }); +}); + describe("$profileChatStatus", () => { const did = "did:plc:user"; -- knotmirror 0.0.1