@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.
- package/CHANGELOG.md +7 -0
- package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.d.ts +4 -0
- package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +1 -0
- package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.js +76 -0
- package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.js.map +1 -0
- package/dist/api/app/bsky/unspecced/getPostThreadV2.d.ts +4 -0
- package/dist/api/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -0
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js +86 -0
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +4 -0
- package/dist/api/index.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/hydration/feed.d.ts.map +1 -1
- package/dist/hydration/feed.js.map +1 -1
- package/dist/lexicon/index.d.ts +6 -2
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +12 -4
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +498 -82
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +259 -42
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts +61 -0
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +1 -0
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js +25 -0
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js.map +1 -0
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts +92 -0
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -0
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.js +52 -0
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.js.map +1 -0
- package/dist/views/index.d.ts +35 -0
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +463 -0
- package/dist/views/index.js.map +1 -1
- package/dist/views/threads-v2.d.ts +62 -0
- package/dist/views/threads-v2.d.ts.map +1 -0
- package/dist/views/threads-v2.js +180 -0
- package/dist/views/threads-v2.js.map +1 -0
- package/package.json +4 -4
- package/src/api/app/bsky/unspecced/getPostThreadHiddenV2.ts +116 -0
- package/src/api/app/bsky/unspecced/getPostThreadV2.ts +130 -0
- package/src/api/index.ts +4 -0
- package/src/config.ts +9 -0
- package/src/hydration/feed.ts +1 -0
- package/src/lexicon/index.ts +33 -9
- package/src/lexicon/lexicons.ts +278 -43
- package/src/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.ts +93 -0
- package/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts +160 -0
- package/src/views/index.ts +742 -0
- package/src/views/threads-v2.ts +351 -0
- package/tests/seed/thread-v2.ts +775 -0
- package/tests/seed/util.ts +52 -0
- package/tests/views/__snapshots__/thread-v2.test.ts.snap +1091 -0
- package/tests/views/thread-v2.test.ts +2009 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- 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
|
+
}
|