Skip to content

Commit

Permalink
Pinned posts (bluesky-social#2771)
Browse files Browse the repository at this point in the history
* pinned posts lexicon

* codegen

* change lexicon, different approach

* codegen 2

* dataplane db migration

* move pinned post lexicon to right place

* add pinned posts optionally to getAuthorFeed

* remove type modification

* Clarify naming, add viewer state, add tests

* return pinnedPost with profileViewDetailed

* allow pinned replies in `posts_and_author_threads`

* clearer variable naming

* annotate type of `items`

* boolean --> varchar

* reuse authorDid in viewerPinned

* simplify test

* make pinned post not top post in test

* update snapshot

* changeset

---------

Co-authored-by: Eric Bailey <[email protected]>
Co-authored-by: dholms <[email protected]>
  • Loading branch information
3 people authored Sep 26, 2024
1 parent ed325d8 commit 2676206
Show file tree
Hide file tree
Showing 34 changed files with 2,339 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-olives-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/api": patch
---

Add pinned posts to profile record and getAuthorFeed
4 changes: 4 additions & 0 deletions lexicons/app/bsky/actor/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
"labels": {
"type": "array",
"items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
},
"pinnedPost": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions lexicons/app/bsky/actor/profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
"type": "ref",
"ref": "com.atproto.repo.strongRef"
},
"pinnedPost": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
},
"createdAt": { "type": "string", "format": "datetime" }
}
}
Expand Down
18 changes: 15 additions & 3 deletions lexicons/app/bsky/feed/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"like": { "type": "string", "format": "at-uri" },
"threadMuted": { "type": "boolean" },
"replyDisabled": { "type": "boolean" },
"embeddingDisabled": { "type": "boolean" }
"embeddingDisabled": { "type": "boolean" },
"pinned": { "type": "boolean" }
}
},
"feedViewPost": {
Expand All @@ -53,7 +54,7 @@
"properties": {
"post": { "type": "ref", "ref": "#postView" },
"reply": { "type": "ref", "ref": "#replyRef" },
"reason": { "type": "union", "refs": ["#reasonRepost"] },
"reason": { "type": "union", "refs": ["#reasonRepost", "#reasonPin"] },
"feedContext": {
"type": "string",
"description": "Context provided by feed generator that may be passed back alongside interactions.",
Expand Down Expand Up @@ -88,6 +89,10 @@
"indexedAt": { "type": "string", "format": "datetime" }
}
},
"reasonPin": {
"type": "object",
"properties": {}
},
"threadViewPost": {
"type": "object",
"required": ["post"],
Expand Down Expand Up @@ -171,7 +176,10 @@
"required": ["post"],
"properties": {
"post": { "type": "string", "format": "at-uri" },
"reason": { "type": "union", "refs": ["#skeletonReasonRepost"] },
"reason": {
"type": "union",
"refs": ["#skeletonReasonRepost", "#skeletonReasonPin"]
},
"feedContext": {
"type": "string",
"description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions.",
Expand All @@ -186,6 +194,10 @@
"repost": { "type": "string", "format": "at-uri" }
}
},
"skeletonReasonPin": {
"type": "object",
"properties": {}
},
"threadgateView": {
"type": "object",
"properties": {
Expand Down
4 changes: 4 additions & 0 deletions lexicons/app/bsky/feed/getAuthorFeed.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
"posts_and_author_threads"
],
"default": "posts_with_replies"
},
"includePins": {
"type": "boolean",
"default": false
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion lexicons/app/bsky/feed/threadgate.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": {
"type": "record",
"key": "tid",
"description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository..",
"description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.",
"record": {
"type": "object",
"required": ["post", "createdAt"],
Expand Down
35 changes: 32 additions & 3 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4190,6 +4190,10 @@ export const schemaDict = {
ref: 'lex:com.atproto.label.defs#label',
},
},
pinnedPost: {
type: 'ref',
ref: 'lex:com.atproto.repo.strongRef',
},
},
},
profileAssociated: {
Expand Down Expand Up @@ -4812,6 +4816,10 @@ export const schemaDict = {
type: 'ref',
ref: 'lex:com.atproto.repo.strongRef',
},
pinnedPost: {
type: 'ref',
ref: 'lex:com.atproto.repo.strongRef',
},
createdAt: {
type: 'string',
format: 'datetime',
Expand Down Expand Up @@ -5468,6 +5476,9 @@ export const schemaDict = {
embeddingDisabled: {
type: 'boolean',
},
pinned: {
type: 'boolean',
},
},
},
feedViewPost: {
Expand All @@ -5484,7 +5495,10 @@ export const schemaDict = {
},
reason: {
type: 'union',
refs: ['lex:app.bsky.feed.defs#reasonRepost'],
refs: [
'lex:app.bsky.feed.defs#reasonRepost',
'lex:app.bsky.feed.defs#reasonPin',
],
},
feedContext: {
type: 'string',
Expand Down Expand Up @@ -5536,6 +5550,10 @@ export const schemaDict = {
},
},
},
reasonPin: {
type: 'object',
properties: {},
},
threadViewPost: {
type: 'object',
required: ['post'],
Expand Down Expand Up @@ -5693,7 +5711,10 @@ export const schemaDict = {
},
reason: {
type: 'union',
refs: ['lex:app.bsky.feed.defs#skeletonReasonRepost'],
refs: [
'lex:app.bsky.feed.defs#skeletonReasonRepost',
'lex:app.bsky.feed.defs#skeletonReasonPin',
],
},
feedContext: {
type: 'string',
Expand All @@ -5713,6 +5734,10 @@ export const schemaDict = {
},
},
},
skeletonReasonPin: {
type: 'object',
properties: {},
},
threadgateView: {
type: 'object',
properties: {
Expand Down Expand Up @@ -6078,6 +6103,10 @@ export const schemaDict = {
],
default: 'posts_with_replies',
},
includePins: {
type: 'boolean',
default: false,
},
},
},
output: {
Expand Down Expand Up @@ -7162,7 +7191,7 @@ export const schemaDict = {
type: 'record',
key: 'tid',
description:
"Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository..",
"Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.",
record: {
type: 'object',
required: ['post', 'createdAt'],
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/client/types/app/bsky/actor/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons'
import { CID } from 'multiformats/cid'
import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs'
import * as AppBskyGraphDefs from '../graph/defs'
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'

export interface ProfileViewBasic {
did: string
Expand Down Expand Up @@ -74,6 +75,7 @@ export interface ProfileViewDetailed {
createdAt?: string
viewer?: ViewerState
labels?: ComAtprotoLabelDefs.Label[]
pinnedPost?: ComAtprotoRepoStrongRef.Main
[k: string]: unknown
}

Expand Down
1 change: 1 addition & 0 deletions packages/api/src/client/types/app/bsky/actor/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Record {
| ComAtprotoLabelDefs.SelfLabels
| { $type: string; [k: string]: unknown }
joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main
pinnedPost?: ComAtprotoRepoStrongRef.Main
createdAt?: string
[k: string]: unknown
}
Expand Down
40 changes: 38 additions & 2 deletions packages/api/src/client/types/app/bsky/feed/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface ViewerState {
threadMuted?: boolean
replyDisabled?: boolean
embeddingDisabled?: boolean
pinned?: boolean
[k: string]: unknown
}

Expand All @@ -73,7 +74,7 @@ export function validateViewerState(v: unknown): ValidationResult {
export interface FeedViewPost {
post: PostView
reply?: ReplyRef
reason?: ReasonRepost | { $type: string; [k: string]: unknown }
reason?: ReasonRepost | ReasonPin | { $type: string; [k: string]: unknown }
/** Context provided by feed generator that may be passed back alongside interactions. */
feedContext?: string
[k: string]: unknown
Expand Down Expand Up @@ -134,6 +135,22 @@ export function validateReasonRepost(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.defs#reasonRepost', v)
}

export interface ReasonPin {
[k: string]: unknown
}

export function isReasonPin(v: unknown): v is ReasonPin {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.feed.defs#reasonPin'
)
}

export function validateReasonPin(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.defs#reasonPin', v)
}

export interface ThreadViewPost {
post: PostView
parent?:
Expand Down Expand Up @@ -265,7 +282,10 @@ export function validateGeneratorViewerState(v: unknown): ValidationResult {

export interface SkeletonFeedPost {
post: string
reason?: SkeletonReasonRepost | { $type: string; [k: string]: unknown }
reason?:
| SkeletonReasonRepost
| SkeletonReasonPin
| { $type: string; [k: string]: unknown }
/** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */
feedContext?: string
[k: string]: unknown
Expand Down Expand Up @@ -300,6 +320,22 @@ export function validateSkeletonReasonRepost(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v)
}

export interface SkeletonReasonPin {
[k: string]: unknown
}

export function isSkeletonReasonPin(v: unknown): v is SkeletonReasonPin {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.feed.defs#skeletonReasonPin'
)
}

export function validateSkeletonReasonPin(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.defs#skeletonReasonPin', v)
}

export interface ThreadgateView {
uri?: string
cid?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface QueryParams {
| 'posts_with_media'
| 'posts_and_author_threads'
| (string & {})
includePins?: boolean
}

export type InputSchema = undefined
Expand Down
39 changes: 32 additions & 7 deletions packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,46 @@ export const skeleton = async (inputs: {
if (clearlyBadCursor(params.cursor)) {
return { actor, filter: params.filter, items: [] }
}

const isFirstPageRequest = !params.cursor
const shouldInsertPinnedPost =
isFirstPageRequest && params.includePins && !!actor.profile?.pinnedPost

const res = await ctx.dataplane.getAuthorFeed({
actorDid: did,
limit: params.limit,
cursor: params.cursor,
feedType: FILTER_TO_FEED_TYPE[params.filter],
})

let items: FeedItem[] = res.items.map((item) => ({
post: { uri: item.uri, cid: item.cid || undefined },
repost: item.repost
? { uri: item.repost, cid: item.repostCid || undefined }
: undefined,
}))

if (shouldInsertPinnedPost && actor.profile?.pinnedPost) {
const pinnedItem = {
post: {
uri: actor.profile.pinnedPost.uri,
cid: actor.profile.pinnedPost.cid,
},
authorPinned: true,
}
if (params.limit === 1) {
items[0] = pinnedItem
} else {
// filter pinned post from first page only
items = items.filter((item) => item.post.uri !== pinnedItem.post.uri)
items.unshift(pinnedItem)
}
}

return {
actor,
filter: params.filter,
items: res.items.map((item) => ({
post: { uri: item.uri, cid: item.cid || undefined },
repost: item.repost
? { uri: item.repost, cid: item.repostCid || undefined }
: undefined,
})),
items,
cursor: parseString(res.cursor),
}
}
Expand Down Expand Up @@ -147,7 +172,7 @@ const noBlocksOrMutedReposts = (inputs: {
skeleton.items = skeleton.items.filter((item) => {
return (
checkBlocksAndMutes(item) &&
(item.repost || selfThread.ok(item.post.uri))
(item.repost || item.authorPinned || selfThread.ok(item.post.uri))
)
})
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Kysely } from 'kysely'

export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable('profile')
.addColumn('pinnedPost', 'varchar')
.execute()
await db.schema
.alterTable('profile')
.addColumn('pinnedPostCid', 'varchar')
.execute()
}

export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('profile').dropColumn('pinnedPost').execute()
await db.schema.alterTable('profile').dropColumn('pinnedPostCid').execute()
}
1 change: 1 addition & 0 deletions packages/bsky/src/data-plane/server/db/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ export * as _20240723T220703655Z from './20240723T220703655Z-quotes'
export * as _20240801T193939827Z from './20240801T193939827Z-post-gate'
export * as _20240808T224251220Z from './20240808T224251220Z-post-gate-flags'
export * as _20240829T211238293Z from './20240829T211238293Z-simplify-actor-sync'
export * as _20240831T134810923Z from './20240831T134810923Z-pinned-posts'
2 changes: 2 additions & 0 deletions packages/bsky/src/data-plane/server/db/tables/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface Profile {
avatarCid: string | null
bannerCid: string | null
joinedViaStarterPackUri: string | null
pinnedPost: string | null
pinnedPostCid: string | null
createdAt: string
indexedAt: string
}
Expand Down
Loading

0 comments on commit 2676206

Please sign in to comment.