@atproto/bsky 0.0.83 → 0.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
  3. package/dist/api/app/bsky/feed/getAuthorFeed.js +27 -7
  4. package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
  5. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js +3 -1
  6. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
  7. package/dist/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.d.ts +4 -0
  8. package/dist/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.d.ts.map +1 -0
  9. package/dist/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.js +20 -0
  10. package/dist/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.js.map +1 -0
  11. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  12. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  13. package/dist/data-plane/server/db/migrations/index.js +2 -1
  14. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  15. package/dist/data-plane/server/db/tables/profile.d.ts +2 -0
  16. package/dist/data-plane/server/db/tables/profile.d.ts.map +1 -1
  17. package/dist/hydration/feed.d.ts +5 -0
  18. package/dist/hydration/feed.d.ts.map +1 -1
  19. package/dist/hydration/feed.js.map +1 -1
  20. package/dist/lexicon/lexicons.d.ts +33 -0
  21. package/dist/lexicon/lexicons.d.ts.map +1 -1
  22. package/dist/lexicon/lexicons.js +42 -3
  23. package/dist/lexicon/lexicons.js.map +1 -1
  24. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +2 -0
  25. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  26. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  27. package/dist/lexicon/types/app/bsky/actor/profile.d.ts +1 -0
  28. package/dist/lexicon/types/app/bsky/actor/profile.d.ts.map +1 -1
  29. package/dist/lexicon/types/app/bsky/actor/profile.js.map +1 -1
  30. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +13 -2
  31. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  32. package/dist/lexicon/types/app/bsky/feed/defs.js +21 -1
  33. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  34. package/dist/lexicon/types/app/bsky/feed/getAuthorFeed.d.ts +1 -0
  35. package/dist/lexicon/types/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
  36. package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts +2 -0
  37. package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
  38. package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts +2 -0
  39. package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts.map +1 -1
  40. package/dist/views/index.d.ts +4 -0
  41. package/dist/views/index.d.ts.map +1 -1
  42. package/dist/views/index.js +22 -1
  43. package/dist/views/index.js.map +1 -1
  44. package/package.json +12 -12
  45. package/src/api/app/bsky/feed/getAuthorFeed.ts +32 -7
  46. package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +4 -1
  47. package/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts +17 -0
  48. package/src/data-plane/server/db/migrations/index.ts +1 -0
  49. package/src/data-plane/server/db/tables/profile.ts +2 -0
  50. package/src/hydration/feed.ts +9 -1
  51. package/src/lexicon/lexicons.ts +44 -3
  52. package/src/lexicon/types/app/bsky/actor/defs.ts +2 -0
  53. package/src/lexicon/types/app/bsky/actor/profile.ts +1 -0
  54. package/src/lexicon/types/app/bsky/feed/defs.ts +38 -2
  55. package/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +1 -0
  56. package/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts +2 -0
  57. package/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +2 -0
  58. package/src/views/index.ts +22 -2
  59. package/tests/views/__snapshots__/author-feed.test.ts.snap +1795 -0
  60. package/tests/views/author-feed.test.ts +132 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/bsky",
3
- "version": "0.0.83",
3
+ "version": "0.0.84",
4
4
  "license": "MIT",
5
5
  "description": "Reference implementation of app.bsky App View (Bluesky API)",
6
6
  "keywords": [
@@ -41,15 +41,15 @@
41
41
  "structured-headers": "^1.0.1",
42
42
  "typed-emitter": "^2.1.0",
43
43
  "uint8arrays": "3.0.0",
44
- "@atproto/api": "^0.13.7",
45
- "@atproto/common": "^0.4.2",
44
+ "@atproto/api": "^0.13.8",
45
+ "@atproto/common": "^0.4.3",
46
46
  "@atproto/crypto": "^0.4.1",
47
- "@atproto/identity": "^0.4.1",
48
- "@atproto/lexicon": "^0.4.1",
49
- "@atproto/repo": "^0.5.1",
50
- "@atproto/sync": "^0.1.1",
47
+ "@atproto/identity": "^0.4.2",
48
+ "@atproto/lexicon": "^0.4.2",
49
+ "@atproto/repo": "^0.5.2",
50
+ "@atproto/sync": "^0.1.2",
51
51
  "@atproto/syntax": "^0.3.0",
52
- "@atproto/xrpc-server": "^0.6.4"
52
+ "@atproto/xrpc-server": "^0.7.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@bufbuild/buf": "^1.28.1",
@@ -64,10 +64,10 @@
64
64
  "axios": "^0.27.2",
65
65
  "jest": "^28.1.2",
66
66
  "ts-node": "^10.8.2",
67
- "@atproto/api": "^0.13.7",
68
- "@atproto/lex-cli": "^0.5.0",
69
- "@atproto/pds": "^0.4.59",
70
- "@atproto/xrpc": "^0.6.2"
67
+ "@atproto/api": "^0.13.8",
68
+ "@atproto/lex-cli": "^0.5.1",
69
+ "@atproto/pds": "^0.4.60",
70
+ "@atproto/xrpc": "^0.6.3"
71
71
  },
72
72
  "scripts": {
73
73
  "codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/*",
@@ -79,21 +79,46 @@ export const skeleton = async (inputs: {
79
79
  if (clearlyBadCursor(params.cursor)) {
80
80
  return { actor, filter: params.filter, items: [] }
81
81
  }
82
+
83
+ const isFirstPageRequest = !params.cursor
84
+ const shouldInsertPinnedPost =
85
+ isFirstPageRequest && params.includePins && !!actor.profile?.pinnedPost
86
+
82
87
  const res = await ctx.dataplane.getAuthorFeed({
83
88
  actorDid: did,
84
89
  limit: params.limit,
85
90
  cursor: params.cursor,
86
91
  feedType: FILTER_TO_FEED_TYPE[params.filter],
87
92
  })
93
+
94
+ let items: FeedItem[] = res.items.map((item) => ({
95
+ post: { uri: item.uri, cid: item.cid || undefined },
96
+ repost: item.repost
97
+ ? { uri: item.repost, cid: item.repostCid || undefined }
98
+ : undefined,
99
+ }))
100
+
101
+ if (shouldInsertPinnedPost && actor.profile?.pinnedPost) {
102
+ const pinnedItem = {
103
+ post: {
104
+ uri: actor.profile.pinnedPost.uri,
105
+ cid: actor.profile.pinnedPost.cid,
106
+ },
107
+ authorPinned: true,
108
+ }
109
+ if (params.limit === 1) {
110
+ items[0] = pinnedItem
111
+ } else {
112
+ // filter pinned post from first page only
113
+ items = items.filter((item) => item.post.uri !== pinnedItem.post.uri)
114
+ items.unshift(pinnedItem)
115
+ }
116
+ }
117
+
88
118
  return {
89
119
  actor,
90
120
  filter: params.filter,
91
- items: res.items.map((item) => ({
92
- post: { uri: item.uri, cid: item.cid || undefined },
93
- repost: item.repost
94
- ? { uri: item.repost, cid: item.repostCid || undefined }
95
- : undefined,
96
- })),
121
+ items,
97
122
  cursor: parseString(res.cursor),
98
123
  }
99
124
  }
@@ -147,7 +172,7 @@ const noBlocksOrMutedReposts = (inputs: {
147
172
  skeleton.items = skeleton.items.filter((item) => {
148
173
  return (
149
174
  checkBlocksAndMutes(item) &&
150
- (item.repost || selfThread.ok(item.post.uri))
175
+ (item.repost || item.authorPinned || selfThread.ok(item.post.uri))
151
176
  )
152
177
  })
153
178
  } else {
@@ -71,6 +71,7 @@ const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
71
71
  { headers: params.headers },
72
72
  )
73
73
  return {
74
+ isFallback: !res.data.relativeToDid,
74
75
  suggestedDids: res.data.actors.map((a) => a.did),
75
76
  headers: res.headers,
76
77
  }
@@ -80,6 +81,7 @@ const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
80
81
  relativeToDid,
81
82
  })
82
83
  return {
84
+ isFallback: true,
83
85
  suggestedDids: dids,
84
86
  }
85
87
  }
@@ -113,7 +115,7 @@ const presentation = (
113
115
  const suggestions = mapDefined(suggestedDids, (did) =>
114
116
  ctx.views.profileDetailed(did, hydration),
115
117
  )
116
- return { suggestions, headers }
118
+ return { isFallback: skeleton.isFallback, suggestions, headers }
117
119
  }
118
120
 
119
121
  type Context = {
@@ -129,6 +131,7 @@ type Params = QueryParams & {
129
131
  }
130
132
 
131
133
  type SkeletonState = {
134
+ isFallback: boolean
132
135
  suggestedDids: string[]
133
136
  headers?: Record<string, string>
134
137
  }
@@ -0,0 +1,17 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .alterTable('profile')
6
+ .addColumn('pinnedPost', 'varchar')
7
+ .execute()
8
+ await db.schema
9
+ .alterTable('profile')
10
+ .addColumn('pinnedPostCid', 'varchar')
11
+ .execute()
12
+ }
13
+
14
+ export async function down(db: Kysely<unknown>): Promise<void> {
15
+ await db.schema.alterTable('profile').dropColumn('pinnedPost').execute()
16
+ await db.schema.alterTable('profile').dropColumn('pinnedPostCid').execute()
17
+ }
@@ -43,3 +43,4 @@ export * as _20240723T220703655Z from './20240723T220703655Z-quotes'
43
43
  export * as _20240801T193939827Z from './20240801T193939827Z-post-gate'
44
44
  export * as _20240808T224251220Z from './20240808T224251220Z-post-gate-flags'
45
45
  export * as _20240829T211238293Z from './20240829T211238293Z-simplify-actor-sync'
46
+ export * as _20240831T134810923Z from './20240831T134810923Z-pinned-posts'
@@ -9,6 +9,8 @@ export interface Profile {
9
9
  avatarCid: string | null
10
10
  bannerCid: string | null
11
11
  joinedViaStarterPackUri: string | null
12
+ pinnedPost: string | null
13
+ pinnedPostCid: string | null
12
14
  createdAt: string
13
15
  indexedAt: string
14
16
  }
@@ -71,7 +71,15 @@ export type ThreadRef = ItemRef & { threadRoot: string }
71
71
 
72
72
  // @NOTE the feed item types in the protos for author feeds and timelines
73
73
  // technically have additional fields, not supported by the mock dataplane.
74
- export type FeedItem = { post: ItemRef; repost?: ItemRef }
74
+ export type FeedItem = {
75
+ post: ItemRef
76
+ repost?: ItemRef
77
+ /**
78
+ * If true, overrides the `reason` with `app.bsky.feed.defs#reasonPin`. Used
79
+ * only in author feeds.
80
+ */
81
+ authorPinned?: boolean
82
+ }
75
83
 
76
84
  export class FeedHydrator {
77
85
  constructor(public dataplane: DataPlaneClient) {}
@@ -4190,6 +4190,10 @@ export const schemaDict = {
4190
4190
  ref: 'lex:com.atproto.label.defs#label',
4191
4191
  },
4192
4192
  },
4193
+ pinnedPost: {
4194
+ type: 'ref',
4195
+ ref: 'lex:com.atproto.repo.strongRef',
4196
+ },
4193
4197
  },
4194
4198
  },
4195
4199
  profileAssociated: {
@@ -4812,6 +4816,10 @@ export const schemaDict = {
4812
4816
  type: 'ref',
4813
4817
  ref: 'lex:com.atproto.repo.strongRef',
4814
4818
  },
4819
+ pinnedPost: {
4820
+ type: 'ref',
4821
+ ref: 'lex:com.atproto.repo.strongRef',
4822
+ },
4815
4823
  createdAt: {
4816
4824
  type: 'string',
4817
4825
  format: 'datetime',
@@ -5468,6 +5476,9 @@ export const schemaDict = {
5468
5476
  embeddingDisabled: {
5469
5477
  type: 'boolean',
5470
5478
  },
5479
+ pinned: {
5480
+ type: 'boolean',
5481
+ },
5471
5482
  },
5472
5483
  },
5473
5484
  feedViewPost: {
@@ -5484,7 +5495,10 @@ export const schemaDict = {
5484
5495
  },
5485
5496
  reason: {
5486
5497
  type: 'union',
5487
- refs: ['lex:app.bsky.feed.defs#reasonRepost'],
5498
+ refs: [
5499
+ 'lex:app.bsky.feed.defs#reasonRepost',
5500
+ 'lex:app.bsky.feed.defs#reasonPin',
5501
+ ],
5488
5502
  },
5489
5503
  feedContext: {
5490
5504
  type: 'string',
@@ -5536,6 +5550,10 @@ export const schemaDict = {
5536
5550
  },
5537
5551
  },
5538
5552
  },
5553
+ reasonPin: {
5554
+ type: 'object',
5555
+ properties: {},
5556
+ },
5539
5557
  threadViewPost: {
5540
5558
  type: 'object',
5541
5559
  required: ['post'],
@@ -5693,7 +5711,10 @@ export const schemaDict = {
5693
5711
  },
5694
5712
  reason: {
5695
5713
  type: 'union',
5696
- refs: ['lex:app.bsky.feed.defs#skeletonReasonRepost'],
5714
+ refs: [
5715
+ 'lex:app.bsky.feed.defs#skeletonReasonRepost',
5716
+ 'lex:app.bsky.feed.defs#skeletonReasonPin',
5717
+ ],
5697
5718
  },
5698
5719
  feedContext: {
5699
5720
  type: 'string',
@@ -5713,6 +5734,10 @@ export const schemaDict = {
5713
5734
  },
5714
5735
  },
5715
5736
  },
5737
+ skeletonReasonPin: {
5738
+ type: 'object',
5739
+ properties: {},
5740
+ },
5716
5741
  threadgateView: {
5717
5742
  type: 'object',
5718
5743
  properties: {
@@ -6078,6 +6103,10 @@ export const schemaDict = {
6078
6103
  ],
6079
6104
  default: 'posts_with_replies',
6080
6105
  },
6106
+ includePins: {
6107
+ type: 'boolean',
6108
+ default: false,
6109
+ },
6081
6110
  },
6082
6111
  },
6083
6112
  output: {
@@ -7162,7 +7191,7 @@ export const schemaDict = {
7162
7191
  type: 'record',
7163
7192
  key: 'tid',
7164
7193
  description:
7165
- "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..",
7194
+ "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.",
7166
7195
  record: {
7167
7196
  type: 'object',
7168
7197
  required: ['post', 'createdAt'],
@@ -8236,6 +8265,12 @@ export const schemaDict = {
8236
8265
  ref: 'lex:app.bsky.actor.defs#profileView',
8237
8266
  },
8238
8267
  },
8268
+ isFallback: {
8269
+ type: 'boolean',
8270
+ description:
8271
+ 'If true, response has fallen-back to generic results, and is not scoped using relativeToDid',
8272
+ default: false,
8273
+ },
8239
8274
  },
8240
8275
  },
8241
8276
  },
@@ -9192,6 +9227,12 @@ export const schemaDict = {
9192
9227
  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',
9193
9228
  },
9194
9229
  },
9230
+ relativeToDid: {
9231
+ type: 'string',
9232
+ format: 'did',
9233
+ description:
9234
+ 'DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.',
9235
+ },
9195
9236
  },
9196
9237
  },
9197
9238
  },
@@ -7,6 +7,7 @@ import { isObj, hasProp } from '../../../../util'
7
7
  import { CID } from 'multiformats/cid'
8
8
  import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs'
9
9
  import * as AppBskyGraphDefs from '../graph/defs'
10
+ import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
10
11
 
11
12
  export interface ProfileViewBasic {
12
13
  did: string
@@ -74,6 +75,7 @@ export interface ProfileViewDetailed {
74
75
  createdAt?: string
75
76
  viewer?: ViewerState
76
77
  labels?: ComAtprotoLabelDefs.Label[]
78
+ pinnedPost?: ComAtprotoRepoStrongRef.Main
77
79
  [k: string]: unknown
78
80
  }
79
81
 
@@ -20,6 +20,7 @@ export interface Record {
20
20
  | ComAtprotoLabelDefs.SelfLabels
21
21
  | { $type: string; [k: string]: unknown }
22
22
  joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main
23
+ pinnedPost?: ComAtprotoRepoStrongRef.Main
23
24
  createdAt?: string
24
25
  [k: string]: unknown
25
26
  }
@@ -55,6 +55,7 @@ export interface ViewerState {
55
55
  threadMuted?: boolean
56
56
  replyDisabled?: boolean
57
57
  embeddingDisabled?: boolean
58
+ pinned?: boolean
58
59
  [k: string]: unknown
59
60
  }
60
61
 
@@ -73,7 +74,7 @@ export function validateViewerState(v: unknown): ValidationResult {
73
74
  export interface FeedViewPost {
74
75
  post: PostView
75
76
  reply?: ReplyRef
76
- reason?: ReasonRepost | { $type: string; [k: string]: unknown }
77
+ reason?: ReasonRepost | ReasonPin | { $type: string; [k: string]: unknown }
77
78
  /** Context provided by feed generator that may be passed back alongside interactions. */
78
79
  feedContext?: string
79
80
  [k: string]: unknown
@@ -134,6 +135,22 @@ export function validateReasonRepost(v: unknown): ValidationResult {
134
135
  return lexicons.validate('app.bsky.feed.defs#reasonRepost', v)
135
136
  }
136
137
 
138
+ export interface ReasonPin {
139
+ [k: string]: unknown
140
+ }
141
+
142
+ export function isReasonPin(v: unknown): v is ReasonPin {
143
+ return (
144
+ isObj(v) &&
145
+ hasProp(v, '$type') &&
146
+ v.$type === 'app.bsky.feed.defs#reasonPin'
147
+ )
148
+ }
149
+
150
+ export function validateReasonPin(v: unknown): ValidationResult {
151
+ return lexicons.validate('app.bsky.feed.defs#reasonPin', v)
152
+ }
153
+
137
154
  export interface ThreadViewPost {
138
155
  post: PostView
139
156
  parent?:
@@ -265,7 +282,10 @@ export function validateGeneratorViewerState(v: unknown): ValidationResult {
265
282
 
266
283
  export interface SkeletonFeedPost {
267
284
  post: string
268
- reason?: SkeletonReasonRepost | { $type: string; [k: string]: unknown }
285
+ reason?:
286
+ | SkeletonReasonRepost
287
+ | SkeletonReasonPin
288
+ | { $type: string; [k: string]: unknown }
269
289
  /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */
270
290
  feedContext?: string
271
291
  [k: string]: unknown
@@ -300,6 +320,22 @@ export function validateSkeletonReasonRepost(v: unknown): ValidationResult {
300
320
  return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v)
301
321
  }
302
322
 
323
+ export interface SkeletonReasonPin {
324
+ [k: string]: unknown
325
+ }
326
+
327
+ export function isSkeletonReasonPin(v: unknown): v is SkeletonReasonPin {
328
+ return (
329
+ isObj(v) &&
330
+ hasProp(v, '$type') &&
331
+ v.$type === 'app.bsky.feed.defs#skeletonReasonPin'
332
+ )
333
+ }
334
+
335
+ export function validateSkeletonReasonPin(v: unknown): ValidationResult {
336
+ return lexicons.validate('app.bsky.feed.defs#skeletonReasonPin', v)
337
+ }
338
+
303
339
  export interface ThreadgateView {
304
340
  uri?: string
305
341
  cid?: string
@@ -20,6 +20,7 @@ export interface QueryParams {
20
20
  | 'posts_with_media'
21
21
  | 'posts_and_author_threads'
22
22
  | (string & {})
23
+ includePins: boolean
23
24
  }
24
25
 
25
26
  export type InputSchema = undefined
@@ -17,6 +17,8 @@ export type InputSchema = undefined
17
17
 
18
18
  export interface OutputSchema {
19
19
  suggestions: AppBskyActorDefs.ProfileView[]
20
+ /** If true, response has fallen-back to generic results, and is not scoped using relativeToDid */
21
+ isFallback?: boolean
20
22
  [k: string]: unknown
21
23
  }
22
24
 
@@ -23,6 +23,8 @@ export type InputSchema = undefined
23
23
  export interface OutputSchema {
24
24
  cursor?: string
25
25
  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]
26
+ /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */
27
+ relativeToDid?: string
26
28
  [k: string]: unknown
27
29
  }
28
30
 
@@ -16,6 +16,7 @@ import {
16
16
  NotFoundPost,
17
17
  PostView,
18
18
  ReasonRepost,
19
+ ReasonPin,
19
20
  ReplyRef,
20
21
  ThreadViewPost,
21
22
  ThreadgateView,
@@ -169,6 +170,7 @@ export class Views {
169
170
  joinedViaStarterPack: actor.profile?.joinedViaStarterPack
170
171
  ? this.starterPackBasic(actor.profile.joinedViaStarterPack.uri, state)
171
172
  : undefined,
173
+ pinnedPost: actor.profile?.pinnedPost,
172
174
  }
173
175
  }
174
176
 
@@ -606,6 +608,7 @@ export class Views {
606
608
  threadMuted: viewer.threadMuted,
607
609
  replyDisabled: this.userReplyDisabled(uri, state),
608
610
  embeddingDisabled: this.userPostEmbeddingDisabled(uri, state),
611
+ pinned: this.viewerPinned(uri, state, authorDid),
609
612
  }
610
613
  : undefined,
611
614
  labels,
@@ -620,8 +623,10 @@ export class Views {
620
623
  state: HydrationState,
621
624
  ): FeedViewPost | undefined {
622
625
  const postInfo = state.posts?.get(item.post.uri)
623
- let reason: ReasonRepost | undefined
624
- if (item.repost) {
626
+ let reason: ReasonRepost | ReasonPin | undefined
627
+ if (item.authorPinned) {
628
+ reason = this.reasonPin()
629
+ } else if (item.repost) {
625
630
  const repost = state.reposts?.get(item.repost.uri)
626
631
  if (!repost) return
627
632
  if (repost.record.subject.uri !== item.post.uri) return
@@ -723,6 +728,12 @@ export class Views {
723
728
  }
724
729
  }
725
730
 
731
+ reasonPin() {
732
+ return {
733
+ $type: 'app.bsky.feed.defs#reasonPin',
734
+ }
735
+ }
736
+
726
737
  // Threads
727
738
  // ------------
728
739
 
@@ -1128,6 +1139,15 @@ export class Views {
1128
1139
  return true
1129
1140
  }
1130
1141
 
1142
+ viewerPinned(uri: string, state: HydrationState, authorDid: string) {
1143
+ if (!state.ctx?.viewer || state.ctx.viewer !== authorDid) return
1144
+ const actor = state.actors?.get(authorDid)
1145
+ if (!actor) return
1146
+ const pinnedPost = actor.profile?.pinnedPost
1147
+ if (!pinnedPost) return undefined
1148
+ return pinnedPost.uri === uri
1149
+ }
1150
+
1131
1151
  notification(
1132
1152
  notif: Notification,
1133
1153
  lastSeenAt: string | undefined,