@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.
- package/CHANGELOG.md +29 -0
- package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/searchPosts.js +18 -3
- package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js +1 -0
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +6 -1
- package/dist/auth-verifier.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/feature-gates.d.ts +17 -6
- package/dist/feature-gates.d.ts.map +1 -1
- package/dist/feature-gates.js +24 -13
- package/dist/feature-gates.js.map +1 -1
- package/dist/hydration/feed.d.ts +4 -1
- package/dist/hydration/feed.d.ts.map +1 -1
- package/dist/hydration/feed.js +10 -2
- package/dist/hydration/feed.js.map +1 -1
- package/dist/hydration/hydrator.d.ts +5 -2
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +17 -3
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/proto/bsky_pb.d.ts +8 -0
- package/dist/proto/bsky_pb.d.ts.map +1 -1
- package/dist/proto/bsky_pb.js +20 -0
- package/dist/proto/bsky_pb.js.map +1 -1
- package/dist/views/index.d.ts +4 -0
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +31 -8
- package/dist/views/index.js.map +1 -1
- package/dist/views/threads-v2.d.ts +3 -1
- package/dist/views/threads-v2.d.ts.map +1 -1
- package/dist/views/threads-v2.js +122 -12
- package/dist/views/threads-v2.js.map +1 -1
- package/package.json +7 -7
- package/proto/bsky.proto +2 -0
- package/src/api/app/bsky/feed/searchPosts.ts +28 -2
- package/src/api/app/bsky/unspecced/getPostThreadV2.ts +4 -0
- package/src/auth-verifier.ts +9 -1
- package/src/config.ts +15 -0
- package/src/feature-gates.ts +28 -9
- package/src/hydration/feed.ts +17 -1
- package/src/hydration/hydrator.ts +17 -1
- package/src/index.ts +2 -0
- package/src/proto/bsky_pb.ts +12 -0
- package/src/views/index.ts +52 -22
- 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({
|
package/src/proto/bsky_pb.ts
CHANGED
|
@@ -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 {
|
package/src/views/index.ts
CHANGED
|
@@ -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(
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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(
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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,
|
package/src/views/threads-v2.ts
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|