@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.
- package/CHANGELOG.md +14 -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 +77 -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/com/atproto/repo/getRecord.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/getRecord.js +1 -1
- package/dist/api/com/atproto/repo/getRecord.js.map +1 -1
- 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 +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.d.ts +4 -0
- package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.d.ts.map +1 -0
- package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.js +11 -0
- package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.js.map +1 -0
- package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
- package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
- package/dist/data-plane/server/db/migrations/index.js +2 -1
- package/dist/data-plane/server/db/migrations/index.js.map +1 -1
- package/dist/data-plane/server/db/tables/record.d.ts +1 -0
- package/dist/data-plane/server/db/tables/record.d.ts.map +1 -1
- package/dist/data-plane/server/db/tables/record.js.map +1 -1
- package/dist/data-plane/server/routes/records.d.ts.map +1 -1
- package/dist/data-plane/server/routes/records.js +1 -0
- package/dist/data-plane/server/routes/records.js.map +1 -1
- package/dist/hydration/feed.d.ts +1 -0
- package/dist/hydration/feed.d.ts.map +1 -1
- package/dist/hydration/feed.js +2 -0
- package/dist/hydration/feed.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/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 +508 -82
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +264 -42
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts +63 -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/proto/bsky_pb.d.ts +4 -0
- package/dist/proto/bsky_pb.d.ts.map +1 -1
- package/dist/proto/bsky_pb.js +16 -0
- package/dist/proto/bsky_pb.js.map +1 -1
- package/dist/proto/bsync_connect.d.ts +19 -1
- package/dist/proto/bsync_connect.d.ts.map +1 -1
- package/dist/proto/bsync_connect.js +18 -0
- package/dist/proto/bsync_connect.js.map +1 -1
- package/dist/proto/bsync_pb.d.ts +150 -0
- package/dist/proto/bsync_pb.d.ts.map +1 -1
- package/dist/proto/bsync_pb.js +401 -1
- package/dist/proto/bsync_pb.js.map +1 -1
- package/dist/views/index.d.ts +40 -0
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +499 -0
- package/dist/views/index.js.map +1 -1
- package/dist/views/threads-v2.d.ts +65 -0
- package/dist/views/threads-v2.d.ts.map +1 -0
- package/dist/views/threads-v2.js +205 -0
- package/dist/views/threads-v2.js.map +1 -0
- package/package.json +5 -5
- package/proto/bsky.proto +1 -0
- package/src/api/app/bsky/unspecced/getPostThreadHiddenV2.ts +117 -0
- package/src/api/app/bsky/unspecced/getPostThreadV2.ts +130 -0
- package/src/api/com/atproto/repo/getRecord.ts +4 -1
- package/src/api/index.ts +4 -0
- package/src/config.ts +24 -0
- package/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts +9 -0
- package/src/data-plane/server/db/migrations/index.ts +1 -0
- package/src/data-plane/server/db/tables/record.ts +1 -0
- package/src/data-plane/server/routes/records.ts +1 -0
- package/src/hydration/feed.ts +4 -0
- package/src/index.ts +2 -0
- package/src/lexicon/index.ts +33 -9
- package/src/lexicon/lexicons.ts +284 -43
- package/src/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.ts +95 -0
- package/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts +160 -0
- package/src/proto/bsky_pb.ts +12 -0
- package/src/proto/bsync_connect.ts +22 -0
- package/src/proto/bsync_pb.ts +355 -0
- package/src/views/index.ts +780 -0
- package/src/views/threads-v2.ts +381 -0
- package/tests/seed/thread-v2.ts +874 -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 +2121 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
package/src/views/index.ts
CHANGED
|
@@ -59,6 +59,11 @@ import {
|
|
|
59
59
|
isRecord as isLabelerRecord,
|
|
60
60
|
} from '../lexicon/types/app/bsky/labeler/service'
|
|
61
61
|
import { RecordDeleted as NotificationRecordDeleted } from '../lexicon/types/app/bsky/notification/defs'
|
|
62
|
+
import { ThreadHiddenItem } from '../lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2'
|
|
63
|
+
import {
|
|
64
|
+
QueryParams as GetPostThreadV2QueryParams,
|
|
65
|
+
ThreadItem,
|
|
66
|
+
} from '../lexicon/types/app/bsky/unspecced/getPostThreadV2'
|
|
62
67
|
import { isSelfLabels } from '../lexicon/types/com/atproto/label/defs'
|
|
63
68
|
import { $Typed, Un$Typed } from '../lexicon/util'
|
|
64
69
|
import { Notification } from '../proto/bsky_pb'
|
|
@@ -69,6 +74,17 @@ import {
|
|
|
69
74
|
uriToDid,
|
|
70
75
|
uriToDid as creatorFromUri,
|
|
71
76
|
} from '../util/uris'
|
|
77
|
+
import {
|
|
78
|
+
ThreadHiddenAnchorPostNode,
|
|
79
|
+
ThreadHiddenPostNode,
|
|
80
|
+
ThreadItemValueBlocked,
|
|
81
|
+
ThreadItemValueNoUnauthenticated,
|
|
82
|
+
ThreadItemValueNotFound,
|
|
83
|
+
ThreadItemValuePost,
|
|
84
|
+
ThreadTree,
|
|
85
|
+
ThreadTreeVisible,
|
|
86
|
+
sortTrimFlattenThreadTree,
|
|
87
|
+
} from './threads-v2'
|
|
72
88
|
import {
|
|
73
89
|
Embed,
|
|
74
90
|
EmbedBlocked,
|
|
@@ -114,11 +130,15 @@ export class Views {
|
|
|
114
130
|
public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder
|
|
115
131
|
public videoUriBuilder: VideoUriBuilder = this.opts.videoUriBuilder
|
|
116
132
|
public indexedAtEpoch: Date | undefined = this.opts.indexedAtEpoch
|
|
133
|
+
private threadTagsBumpDown: readonly string[] = this.opts.threadTagsBumpDown
|
|
134
|
+
private threadTagsHide: readonly string[] = this.opts.threadTagsHide
|
|
117
135
|
constructor(
|
|
118
136
|
private opts: {
|
|
119
137
|
imgUriBuilder: ImageUriBuilder
|
|
120
138
|
videoUriBuilder: VideoUriBuilder
|
|
121
139
|
indexedAtEpoch: Date | undefined
|
|
140
|
+
threadTagsBumpDown: readonly string[]
|
|
141
|
+
threadTagsHide: readonly string[]
|
|
122
142
|
},
|
|
123
143
|
) {}
|
|
124
144
|
|
|
@@ -145,6 +165,13 @@ export class Views {
|
|
|
145
165
|
return false
|
|
146
166
|
}
|
|
147
167
|
|
|
168
|
+
noUnauthenticatedPost(state: HydrationState, post: PostView): boolean {
|
|
169
|
+
const isNoUnauthenticated = post.author.labels?.some(
|
|
170
|
+
(l) => l.val === '!no-unauthenticated',
|
|
171
|
+
)
|
|
172
|
+
return !state.ctx?.viewer && !!isNoUnauthenticated
|
|
173
|
+
}
|
|
174
|
+
|
|
148
175
|
viewerBlockExists(did: string, state: HydrationState): boolean {
|
|
149
176
|
const viewer = state.profileViewers?.get(did)
|
|
150
177
|
if (!viewer) return false
|
|
@@ -1114,6 +1141,759 @@ export class Views {
|
|
|
1114
1141
|
})
|
|
1115
1142
|
}
|
|
1116
1143
|
|
|
1144
|
+
// Threads V2
|
|
1145
|
+
// ------------
|
|
1146
|
+
|
|
1147
|
+
threadV2(
|
|
1148
|
+
skeleton: { anchor: string; uris: string[] },
|
|
1149
|
+
state: HydrationState,
|
|
1150
|
+
{
|
|
1151
|
+
above,
|
|
1152
|
+
below,
|
|
1153
|
+
branchingFactor,
|
|
1154
|
+
prioritizeFollowedUsers,
|
|
1155
|
+
sort,
|
|
1156
|
+
}: {
|
|
1157
|
+
above: number
|
|
1158
|
+
below: number
|
|
1159
|
+
branchingFactor: number
|
|
1160
|
+
prioritizeFollowedUsers: boolean
|
|
1161
|
+
sort: GetPostThreadV2QueryParams['sort']
|
|
1162
|
+
},
|
|
1163
|
+
): { hasHiddenReplies: boolean; thread: ThreadItem[] } {
|
|
1164
|
+
const { anchor: anchorUri, uris } = skeleton
|
|
1165
|
+
|
|
1166
|
+
// Not found.
|
|
1167
|
+
const postView = this.post(anchorUri, state)
|
|
1168
|
+
const post = state.posts?.get(anchorUri)
|
|
1169
|
+
if (!post || !postView) {
|
|
1170
|
+
return {
|
|
1171
|
+
hasHiddenReplies: false,
|
|
1172
|
+
thread: [
|
|
1173
|
+
this.threadV2ItemNotFound({
|
|
1174
|
+
uri: anchorUri,
|
|
1175
|
+
depth: 0,
|
|
1176
|
+
}),
|
|
1177
|
+
],
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Blocked (only 1p for anchor).
|
|
1182
|
+
if (this.viewerBlockExists(postView.author.did, state)) {
|
|
1183
|
+
return {
|
|
1184
|
+
hasHiddenReplies: false,
|
|
1185
|
+
thread: [
|
|
1186
|
+
this.threadV2ItemBlocked({
|
|
1187
|
+
uri: anchorUri,
|
|
1188
|
+
depth: 0,
|
|
1189
|
+
authorDid: postView.author.did,
|
|
1190
|
+
state,
|
|
1191
|
+
}),
|
|
1192
|
+
],
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const childrenByParentUri = this.groupThreadChildrenByParent(
|
|
1197
|
+
anchorUri,
|
|
1198
|
+
uris,
|
|
1199
|
+
state,
|
|
1200
|
+
)
|
|
1201
|
+
const rootUri = getRootUri(anchorUri, post)
|
|
1202
|
+
const opDid = uriToDid(rootUri)
|
|
1203
|
+
const authorDid = postView.author.did
|
|
1204
|
+
const isOPPost = authorDid === opDid
|
|
1205
|
+
const anchorViolatesThreadGate = post.violatesThreadGate
|
|
1206
|
+
|
|
1207
|
+
// Builds the parent tree, and whether it is a contiguous OP thread.
|
|
1208
|
+
const parentTree = !anchorViolatesThreadGate
|
|
1209
|
+
? this.threadV2Parent(
|
|
1210
|
+
{
|
|
1211
|
+
childUri: anchorUri,
|
|
1212
|
+
opDid,
|
|
1213
|
+
rootUri,
|
|
1214
|
+
|
|
1215
|
+
above,
|
|
1216
|
+
depth: -1,
|
|
1217
|
+
},
|
|
1218
|
+
state,
|
|
1219
|
+
)
|
|
1220
|
+
: undefined
|
|
1221
|
+
|
|
1222
|
+
const { tree: parent, isOPThread: isOPThreadFromRootToParent } =
|
|
1223
|
+
parentTree ?? { tree: undefined, isOPThread: false }
|
|
1224
|
+
|
|
1225
|
+
const isOPThread = parent
|
|
1226
|
+
? isOPThreadFromRootToParent && isOPPost
|
|
1227
|
+
: isOPPost
|
|
1228
|
+
|
|
1229
|
+
const anchorDepth = 0 // The depth of the anchor post is always 0.
|
|
1230
|
+
let anchorTree: ThreadTree
|
|
1231
|
+
let hasHiddenReplies = false
|
|
1232
|
+
|
|
1233
|
+
if (this.noUnauthenticatedPost(state, postView)) {
|
|
1234
|
+
anchorTree = {
|
|
1235
|
+
type: 'noUnauthenticated',
|
|
1236
|
+
item: this.threadV2ItemNoUnauthenticated({
|
|
1237
|
+
uri: anchorUri,
|
|
1238
|
+
depth: anchorDepth,
|
|
1239
|
+
}),
|
|
1240
|
+
parent,
|
|
1241
|
+
}
|
|
1242
|
+
} else {
|
|
1243
|
+
const { replies, hasHiddenReplies: hasHiddenRepliesShadow } =
|
|
1244
|
+
!anchorViolatesThreadGate
|
|
1245
|
+
? this.threadV2Replies(
|
|
1246
|
+
{
|
|
1247
|
+
parentUri: anchorUri,
|
|
1248
|
+
isOPThread,
|
|
1249
|
+
opDid,
|
|
1250
|
+
rootUri,
|
|
1251
|
+
childrenByParentUri,
|
|
1252
|
+
below,
|
|
1253
|
+
depth: 1,
|
|
1254
|
+
branchingFactor,
|
|
1255
|
+
prioritizeFollowedUsers,
|
|
1256
|
+
},
|
|
1257
|
+
state,
|
|
1258
|
+
)
|
|
1259
|
+
: { replies: undefined, hasHiddenReplies: false }
|
|
1260
|
+
hasHiddenReplies = hasHiddenRepliesShadow
|
|
1261
|
+
|
|
1262
|
+
anchorTree = {
|
|
1263
|
+
type: 'post',
|
|
1264
|
+
item: this.threadV2ItemPost({
|
|
1265
|
+
depth: anchorDepth,
|
|
1266
|
+
isOPThread,
|
|
1267
|
+
postView,
|
|
1268
|
+
repliesAllowance: Infinity, // While we don't have pagination.
|
|
1269
|
+
uri: anchorUri,
|
|
1270
|
+
}),
|
|
1271
|
+
tags: post.tags,
|
|
1272
|
+
hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
|
|
1273
|
+
parent,
|
|
1274
|
+
replies,
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const thread = sortTrimFlattenThreadTree<ThreadItem>(anchorTree, {
|
|
1279
|
+
opDid,
|
|
1280
|
+
branchingFactor,
|
|
1281
|
+
sort,
|
|
1282
|
+
prioritizeFollowedUsers,
|
|
1283
|
+
viewer: state.ctx?.viewer ?? null,
|
|
1284
|
+
threadTagsBumpDown: this.threadTagsBumpDown,
|
|
1285
|
+
threadTagsHide: this.threadTagsHide,
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
hasHiddenReplies,
|
|
1290
|
+
thread,
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
private threadV2Parent(
|
|
1295
|
+
{
|
|
1296
|
+
childUri,
|
|
1297
|
+
opDid,
|
|
1298
|
+
rootUri,
|
|
1299
|
+
above,
|
|
1300
|
+
depth,
|
|
1301
|
+
}: {
|
|
1302
|
+
childUri: string
|
|
1303
|
+
opDid: string
|
|
1304
|
+
rootUri: string
|
|
1305
|
+
above: number
|
|
1306
|
+
depth: number
|
|
1307
|
+
},
|
|
1308
|
+
state: HydrationState,
|
|
1309
|
+
): { tree: ThreadTreeVisible; isOPThread: boolean } | undefined {
|
|
1310
|
+
// Reached the `above` limit.
|
|
1311
|
+
if (Math.abs(depth) > above) {
|
|
1312
|
+
return undefined
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Not found.
|
|
1316
|
+
const uri = state.posts?.get(childUri)?.record.reply?.parent.uri
|
|
1317
|
+
if (!uri) {
|
|
1318
|
+
return undefined
|
|
1319
|
+
}
|
|
1320
|
+
const postView = this.post(uri, state)
|
|
1321
|
+
const post = state.posts?.get(uri)
|
|
1322
|
+
if (!post || !postView) {
|
|
1323
|
+
return {
|
|
1324
|
+
tree: {
|
|
1325
|
+
type: 'notFound',
|
|
1326
|
+
item: this.threadV2ItemNotFound({ uri, depth }),
|
|
1327
|
+
},
|
|
1328
|
+
isOPThread: false,
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (rootUri !== getRootUri(uri, post)) {
|
|
1332
|
+
// Outside thread boundary.
|
|
1333
|
+
return undefined
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Blocked (1p and 3p for parent).
|
|
1337
|
+
const authorDid = postView.author.did
|
|
1338
|
+
const has1pBlock = this.viewerBlockExists(authorDid, state)
|
|
1339
|
+
const has3pBlock =
|
|
1340
|
+
!state.ctx?.include3pBlocks && state.postBlocks?.get(childUri)?.parent
|
|
1341
|
+
if (has1pBlock || has3pBlock) {
|
|
1342
|
+
return {
|
|
1343
|
+
tree: {
|
|
1344
|
+
type: 'blocked',
|
|
1345
|
+
item: this.threadV2ItemBlocked({
|
|
1346
|
+
uri,
|
|
1347
|
+
depth,
|
|
1348
|
+
authorDid,
|
|
1349
|
+
state,
|
|
1350
|
+
}),
|
|
1351
|
+
},
|
|
1352
|
+
isOPThread: false,
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Recurse up.
|
|
1357
|
+
const parentTree = this.threadV2Parent(
|
|
1358
|
+
{
|
|
1359
|
+
childUri: uri,
|
|
1360
|
+
opDid,
|
|
1361
|
+
rootUri,
|
|
1362
|
+
above,
|
|
1363
|
+
depth: depth - 1,
|
|
1364
|
+
},
|
|
1365
|
+
state,
|
|
1366
|
+
)
|
|
1367
|
+
const { tree: parent, isOPThread: isOPThreadFromRootToParent } =
|
|
1368
|
+
parentTree ?? { tree: undefined, isOPThread: false }
|
|
1369
|
+
|
|
1370
|
+
const isOPPost = authorDid === opDid
|
|
1371
|
+
const isOPThread = parent
|
|
1372
|
+
? isOPThreadFromRootToParent && isOPPost
|
|
1373
|
+
: isOPPost
|
|
1374
|
+
|
|
1375
|
+
if (this.noUnauthenticatedPost(state, postView)) {
|
|
1376
|
+
return {
|
|
1377
|
+
tree: {
|
|
1378
|
+
type: 'noUnauthenticated',
|
|
1379
|
+
item: this.threadV2ItemNoUnauthenticated({
|
|
1380
|
+
uri,
|
|
1381
|
+
depth,
|
|
1382
|
+
}),
|
|
1383
|
+
parent,
|
|
1384
|
+
},
|
|
1385
|
+
isOPThread,
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const parentUri = post.record.reply?.parent.uri
|
|
1390
|
+
const hasMoreParents = !!parentUri && !parent
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
tree: {
|
|
1394
|
+
type: 'post',
|
|
1395
|
+
item: this.threadV2ItemPost({
|
|
1396
|
+
depth,
|
|
1397
|
+
isOPThread,
|
|
1398
|
+
moreParents: hasMoreParents,
|
|
1399
|
+
postView,
|
|
1400
|
+
uri,
|
|
1401
|
+
}),
|
|
1402
|
+
tags: post.tags,
|
|
1403
|
+
hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
|
|
1404
|
+
parent,
|
|
1405
|
+
replies: undefined,
|
|
1406
|
+
},
|
|
1407
|
+
isOPThread,
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
private threadV2Replies(
|
|
1412
|
+
{
|
|
1413
|
+
parentUri,
|
|
1414
|
+
isOPThread: isOPThreadFromRootToParent,
|
|
1415
|
+
opDid,
|
|
1416
|
+
rootUri,
|
|
1417
|
+
childrenByParentUri,
|
|
1418
|
+
below,
|
|
1419
|
+
depth,
|
|
1420
|
+
branchingFactor,
|
|
1421
|
+
prioritizeFollowedUsers,
|
|
1422
|
+
}: {
|
|
1423
|
+
parentUri: string
|
|
1424
|
+
isOPThread: boolean
|
|
1425
|
+
opDid: string
|
|
1426
|
+
rootUri: string
|
|
1427
|
+
childrenByParentUri: Record<string, string[]>
|
|
1428
|
+
below: number
|
|
1429
|
+
depth: number
|
|
1430
|
+
branchingFactor: number
|
|
1431
|
+
prioritizeFollowedUsers: boolean
|
|
1432
|
+
},
|
|
1433
|
+
state: HydrationState,
|
|
1434
|
+
): { replies: ThreadTreeVisible[] | undefined; hasHiddenReplies: boolean } {
|
|
1435
|
+
// Reached the `below` limit.
|
|
1436
|
+
if (depth > below) {
|
|
1437
|
+
return { replies: undefined, hasHiddenReplies: false }
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const childrenUris = childrenByParentUri[parentUri] ?? []
|
|
1441
|
+
let hasHiddenReplies = false
|
|
1442
|
+
const replies = mapDefined(childrenUris, (uri) => {
|
|
1443
|
+
const replyInclusion = this.checkThreadV2ReplyInclusion({
|
|
1444
|
+
uri,
|
|
1445
|
+
rootUri,
|
|
1446
|
+
state,
|
|
1447
|
+
})
|
|
1448
|
+
if (!replyInclusion) {
|
|
1449
|
+
return undefined
|
|
1450
|
+
}
|
|
1451
|
+
const { authorDid, post, postView } = replyInclusion
|
|
1452
|
+
|
|
1453
|
+
// Hidden.
|
|
1454
|
+
const { isHidden } = this.isHiddenThreadPost(
|
|
1455
|
+
{ post, postView, prioritizeFollowedUsers, rootUri, uri },
|
|
1456
|
+
state,
|
|
1457
|
+
)
|
|
1458
|
+
if (isHidden) {
|
|
1459
|
+
// Only care about anchor replies
|
|
1460
|
+
if (depth === 1) {
|
|
1461
|
+
hasHiddenReplies = true
|
|
1462
|
+
}
|
|
1463
|
+
return undefined
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Recurse down.
|
|
1467
|
+
const isOPThread = isOPThreadFromRootToParent && authorDid === opDid
|
|
1468
|
+
const { replies: nestedReplies } = this.threadV2Replies(
|
|
1469
|
+
{
|
|
1470
|
+
parentUri: uri,
|
|
1471
|
+
isOPThread,
|
|
1472
|
+
opDid,
|
|
1473
|
+
rootUri,
|
|
1474
|
+
childrenByParentUri,
|
|
1475
|
+
below,
|
|
1476
|
+
depth: depth + 1,
|
|
1477
|
+
branchingFactor,
|
|
1478
|
+
prioritizeFollowedUsers,
|
|
1479
|
+
},
|
|
1480
|
+
state,
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
const reachedDepth = depth === below
|
|
1484
|
+
const repliesAllowance = reachedDepth ? 0 : branchingFactor
|
|
1485
|
+
|
|
1486
|
+
const tree: ThreadTree = {
|
|
1487
|
+
type: 'post',
|
|
1488
|
+
item: this.threadV2ItemPost({
|
|
1489
|
+
depth,
|
|
1490
|
+
isOPThread,
|
|
1491
|
+
postView,
|
|
1492
|
+
repliesAllowance,
|
|
1493
|
+
uri,
|
|
1494
|
+
}),
|
|
1495
|
+
tags: post.tags,
|
|
1496
|
+
hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
|
|
1497
|
+
parent: undefined,
|
|
1498
|
+
replies: nestedReplies,
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
return tree
|
|
1502
|
+
})
|
|
1503
|
+
|
|
1504
|
+
return {
|
|
1505
|
+
replies,
|
|
1506
|
+
hasHiddenReplies,
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
private threadV2ItemPost({
|
|
1511
|
+
depth,
|
|
1512
|
+
isOPThread,
|
|
1513
|
+
moreParents,
|
|
1514
|
+
postView,
|
|
1515
|
+
repliesAllowance,
|
|
1516
|
+
uri,
|
|
1517
|
+
}: {
|
|
1518
|
+
depth: number
|
|
1519
|
+
isOPThread: boolean
|
|
1520
|
+
moreParents?: boolean
|
|
1521
|
+
postView: PostView
|
|
1522
|
+
repliesAllowance?: number
|
|
1523
|
+
uri: string
|
|
1524
|
+
}): ThreadItemValuePost {
|
|
1525
|
+
const moreReplies =
|
|
1526
|
+
repliesAllowance === undefined
|
|
1527
|
+
? 0
|
|
1528
|
+
: Math.max((postView.replyCount ?? 0) - repliesAllowance, 0)
|
|
1529
|
+
|
|
1530
|
+
return {
|
|
1531
|
+
uri,
|
|
1532
|
+
depth,
|
|
1533
|
+
value: {
|
|
1534
|
+
$type: 'app.bsky.unspecced.getPostThreadV2#threadItemPost',
|
|
1535
|
+
post: postView,
|
|
1536
|
+
moreParents: moreParents ?? false,
|
|
1537
|
+
moreReplies,
|
|
1538
|
+
opThread: isOPThread,
|
|
1539
|
+
},
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
private threadV2ItemNoUnauthenticated({
|
|
1544
|
+
uri,
|
|
1545
|
+
depth,
|
|
1546
|
+
}: {
|
|
1547
|
+
uri: string
|
|
1548
|
+
depth: number
|
|
1549
|
+
}): ThreadItemValueNoUnauthenticated {
|
|
1550
|
+
return {
|
|
1551
|
+
uri,
|
|
1552
|
+
depth,
|
|
1553
|
+
value: {
|
|
1554
|
+
$type: 'app.bsky.unspecced.getPostThreadV2#threadItemNoUnauthenticated',
|
|
1555
|
+
},
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private threadV2ItemNotFound({
|
|
1560
|
+
uri,
|
|
1561
|
+
depth,
|
|
1562
|
+
}: {
|
|
1563
|
+
uri: string
|
|
1564
|
+
depth: number
|
|
1565
|
+
}): ThreadItemValueNotFound {
|
|
1566
|
+
return {
|
|
1567
|
+
uri,
|
|
1568
|
+
depth,
|
|
1569
|
+
value: {
|
|
1570
|
+
$type: 'app.bsky.unspecced.getPostThreadV2#threadItemNotFound',
|
|
1571
|
+
},
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
private threadV2ItemBlocked({
|
|
1576
|
+
uri,
|
|
1577
|
+
depth,
|
|
1578
|
+
authorDid,
|
|
1579
|
+
state,
|
|
1580
|
+
}: {
|
|
1581
|
+
uri: string
|
|
1582
|
+
depth: number
|
|
1583
|
+
authorDid: string
|
|
1584
|
+
state: HydrationState
|
|
1585
|
+
}): ThreadItemValueBlocked {
|
|
1586
|
+
return {
|
|
1587
|
+
uri,
|
|
1588
|
+
depth,
|
|
1589
|
+
value: {
|
|
1590
|
+
$type: 'app.bsky.unspecced.getPostThreadV2#threadItemBlocked',
|
|
1591
|
+
author: {
|
|
1592
|
+
did: authorDid,
|
|
1593
|
+
viewer: this.blockedProfileViewer(authorDid, state),
|
|
1594
|
+
},
|
|
1595
|
+
},
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
threadHiddenV2(
|
|
1600
|
+
skeleton: { anchor: string; uris: string[] },
|
|
1601
|
+
state: HydrationState,
|
|
1602
|
+
{
|
|
1603
|
+
below,
|
|
1604
|
+
branchingFactor,
|
|
1605
|
+
prioritizeFollowedUsers,
|
|
1606
|
+
}: {
|
|
1607
|
+
below: number
|
|
1608
|
+
branchingFactor: number
|
|
1609
|
+
prioritizeFollowedUsers: boolean
|
|
1610
|
+
},
|
|
1611
|
+
): ThreadHiddenItem[] {
|
|
1612
|
+
const { anchor: anchorUri, uris } = skeleton
|
|
1613
|
+
|
|
1614
|
+
// Not found.
|
|
1615
|
+
const postView = this.post(anchorUri, state)
|
|
1616
|
+
const post = state.posts?.get(anchorUri)
|
|
1617
|
+
if (!post || !postView) {
|
|
1618
|
+
return []
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Blocked (only 1p for anchor).
|
|
1622
|
+
if (this.viewerBlockExists(postView.author.did, state)) {
|
|
1623
|
+
return []
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const childrenByParentUri = this.groupThreadChildrenByParent(
|
|
1627
|
+
anchorUri,
|
|
1628
|
+
uris,
|
|
1629
|
+
state,
|
|
1630
|
+
)
|
|
1631
|
+
const rootUri = getRootUri(anchorUri, post)
|
|
1632
|
+
const opDid = uriToDid(rootUri)
|
|
1633
|
+
|
|
1634
|
+
const anchorTree: ThreadHiddenAnchorPostNode = {
|
|
1635
|
+
type: 'hiddenAnchor',
|
|
1636
|
+
item: this.threadHiddenV2ItemPostAnchor({ depth: 0, uri: anchorUri }),
|
|
1637
|
+
replies: this.threadHiddenV2Replies(
|
|
1638
|
+
{
|
|
1639
|
+
parentUri: anchorUri,
|
|
1640
|
+
rootUri,
|
|
1641
|
+
childrenByParentUri,
|
|
1642
|
+
below,
|
|
1643
|
+
depth: 1,
|
|
1644
|
+
prioritizeFollowedUsers,
|
|
1645
|
+
},
|
|
1646
|
+
state,
|
|
1647
|
+
),
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
return sortTrimFlattenThreadTree(anchorTree, {
|
|
1651
|
+
opDid,
|
|
1652
|
+
branchingFactor,
|
|
1653
|
+
prioritizeFollowedUsers: false,
|
|
1654
|
+
viewer: state.ctx?.viewer ?? null,
|
|
1655
|
+
threadTagsBumpDown: this.threadTagsBumpDown,
|
|
1656
|
+
threadTagsHide: this.threadTagsHide,
|
|
1657
|
+
})
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
private threadHiddenV2Replies(
|
|
1661
|
+
{
|
|
1662
|
+
parentUri,
|
|
1663
|
+
rootUri,
|
|
1664
|
+
childrenByParentUri,
|
|
1665
|
+
below,
|
|
1666
|
+
depth,
|
|
1667
|
+
prioritizeFollowedUsers,
|
|
1668
|
+
}: {
|
|
1669
|
+
parentUri: string
|
|
1670
|
+
rootUri: string
|
|
1671
|
+
childrenByParentUri: Record<string, string[]>
|
|
1672
|
+
below: number
|
|
1673
|
+
depth: number
|
|
1674
|
+
prioritizeFollowedUsers: boolean
|
|
1675
|
+
},
|
|
1676
|
+
state: HydrationState,
|
|
1677
|
+
): ThreadHiddenPostNode[] | undefined {
|
|
1678
|
+
// Reached the `below` limit.
|
|
1679
|
+
if (depth > below) {
|
|
1680
|
+
return undefined
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const childrenUris = childrenByParentUri[parentUri] ?? []
|
|
1684
|
+
return mapDefined(childrenUris, (uri) => {
|
|
1685
|
+
const replyInclusion = this.checkThreadV2ReplyInclusion({
|
|
1686
|
+
uri,
|
|
1687
|
+
rootUri,
|
|
1688
|
+
state,
|
|
1689
|
+
})
|
|
1690
|
+
if (!replyInclusion) {
|
|
1691
|
+
return undefined
|
|
1692
|
+
}
|
|
1693
|
+
const { post, postView } = replyInclusion
|
|
1694
|
+
|
|
1695
|
+
// Hidden.
|
|
1696
|
+
const { isHidden, hiddenByThreadgate, mutedByViewer } =
|
|
1697
|
+
this.isHiddenThreadPost(
|
|
1698
|
+
{ post, postView, rootUri, prioritizeFollowedUsers, uri },
|
|
1699
|
+
state,
|
|
1700
|
+
)
|
|
1701
|
+
if (isHidden) {
|
|
1702
|
+
// Only show hidden anchor replies, not all hidden.
|
|
1703
|
+
if (depth > 1) {
|
|
1704
|
+
return undefined
|
|
1705
|
+
}
|
|
1706
|
+
} else if (depth === 1) {
|
|
1707
|
+
// Don't include non-hidden anchor replies.
|
|
1708
|
+
return undefined
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Recurse down.
|
|
1712
|
+
const replies = this.threadHiddenV2Replies(
|
|
1713
|
+
{
|
|
1714
|
+
parentUri: uri,
|
|
1715
|
+
rootUri,
|
|
1716
|
+
childrenByParentUri,
|
|
1717
|
+
below,
|
|
1718
|
+
depth: depth + 1,
|
|
1719
|
+
prioritizeFollowedUsers,
|
|
1720
|
+
},
|
|
1721
|
+
state,
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
const item = this.threadHiddenV2ItemPost({
|
|
1725
|
+
depth,
|
|
1726
|
+
hiddenByThreadgate,
|
|
1727
|
+
mutedByViewer,
|
|
1728
|
+
postView,
|
|
1729
|
+
uri,
|
|
1730
|
+
})
|
|
1731
|
+
|
|
1732
|
+
const tree: ThreadHiddenPostNode = {
|
|
1733
|
+
type: 'hiddenPost',
|
|
1734
|
+
item: item,
|
|
1735
|
+
tags: post.tags,
|
|
1736
|
+
replies,
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
return tree
|
|
1740
|
+
})
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
private threadHiddenV2ItemPostAnchor({
|
|
1744
|
+
depth,
|
|
1745
|
+
uri,
|
|
1746
|
+
}: {
|
|
1747
|
+
depth: number
|
|
1748
|
+
uri: string
|
|
1749
|
+
}): ThreadHiddenAnchorPostNode['item'] {
|
|
1750
|
+
return {
|
|
1751
|
+
uri,
|
|
1752
|
+
depth,
|
|
1753
|
+
// In hidden replies, the anchor value is undefined, so it doesn't include the anchor in the result.
|
|
1754
|
+
// This is helpful so we can use the same internal structure for hidden and non-hidden, while omitting anchor for hidden.
|
|
1755
|
+
value: undefined,
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
private threadHiddenV2ItemPost({
|
|
1760
|
+
depth,
|
|
1761
|
+
hiddenByThreadgate,
|
|
1762
|
+
mutedByViewer,
|
|
1763
|
+
postView,
|
|
1764
|
+
uri,
|
|
1765
|
+
}: {
|
|
1766
|
+
depth: number
|
|
1767
|
+
hiddenByThreadgate: boolean
|
|
1768
|
+
mutedByViewer: boolean
|
|
1769
|
+
postView: PostView
|
|
1770
|
+
uri: string
|
|
1771
|
+
}): ThreadHiddenPostNode['item'] {
|
|
1772
|
+
const base = this.threadHiddenV2ItemPostAnchor({ depth, uri })
|
|
1773
|
+
return {
|
|
1774
|
+
...base,
|
|
1775
|
+
value: {
|
|
1776
|
+
$type: 'app.bsky.unspecced.getPostThreadHiddenV2#threadHiddenItemPost',
|
|
1777
|
+
post: postView,
|
|
1778
|
+
hiddenByThreadgate,
|
|
1779
|
+
mutedByViewer,
|
|
1780
|
+
},
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
private checkThreadV2ReplyInclusion({
|
|
1785
|
+
uri,
|
|
1786
|
+
rootUri,
|
|
1787
|
+
state,
|
|
1788
|
+
}: {
|
|
1789
|
+
uri: string
|
|
1790
|
+
rootUri: string
|
|
1791
|
+
state: HydrationState
|
|
1792
|
+
}): {
|
|
1793
|
+
authorDid: string
|
|
1794
|
+
post: Post
|
|
1795
|
+
postView: PostView
|
|
1796
|
+
} | null {
|
|
1797
|
+
// Not found.
|
|
1798
|
+
const post = state.posts?.get(uri)
|
|
1799
|
+
if (post?.violatesThreadGate) {
|
|
1800
|
+
return null
|
|
1801
|
+
}
|
|
1802
|
+
const postView = this.post(uri, state)
|
|
1803
|
+
if (!post || !postView) {
|
|
1804
|
+
return null
|
|
1805
|
+
}
|
|
1806
|
+
const authorDid = postView.author.did
|
|
1807
|
+
if (rootUri !== getRootUri(uri, post)) {
|
|
1808
|
+
// outside thread boundary
|
|
1809
|
+
return null
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Blocked (1p and 3p for replies).
|
|
1813
|
+
const has1pBlock = this.viewerBlockExists(authorDid, state)
|
|
1814
|
+
const has3pBlock =
|
|
1815
|
+
!state.ctx?.include3pBlocks && state.postBlocks?.get(uri)?.parent
|
|
1816
|
+
if (has1pBlock || has3pBlock) {
|
|
1817
|
+
return null
|
|
1818
|
+
}
|
|
1819
|
+
if (!this.viewerSeesNeedsReview({ uri, did: authorDid }, state)) {
|
|
1820
|
+
return null
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// No unauthenticated.
|
|
1824
|
+
if (this.noUnauthenticatedPost(state, postView)) {
|
|
1825
|
+
return null
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
return { authorDid, post, postView }
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
private isHiddenThreadPost(
|
|
1832
|
+
{
|
|
1833
|
+
post,
|
|
1834
|
+
postView,
|
|
1835
|
+
prioritizeFollowedUsers,
|
|
1836
|
+
rootUri,
|
|
1837
|
+
uri,
|
|
1838
|
+
}: {
|
|
1839
|
+
post: Post
|
|
1840
|
+
postView: PostView
|
|
1841
|
+
prioritizeFollowedUsers: boolean
|
|
1842
|
+
rootUri: string
|
|
1843
|
+
uri: string
|
|
1844
|
+
},
|
|
1845
|
+
state: HydrationState,
|
|
1846
|
+
): {
|
|
1847
|
+
isHidden: boolean
|
|
1848
|
+
hiddenByTag: boolean
|
|
1849
|
+
hiddenByThreadgate: boolean
|
|
1850
|
+
mutedByViewer: boolean
|
|
1851
|
+
} {
|
|
1852
|
+
const opDid = creatorFromUri(rootUri)
|
|
1853
|
+
const authorDid = creatorFromUri(uri)
|
|
1854
|
+
|
|
1855
|
+
const showBecauseFollowing =
|
|
1856
|
+
prioritizeFollowedUsers && !!postView.author.viewer?.following
|
|
1857
|
+
const hiddenByTag =
|
|
1858
|
+
authorDid !== opDid &&
|
|
1859
|
+
authorDid !== state.ctx?.viewer &&
|
|
1860
|
+
!showBecauseFollowing &&
|
|
1861
|
+
this.threadTagsHide.some((t) => post.tags.has(t))
|
|
1862
|
+
|
|
1863
|
+
const hiddenByThreadgate =
|
|
1864
|
+
state.ctx?.viewer !== authorDid &&
|
|
1865
|
+
this.replyIsHiddenByThreadgate(uri, rootUri, state)
|
|
1866
|
+
|
|
1867
|
+
const mutedByViewer = this.viewerMuteExists(authorDid, state)
|
|
1868
|
+
|
|
1869
|
+
return {
|
|
1870
|
+
isHidden: hiddenByTag || hiddenByThreadgate || mutedByViewer,
|
|
1871
|
+
hiddenByTag,
|
|
1872
|
+
hiddenByThreadgate,
|
|
1873
|
+
mutedByViewer,
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
private groupThreadChildrenByParent(
|
|
1878
|
+
anchorUri: string,
|
|
1879
|
+
uris: string[],
|
|
1880
|
+
state: HydrationState,
|
|
1881
|
+
): Record<string, string[]> {
|
|
1882
|
+
// Groups children of each parent.
|
|
1883
|
+
const includedPosts = new Set<string>([anchorUri])
|
|
1884
|
+
const childrenByParentUri: Record<string, string[]> = {}
|
|
1885
|
+
uris.forEach((uri) => {
|
|
1886
|
+
const post = state.posts?.get(uri)
|
|
1887
|
+
const parentUri = post?.record.reply?.parent.uri
|
|
1888
|
+
if (!parentUri) return
|
|
1889
|
+
if (includedPosts.has(uri)) return
|
|
1890
|
+
includedPosts.add(uri)
|
|
1891
|
+
childrenByParentUri[parentUri] ??= []
|
|
1892
|
+
childrenByParentUri[parentUri].push(uri)
|
|
1893
|
+
})
|
|
1894
|
+
return childrenByParentUri
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1117
1897
|
// Embeds
|
|
1118
1898
|
// ------------
|
|
1119
1899
|
|