@atproto/bsky 0.0.195 → 0.0.197

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 (54) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
  3. package/dist/api/app/bsky/feed/searchPosts.js +18 -3
  4. package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
  5. package/dist/api/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -1
  6. package/dist/api/app/bsky/unspecced/getPostThreadV2.js +1 -0
  7. package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
  8. package/dist/auth-verifier.d.ts.map +1 -1
  9. package/dist/auth-verifier.js +6 -1
  10. package/dist/auth-verifier.js.map +1 -1
  11. package/dist/config.d.ts +4 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +10 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/feature-gates.d.ts +17 -6
  16. package/dist/feature-gates.d.ts.map +1 -1
  17. package/dist/feature-gates.js +24 -13
  18. package/dist/feature-gates.js.map +1 -1
  19. package/dist/hydration/feed.d.ts +4 -1
  20. package/dist/hydration/feed.d.ts.map +1 -1
  21. package/dist/hydration/feed.js +10 -2
  22. package/dist/hydration/feed.js.map +1 -1
  23. package/dist/hydration/hydrator.d.ts +5 -2
  24. package/dist/hydration/hydrator.d.ts.map +1 -1
  25. package/dist/hydration/hydrator.js +17 -3
  26. package/dist/hydration/hydrator.js.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/proto/bsky_pb.d.ts +8 -0
  31. package/dist/proto/bsky_pb.d.ts.map +1 -1
  32. package/dist/proto/bsky_pb.js +20 -0
  33. package/dist/proto/bsky_pb.js.map +1 -1
  34. package/dist/views/index.d.ts +4 -0
  35. package/dist/views/index.d.ts.map +1 -1
  36. package/dist/views/index.js +31 -8
  37. package/dist/views/index.js.map +1 -1
  38. package/dist/views/threads-v2.d.ts +3 -1
  39. package/dist/views/threads-v2.d.ts.map +1 -1
  40. package/dist/views/threads-v2.js +122 -12
  41. package/dist/views/threads-v2.js.map +1 -1
  42. package/package.json +7 -7
  43. package/proto/bsky.proto +2 -0
  44. package/src/api/app/bsky/feed/searchPosts.ts +28 -2
  45. package/src/api/app/bsky/unspecced/getPostThreadV2.ts +4 -0
  46. package/src/auth-verifier.ts +9 -1
  47. package/src/config.ts +15 -0
  48. package/src/feature-gates.ts +28 -9
  49. package/src/hydration/feed.ts +17 -1
  50. package/src/hydration/hydrator.ts +17 -1
  51. package/src/index.ts +2 -0
  52. package/src/proto/bsky_pb.ts +12 -0
  53. package/src/views/index.ts +52 -22
  54. package/src/views/threads-v2.ts +156 -13
package/src/index.ts CHANGED
@@ -132,6 +132,8 @@ export class BskyAppView {
132
132
  indexedAtEpoch: config.indexedAtEpoch,
133
133
  threadTagsBumpDown: [...config.threadTagsBumpDown],
134
134
  threadTagsHide: [...config.threadTagsHide],
135
+ visibilityTagHide: config.visibilityTagHide,
136
+ visibilityTagRankPrefix: config.visibilityTagRankPrefix,
135
137
  })
136
138
 
137
139
  const bsyncClient = createBsyncClient({
@@ -775,6 +775,16 @@ export class GetPostRecordsRequest extends Message<GetPostRecordsRequest> {
775
775
  */
776
776
  uris: string[] = [];
777
777
 
778
+ /**
779
+ * @generated from field: optional string process_dynamic_tags_for_view = 2;
780
+ */
781
+ processDynamicTagsForView?: string;
782
+
783
+ /**
784
+ * @generated from field: optional string viewer_did = 3;
785
+ */
786
+ viewerDid?: string;
787
+
778
788
  constructor(data?: PartialMessage<GetPostRecordsRequest>) {
779
789
  super();
780
790
  proto3.util.initPartial(data, this);
@@ -784,6 +794,8 @@ export class GetPostRecordsRequest extends Message<GetPostRecordsRequest> {
784
794
  static readonly typeName = "bsky.GetPostRecordsRequest";
785
795
  static readonly fields: FieldList = proto3.util.newFieldList(() => [
786
796
  { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
797
+ { no: 2, name: "process_dynamic_tags_for_view", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
798
+ { no: 3, name: "viewer_did", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
787
799
  ]);
788
800
 
789
801
  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPostRecordsRequest {
@@ -1,5 +1,6 @@
1
1
  import { HOUR, MINUTE, mapDefined } from '@atproto/common'
2
2
  import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'
3
+ import { FeatureGateID } from '../feature-gates'
3
4
  import { Actor, ProfileViewerState } from '../hydration/actor'
4
5
  import { FeedItem, Like, Post, Repost } from '../hydration/feed'
5
6
  import { Follow, Verification } from '../hydration/graph'
@@ -139,6 +140,8 @@ export class Views {
139
140
  public indexedAtEpoch: Date | undefined = this.opts.indexedAtEpoch
140
141
  private threadTagsBumpDown: readonly string[] = this.opts.threadTagsBumpDown
141
142
  private threadTagsHide: readonly string[] = this.opts.threadTagsHide
143
+ private visibilityTagHide: string = this.opts.visibilityTagHide
144
+ private visibilityTagRankPrefix: string = this.opts.visibilityTagRankPrefix
142
145
  constructor(
143
146
  private opts: {
144
147
  imgUriBuilder: ImageUriBuilder
@@ -146,6 +149,8 @@ export class Views {
146
149
  indexedAtEpoch: Date | undefined
147
150
  threadTagsBumpDown: readonly string[]
148
151
  threadTagsHide: readonly string[]
152
+ visibilityTagHide: string
153
+ visibilityTagRankPrefix: string
149
154
  },
150
155
  ) {}
151
156
 
@@ -1367,14 +1372,21 @@ export class Views {
1367
1372
  }
1368
1373
  }
1369
1374
 
1370
- const thread = sortTrimFlattenThreadTree(anchorTree, {
1371
- opDid,
1372
- branchingFactor,
1373
- sort,
1374
- viewer: state.ctx?.viewer ?? null,
1375
- threadTagsBumpDown: this.threadTagsBumpDown,
1376
- threadTagsHide: this.threadTagsHide,
1377
- })
1375
+ const thread = sortTrimFlattenThreadTree(
1376
+ anchorTree,
1377
+ {
1378
+ opDid,
1379
+ branchingFactor,
1380
+ sort,
1381
+ viewer: state.ctx?.viewer ?? null,
1382
+ threadTagsBumpDown: this.threadTagsBumpDown,
1383
+ threadTagsHide: this.threadTagsHide,
1384
+ visibilityTagRankPrefix: this.visibilityTagRankPrefix,
1385
+ },
1386
+ state.ctx?.featureGates.get(
1387
+ FeatureGateID.ThreadsV2ReplyRankingExploration,
1388
+ ),
1389
+ )
1378
1390
 
1379
1391
  return {
1380
1392
  hasOtherReplies,
@@ -1734,13 +1746,20 @@ export class Views {
1734
1746
  ),
1735
1747
  }
1736
1748
 
1737
- return sortTrimFlattenThreadTree(anchorTree, {
1738
- opDid,
1739
- branchingFactor,
1740
- viewer: state.ctx?.viewer ?? null,
1741
- threadTagsBumpDown: this.threadTagsBumpDown,
1742
- threadTagsHide: this.threadTagsHide,
1743
- })
1749
+ return sortTrimFlattenThreadTree(
1750
+ anchorTree,
1751
+ {
1752
+ opDid,
1753
+ branchingFactor,
1754
+ viewer: state.ctx?.viewer ?? null,
1755
+ threadTagsBumpDown: this.threadTagsBumpDown,
1756
+ threadTagsHide: this.threadTagsHide,
1757
+ visibilityTagRankPrefix: this.visibilityTagRankPrefix,
1758
+ },
1759
+ state.ctx?.featureGates.get(
1760
+ FeatureGateID.ThreadsV2ReplyRankingExploration,
1761
+ ),
1762
+ )
1744
1763
  }
1745
1764
 
1746
1765
  private threadOtherV2Replies(
@@ -1933,21 +1952,32 @@ export class Views {
1933
1952
  const opDid = creatorFromUri(rootUri)
1934
1953
  const authorDid = creatorFromUri(uri)
1935
1954
 
1936
- const showBecauseFollowing = !!postView.author.viewer?.following
1937
- const hiddenByTag =
1938
- authorDid !== opDid &&
1939
- authorDid !== state.ctx?.viewer &&
1940
- !showBecauseFollowing &&
1941
- this.threadTagsHide.some((t) => post.tags.has(t))
1955
+ let hiddenByTag = false
1956
+ if (
1957
+ state.ctx?.featureGates.get(
1958
+ FeatureGateID.ThreadsV2ReplyRankingExploration,
1959
+ )
1960
+ ) {
1961
+ hiddenByTag = authorDid !== opDid && post.tags.has(this.visibilityTagHide)
1962
+ } else {
1963
+ const showBecauseFollowing = !!postView.author.viewer?.following
1964
+ hiddenByTag =
1965
+ authorDid !== opDid &&
1966
+ authorDid !== state.ctx?.viewer &&
1967
+ !showBecauseFollowing &&
1968
+ this.threadTagsHide.some((t) => post.tags.has(t))
1969
+ }
1942
1970
 
1943
1971
  const hiddenByThreadgate =
1944
1972
  state.ctx?.viewer !== authorDid &&
1945
1973
  this.replyIsHiddenByThreadgate(uri, rootUri, state)
1946
1974
 
1947
1975
  const mutedByViewer = this.viewerMuteExists(authorDid, state)
1976
+ const isPushPin =
1977
+ isPostRecord(post.record) && post.record.text.trim() === '📌'
1948
1978
 
1949
1979
  return {
1950
- isOther: hiddenByTag || hiddenByThreadgate || mutedByViewer,
1980
+ isOther: hiddenByTag || hiddenByThreadgate || mutedByViewer || isPushPin,
1951
1981
  hiddenByTag,
1952
1982
  hiddenByThreadgate,
1953
1983
  mutedByViewer,
@@ -1,6 +1,4 @@
1
- import { asPredicate } from '@atproto/api'
2
1
  import { HydrateCtx } from '../hydration/hydrator'
3
- import { validateRecord as validatePostRecord } from '../lexicon/types/app/bsky/feed/post'
4
2
  import {
5
3
  ThreadItemBlocked,
6
4
  ThreadItemNoUnauthenticated,
@@ -110,8 +108,11 @@ export type ThreadTree = ThreadTreeVisible | ThreadTreeOther
110
108
  export function sortTrimFlattenThreadTree(
111
109
  anchorTree: ThreadTree,
112
110
  options: SortTrimFlattenOptions,
111
+ useExploration?: boolean,
113
112
  ) {
114
- const sortedAnchorTree = sortTrimThreadTree(anchorTree, options)
113
+ const sortedAnchorTree = useExploration
114
+ ? sortTrimThreadTreeExploration(anchorTree, options)
115
+ : sortTrimThreadTree(anchorTree, options)
115
116
 
116
117
  return flattenTree(sortedAnchorTree)
117
118
  }
@@ -123,10 +124,9 @@ type SortTrimFlattenOptions = {
123
124
  viewer: HydrateCtx['viewer']
124
125
  threadTagsBumpDown: readonly string[]
125
126
  threadTagsHide: readonly string[]
127
+ visibilityTagRankPrefix: string
126
128
  }
127
129
 
128
- const isPostRecord = asPredicate(validatePostRecord)
129
-
130
130
  /** This function mutates the tree parameter. */
131
131
  function sortTrimThreadTree(
132
132
  n: ThreadTree,
@@ -227,14 +227,6 @@ function applyBumping(
227
227
  'down',
228
228
  (i) => i.type === 'post' && threadTagsBumpDown.some((t) => i.tags.has(t)),
229
229
  ],
230
- // Pushpin-only.
231
- [
232
- 'down',
233
- (i) =>
234
- i.type === 'post' &&
235
- isPostRecord(i.item.value.post.record) &&
236
- i.item.value.post.record.text.trim() === '📌',
237
- ],
238
230
 
239
231
  /*
240
232
  Bumps within hidden replies.
@@ -367,3 +359,154 @@ function* flattenInDirection({
367
359
  }
368
360
  }
369
361
  }
362
+
363
+ export function sortTrimThreadTreeExploration(
364
+ n: ThreadTree,
365
+ opts: SortTrimFlattenOptions,
366
+ ): ThreadTree {
367
+ if (!isNodeWithReplies(n)) {
368
+ return n
369
+ }
370
+ const node: ThreadNodeWithReplies = n
371
+
372
+ if (node.replies) {
373
+ node.replies.sort((an: ThreadTree, bn: ThreadTree) => {
374
+ if (!isPostNode(an)) {
375
+ return 1
376
+ }
377
+ if (!isPostNode(bn)) {
378
+ return -1
379
+ }
380
+ const aNode: ThreadMaybeOtherPostNode = an
381
+ const bNode: ThreadMaybeOtherPostNode = bn
382
+
383
+ // First applies bumping.
384
+ const bump = applyBumpingExploration(aNode, bNode, opts)
385
+ if (bump !== null) {
386
+ return bump
387
+ }
388
+
389
+ // Then applies sorting.
390
+ return applySortingExploration(aNode, bNode, opts)
391
+ })
392
+
393
+ // Trimming: after sorting, apply branching factor to all levels of replies except the anchor direct replies.
394
+ if (node.item.depth !== 0) {
395
+ node.replies = node.replies.slice(0, opts.branchingFactor)
396
+ }
397
+
398
+ node.replies.forEach((reply) => sortTrimThreadTreeExploration(reply, opts))
399
+ }
400
+
401
+ return node
402
+ }
403
+
404
+ function applyBumpingExploration(
405
+ aNode: ThreadMaybeOtherPostNode,
406
+ bNode: ThreadMaybeOtherPostNode,
407
+ opts: SortTrimFlattenOptions,
408
+ ): number | null {
409
+ if (!isPostNode(aNode)) {
410
+ return null
411
+ }
412
+ if (!isPostNode(bNode)) {
413
+ return null
414
+ }
415
+
416
+ const { opDid, viewer } = opts
417
+
418
+ type BumpDirection = 'up' | 'down'
419
+ type BumpPredicateFn = (i: ThreadMaybeOtherPostNode) => boolean
420
+
421
+ const maybeBump = (
422
+ bump: BumpDirection,
423
+ predicateFn: BumpPredicateFn,
424
+ ): number | null => {
425
+ const aPredicate = predicateFn(aNode)
426
+ const bPredicate = predicateFn(bNode)
427
+ if (aPredicate && bPredicate) {
428
+ return applySortingExploration(aNode, bNode, opts)
429
+ } else if (aPredicate) {
430
+ return bump === 'up' ? -1 : 1
431
+ } else if (bPredicate) {
432
+ return bump === 'up' ? 1 : -1
433
+ }
434
+ return null
435
+ }
436
+
437
+ // The order of the bumps determines the priority with which they are applied.
438
+ // Bumps-up applied first make the item appear higher in the list than later bumps-up.
439
+ // Bumps-down applied first make the item appear lower in the list than later bumps-down.
440
+ const bumps: [BumpDirection, BumpPredicateFn][] = [
441
+ /*
442
+ General bumps.
443
+ */
444
+ // OP replies.
445
+ ['up', (i) => i.item.value.post.author.did === opDid],
446
+ // Viewer replies.
447
+ ['up', (i) => i.item.value.post.author.did === viewer],
448
+ ]
449
+
450
+ for (const [bump, predicateFn] of bumps) {
451
+ const bumpResult = maybeBump(bump, predicateFn)
452
+ if (bumpResult !== null) {
453
+ return bumpResult
454
+ }
455
+ }
456
+
457
+ return null
458
+ }
459
+
460
+ function applySortingExploration(
461
+ aNode: ThreadMaybeOtherPostNode,
462
+ bNode: ThreadMaybeOtherPostNode,
463
+ opts: SortTrimFlattenOptions,
464
+ ): number {
465
+ const { visibilityTagRankPrefix: rp } = opts
466
+
467
+ const a = aNode.item.value
468
+ const ar = !rp ? 0 : parseRankFromTag(rp, findRankTag(aNode.tags, rp))
469
+ const b = bNode.item.value
470
+ const br = !rp ? 0 : parseRankFromTag(rp, findRankTag(bNode.tags, rp))
471
+
472
+ // Only customize sort for visible posts.
473
+ if (aNode.type === 'post' && bNode.type === 'post') {
474
+ const { sort } = opts
475
+
476
+ if (sort === 'oldest') {
477
+ return a.post.indexedAt.localeCompare(b.post.indexedAt)
478
+ }
479
+ if (sort === 'top') {
480
+ const aLikes = a.post.likeCount ?? 0
481
+ const bLikes = b.post.likeCount ?? 0
482
+ const aTop = topSortValue(aLikes, aNode.hasOPLike)
483
+ const bTop = topSortValue(bLikes, bNode.hasOPLike)
484
+ const aRank = aTop + ar
485
+ const bRank = bTop + br
486
+ if (aRank !== bRank) {
487
+ return bRank - aRank
488
+ }
489
+ }
490
+ }
491
+
492
+ // Fallback to newest.
493
+ return b.post.indexedAt.localeCompare(a.post.indexedAt)
494
+ }
495
+
496
+ function findRankTag(tags: Set<string>, prefix: string) {
497
+ return Array.from(tags.values()).find((tag) => tag.startsWith(prefix))
498
+ }
499
+
500
+ function parseRankFromTag(prefix: string, tag?: string) {
501
+ if (!tag) return 0
502
+
503
+ try {
504
+ const rank = parseInt(tag.slice(prefix.length), 10)
505
+ if (typeof rank !== 'number' || isNaN(rank)) {
506
+ return 0
507
+ }
508
+ return rank
509
+ } catch (e) {
510
+ return 0
511
+ }
512
+ }