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