@atproto/bsky 0.0.152 → 0.0.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.js +2 -1
  3. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.js.map +1 -1
  4. package/dist/api/com/atproto/repo/getRecord.d.ts.map +1 -1
  5. package/dist/api/com/atproto/repo/getRecord.js +1 -1
  6. package/dist/api/com/atproto/repo/getRecord.js.map +1 -1
  7. package/dist/config.d.ts +4 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +10 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.d.ts +4 -0
  12. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.d.ts.map +1 -0
  13. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.js +11 -0
  14. package/dist/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.js.map +1 -0
  15. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  16. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  17. package/dist/data-plane/server/db/migrations/index.js +2 -1
  18. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  19. package/dist/data-plane/server/db/tables/record.d.ts +1 -0
  20. package/dist/data-plane/server/db/tables/record.d.ts.map +1 -1
  21. package/dist/data-plane/server/db/tables/record.js.map +1 -1
  22. package/dist/data-plane/server/routes/records.d.ts.map +1 -1
  23. package/dist/data-plane/server/routes/records.js +1 -0
  24. package/dist/data-plane/server/routes/records.js.map +1 -1
  25. package/dist/hydration/feed.d.ts +1 -0
  26. package/dist/hydration/feed.d.ts.map +1 -1
  27. package/dist/hydration/feed.js +2 -0
  28. package/dist/hydration/feed.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/lexicon/lexicons.d.ts +112 -122
  33. package/dist/lexicon/lexicons.d.ts.map +1 -1
  34. package/dist/lexicon/lexicons.js +66 -66
  35. package/dist/lexicon/lexicons.js.map +1 -1
  36. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts +33 -0
  37. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts.map +1 -1
  38. package/dist/lexicon/types/app/bsky/unspecced/defs.js +36 -0
  39. package/dist/lexicon/types/app/bsky/unspecced/defs.js.map +1 -1
  40. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts +5 -13
  41. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +1 -1
  42. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js +0 -9
  43. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js.map +1 -1
  44. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts +2 -29
  45. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -1
  46. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.js +0 -36
  47. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
  48. package/dist/proto/bsky_pb.d.ts +4 -0
  49. package/dist/proto/bsky_pb.d.ts.map +1 -1
  50. package/dist/proto/bsky_pb.js +16 -0
  51. package/dist/proto/bsky_pb.js.map +1 -1
  52. package/dist/proto/bsync_connect.d.ts +19 -1
  53. package/dist/proto/bsync_connect.d.ts.map +1 -1
  54. package/dist/proto/bsync_connect.js +18 -0
  55. package/dist/proto/bsync_connect.js.map +1 -1
  56. package/dist/proto/bsync_pb.d.ts +150 -0
  57. package/dist/proto/bsync_pb.d.ts.map +1 -1
  58. package/dist/proto/bsync_pb.js +401 -1
  59. package/dist/proto/bsync_pb.js.map +1 -1
  60. package/dist/views/index.d.ts +6 -1
  61. package/dist/views/index.d.ts.map +1 -1
  62. package/dist/views/index.js +68 -27
  63. package/dist/views/index.js.map +1 -1
  64. package/dist/views/threads-v2.d.ts +9 -5
  65. package/dist/views/threads-v2.d.ts.map +1 -1
  66. package/dist/views/threads-v2.js +50 -25
  67. package/dist/views/threads-v2.js.map +1 -1
  68. package/package.json +7 -7
  69. package/proto/bsky.proto +1 -0
  70. package/src/api/app/bsky/unspecced/getPostThreadHiddenV2.ts +2 -1
  71. package/src/api/com/atproto/repo/getRecord.ts +4 -1
  72. package/src/config.ts +15 -0
  73. package/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts +9 -0
  74. package/src/data-plane/server/db/migrations/index.ts +1 -0
  75. package/src/data-plane/server/db/tables/record.ts +1 -0
  76. package/src/data-plane/server/routes/records.ts +1 -0
  77. package/src/hydration/feed.ts +3 -0
  78. package/src/index.ts +2 -0
  79. package/src/lexicon/lexicons.ts +72 -71
  80. package/src/lexicon/types/app/bsky/unspecced/defs.ts +73 -0
  81. package/src/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.ts +5 -22
  82. package/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts +5 -72
  83. package/src/proto/bsky_pb.ts +12 -0
  84. package/src/proto/bsync_connect.ts +22 -0
  85. package/src/proto/bsync_pb.ts +355 -0
  86. package/src/views/index.ts +102 -58
  87. package/src/views/threads-v2.ts +88 -60
  88. package/tests/seed/thread-v2.ts +131 -32
  89. package/tests/views/__snapshots__/thread-v2.test.ts.snap +69 -23
  90. package/tests/views/thread-v2.test.ts +173 -69
  91. package/tsconfig.build.tsbuildinfo +1 -1
@@ -76,6 +76,7 @@ import {
76
76
  } from '../util/uris'
77
77
  import {
78
78
  ThreadHiddenAnchorPostNode,
79
+ ThreadHiddenItemValuePost,
79
80
  ThreadHiddenPostNode,
80
81
  ThreadItemValueBlocked,
81
82
  ThreadItemValueNoUnauthenticated,
@@ -130,11 +131,15 @@ export class Views {
130
131
  public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder
131
132
  public videoUriBuilder: VideoUriBuilder = this.opts.videoUriBuilder
132
133
  public indexedAtEpoch: Date | undefined = this.opts.indexedAtEpoch
134
+ private threadTagsBumpDown: readonly string[] = this.opts.threadTagsBumpDown
135
+ private threadTagsHide: readonly string[] = this.opts.threadTagsHide
133
136
  constructor(
134
137
  private opts: {
135
138
  imgUriBuilder: ImageUriBuilder
136
139
  videoUriBuilder: VideoUriBuilder
137
140
  indexedAtEpoch: Date | undefined
141
+ threadTagsBumpDown: readonly string[]
142
+ threadTagsHide: readonly string[]
138
143
  },
139
144
  ) {}
140
145
 
@@ -1248,6 +1253,7 @@ export class Views {
1248
1253
  below,
1249
1254
  depth: 1,
1250
1255
  branchingFactor,
1256
+ prioritizeFollowedUsers,
1251
1257
  },
1252
1258
  state,
1253
1259
  )
@@ -1263,19 +1269,21 @@ export class Views {
1263
1269
  repliesAllowance: Infinity, // While we don't have pagination.
1264
1270
  uri: anchorUri,
1265
1271
  }),
1272
+ tags: post.tags,
1266
1273
  hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1267
1274
  parent,
1268
1275
  replies,
1269
1276
  }
1270
1277
  }
1271
1278
 
1272
- const thread = sortTrimFlattenThreadTree<ThreadItem>(anchorTree, {
1279
+ const thread = sortTrimFlattenThreadTree(anchorTree, {
1273
1280
  opDid,
1274
1281
  branchingFactor,
1275
1282
  sort,
1276
1283
  prioritizeFollowedUsers,
1277
1284
  viewer: state.ctx?.viewer ?? null,
1278
- fetchedAt: Date.now(),
1285
+ threadTagsBumpDown: this.threadTagsBumpDown,
1286
+ threadTagsHide: this.threadTagsHide,
1279
1287
  })
1280
1288
 
1281
1289
  return {
@@ -1392,6 +1400,7 @@ export class Views {
1392
1400
  postView,
1393
1401
  uri,
1394
1402
  }),
1403
+ tags: post.tags,
1395
1404
  hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1396
1405
  parent,
1397
1406
  replies: undefined,
@@ -1410,6 +1419,7 @@ export class Views {
1410
1419
  below,
1411
1420
  depth,
1412
1421
  branchingFactor,
1422
+ prioritizeFollowedUsers,
1413
1423
  }: {
1414
1424
  parentUri: string
1415
1425
  isOPThread: boolean
@@ -1419,6 +1429,7 @@ export class Views {
1419
1429
  below: number
1420
1430
  depth: number
1421
1431
  branchingFactor: number
1432
+ prioritizeFollowedUsers: boolean
1422
1433
  },
1423
1434
  state: HydrationState,
1424
1435
  ): { replies: ThreadTreeVisible[] | undefined; hasHiddenReplies: boolean } {
@@ -1430,23 +1441,22 @@ export class Views {
1430
1441
  const childrenUris = childrenByParentUri[parentUri] ?? []
1431
1442
  let hasHiddenReplies = false
1432
1443
  const replies = mapDefined(childrenUris, (uri) => {
1433
- const replyInclusion = this.checkThreadV2ReplyInclusion(
1444
+ const replyInclusion = this.checkThreadV2ReplyInclusion({
1434
1445
  uri,
1435
1446
  rootUri,
1436
1447
  state,
1437
- )
1448
+ })
1438
1449
  if (!replyInclusion) {
1439
1450
  return undefined
1440
1451
  }
1441
- const { authorDid, postView } = replyInclusion
1452
+ const { authorDid, post, postView } = replyInclusion
1442
1453
 
1443
1454
  // Hidden.
1444
- const { hiddenByThreadgate, mutedByViewer } = this.isHiddenThreadPost(
1445
- { rootUri, uri },
1455
+ const { isHidden } = this.isHiddenThreadPost(
1456
+ { post, postView, prioritizeFollowedUsers, rootUri, uri },
1446
1457
  state,
1447
1458
  )
1448
- // Is hidden reply.
1449
- if (hiddenByThreadgate || mutedByViewer) {
1459
+ if (isHidden) {
1450
1460
  // Only care about anchor replies
1451
1461
  if (depth === 1) {
1452
1462
  hasHiddenReplies = true
@@ -1466,6 +1476,7 @@ export class Views {
1466
1476
  below,
1467
1477
  depth: depth + 1,
1468
1478
  branchingFactor,
1479
+ prioritizeFollowedUsers,
1469
1480
  },
1470
1481
  state,
1471
1482
  )
@@ -1482,6 +1493,7 @@ export class Views {
1482
1493
  repliesAllowance,
1483
1494
  uri,
1484
1495
  }),
1496
+ tags: post.tags,
1485
1497
  hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1486
1498
  parent: undefined,
1487
1499
  replies: nestedReplies,
@@ -1520,11 +1532,13 @@ export class Views {
1520
1532
  uri,
1521
1533
  depth,
1522
1534
  value: {
1523
- $type: 'app.bsky.unspecced.getPostThreadV2#threadItemPost',
1535
+ $type: 'app.bsky.unspecced.defs#threadItemPost',
1524
1536
  post: postView,
1525
1537
  moreParents: moreParents ?? false,
1526
1538
  moreReplies,
1527
1539
  opThread: isOPThread,
1540
+ hiddenByThreadgate: false, // Hidden posts are handled by threadHiddenV2
1541
+ mutedByViewer: false, // Hidden posts are handled by threadHiddenV2
1528
1542
  },
1529
1543
  }
1530
1544
  }
@@ -1540,7 +1554,7 @@ export class Views {
1540
1554
  uri,
1541
1555
  depth,
1542
1556
  value: {
1543
- $type: 'app.bsky.unspecced.getPostThreadV2#threadItemNoUnauthenticated',
1557
+ $type: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated',
1544
1558
  },
1545
1559
  }
1546
1560
  }
@@ -1556,7 +1570,7 @@ export class Views {
1556
1570
  uri,
1557
1571
  depth,
1558
1572
  value: {
1559
- $type: 'app.bsky.unspecced.getPostThreadV2#threadItemNotFound',
1573
+ $type: 'app.bsky.unspecced.defs#threadItemNotFound',
1560
1574
  },
1561
1575
  }
1562
1576
  }
@@ -1576,7 +1590,7 @@ export class Views {
1576
1590
  uri,
1577
1591
  depth,
1578
1592
  value: {
1579
- $type: 'app.bsky.unspecced.getPostThreadV2#threadItemBlocked',
1593
+ $type: 'app.bsky.unspecced.defs#threadItemBlocked',
1580
1594
  author: {
1581
1595
  did: authorDid,
1582
1596
  viewer: this.blockedProfileViewer(authorDid, state),
@@ -1591,9 +1605,11 @@ export class Views {
1591
1605
  {
1592
1606
  below,
1593
1607
  branchingFactor,
1608
+ prioritizeFollowedUsers,
1594
1609
  }: {
1595
1610
  below: number
1596
1611
  branchingFactor: number
1612
+ prioritizeFollowedUsers: boolean
1597
1613
  },
1598
1614
  ): ThreadHiddenItem[] {
1599
1615
  const { anchor: anchorUri, uris } = skeleton
@@ -1628,6 +1644,7 @@ export class Views {
1628
1644
  childrenByParentUri,
1629
1645
  below,
1630
1646
  depth: 1,
1647
+ prioritizeFollowedUsers,
1631
1648
  },
1632
1649
  state,
1633
1650
  ),
@@ -1638,7 +1655,8 @@ export class Views {
1638
1655
  branchingFactor,
1639
1656
  prioritizeFollowedUsers: false,
1640
1657
  viewer: state.ctx?.viewer ?? null,
1641
- fetchedAt: Date.now(),
1658
+ threadTagsBumpDown: this.threadTagsBumpDown,
1659
+ threadTagsHide: this.threadTagsHide,
1642
1660
  })
1643
1661
  }
1644
1662
 
@@ -1649,12 +1667,14 @@ export class Views {
1649
1667
  childrenByParentUri,
1650
1668
  below,
1651
1669
  depth,
1670
+ prioritizeFollowedUsers,
1652
1671
  }: {
1653
1672
  parentUri: string
1654
1673
  rootUri: string
1655
1674
  childrenByParentUri: Record<string, string[]>
1656
1675
  below: number
1657
1676
  depth: number
1677
+ prioritizeFollowedUsers: boolean
1658
1678
  },
1659
1679
  state: HydrationState,
1660
1680
  ): ThreadHiddenPostNode[] | undefined {
@@ -1665,23 +1685,23 @@ export class Views {
1665
1685
 
1666
1686
  const childrenUris = childrenByParentUri[parentUri] ?? []
1667
1687
  return mapDefined(childrenUris, (uri) => {
1668
- const replyInclusion = this.checkThreadV2ReplyInclusion(
1688
+ const replyInclusion = this.checkThreadV2ReplyInclusion({
1669
1689
  uri,
1670
1690
  rootUri,
1671
1691
  state,
1672
- )
1692
+ })
1673
1693
  if (!replyInclusion) {
1674
1694
  return undefined
1675
1695
  }
1676
- const { postView } = replyInclusion
1696
+ const { post, postView } = replyInclusion
1677
1697
 
1678
1698
  // Hidden.
1679
- const { hiddenByThreadgate, mutedByViewer } = this.isHiddenThreadPost(
1680
- { rootUri, uri },
1681
- state,
1682
- )
1683
- // Is hidden reply.
1684
- if (hiddenByThreadgate || mutedByViewer) {
1699
+ const { isHidden, hiddenByThreadgate, mutedByViewer } =
1700
+ this.isHiddenThreadPost(
1701
+ { post, postView, rootUri, prioritizeFollowedUsers, uri },
1702
+ state,
1703
+ )
1704
+ if (isHidden) {
1685
1705
  // Only show hidden anchor replies, not all hidden.
1686
1706
  if (depth > 1) {
1687
1707
  return undefined
@@ -1699,23 +1719,23 @@ export class Views {
1699
1719
  childrenByParentUri,
1700
1720
  below,
1701
1721
  depth: depth + 1,
1722
+ prioritizeFollowedUsers,
1702
1723
  },
1703
1724
  state,
1704
1725
  )
1705
1726
 
1706
- const item = this.threadHiddenV2ItemPost(
1707
- {
1708
- depth,
1709
- postView,
1710
- rootUri,
1711
- uri,
1712
- },
1713
- state,
1714
- )
1727
+ const item = this.threadHiddenV2ItemPost({
1728
+ depth,
1729
+ hiddenByThreadgate,
1730
+ mutedByViewer,
1731
+ postView,
1732
+ uri,
1733
+ })
1715
1734
 
1716
1735
  const tree: ThreadHiddenPostNode = {
1717
1736
  type: 'hiddenPost',
1718
1737
  item: item,
1738
+ tags: post.tags,
1719
1739
  replies,
1720
1740
  }
1721
1741
 
@@ -1739,42 +1759,47 @@ export class Views {
1739
1759
  }
1740
1760
  }
1741
1761
 
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
-
1762
+ private threadHiddenV2ItemPost({
1763
+ depth,
1764
+ hiddenByThreadgate,
1765
+ mutedByViewer,
1766
+ postView,
1767
+ uri,
1768
+ }: {
1769
+ depth: number
1770
+ hiddenByThreadgate: boolean
1771
+ mutedByViewer: boolean
1772
+ postView: PostView
1773
+ uri: string
1774
+ }): ThreadHiddenItemValuePost {
1761
1775
  const base = this.threadHiddenV2ItemPostAnchor({ depth, uri })
1762
1776
  return {
1763
1777
  ...base,
1764
1778
  value: {
1765
- $type: 'app.bsky.unspecced.getPostThreadHiddenV2#threadHiddenItemPost',
1779
+ $type: 'app.bsky.unspecced.defs#threadItemPost',
1766
1780
  post: postView,
1767
1781
  hiddenByThreadgate,
1768
1782
  mutedByViewer,
1783
+ moreParents: false, // Hidden replies don't have parents.
1784
+ moreReplies: 0, // Hidden replies don't have replies hydrated.
1785
+ opThread: false, // Hidden replies don't contain OP threads.
1769
1786
  },
1770
1787
  }
1771
1788
  }
1772
1789
 
1773
- private checkThreadV2ReplyInclusion(
1774
- uri: string,
1775
- rootUri: string,
1776
- state: HydrationState,
1777
- ): { authorDid: string; postView: PostView } | null {
1790
+ private checkThreadV2ReplyInclusion({
1791
+ uri,
1792
+ rootUri,
1793
+ state,
1794
+ }: {
1795
+ uri: string
1796
+ rootUri: string
1797
+ state: HydrationState
1798
+ }): {
1799
+ authorDid: string
1800
+ post: Post
1801
+ postView: PostView
1802
+ } | null {
1778
1803
  // Not found.
1779
1804
  const post = state.posts?.get(uri)
1780
1805
  if (post?.violatesThreadGate) {
@@ -1806,24 +1831,41 @@ export class Views {
1806
1831
  return null
1807
1832
  }
1808
1833
 
1809
- return { authorDid, postView }
1834
+ return { authorDid, post, postView }
1810
1835
  }
1811
1836
 
1812
1837
  private isHiddenThreadPost(
1813
1838
  {
1839
+ post,
1840
+ postView,
1841
+ prioritizeFollowedUsers,
1814
1842
  rootUri,
1815
1843
  uri,
1816
1844
  }: {
1845
+ post: Post
1846
+ postView: PostView
1847
+ prioritizeFollowedUsers: boolean
1817
1848
  rootUri: string
1818
1849
  uri: string
1819
1850
  },
1820
1851
  state: HydrationState,
1821
1852
  ): {
1853
+ isHidden: boolean
1854
+ hiddenByTag: boolean
1822
1855
  hiddenByThreadgate: boolean
1823
1856
  mutedByViewer: boolean
1824
1857
  } {
1858
+ const opDid = creatorFromUri(rootUri)
1825
1859
  const authorDid = creatorFromUri(uri)
1826
1860
 
1861
+ const showBecauseFollowing =
1862
+ prioritizeFollowedUsers && !!postView.author.viewer?.following
1863
+ const hiddenByTag =
1864
+ authorDid !== opDid &&
1865
+ authorDid !== state.ctx?.viewer &&
1866
+ !showBecauseFollowing &&
1867
+ this.threadTagsHide.some((t) => post.tags.has(t))
1868
+
1827
1869
  const hiddenByThreadgate =
1828
1870
  state.ctx?.viewer !== authorDid &&
1829
1871
  this.replyIsHiddenByThreadgate(uri, rootUri, state)
@@ -1831,6 +1873,8 @@ export class Views {
1831
1873
  const mutedByViewer = this.viewerMuteExists(authorDid, state)
1832
1874
 
1833
1875
  return {
1876
+ isHidden: hiddenByTag || hiddenByThreadgate || mutedByViewer,
1877
+ hiddenByTag,
1834
1878
  hiddenByThreadgate,
1835
1879
  mutedByViewer,
1836
1880
  }
@@ -2,17 +2,15 @@ import { asPredicate } from '@atproto/api'
2
2
  import { HydrateCtx } from '../hydration/hydrator'
3
3
  import { validateRecord as validatePostRecord } from '../lexicon/types/app/bsky/feed/post'
4
4
  import {
5
- ThreadHiddenItem,
6
- ThreadHiddenItemPost,
7
- isThreadHiddenItemPost,
8
- } from '../lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2'
9
- import {
10
- QueryParams as GetPostThreadV2QueryParams,
11
- ThreadItem,
12
5
  ThreadItemBlocked,
13
6
  ThreadItemNoUnauthenticated,
14
7
  ThreadItemNotFound,
15
8
  ThreadItemPost,
9
+ } from '../lexicon/types/app/bsky/unspecced/defs'
10
+ import { ThreadHiddenItem } from '../lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2'
11
+ import {
12
+ QueryParams as GetPostThreadV2QueryParams,
13
+ ThreadItem,
16
14
  } from '../lexicon/types/app/bsky/unspecced/getPostThreadV2'
17
15
  import { $Typed } from '../lexicon/util'
18
16
 
@@ -59,6 +57,7 @@ type ThreadNotFoundNode = {
59
57
  type ThreadPostNode = {
60
58
  type: 'post'
61
59
  item: ThreadItemValuePost
60
+ tags: Set<string>
62
61
  hasOPLike: boolean
63
62
  parent: ThreadTree | undefined
64
63
  replies: ThreadTree[] | undefined
@@ -72,7 +71,7 @@ type ThreadHiddenItemValue<T extends ThreadHiddenItem['value']> = Omit<
72
71
  }
73
72
 
74
73
  export type ThreadHiddenItemValuePost = ThreadHiddenItemValue<
75
- $Typed<ThreadHiddenItemPost>
74
+ $Typed<ThreadItemPost>
76
75
  >
77
76
 
78
77
  // This is an intermediary type that doesn't map to the views.
@@ -87,6 +86,7 @@ export type ThreadHiddenAnchorPostNode = {
87
86
  export type ThreadHiddenPostNode = {
88
87
  type: 'hiddenPost'
89
88
  item: ThreadHiddenItemValuePost
89
+ tags: Set<string>
90
90
  replies: ThreadHiddenPostNode[] | undefined
91
91
  }
92
92
 
@@ -107,21 +107,23 @@ export type ThreadTreeHidden = ThreadHiddenAnchorPostNode | ThreadHiddenPostNode
107
107
  export type ThreadTree = ThreadTreeVisible | ThreadTreeHidden
108
108
 
109
109
  /** This function mutates the tree parameter. */
110
- export function sortTrimFlattenThreadTree<
111
- TItem extends ThreadItem | ThreadHiddenItem,
112
- >(anchorTree: ThreadTree, options: SortTrimFlattenOptions): TItem[] {
110
+ export function sortTrimFlattenThreadTree(
111
+ anchorTree: ThreadTree,
112
+ options: SortTrimFlattenOptions,
113
+ ) {
113
114
  const sortedAnchorTree = sortTrimThreadTree(anchorTree, options)
114
115
 
115
- return flattenTree<TItem>(sortedAnchorTree)
116
+ return flattenTree(sortedAnchorTree)
116
117
  }
117
118
 
118
119
  type SortTrimFlattenOptions = {
119
120
  branchingFactor: GetPostThreadV2QueryParams['branchingFactor']
120
- fetchedAt: number
121
121
  opDid: string
122
122
  prioritizeFollowedUsers: boolean
123
123
  sort?: GetPostThreadV2QueryParams['sort']
124
124
  viewer: HydrateCtx['viewer']
125
+ threadTagsBumpDown: readonly string[]
126
+ threadTagsHide: readonly string[]
125
127
  }
126
128
 
127
129
  const isPostRecord = asPredicate(validatePostRecord)
@@ -136,15 +138,6 @@ function sortTrimThreadTree(
136
138
  }
137
139
  const node: ThreadNodeWithReplies = n
138
140
 
139
- const {
140
- branchingFactor,
141
- fetchedAt,
142
- opDid,
143
- prioritizeFollowedUsers,
144
- sort,
145
- viewer,
146
- } = opts
147
-
148
141
  if (node.replies) {
149
142
  node.replies.sort((an: ThreadTree, bn: ThreadTree) => {
150
143
  if (!isPostNode(an)) {
@@ -168,19 +161,10 @@ function sortTrimThreadTree(
168
161
 
169
162
  // Trimming: after sorting, apply branching factor to all levels of replies except the anchor direct replies.
170
163
  if (node.item.depth !== 0) {
171
- node.replies = node.replies.slice(0, branchingFactor)
164
+ node.replies = node.replies.slice(0, opts.branchingFactor)
172
165
  }
173
166
 
174
- node.replies.forEach((reply) =>
175
- sortTrimThreadTree(reply, {
176
- branchingFactor,
177
- fetchedAt,
178
- opDid,
179
- prioritizeFollowedUsers,
180
- sort,
181
- viewer,
182
- }),
183
- )
167
+ node.replies.forEach((reply) => sortTrimThreadTree(reply, opts))
184
168
  }
185
169
 
186
170
  return node
@@ -191,19 +175,30 @@ function applyBumping(
191
175
  bNode: ThreadMaybeHiddenPostNode,
192
176
  opts: SortTrimFlattenOptions,
193
177
  ): number | null {
194
- const a = aNode.item.value
195
- const b = bNode.item.value
196
- const { opDid, prioritizeFollowedUsers, viewer } = opts
178
+ if (!isPostNode(aNode)) {
179
+ return null
180
+ }
181
+ if (!isPostNode(bNode)) {
182
+ return null
183
+ }
184
+
185
+ const {
186
+ opDid,
187
+ prioritizeFollowedUsers,
188
+ viewer,
189
+ threadTagsBumpDown,
190
+ threadTagsHide,
191
+ } = opts
197
192
 
198
193
  type BumpDirection = 'up' | 'down'
199
- type BumpPredicateFn = (i: ThreadItemPost | ThreadHiddenItemPost) => boolean
194
+ type BumpPredicateFn = (i: ThreadMaybeHiddenPostNode) => boolean
200
195
 
201
196
  const maybeBump = (
202
197
  bump: BumpDirection,
203
198
  predicateFn: BumpPredicateFn,
204
199
  ): number | null => {
205
- const aPredicate = predicateFn(a)
206
- const bPredicate = predicateFn(b)
200
+ const aPredicate = predicateFn(aNode)
201
+ const bPredicate = predicateFn(bNode)
207
202
  if (aPredicate && bPredicate) {
208
203
  return applySorting(aNode, bNode, opts)
209
204
  } else if (aPredicate) {
@@ -218,21 +213,56 @@ function applyBumping(
218
213
  // Bumps-up applied first make the item appear higher in the list than later bumps-up.
219
214
  // Bumps-down applied first make the item appear lower in the list than later bumps-down.
220
215
  const bumps: [BumpDirection, BumpPredicateFn][] = [
216
+ /*
217
+ General bumps.
218
+ */
221
219
  // OP replies.
222
- ['up', (i) => i.post.author.did === opDid],
220
+ ['up', (i) => i.item.value.post.author.did === opDid],
223
221
  // Viewer replies.
224
- ['up', (i) => i.post.author.did === viewer],
225
- // Muted account by the viewer.
226
- ['down', (i) => isThreadHiddenItemPost(i) && i.mutedByViewer],
227
- // Hidden by threadgate.
228
- ['down', (i) => isThreadHiddenItemPost(i) && i.hiddenByThreadgate],
222
+ ['up', (i) => i.item.value.post.author.did === viewer],
223
+
224
+ /*
225
+ Bumps within visible replies.
226
+ */
227
+ // Followers posts.
228
+ [
229
+ 'up',
230
+ (i) =>
231
+ i.type === 'post' &&
232
+ prioritizeFollowedUsers &&
233
+ !!i.item.value.post.author.viewer?.following,
234
+ ],
235
+ // Bump-down tags.
236
+ [
237
+ 'down',
238
+ (i) => i.type === 'post' && threadTagsBumpDown.some((t) => i.tags.has(t)),
239
+ ],
229
240
  // Pushpin-only.
230
241
  [
231
242
  'down',
232
- (i) => isPostRecord(i.post.record) && i.post.record.text.trim() === '📌',
243
+ (i) =>
244
+ i.type === 'post' &&
245
+ isPostRecord(i.item.value.post.record) &&
246
+ i.item.value.post.record.text.trim() === '📌',
233
247
  ],
234
- // Followers posts.
235
- ['up', (i) => prioritizeFollowedUsers && !!i.post.author.viewer?.following],
248
+
249
+ /*
250
+ Bumps within hidden replies.
251
+ This determines the order of hidden replies:
252
+ 1. hidden by threadgate.
253
+ 2. hidden by tags.
254
+ 3. muted by viewer.
255
+ */
256
+ // Muted account by the viewer.
257
+ ['down', (i) => i.type === 'hiddenPost' && i.item.value.mutedByViewer],
258
+ // Hidden by tags.
259
+ [
260
+ 'down',
261
+ (i) =>
262
+ i.type === 'hiddenPost' && threadTagsHide.some((t) => i.tags.has(t)),
263
+ ],
264
+ // Hidden by threadgate.
265
+ ['down', (i) => i.type === 'hiddenPost' && i.item.value.hiddenByThreadgate],
236
266
  ]
237
267
 
238
268
  for (const [bump, predicateFn] of bumps) {
@@ -279,13 +309,11 @@ function topSortValue(likeCount: number, hasOPLike: boolean): number {
279
309
  return Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0)
280
310
  }
281
311
 
282
- function flattenTree<TItem extends ThreadItem | ThreadHiddenItem>(
283
- tree: ThreadTree,
284
- ): TItem[] {
312
+ function flattenTree(tree: ThreadTree) {
285
313
  return [
286
314
  // All parents above.
287
315
  ...Array.from(
288
- flattenInDirection<TItem>({
316
+ flattenInDirection({
289
317
  tree,
290
318
  direction: 'up',
291
319
  }),
@@ -293,11 +321,11 @@ function flattenTree<TItem extends ThreadItem | ThreadHiddenItem>(
293
321
 
294
322
  // The anchor.
295
323
  // In the case of hidden replies, the anchor item itself is undefined.
296
- ...(tree.item.value ? ([tree.item] as TItem[]) : []),
324
+ ...(tree.item.value ? [tree.item] : []),
297
325
 
298
326
  // All replies below.
299
327
  ...Array.from(
300
- flattenInDirection<TItem>({
328
+ flattenInDirection({
301
329
  tree,
302
330
  direction: 'down',
303
331
  }),
@@ -305,18 +333,18 @@ function flattenTree<TItem extends ThreadItem | ThreadHiddenItem>(
305
333
  ]
306
334
  }
307
335
 
308
- function* flattenInDirection<TItem extends ThreadItem | ThreadHiddenItem>({
336
+ function* flattenInDirection({
309
337
  tree,
310
338
  direction,
311
339
  }: {
312
340
  tree: ThreadTree
313
341
  direction: 'up' | 'down'
314
- }): Generator<TItem, void> {
342
+ }) {
315
343
  if (tree.type === 'noUnauthenticated') {
316
344
  if (direction === 'up') {
317
345
  if (tree.parent) {
318
346
  // Unfold all parents above.
319
- yield* flattenTree<TItem>(tree.parent)
347
+ yield* flattenTree(tree.parent)
320
348
  }
321
349
  }
322
350
  }
@@ -325,13 +353,13 @@ function* flattenInDirection<TItem extends ThreadItem | ThreadHiddenItem>({
325
353
  if (direction === 'up') {
326
354
  if (tree.parent) {
327
355
  // Unfold all parents above.
328
- yield* flattenTree<TItem>(tree.parent)
356
+ yield* flattenTree(tree.parent)
329
357
  }
330
358
  } else {
331
359
  // Unfold all replies below.
332
360
  if (tree.replies?.length) {
333
361
  for (const reply of tree.replies) {
334
- yield* flattenTree<TItem>(reply)
362
+ yield* flattenTree(reply)
335
363
  }
336
364
  }
337
365
  }
@@ -343,7 +371,7 @@ function* flattenInDirection<TItem extends ThreadItem | ThreadHiddenItem>({
343
371
  // Unfold all replies below.
344
372
  if (tree.replies?.length) {
345
373
  for (const reply of tree.replies) {
346
- yield* flattenTree<TItem>(reply)
374
+ yield* flattenTree(reply)
347
375
  }
348
376
  }
349
377
  }