@atproto/bsky 0.0.152 → 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +7 -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 +10 -0
  33. package/dist/lexicon/lexicons.d.ts.map +1 -1
  34. package/dist/lexicon/lexicons.js +5 -0
  35. package/dist/lexicon/lexicons.js.map +1 -1
  36. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts +2 -0
  37. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +1 -1
  38. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js.map +1 -1
  39. package/dist/proto/bsky_pb.d.ts +4 -0
  40. package/dist/proto/bsky_pb.d.ts.map +1 -1
  41. package/dist/proto/bsky_pb.js +16 -0
  42. package/dist/proto/bsky_pb.js.map +1 -1
  43. package/dist/proto/bsync_connect.d.ts +19 -1
  44. package/dist/proto/bsync_connect.d.ts.map +1 -1
  45. package/dist/proto/bsync_connect.js +18 -0
  46. package/dist/proto/bsync_connect.js.map +1 -1
  47. package/dist/proto/bsync_pb.d.ts +150 -0
  48. package/dist/proto/bsync_pb.d.ts.map +1 -1
  49. package/dist/proto/bsync_pb.js +401 -1
  50. package/dist/proto/bsync_pb.js.map +1 -1
  51. package/dist/views/index.d.ts +6 -1
  52. package/dist/views/index.d.ts.map +1 -1
  53. package/dist/views/index.js +58 -22
  54. package/dist/views/index.js.map +1 -1
  55. package/dist/views/threads-v2.d.ts +4 -1
  56. package/dist/views/threads-v2.d.ts.map +1 -1
  57. package/dist/views/threads-v2.js +50 -25
  58. package/dist/views/threads-v2.js.map +1 -1
  59. package/package.json +5 -5
  60. package/proto/bsky.proto +1 -0
  61. package/src/api/app/bsky/unspecced/getPostThreadHiddenV2.ts +2 -1
  62. package/src/api/com/atproto/repo/getRecord.ts +4 -1
  63. package/src/config.ts +15 -0
  64. package/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts +9 -0
  65. package/src/data-plane/server/db/migrations/index.ts +1 -0
  66. package/src/data-plane/server/db/tables/record.ts +1 -0
  67. package/src/data-plane/server/routes/records.ts +1 -0
  68. package/src/hydration/feed.ts +3 -0
  69. package/src/index.ts +2 -0
  70. package/src/lexicon/lexicons.ts +6 -0
  71. package/src/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.ts +2 -0
  72. package/src/proto/bsky_pb.ts +12 -0
  73. package/src/proto/bsync_connect.ts +22 -0
  74. package/src/proto/bsync_pb.ts +355 -0
  75. package/src/views/index.ts +90 -52
  76. package/src/views/threads-v2.ts +67 -37
  77. package/tests/seed/thread-v2.ts +131 -32
  78. package/tests/views/thread-v2.test.ts +139 -27
  79. package/tsconfig.build.tsbuildinfo +1 -1
@@ -130,11 +130,15 @@ export class Views {
130
130
  public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder
131
131
  public videoUriBuilder: VideoUriBuilder = this.opts.videoUriBuilder
132
132
  public indexedAtEpoch: Date | undefined = this.opts.indexedAtEpoch
133
+ private threadTagsBumpDown: readonly string[] = this.opts.threadTagsBumpDown
134
+ private threadTagsHide: readonly string[] = this.opts.threadTagsHide
133
135
  constructor(
134
136
  private opts: {
135
137
  imgUriBuilder: ImageUriBuilder
136
138
  videoUriBuilder: VideoUriBuilder
137
139
  indexedAtEpoch: Date | undefined
140
+ threadTagsBumpDown: readonly string[]
141
+ threadTagsHide: readonly string[]
138
142
  },
139
143
  ) {}
140
144
 
@@ -1248,6 +1252,7 @@ export class Views {
1248
1252
  below,
1249
1253
  depth: 1,
1250
1254
  branchingFactor,
1255
+ prioritizeFollowedUsers,
1251
1256
  },
1252
1257
  state,
1253
1258
  )
@@ -1263,6 +1268,7 @@ export class Views {
1263
1268
  repliesAllowance: Infinity, // While we don't have pagination.
1264
1269
  uri: anchorUri,
1265
1270
  }),
1271
+ tags: post.tags,
1266
1272
  hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1267
1273
  parent,
1268
1274
  replies,
@@ -1275,7 +1281,8 @@ export class Views {
1275
1281
  sort,
1276
1282
  prioritizeFollowedUsers,
1277
1283
  viewer: state.ctx?.viewer ?? null,
1278
- fetchedAt: Date.now(),
1284
+ threadTagsBumpDown: this.threadTagsBumpDown,
1285
+ threadTagsHide: this.threadTagsHide,
1279
1286
  })
1280
1287
 
1281
1288
  return {
@@ -1392,6 +1399,7 @@ export class Views {
1392
1399
  postView,
1393
1400
  uri,
1394
1401
  }),
1402
+ tags: post.tags,
1395
1403
  hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1396
1404
  parent,
1397
1405
  replies: undefined,
@@ -1410,6 +1418,7 @@ export class Views {
1410
1418
  below,
1411
1419
  depth,
1412
1420
  branchingFactor,
1421
+ prioritizeFollowedUsers,
1413
1422
  }: {
1414
1423
  parentUri: string
1415
1424
  isOPThread: boolean
@@ -1419,6 +1428,7 @@ export class Views {
1419
1428
  below: number
1420
1429
  depth: number
1421
1430
  branchingFactor: number
1431
+ prioritizeFollowedUsers: boolean
1422
1432
  },
1423
1433
  state: HydrationState,
1424
1434
  ): { replies: ThreadTreeVisible[] | undefined; hasHiddenReplies: boolean } {
@@ -1430,23 +1440,22 @@ export class Views {
1430
1440
  const childrenUris = childrenByParentUri[parentUri] ?? []
1431
1441
  let hasHiddenReplies = false
1432
1442
  const replies = mapDefined(childrenUris, (uri) => {
1433
- const replyInclusion = this.checkThreadV2ReplyInclusion(
1443
+ const replyInclusion = this.checkThreadV2ReplyInclusion({
1434
1444
  uri,
1435
1445
  rootUri,
1436
1446
  state,
1437
- )
1447
+ })
1438
1448
  if (!replyInclusion) {
1439
1449
  return undefined
1440
1450
  }
1441
- const { authorDid, postView } = replyInclusion
1451
+ const { authorDid, post, postView } = replyInclusion
1442
1452
 
1443
1453
  // Hidden.
1444
- const { hiddenByThreadgate, mutedByViewer } = this.isHiddenThreadPost(
1445
- { rootUri, uri },
1454
+ const { isHidden } = this.isHiddenThreadPost(
1455
+ { post, postView, prioritizeFollowedUsers, rootUri, uri },
1446
1456
  state,
1447
1457
  )
1448
- // Is hidden reply.
1449
- if (hiddenByThreadgate || mutedByViewer) {
1458
+ if (isHidden) {
1450
1459
  // Only care about anchor replies
1451
1460
  if (depth === 1) {
1452
1461
  hasHiddenReplies = true
@@ -1466,6 +1475,7 @@ export class Views {
1466
1475
  below,
1467
1476
  depth: depth + 1,
1468
1477
  branchingFactor,
1478
+ prioritizeFollowedUsers,
1469
1479
  },
1470
1480
  state,
1471
1481
  )
@@ -1482,6 +1492,7 @@ export class Views {
1482
1492
  repliesAllowance,
1483
1493
  uri,
1484
1494
  }),
1495
+ tags: post.tags,
1485
1496
  hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1486
1497
  parent: undefined,
1487
1498
  replies: nestedReplies,
@@ -1591,9 +1602,11 @@ export class Views {
1591
1602
  {
1592
1603
  below,
1593
1604
  branchingFactor,
1605
+ prioritizeFollowedUsers,
1594
1606
  }: {
1595
1607
  below: number
1596
1608
  branchingFactor: number
1609
+ prioritizeFollowedUsers: boolean
1597
1610
  },
1598
1611
  ): ThreadHiddenItem[] {
1599
1612
  const { anchor: anchorUri, uris } = skeleton
@@ -1628,6 +1641,7 @@ export class Views {
1628
1641
  childrenByParentUri,
1629
1642
  below,
1630
1643
  depth: 1,
1644
+ prioritizeFollowedUsers,
1631
1645
  },
1632
1646
  state,
1633
1647
  ),
@@ -1638,7 +1652,8 @@ export class Views {
1638
1652
  branchingFactor,
1639
1653
  prioritizeFollowedUsers: false,
1640
1654
  viewer: state.ctx?.viewer ?? null,
1641
- fetchedAt: Date.now(),
1655
+ threadTagsBumpDown: this.threadTagsBumpDown,
1656
+ threadTagsHide: this.threadTagsHide,
1642
1657
  })
1643
1658
  }
1644
1659
 
@@ -1649,12 +1664,14 @@ export class Views {
1649
1664
  childrenByParentUri,
1650
1665
  below,
1651
1666
  depth,
1667
+ prioritizeFollowedUsers,
1652
1668
  }: {
1653
1669
  parentUri: string
1654
1670
  rootUri: string
1655
1671
  childrenByParentUri: Record<string, string[]>
1656
1672
  below: number
1657
1673
  depth: number
1674
+ prioritizeFollowedUsers: boolean
1658
1675
  },
1659
1676
  state: HydrationState,
1660
1677
  ): ThreadHiddenPostNode[] | undefined {
@@ -1665,23 +1682,23 @@ export class Views {
1665
1682
 
1666
1683
  const childrenUris = childrenByParentUri[parentUri] ?? []
1667
1684
  return mapDefined(childrenUris, (uri) => {
1668
- const replyInclusion = this.checkThreadV2ReplyInclusion(
1685
+ const replyInclusion = this.checkThreadV2ReplyInclusion({
1669
1686
  uri,
1670
1687
  rootUri,
1671
1688
  state,
1672
- )
1689
+ })
1673
1690
  if (!replyInclusion) {
1674
1691
  return undefined
1675
1692
  }
1676
- const { postView } = replyInclusion
1693
+ const { post, postView } = replyInclusion
1677
1694
 
1678
1695
  // Hidden.
1679
- const { hiddenByThreadgate, mutedByViewer } = this.isHiddenThreadPost(
1680
- { rootUri, uri },
1681
- state,
1682
- )
1683
- // Is hidden reply.
1684
- if (hiddenByThreadgate || mutedByViewer) {
1696
+ const { isHidden, hiddenByThreadgate, mutedByViewer } =
1697
+ this.isHiddenThreadPost(
1698
+ { post, postView, rootUri, prioritizeFollowedUsers, uri },
1699
+ state,
1700
+ )
1701
+ if (isHidden) {
1685
1702
  // Only show hidden anchor replies, not all hidden.
1686
1703
  if (depth > 1) {
1687
1704
  return undefined
@@ -1699,23 +1716,23 @@ export class Views {
1699
1716
  childrenByParentUri,
1700
1717
  below,
1701
1718
  depth: depth + 1,
1719
+ prioritizeFollowedUsers,
1702
1720
  },
1703
1721
  state,
1704
1722
  )
1705
1723
 
1706
- const item = this.threadHiddenV2ItemPost(
1707
- {
1708
- depth,
1709
- postView,
1710
- rootUri,
1711
- uri,
1712
- },
1713
- state,
1714
- )
1724
+ const item = this.threadHiddenV2ItemPost({
1725
+ depth,
1726
+ hiddenByThreadgate,
1727
+ mutedByViewer,
1728
+ postView,
1729
+ uri,
1730
+ })
1715
1731
 
1716
1732
  const tree: ThreadHiddenPostNode = {
1717
1733
  type: 'hiddenPost',
1718
1734
  item: item,
1735
+ tags: post.tags,
1719
1736
  replies,
1720
1737
  }
1721
1738
 
@@ -1739,25 +1756,19 @@ export class Views {
1739
1756
  }
1740
1757
  }
1741
1758
 
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
-
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'] {
1761
1772
  const base = this.threadHiddenV2ItemPostAnchor({ depth, uri })
1762
1773
  return {
1763
1774
  ...base,
@@ -1770,11 +1781,19 @@ export class Views {
1770
1781
  }
1771
1782
  }
1772
1783
 
1773
- private checkThreadV2ReplyInclusion(
1774
- uri: string,
1775
- rootUri: string,
1776
- state: HydrationState,
1777
- ): { authorDid: string; postView: PostView } | null {
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 {
1778
1797
  // Not found.
1779
1798
  const post = state.posts?.get(uri)
1780
1799
  if (post?.violatesThreadGate) {
@@ -1806,24 +1825,41 @@ export class Views {
1806
1825
  return null
1807
1826
  }
1808
1827
 
1809
- return { authorDid, postView }
1828
+ return { authorDid, post, postView }
1810
1829
  }
1811
1830
 
1812
1831
  private isHiddenThreadPost(
1813
1832
  {
1833
+ post,
1834
+ postView,
1835
+ prioritizeFollowedUsers,
1814
1836
  rootUri,
1815
1837
  uri,
1816
1838
  }: {
1839
+ post: Post
1840
+ postView: PostView
1841
+ prioritizeFollowedUsers: boolean
1817
1842
  rootUri: string
1818
1843
  uri: string
1819
1844
  },
1820
1845
  state: HydrationState,
1821
1846
  ): {
1847
+ isHidden: boolean
1848
+ hiddenByTag: boolean
1822
1849
  hiddenByThreadgate: boolean
1823
1850
  mutedByViewer: boolean
1824
1851
  } {
1852
+ const opDid = creatorFromUri(rootUri)
1825
1853
  const authorDid = creatorFromUri(uri)
1826
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
+
1827
1863
  const hiddenByThreadgate =
1828
1864
  state.ctx?.viewer !== authorDid &&
1829
1865
  this.replyIsHiddenByThreadgate(uri, rootUri, state)
@@ -1831,6 +1867,8 @@ export class Views {
1831
1867
  const mutedByViewer = this.viewerMuteExists(authorDid, state)
1832
1868
 
1833
1869
  return {
1870
+ isHidden: hiddenByTag || hiddenByThreadgate || mutedByViewer,
1871
+ hiddenByTag,
1834
1872
  hiddenByThreadgate,
1835
1873
  mutedByViewer,
1836
1874
  }
@@ -4,7 +4,6 @@ import { validateRecord as validatePostRecord } from '../lexicon/types/app/bsky/
4
4
  import {
5
5
  ThreadHiddenItem,
6
6
  ThreadHiddenItemPost,
7
- isThreadHiddenItemPost,
8
7
  } from '../lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2'
9
8
  import {
10
9
  QueryParams as GetPostThreadV2QueryParams,
@@ -59,6 +58,7 @@ type ThreadNotFoundNode = {
59
58
  type ThreadPostNode = {
60
59
  type: 'post'
61
60
  item: ThreadItemValuePost
61
+ tags: Set<string>
62
62
  hasOPLike: boolean
63
63
  parent: ThreadTree | undefined
64
64
  replies: ThreadTree[] | undefined
@@ -87,6 +87,7 @@ export type ThreadHiddenAnchorPostNode = {
87
87
  export type ThreadHiddenPostNode = {
88
88
  type: 'hiddenPost'
89
89
  item: ThreadHiddenItemValuePost
90
+ tags: Set<string>
90
91
  replies: ThreadHiddenPostNode[] | undefined
91
92
  }
92
93
 
@@ -117,11 +118,12 @@ export function sortTrimFlattenThreadTree<
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) {