@atproto/bsky 0.0.151 → 0.0.152

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