diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index b7cd668c996..15305b00a99 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -93,10 +93,7 @@ const hydration = async (inputs: { const [feedPostState, profileViewerState = {}] = await Promise.all([ ctx.hydrator.hydrateFeedItems(skeleton.items, params.viewer), params.viewer - ? ctx.hydrator.actor.getProfileViewerStates( - [skeleton.actor.did], - params.viewer, - ) + ? ctx.hydrator.hydrateProfileViewers([skeleton.actor.did], params.viewer) : undefined, ]) return mergeStates(feedPostState, profileViewerState) diff --git a/packages/bsky/src/api/app/bsky/graph/getRelationships.ts b/packages/bsky/src/api/app/bsky/graph/getRelationships.ts index d0fc43d53ab..47aaa6cd083 100644 --- a/packages/bsky/src/api/app/bsky/graph/getRelationships.ts +++ b/packages/bsky/src/api/app/bsky/graph/getRelationships.ts @@ -14,7 +14,10 @@ export default function (server: Server, ctx: AppContext) { }, } } - const res = await ctx.hydrator.actor.getProfileViewerStates(others, actor) + const res = await ctx.hydrator.actor.getProfileViewerStatesNaive( + others, + actor, + ) const relationships = others.map((did) => { const subject = res.get(did) return subject diff --git a/packages/bsky/src/data-plane/server/routes/relationships.ts b/packages/bsky/src/data-plane/server/routes/relationships.ts index c877c1112f3..d6029e169e8 100644 --- a/packages/bsky/src/data-plane/server/routes/relationships.ts +++ b/packages/bsky/src/data-plane/server/routes/relationships.ts @@ -26,8 +26,6 @@ export default (db: Database): Partial> => ({ db.db .selectFrom('list_item') .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') .where('list_mute.mutedByDid', '=', actorDid) .whereRef('list_item.subjectDid', '=', ref('actor.did')) .select('list_item.listUri') @@ -47,8 +45,6 @@ export default (db: Database): Partial> => ({ db.db .selectFrom('list_item') .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') .where('list_block.creator', '=', actorDid) .whereRef('list_item.subjectDid', '=', ref('actor.did')) .select('list_item.listUri') @@ -56,8 +52,6 @@ export default (db: Database): Partial> => ({ db.db .selectFrom('list_item') .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') .where('list_item.subjectDid', '=', actorDid) .whereRef('list_block.creator', '=', ref('actor.did')) .select('list_item.listUri') diff --git a/packages/bsky/src/hydration/actor.ts b/packages/bsky/src/hydration/actor.ts index f340221067d..de032468c22 100644 --- a/packages/bsky/src/hydration/actor.ts +++ b/packages/bsky/src/hydration/actor.ts @@ -106,7 +106,10 @@ export class ActorHydrator { }, new HydrationMap()) } - async getProfileViewerStates( + // "naive" because this method does not verify the existence of the list itself + // a later check in the main hydrator will remove list uris that have been deleted or + // repurposed to "curate lists" + async getProfileViewerStatesNaive( dids: string[], viewer: string, ): Promise { diff --git a/packages/bsky/src/hydration/hydrator.ts b/packages/bsky/src/hydration/hydrator.ts index f6b70f7173a..d7ead860336 100644 --- a/packages/bsky/src/hydration/hydrator.ts +++ b/packages/bsky/src/hydration/hydrator.ts @@ -86,6 +86,33 @@ export class Hydrator { this.label = new LabelHydrator(dataplane, opts) } + // app.bsky.actor.defs#profileView + // - profile viewer + // - list basic + // Note: builds on the naive profile viewer hydrator and removes references to lists that have been deleted + async hydrateProfileViewers( + dids: string[], + viewer: string, + ): Promise { + const profileViewers = await this.actor.getProfileViewerStatesNaive( + dids, + viewer, + ) + const listUris: string[] = [] + profileViewers?.forEach((item) => { + listUris.push(...listUrisFromProfileViewer(item)) + }) + const listState = await this.hydrateListsBasic(listUris, viewer) + // if a list no longer exists or is not a mod list, then remove from viewer state + profileViewers?.forEach((item) => { + removeNonModListsFromProfileViewer(item, listState) + }) + return mergeStates(listState, { + profileViewers, + viewer, + }) + } + // app.bsky.actor.defs#profileView // - profile // - list basic @@ -94,20 +121,14 @@ export class Hydrator { viewer: string | null, includeTakedowns = false, ): Promise { - const [actors, labels, profileViewers] = await Promise.all([ + const [actors, labels, profileViewersState] = await Promise.all([ this.actor.getActors(dids, includeTakedowns), this.label.getLabelsForSubjects(labelSubjectsForDid(dids)), - viewer ? this.actor.getProfileViewerStates(dids, viewer) : undefined, + viewer ? this.hydrateProfileViewers(dids, viewer) : undefined, ]) - const listUris: string[] = [] - profileViewers?.forEach((item) => { - listUris.push(...listUrisFromProfileViewer(item)) - }) - const listState = await this.hydrateListsBasic(listUris, viewer) - return mergeStates(listState, { + return mergeStates(profileViewersState ?? {}, { actors, labels, - profileViewers, viewer, }) } @@ -563,9 +584,37 @@ const listUrisFromProfileViewer = (item: ProfileViewerState | null) => { if (item?.blockingByList) { listUris.push(item.blockingByList) } + // blocked-by list does not appear in views, but will be used to evaluate the existence of a block between users. + if (item?.blockedByList) { + listUris.push(item.blockedByList) + } return listUris } +const removeNonModListsFromProfileViewer = ( + item: ProfileViewerState | null, + state: HydrationState, +) => { + if (!isModList(item?.mutedByList, state)) { + delete item?.mutedByList + } + if (!isModList(item?.blockingByList, state)) { + delete item?.blockingByList + } + if (!isModList(item?.blockedByList, state)) { + delete item?.blockedByList + } +} + +const isModList = ( + listUri: string | undefined, + state: HydrationState, +): boolean => { + if (!listUri) return false + const list = state.lists?.get(listUri) + return list?.record.purpose === 'app.bsky.graph.defs#modlist' +} + const labelSubjectsForDid = (dids: string[]) => { return [ ...dids,