Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinned posts #2771

Merged
merged 19 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, does this affect the presentation of a post elsewhere in the app (other than a user's own author feed) when viewing their own pinned post?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use it to show "Pin post" or "Unpin post" in the context menu

}
},
"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": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually like that this could be used in the future for a feed gen to "pin" a post to the top of a feed

"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
38 changes: 32 additions & 6 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,47 @@ export const skeleton = async (inputs: {
if (clearlyBadCursor(params.cursor)) {
return { actor, filter: params.filter, items: [] }
}

const isFirstPageRequest = !params.cursor
const hasPinnedPost =
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 = res.items.map((item) => ({
post: { uri: item.uri, cid: item.cid || undefined },
repost: item.repost
? { uri: item.repost, cid: item.repostCid || undefined }
: undefined,
}))

if (hasPinnedPost && actor.profile?.pinnedPost) {
mozzius marked this conversation as resolved.
Show resolved Hide resolved
const pinnedItem = {
post: {
uri: actor.profile.pinnedPost.uri,
cid: actor.profile.pinnedPost.cid,
},
repost: undefined,
mozzius marked this conversation as resolved.
Show resolved Hide resolved
authorPinned: true,
}
Copy link
Member Author

@mozzius mozzius Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set authorPinned here, I'm not expecting the value to come from the dataplane or anything so I imagine it should be fine (but would be good to confirm)

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Kysely } from 'kysely'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend isn't using Scylla? What is this kysely about? Just curious

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the Postgres-based reference implementation


export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable('profile')
.addColumn('pinnedPost', 'boolean')
mozzius marked this conversation as resolved.
Show resolved Hide resolved
.execute()
await db.schema
.alterTable('profile')
.addColumn('pinnedPostCid', 'boolean')
.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