diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 55075180ae..0e221dfafb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -79,6 +79,20 @@ export const skeleton = async (inputs: { if (clearlyBadCursor(params.cursor)) { return { actor, filter: params.filter, items: [] } } + + if (params.filter === 'pinned_posts') { + return { + actor, + filter: params.filter, + items: + actor.profile?.pinnedPosts?.map((uri) => ({ + post: { uri: uri, cid: undefined }, + repost: undefined, + })) || [], + cursor: undefined, + } + } + const res = await ctx.dataplane.getAuthorFeed({ actorDid: did, limit: params.limit, diff --git a/packages/bsky/src/hydration/feed.ts b/packages/bsky/src/hydration/feed.ts index edd29b851e..27f9ea2b12 100644 --- a/packages/bsky/src/hydration/feed.ts +++ b/packages/bsky/src/hydration/feed.ts @@ -20,6 +20,7 @@ export type Posts = HydrationMap export type PostViewerState = { like?: string repost?: string + pinned?: boolean } export type PostViewerStates = HydrationMap @@ -86,16 +87,16 @@ export class FeedHydrator { async getPostViewerStates( refs: ItemRef[], - viewer: string, + { did, pinnedPosts }: { did: string; pinnedPosts?: string[] }, ): Promise { if (!refs.length) return new HydrationMap() const [likes, reposts] = await Promise.all([ this.dataplane.getLikesByActorAndSubjects({ - actorDid: viewer, + actorDid: did, refs, }), this.dataplane.getRepostsByActorAndSubjects({ - actorDid: viewer, + actorDid: did, refs, }), ]) @@ -103,6 +104,7 @@ export class FeedHydrator { return acc.set(uri, { like: parseString(likes.uris[i]), repost: parseString(reposts.uris[i]), + pinned: pinnedPosts?.includes(uri) || undefined, }) }, new HydrationMap()) } diff --git a/packages/bsky/src/hydration/hydrator.ts b/packages/bsky/src/hydration/hydrator.ts index 4fa7ee8089..b61a760607 100644 --- a/packages/bsky/src/hydration/hydrator.ts +++ b/packages/bsky/src/hydration/hydrator.ts @@ -13,6 +13,7 @@ import { Actors, ProfileViewerStates, ProfileViewerState, + Actor, } from './actor' import { Follows, @@ -269,11 +270,14 @@ export class Hydrator { state: HydrationState = {}, ): Promise { const uris = refs.map((ref) => ref.uri) - const postsLayer0 = await this.feed.getPosts( - uris, - ctx.includeTakedowns, - state.posts, - ) + const [postsLayer0, viewer] = await Promise.all([ + this.feed.getPosts(uris, ctx.includeTakedowns, state.posts), + ctx.viewer + ? this.actor + .getActors([ctx.viewer]) + .then((v) => v.get(ctx.viewer as string)) + : undefined, + ]) // first level embeds plus thread roots we haven't fetched yet const urisLayer1 = nestedRecordUrisFromPosts(postsLayer0) const additionalRootUris = rootUrisFromPosts(postsLayer0) // supports computing threadgates @@ -329,7 +333,12 @@ export class Hydrator { ...postUrisLayer1.map(uriToRef), // supports aggregates on embed #viewRecords ...postUrisLayer2.map(uriToRef), ]), - ctx.viewer ? this.feed.getPostViewerStates(refs, ctx.viewer) : undefined, + ctx.viewer + ? this.feed.getPostViewerStates(refs, { + did: ctx.viewer, + pinnedPosts: viewer?.profile?.pinnedPosts, + }) + : undefined, this.label.getLabelsForSubjects(allPostUris, ctx.labelers), this.hydratePostBlocks(posts), this.hydrateProfiles(allPostUris.map(didFromUri), ctx), diff --git a/packages/bsky/src/views/index.ts b/packages/bsky/src/views/index.ts index 41e720a2e0..c9d0b8069e 100644 --- a/packages/bsky/src/views/index.ts +++ b/packages/bsky/src/views/index.ts @@ -109,6 +109,7 @@ export class Views { lists: profileAggs?.lists, feedgens: profileAggs?.feeds, labeler: actor?.isLabeler, + pinnedPosts: actor.profile?.pinnedPosts?.length, }, } } @@ -443,6 +444,7 @@ export class Views { ? { repost: viewer.repost, like: viewer.like, + pinned: viewer.pinned, replyDisabled: this.userReplyDisabled(uri, state), } : undefined,