@atproto/bsky 0.0.150 → 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 +16 -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/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.d.ts +4 -0
- package/dist/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.d.ts.map +1 -0
- package/dist/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.js +17 -0
- package/dist/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.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/like.d.ts +2 -0
- package/dist/data-plane/server/db/tables/like.d.ts.map +1 -1
- package/dist/data-plane/server/db/tables/repost.d.ts +2 -0
- package/dist/data-plane/server/db/tables/repost.d.ts.map +1 -1
- package/dist/data-plane/server/indexing/plugins/like.d.ts.map +1 -1
- package/dist/data-plane/server/indexing/plugins/like.js +32 -9
- package/dist/data-plane/server/indexing/plugins/like.js.map +1 -1
- package/dist/data-plane/server/indexing/plugins/repost.d.ts.map +1 -1
- package/dist/data-plane/server/indexing/plugins/repost.js +32 -9
- package/dist/data-plane/server/indexing/plugins/repost.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 +536 -88
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +279 -44
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts +2 -0
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/like.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/feed/like.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/like.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/repost.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/feed/repost.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/repost.js.map +1 -1
- package/dist/lexicon/types/app/bsky/notification/listNotifications.d.ts +2 -2
- package/dist/lexicon/types/app/bsky/notification/listNotifications.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/notification/listNotifications.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 +36 -1
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +468 -2
- 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/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.ts +17 -0
- package/src/data-plane/server/db/migrations/index.ts +1 -0
- package/src/data-plane/server/db/tables/like.ts +2 -0
- package/src/data-plane/server/db/tables/repost.ts +2 -0
- package/src/data-plane/server/indexing/plugins/like.ts +39 -9
- package/src/data-plane/server/indexing/plugins/repost.ts +38 -9
- package/src/hydration/feed.ts +1 -0
- package/src/lexicon/index.ts +33 -9
- package/src/lexicon/lexicons.ts +298 -45
- package/src/lexicon/types/app/bsky/feed/defs.ts +2 -0
- package/src/lexicon/types/app/bsky/feed/like.ts +1 -0
- package/src/lexicon/types/app/bsky/feed/repost.ts +1 -0
- package/src/lexicon/types/app/bsky/notification/listNotifications.ts +3 -1
- 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 +747 -2
- package/src/views/threads-v2.ts +351 -0
- package/tests/__snapshots__/feed-generation.test.ts.snap +2 -0
- package/tests/seed/thread-v2.ts +775 -0
- package/tests/seed/util.ts +52 -0
- package/tests/views/__snapshots__/author-feed.test.ts.snap +30 -24
- package/tests/views/__snapshots__/notifications.test.ts.snap +1164 -229
- package/tests/views/__snapshots__/thread-v2.test.ts.snap +1091 -0
- package/tests/views/__snapshots__/timeline.test.ts.snap +398 -376
- package/tests/views/notifications.test.ts +177 -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
|
|
@@ -868,7 +891,7 @@ export class Views {
|
|
|
868
891
|
const repost = state.reposts?.get(item.repost.uri)
|
|
869
892
|
if (!repost) return
|
|
870
893
|
if (repost.record.subject.uri !== item.post.uri) return
|
|
871
|
-
reason = this.reasonRepost(
|
|
894
|
+
reason = this.reasonRepost(item.repost.uri, repost, state)
|
|
872
895
|
if (!reason) return
|
|
873
896
|
}
|
|
874
897
|
const post = this.post(item.post.uri, state)
|
|
@@ -958,15 +981,18 @@ export class Views {
|
|
|
958
981
|
}
|
|
959
982
|
|
|
960
983
|
reasonRepost(
|
|
961
|
-
|
|
984
|
+
uri: string,
|
|
962
985
|
repost: Repost,
|
|
963
986
|
state: HydrationState,
|
|
964
987
|
): $Typed<ReasonRepost> | undefined {
|
|
988
|
+
const creatorDid = creatorFromUri(uri)
|
|
965
989
|
const creator = this.profileBasic(creatorDid, state)
|
|
966
990
|
if (!creator) return
|
|
967
991
|
return {
|
|
968
992
|
$type: 'app.bsky.feed.defs#reasonRepost',
|
|
969
993
|
by: creator,
|
|
994
|
+
uri,
|
|
995
|
+
cid: repost.cid,
|
|
970
996
|
indexedAt: this.indexedAt(repost).toISOString(),
|
|
971
997
|
}
|
|
972
998
|
}
|
|
@@ -1111,6 +1137,725 @@ export class Views {
|
|
|
1111
1137
|
})
|
|
1112
1138
|
}
|
|
1113
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
|
+
|
|
1114
1859
|
// Embeds
|
|
1115
1860
|
// ------------
|
|
1116
1861
|
|