@atproto/bsky 0.0.151 → 0.0.153

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 (104) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.d.ts +4 -0
  3. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +1 -0
  4. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.js +77 -0
  5. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.js.map +1 -0
  6. package/dist/api/app/bsky/unspecced/getPostThreadV2.d.ts +4 -0
  7. package/dist/api/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -0
  8. package/dist/api/app/bsky/unspecced/getPostThreadV2.js +86 -0
  9. package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -0
  10. package/dist/api/com/atproto/repo/getRecord.d.ts.map +1 -1
  11. package/dist/api/com/atproto/repo/getRecord.js +1 -1
  12. package/dist/api/com/atproto/repo/getRecord.js.map +1 -1
  13. package/dist/api/index.d.ts.map +1 -1
  14. package/dist/api/index.js +4 -0
  15. package/dist/api/index.js.map +1 -1
  16. package/dist/config.d.ts +6 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +17 -0
  19. package/dist/config.js.map +1 -1
  20. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.d.ts +4 -0
  21. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.d.ts.map +1 -0
  22. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.js +11 -0
  23. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.js.map +1 -0
  24. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  25. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  26. package/dist/data-plane/server/db/migrations/index.js +2 -1
  27. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  28. package/dist/data-plane/server/db/tables/record.d.ts +1 -0
  29. package/dist/data-plane/server/db/tables/record.d.ts.map +1 -1
  30. package/dist/data-plane/server/db/tables/record.js.map +1 -1
  31. package/dist/data-plane/server/routes/records.d.ts.map +1 -1
  32. package/dist/data-plane/server/routes/records.js +1 -0
  33. package/dist/data-plane/server/routes/records.js.map +1 -1
  34. package/dist/hydration/feed.d.ts +1 -0
  35. package/dist/hydration/feed.d.ts.map +1 -1
  36. package/dist/hydration/feed.js +2 -0
  37. package/dist/hydration/feed.js.map +1 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/lexicon/index.d.ts +6 -2
  42. package/dist/lexicon/index.d.ts.map +1 -1
  43. package/dist/lexicon/index.js +12 -4
  44. package/dist/lexicon/index.js.map +1 -1
  45. package/dist/lexicon/lexicons.d.ts +508 -82
  46. package/dist/lexicon/lexicons.d.ts.map +1 -1
  47. package/dist/lexicon/lexicons.js +264 -42
  48. package/dist/lexicon/lexicons.js.map +1 -1
  49. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts +63 -0
  50. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +1 -0
  51. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js +25 -0
  52. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js.map +1 -0
  53. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts +92 -0
  54. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -0
  55. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.js +52 -0
  56. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.js.map +1 -0
  57. package/dist/proto/bsky_pb.d.ts +4 -0
  58. package/dist/proto/bsky_pb.d.ts.map +1 -1
  59. package/dist/proto/bsky_pb.js +16 -0
  60. package/dist/proto/bsky_pb.js.map +1 -1
  61. package/dist/proto/bsync_connect.d.ts +19 -1
  62. package/dist/proto/bsync_connect.d.ts.map +1 -1
  63. package/dist/proto/bsync_connect.js +18 -0
  64. package/dist/proto/bsync_connect.js.map +1 -1
  65. package/dist/proto/bsync_pb.d.ts +150 -0
  66. package/dist/proto/bsync_pb.d.ts.map +1 -1
  67. package/dist/proto/bsync_pb.js +401 -1
  68. package/dist/proto/bsync_pb.js.map +1 -1
  69. package/dist/views/index.d.ts +40 -0
  70. package/dist/views/index.d.ts.map +1 -1
  71. package/dist/views/index.js +499 -0
  72. package/dist/views/index.js.map +1 -1
  73. package/dist/views/threads-v2.d.ts +65 -0
  74. package/dist/views/threads-v2.d.ts.map +1 -0
  75. package/dist/views/threads-v2.js +205 -0
  76. package/dist/views/threads-v2.js.map +1 -0
  77. package/package.json +5 -5
  78. package/proto/bsky.proto +1 -0
  79. package/src/api/app/bsky/unspecced/getPostThreadHiddenV2.ts +117 -0
  80. package/src/api/app/bsky/unspecced/getPostThreadV2.ts +130 -0
  81. package/src/api/com/atproto/repo/getRecord.ts +4 -1
  82. package/src/api/index.ts +4 -0
  83. package/src/config.ts +24 -0
  84. package/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts +9 -0
  85. package/src/data-plane/server/db/migrations/index.ts +1 -0
  86. package/src/data-plane/server/db/tables/record.ts +1 -0
  87. package/src/data-plane/server/routes/records.ts +1 -0
  88. package/src/hydration/feed.ts +4 -0
  89. package/src/index.ts +2 -0
  90. package/src/lexicon/index.ts +33 -9
  91. package/src/lexicon/lexicons.ts +284 -43
  92. package/src/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.ts +95 -0
  93. package/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts +160 -0
  94. package/src/proto/bsky_pb.ts +12 -0
  95. package/src/proto/bsync_connect.ts +22 -0
  96. package/src/proto/bsync_pb.ts +355 -0
  97. package/src/views/index.ts +780 -0
  98. package/src/views/threads-v2.ts +381 -0
  99. package/tests/seed/thread-v2.ts +874 -0
  100. package/tests/seed/util.ts +52 -0
  101. package/tests/views/__snapshots__/thread-v2.test.ts.snap +1091 -0
  102. package/tests/views/thread-v2.test.ts +2121 -0
  103. package/tsconfig.build.tsbuildinfo +1 -1
  104. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,381 @@
1
+ import { asPredicate } from '@atproto/api'
2
+ import { HydrateCtx } from '../hydration/hydrator'
3
+ import { validateRecord as validatePostRecord } from '../lexicon/types/app/bsky/feed/post'
4
+ import {
5
+ ThreadHiddenItem,
6
+ ThreadHiddenItemPost,
7
+ } from '../lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2'
8
+ import {
9
+ QueryParams as GetPostThreadV2QueryParams,
10
+ ThreadItem,
11
+ ThreadItemBlocked,
12
+ ThreadItemNoUnauthenticated,
13
+ ThreadItemNotFound,
14
+ ThreadItemPost,
15
+ } from '../lexicon/types/app/bsky/unspecced/getPostThreadV2'
16
+ import { $Typed } from '../lexicon/util'
17
+
18
+ type ThreadMaybeHiddenPostNode = ThreadPostNode | ThreadHiddenPostNode
19
+ type ThreadNodeWithReplies =
20
+ | ThreadPostNode
21
+ | ThreadHiddenPostNode
22
+ | ThreadHiddenAnchorPostNode
23
+
24
+ type ThreadItemValue<T extends ThreadItem['value']> = Omit<
25
+ ThreadItem,
26
+ 'value'
27
+ > & {
28
+ value: T
29
+ }
30
+
31
+ export type ThreadItemValueBlocked = ThreadItemValue<$Typed<ThreadItemBlocked>>
32
+
33
+ export type ThreadItemValueNoUnauthenticated = ThreadItemValue<
34
+ $Typed<ThreadItemNoUnauthenticated>
35
+ >
36
+
37
+ export type ThreadItemValueNotFound = ThreadItemValue<
38
+ $Typed<ThreadItemNotFound>
39
+ >
40
+
41
+ export type ThreadItemValuePost = ThreadItemValue<$Typed<ThreadItemPost>>
42
+
43
+ type ThreadBlockedNode = {
44
+ type: 'blocked'
45
+ item: ThreadItemValueBlocked
46
+ }
47
+ type ThreadNoUnauthenticatedNode = {
48
+ type: 'noUnauthenticated'
49
+ parent: ThreadTree | undefined
50
+ item: ThreadItemValueNoUnauthenticated
51
+ }
52
+
53
+ type ThreadNotFoundNode = {
54
+ type: 'notFound'
55
+ item: ThreadItemValueNotFound
56
+ }
57
+
58
+ type ThreadPostNode = {
59
+ type: 'post'
60
+ item: ThreadItemValuePost
61
+ tags: Set<string>
62
+ hasOPLike: boolean
63
+ parent: ThreadTree | undefined
64
+ replies: ThreadTree[] | undefined
65
+ }
66
+
67
+ type ThreadHiddenItemValue<T extends ThreadHiddenItem['value']> = Omit<
68
+ ThreadHiddenItem,
69
+ 'value'
70
+ > & {
71
+ value: T
72
+ }
73
+
74
+ export type ThreadHiddenItemValuePost = ThreadHiddenItemValue<
75
+ $Typed<ThreadHiddenItemPost>
76
+ >
77
+
78
+ // This is an intermediary type that doesn't map to the views.
79
+ // It is useful to differentiate between the anchor post and the replies for the hidden case,
80
+ // while also differentiating between hidden and visible cases.
81
+ export type ThreadHiddenAnchorPostNode = {
82
+ type: 'hiddenAnchor'
83
+ item: Omit<ThreadHiddenItem, 'value'> & { value: undefined }
84
+ replies: ThreadHiddenPostNode[] | undefined
85
+ }
86
+
87
+ export type ThreadHiddenPostNode = {
88
+ type: 'hiddenPost'
89
+ item: ThreadHiddenItemValuePost
90
+ tags: Set<string>
91
+ replies: ThreadHiddenPostNode[] | undefined
92
+ }
93
+
94
+ const isNodeWithReplies = (node: ThreadTree): node is ThreadNodeWithReplies =>
95
+ 'replies' in node && node.replies !== undefined
96
+
97
+ const isPostNode = (node: ThreadTree): node is ThreadMaybeHiddenPostNode =>
98
+ node.type === 'post' || node.type === 'hiddenPost'
99
+
100
+ export type ThreadTreeVisible =
101
+ | ThreadBlockedNode
102
+ | ThreadNoUnauthenticatedNode
103
+ | ThreadNotFoundNode
104
+ | ThreadPostNode
105
+
106
+ export type ThreadTreeHidden = ThreadHiddenAnchorPostNode | ThreadHiddenPostNode
107
+
108
+ export type ThreadTree = ThreadTreeVisible | ThreadTreeHidden
109
+
110
+ /** This function mutates the tree parameter. */
111
+ export function sortTrimFlattenThreadTree<
112
+ TItem extends ThreadItem | ThreadHiddenItem,
113
+ >(anchorTree: ThreadTree, options: SortTrimFlattenOptions): TItem[] {
114
+ const sortedAnchorTree = sortTrimThreadTree(anchorTree, options)
115
+
116
+ return flattenTree<TItem>(sortedAnchorTree)
117
+ }
118
+
119
+ type SortTrimFlattenOptions = {
120
+ branchingFactor: GetPostThreadV2QueryParams['branchingFactor']
121
+ opDid: string
122
+ prioritizeFollowedUsers: boolean
123
+ sort?: GetPostThreadV2QueryParams['sort']
124
+ viewer: HydrateCtx['viewer']
125
+ threadTagsBumpDown: readonly string[]
126
+ threadTagsHide: readonly string[]
127
+ }
128
+
129
+ const isPostRecord = asPredicate(validatePostRecord)
130
+
131
+ /** This function mutates the tree parameter. */
132
+ function sortTrimThreadTree(
133
+ n: ThreadTree,
134
+ opts: SortTrimFlattenOptions,
135
+ ): ThreadTree {
136
+ if (!isNodeWithReplies(n)) {
137
+ return n
138
+ }
139
+ const node: ThreadNodeWithReplies = n
140
+
141
+ if (node.replies) {
142
+ node.replies.sort((an: ThreadTree, bn: ThreadTree) => {
143
+ if (!isPostNode(an)) {
144
+ return 1
145
+ }
146
+ if (!isPostNode(bn)) {
147
+ return -1
148
+ }
149
+ const aNode: ThreadMaybeHiddenPostNode = an
150
+ const bNode: ThreadMaybeHiddenPostNode = bn
151
+
152
+ // First applies bumping.
153
+ const bump = applyBumping(aNode, bNode, opts)
154
+ if (bump !== null) {
155
+ return bump
156
+ }
157
+
158
+ // Then applies sorting.
159
+ return applySorting(aNode, bNode, opts)
160
+ })
161
+
162
+ // Trimming: after sorting, apply branching factor to all levels of replies except the anchor direct replies.
163
+ if (node.item.depth !== 0) {
164
+ node.replies = node.replies.slice(0, opts.branchingFactor)
165
+ }
166
+
167
+ node.replies.forEach((reply) => sortTrimThreadTree(reply, opts))
168
+ }
169
+
170
+ return node
171
+ }
172
+
173
+ function applyBumping(
174
+ aNode: ThreadMaybeHiddenPostNode,
175
+ bNode: ThreadMaybeHiddenPostNode,
176
+ opts: SortTrimFlattenOptions,
177
+ ): number | null {
178
+ if (!isPostNode(aNode)) {
179
+ return null
180
+ }
181
+ if (!isPostNode(bNode)) {
182
+ return null
183
+ }
184
+
185
+ const {
186
+ opDid,
187
+ prioritizeFollowedUsers,
188
+ viewer,
189
+ threadTagsBumpDown,
190
+ threadTagsHide,
191
+ } = opts
192
+
193
+ type BumpDirection = 'up' | 'down'
194
+ type BumpPredicateFn = (i: ThreadMaybeHiddenPostNode) => boolean
195
+
196
+ const maybeBump = (
197
+ bump: BumpDirection,
198
+ predicateFn: BumpPredicateFn,
199
+ ): number | null => {
200
+ const aPredicate = predicateFn(aNode)
201
+ const bPredicate = predicateFn(bNode)
202
+ if (aPredicate && bPredicate) {
203
+ return applySorting(aNode, bNode, opts)
204
+ } else if (aPredicate) {
205
+ return bump === 'up' ? -1 : 1
206
+ } else if (bPredicate) {
207
+ return bump === 'up' ? 1 : -1
208
+ }
209
+ return null
210
+ }
211
+
212
+ // The order of the bumps determines the priority with which they are applied.
213
+ // Bumps-up applied first make the item appear higher in the list than later bumps-up.
214
+ // Bumps-down applied first make the item appear lower in the list than later bumps-down.
215
+ const bumps: [BumpDirection, BumpPredicateFn][] = [
216
+ /*
217
+ General bumps.
218
+ */
219
+ // OP replies.
220
+ ['up', (i) => i.item.value.post.author.did === opDid],
221
+ // Viewer replies.
222
+ ['up', (i) => i.item.value.post.author.did === viewer],
223
+
224
+ /*
225
+ Bumps within visible replies.
226
+ */
227
+ // Followers posts.
228
+ [
229
+ 'up',
230
+ (i) =>
231
+ i.type === 'post' &&
232
+ prioritizeFollowedUsers &&
233
+ !!i.item.value.post.author.viewer?.following,
234
+ ],
235
+ // Bump-down tags.
236
+ [
237
+ 'down',
238
+ (i) => i.type === 'post' && threadTagsBumpDown.some((t) => i.tags.has(t)),
239
+ ],
240
+ // Pushpin-only.
241
+ [
242
+ 'down',
243
+ (i) =>
244
+ i.type === 'post' &&
245
+ isPostRecord(i.item.value.post.record) &&
246
+ i.item.value.post.record.text.trim() === '📌',
247
+ ],
248
+
249
+ /*
250
+ Bumps within hidden replies.
251
+ This determines the order of hidden replies:
252
+ 1. hidden by threadgate.
253
+ 2. hidden by tags.
254
+ 3. muted by viewer.
255
+ */
256
+ // Muted account by the viewer.
257
+ ['down', (i) => i.type === 'hiddenPost' && i.item.value.mutedByViewer],
258
+ // Hidden by tags.
259
+ [
260
+ 'down',
261
+ (i) =>
262
+ i.type === 'hiddenPost' && threadTagsHide.some((t) => i.tags.has(t)),
263
+ ],
264
+ // Hidden by threadgate.
265
+ ['down', (i) => i.type === 'hiddenPost' && i.item.value.hiddenByThreadgate],
266
+ ]
267
+
268
+ for (const [bump, predicateFn] of bumps) {
269
+ const bumpResult = maybeBump(bump, predicateFn)
270
+ if (bumpResult !== null) {
271
+ return bumpResult
272
+ }
273
+ }
274
+
275
+ return null
276
+ }
277
+
278
+ function applySorting(
279
+ aNode: ThreadMaybeHiddenPostNode,
280
+ bNode: ThreadMaybeHiddenPostNode,
281
+ opts: SortTrimFlattenOptions,
282
+ ): number {
283
+ const a = aNode.item.value
284
+ const b = bNode.item.value
285
+
286
+ // Only customize sort for visible posts.
287
+ if (aNode.type === 'post' && bNode.type === 'post') {
288
+ const { sort } = opts
289
+
290
+ if (sort === 'oldest') {
291
+ return a.post.indexedAt.localeCompare(b.post.indexedAt)
292
+ }
293
+ if (sort === 'top') {
294
+ const aLikes = a.post.likeCount ?? 0
295
+ const bLikes = b.post.likeCount ?? 0
296
+ const aTop = topSortValue(aLikes, aNode.hasOPLike)
297
+ const bTop = topSortValue(bLikes, bNode.hasOPLike)
298
+ if (aTop !== bTop) {
299
+ return bTop - aTop
300
+ }
301
+ }
302
+ }
303
+
304
+ // Fallback to newest.
305
+ return b.post.indexedAt.localeCompare(a.post.indexedAt)
306
+ }
307
+
308
+ function topSortValue(likeCount: number, hasOPLike: boolean): number {
309
+ return Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0)
310
+ }
311
+
312
+ function flattenTree<TItem extends ThreadItem | ThreadHiddenItem>(
313
+ tree: ThreadTree,
314
+ ): TItem[] {
315
+ return [
316
+ // All parents above.
317
+ ...Array.from(
318
+ flattenInDirection<TItem>({
319
+ tree,
320
+ direction: 'up',
321
+ }),
322
+ ),
323
+
324
+ // The anchor.
325
+ // In the case of hidden replies, the anchor item itself is undefined.
326
+ ...(tree.item.value ? ([tree.item] as TItem[]) : []),
327
+
328
+ // All replies below.
329
+ ...Array.from(
330
+ flattenInDirection<TItem>({
331
+ tree,
332
+ direction: 'down',
333
+ }),
334
+ ),
335
+ ]
336
+ }
337
+
338
+ function* flattenInDirection<TItem extends ThreadItem | ThreadHiddenItem>({
339
+ tree,
340
+ direction,
341
+ }: {
342
+ tree: ThreadTree
343
+ direction: 'up' | 'down'
344
+ }): Generator<TItem, void> {
345
+ if (tree.type === 'noUnauthenticated') {
346
+ if (direction === 'up') {
347
+ if (tree.parent) {
348
+ // Unfold all parents above.
349
+ yield* flattenTree<TItem>(tree.parent)
350
+ }
351
+ }
352
+ }
353
+
354
+ if (tree.type === 'post') {
355
+ if (direction === 'up') {
356
+ if (tree.parent) {
357
+ // Unfold all parents above.
358
+ yield* flattenTree<TItem>(tree.parent)
359
+ }
360
+ } else {
361
+ // Unfold all replies below.
362
+ if (tree.replies?.length) {
363
+ for (const reply of tree.replies) {
364
+ yield* flattenTree<TItem>(reply)
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ // For the first level of hidden replies, the items are undefined.
371
+ if (tree.type === 'hiddenAnchor' || tree.type === 'hiddenPost') {
372
+ if (direction === 'down') {
373
+ // Unfold all replies below.
374
+ if (tree.replies?.length) {
375
+ for (const reply of tree.replies) {
376
+ yield* flattenTree<TItem>(reply)
377
+ }
378
+ }
379
+ }
380
+ }
381
+ }