@atproto/bsky 0.0.60 → 0.0.62

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 (145) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/api/app/bsky/graph/getKnownFollowers.d.ts +4 -0
  3. package/dist/api/app/bsky/graph/getKnownFollowers.d.ts.map +1 -0
  4. package/dist/api/app/bsky/graph/getKnownFollowers.js +71 -0
  5. package/dist/api/app/bsky/graph/getKnownFollowers.js.map +1 -0
  6. package/dist/api/app/bsky/graph/muteThread.d.ts +4 -0
  7. package/dist/api/app/bsky/graph/muteThread.d.ts.map +1 -0
  8. package/dist/api/app/bsky/graph/muteThread.js +19 -0
  9. package/dist/api/app/bsky/graph/muteThread.js.map +1 -0
  10. package/dist/api/app/bsky/graph/unmuteThread.d.ts +4 -0
  11. package/dist/api/app/bsky/graph/unmuteThread.d.ts.map +1 -0
  12. package/dist/api/app/bsky/graph/unmuteThread.js +19 -0
  13. package/dist/api/app/bsky/graph/unmuteThread.js.map +1 -0
  14. package/dist/api/index.d.ts.map +1 -1
  15. package/dist/api/index.js +6 -0
  16. package/dist/api/index.js.map +1 -1
  17. package/dist/data-plane/bsync/index.d.ts.map +1 -1
  18. package/dist/data-plane/bsync/index.js +41 -15
  19. package/dist/data-plane/bsync/index.js.map +1 -1
  20. package/dist/data-plane/server/db/database-schema.d.ts +2 -1
  21. package/dist/data-plane/server/db/database-schema.d.ts.map +1 -1
  22. package/dist/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.d.ts +4 -0
  23. package/dist/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.d.ts.map +1 -0
  24. package/dist/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.js +18 -0
  25. package/dist/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.js.map +1 -0
  26. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  27. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  28. package/dist/data-plane/server/db/migrations/index.js +2 -1
  29. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  30. package/dist/data-plane/server/db/tables/thread-mute.d.ts +10 -0
  31. package/dist/data-plane/server/db/tables/thread-mute.d.ts.map +1 -0
  32. package/dist/data-plane/server/db/tables/thread-mute.js +5 -0
  33. package/dist/data-plane/server/db/tables/thread-mute.js.map +1 -0
  34. package/dist/data-plane/server/indexing/processor.d.ts +2 -0
  35. package/dist/data-plane/server/indexing/processor.d.ts.map +1 -1
  36. package/dist/data-plane/server/indexing/processor.js +28 -1
  37. package/dist/data-plane/server/indexing/processor.js.map +1 -1
  38. package/dist/data-plane/server/routes/follows.d.ts.map +1 -1
  39. package/dist/data-plane/server/routes/follows.js +34 -2
  40. package/dist/data-plane/server/routes/follows.js.map +1 -1
  41. package/dist/data-plane/server/routes/mutes.d.ts.map +1 -1
  42. package/dist/data-plane/server/routes/mutes.js +16 -0
  43. package/dist/data-plane/server/routes/mutes.js.map +1 -1
  44. package/dist/data-plane/server/util.d.ts +6 -6
  45. package/dist/hydration/actor.d.ts +6 -0
  46. package/dist/hydration/actor.d.ts.map +1 -1
  47. package/dist/hydration/actor.js +17 -0
  48. package/dist/hydration/actor.js.map +1 -1
  49. package/dist/hydration/feed.d.ts +6 -1
  50. package/dist/hydration/feed.d.ts.map +1 -1
  51. package/dist/hydration/feed.js +16 -2
  52. package/dist/hydration/feed.js.map +1 -1
  53. package/dist/hydration/hydrator.d.ts +2 -1
  54. package/dist/hydration/hydrator.d.ts.map +1 -1
  55. package/dist/hydration/hydrator.js +28 -7
  56. package/dist/hydration/hydrator.js.map +1 -1
  57. package/dist/lexicon/index.d.ts +6 -0
  58. package/dist/lexicon/index.d.ts.map +1 -1
  59. package/dist/lexicon/index.js +12 -0
  60. package/dist/lexicon/index.js.map +1 -1
  61. package/dist/lexicon/lexicons.d.ts +127 -0
  62. package/dist/lexicon/lexicons.d.ts.map +1 -1
  63. package/dist/lexicon/lexicons.js +127 -0
  64. package/dist/lexicon/lexicons.js.map +1 -1
  65. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +9 -0
  66. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  67. package/dist/lexicon/types/app/bsky/actor/defs.js +11 -1
  68. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  69. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
  70. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  71. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  72. package/dist/lexicon/types/app/bsky/graph/getKnownFollowers.d.ts +40 -0
  73. package/dist/lexicon/types/app/bsky/graph/getKnownFollowers.d.ts.map +1 -0
  74. package/dist/lexicon/types/app/bsky/graph/getKnownFollowers.js +3 -0
  75. package/dist/lexicon/types/app/bsky/graph/getKnownFollowers.js.map +1 -0
  76. package/dist/lexicon/types/app/bsky/graph/muteThread.d.ts +29 -0
  77. package/dist/lexicon/types/app/bsky/graph/muteThread.d.ts.map +1 -0
  78. package/dist/lexicon/types/app/bsky/graph/muteThread.js +3 -0
  79. package/dist/lexicon/types/app/bsky/graph/muteThread.js.map +1 -0
  80. package/dist/lexicon/types/app/bsky/graph/unmuteThread.d.ts +29 -0
  81. package/dist/lexicon/types/app/bsky/graph/unmuteThread.d.ts.map +1 -0
  82. package/dist/lexicon/types/app/bsky/graph/unmuteThread.js +3 -0
  83. package/dist/lexicon/types/app/bsky/graph/unmuteThread.js.map +1 -0
  84. package/dist/logger.d.ts +1 -0
  85. package/dist/logger.d.ts.map +1 -1
  86. package/dist/logger.js +2 -1
  87. package/dist/logger.js.map +1 -1
  88. package/dist/proto/bsky_connect.d.ts +39 -1
  89. package/dist/proto/bsky_connect.d.ts.map +1 -1
  90. package/dist/proto/bsky_connect.js +38 -0
  91. package/dist/proto/bsky_connect.js.map +1 -1
  92. package/dist/proto/bsky_pb.d.ts +161 -4
  93. package/dist/proto/bsky_pb.d.ts.map +1 -1
  94. package/dist/proto/bsky_pb.js +512 -12
  95. package/dist/proto/bsky_pb.js.map +1 -1
  96. package/dist/views/index.d.ts +5 -0
  97. package/dist/views/index.d.ts.map +1 -1
  98. package/dist/views/index.js +20 -0
  99. package/dist/views/index.js.map +1 -1
  100. package/package.json +5 -5
  101. package/proto/bsky.proto +47 -2
  102. package/src/api/app/bsky/graph/getKnownFollowers.ts +119 -0
  103. package/src/api/app/bsky/graph/muteThread.ts +18 -0
  104. package/src/api/app/bsky/graph/unmuteThread.ts +18 -0
  105. package/src/api/index.ts +6 -0
  106. package/src/data-plane/bsync/index.ts +39 -15
  107. package/src/data-plane/server/db/database-schema.ts +2 -0
  108. package/src/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.ts +15 -0
  109. package/src/data-plane/server/db/migrations/index.ts +1 -0
  110. package/src/data-plane/server/db/tables/thread-mute.ts +9 -0
  111. package/src/data-plane/server/indexing/processor.ts +29 -1
  112. package/src/data-plane/server/routes/follows.ts +45 -2
  113. package/src/data-plane/server/routes/mutes.ts +17 -0
  114. package/src/hydration/actor.ts +30 -0
  115. package/src/hydration/feed.ts +23 -3
  116. package/src/hydration/hydrator.ts +36 -7
  117. package/src/lexicon/index.ts +36 -0
  118. package/src/lexicon/lexicons.ts +129 -0
  119. package/src/lexicon/types/app/bsky/actor/defs.ts +20 -0
  120. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
  121. package/src/lexicon/types/app/bsky/graph/getKnownFollowers.ts +50 -0
  122. package/src/lexicon/types/app/bsky/graph/muteThread.ts +38 -0
  123. package/src/lexicon/types/app/bsky/graph/unmuteThread.ts +38 -0
  124. package/src/logger.ts +2 -0
  125. package/src/proto/bsky_connect.ts +46 -0
  126. package/src/proto/bsky_pb.ts +540 -8
  127. package/src/views/index.ts +25 -0
  128. package/tests/__snapshots__/feed-generation.test.ts.snap +38 -12
  129. package/tests/data-plane/__snapshots__/indexing.test.ts.snap +73 -37
  130. package/tests/data-plane/thread-mutes.test.ts +140 -0
  131. package/tests/label-hydration.test.ts +0 -2
  132. package/tests/query-labels.test.ts +0 -3
  133. package/tests/views/__snapshots__/author-feed.test.ts.snap +62 -17
  134. package/tests/views/__snapshots__/block-lists.test.ts.snap +7 -2
  135. package/tests/views/__snapshots__/blocks.test.ts.snap +13 -4
  136. package/tests/views/__snapshots__/list-feed.test.ts.snap +23 -6
  137. package/tests/views/__snapshots__/mute-lists.test.ts.snap +10 -3
  138. package/tests/views/__snapshots__/mutes.test.ts.snap +9 -3
  139. package/tests/views/__snapshots__/posts.test.ts.snap +16 -5
  140. package/tests/views/__snapshots__/profile.test.ts.snap +230 -10
  141. package/tests/views/__snapshots__/thread.test.ts.snap +53 -14
  142. package/tests/views/__snapshots__/timeline.test.ts.snap +213 -64
  143. package/tests/views/blocks.test.ts +13 -0
  144. package/tests/views/profile.test.ts +35 -0
  145. package/tests/views/threadgating.test.ts +19 -19
@@ -0,0 +1,9 @@
1
+ export const tableName = 'thread_mute'
2
+
3
+ export interface ThreadMute {
4
+ rootUri: string
5
+ mutedByDid: string
6
+ createdAt: string
7
+ }
8
+
9
+ export type PartialDB = { [tableName]: ThreadMute }
@@ -247,7 +247,8 @@ export class RecordProcessor<T, S> {
247
247
  }
248
248
  for (const chunk of chunkArray(notifs, 500)) {
249
249
  runOnCommit.push(async (db) => {
250
- await db.db.insertInto('notification').values(chunk).execute()
250
+ const filtered = await this.filterNotifsForThreadMutes(chunk)
251
+ await db.db.insertInto('notification').values(filtered).execute()
251
252
  })
252
253
  }
253
254
  // Need to ensure notif deletion always happens before creation, otherwise delete may clobber in a race.
@@ -256,6 +257,33 @@ export class RecordProcessor<T, S> {
256
257
  }
257
258
  }
258
259
 
260
+ async filterNotifsForThreadMutes(notifs: Notif[]): Promise<Notif[]> {
261
+ const isBlocked = await Promise.all(
262
+ notifs.map((n) => this.isNotifBlockedByThreadMute(n)),
263
+ )
264
+ return notifs.filter((_, i) => !isBlocked[i])
265
+ }
266
+
267
+ async isNotifBlockedByThreadMute(notif: Notif): Promise<boolean> {
268
+ const subject = notif.reasonSubject
269
+ if (!subject) return false
270
+ if (subject.startsWith('did:')) return false
271
+ const post = await this.db
272
+ .selectFrom('post')
273
+ .select(['uri', 'replyRoot'])
274
+ .where('uri', '=', subject)
275
+ .executeTakeFirst()
276
+ if (!post) return false
277
+ const threadRoot = post.replyRoot ?? post.uri
278
+ const threadMute = await this.db
279
+ .selectFrom('thread_mute')
280
+ .selectAll()
281
+ .where('mutedByDid', '=', notif.did)
282
+ .where('rootUri', '=', threadRoot)
283
+ .executeTakeFirst()
284
+ return !!threadMute
285
+ }
286
+
259
287
  aggregateOnCommit(indexed: S) {
260
288
  const { updateAggregates } = this.params
261
289
  if (!updateAggregates) return
@@ -1,6 +1,7 @@
1
1
  import { keyBy } from '@atproto/common'
2
2
  import { ServiceImpl } from '@connectrpc/connect'
3
3
  import { Service } from '../../../proto/bsky_connect'
4
+ import { FollowsFollowing } from '../../../proto/bsky_pb'
4
5
  import { Database } from '../db'
5
6
  import { TimeCidKeyset, paginate } from '../db/pagination'
6
7
 
@@ -92,7 +93,49 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
92
93
  cursor: keyset.packFromResult(follows),
93
94
  }
94
95
  },
95
- async getFollowsFollowing() {
96
- throw new Error('not implemented')
96
+
97
+ /**
98
+ * Return known followers of a given actor.
99
+ *
100
+ * Example:
101
+ * - Alice follows Bob
102
+ * - Bob follows Dan
103
+ *
104
+ * If Alice (the viewer) looks at Dan's profile (the subject), she should see that Bob follows Dan
105
+ */
106
+ async getFollowsFollowing(req) {
107
+ const { actorDid: viewerDid, targetDids: subjectDids } = req
108
+
109
+ /*
110
+ * 1. Get all the people the Alice is following
111
+ * 2. Get all the people the Dan is followed by
112
+ * 3. Find the intersection
113
+ */
114
+
115
+ const results: FollowsFollowing[] = []
116
+
117
+ for (const subjectDid of subjectDids) {
118
+ const followsReq = db.db
119
+ .selectFrom('follow')
120
+ .where('follow.creator', '=', viewerDid)
121
+ .where(
122
+ 'follow.subjectDid',
123
+ 'in',
124
+ db.db
125
+ .selectFrom('follow')
126
+ .where('follow.subjectDid', '=', subjectDid)
127
+ .select(['creator']),
128
+ )
129
+ .select(['subjectDid'])
130
+ const rows = await followsReq.execute()
131
+ results.push(
132
+ new FollowsFollowing({
133
+ targetDid: subjectDid,
134
+ dids: rows.map((r) => r.subjectDid),
135
+ }),
136
+ )
137
+ }
138
+
139
+ return { results }
97
140
  },
98
141
  })
@@ -5,6 +5,7 @@ import { ids } from '../../../lexicon/lexicons'
5
5
  import { Service } from '../../../proto/bsky_connect'
6
6
  import { Database } from '../db'
7
7
  import { CreatedAtDidKeyset, TimeCidKeyset, paginate } from '../db/pagination'
8
+ import { keyBy } from '@atproto/common'
8
9
 
9
10
  export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
10
11
  async getActorMutesActor(req) {
@@ -166,6 +167,22 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
166
167
  .where('mutedByDid', '=', actorDid)
167
168
  .execute()
168
169
  },
170
+
171
+ async getThreadMutesOnSubjects(req) {
172
+ const { actorDid, threadRoots } = req
173
+ if (threadRoots.length === 0) {
174
+ return { muted: [] }
175
+ }
176
+ const res = await db.db
177
+ .selectFrom('thread_mute')
178
+ .selectAll()
179
+ .where('mutedByDid', '=', actorDid)
180
+ .where('rootUri', 'in', threadRoots)
181
+ .execute()
182
+ const byRootUri = keyBy(res, 'rootUri')
183
+ const muted = threadRoots.map((uri) => !!byRootUri[uri])
184
+ return { muted }
185
+ },
169
186
  })
170
187
 
171
188
  const isListUri = (uri: string) =>
@@ -38,10 +38,16 @@ export type ProfileViewerState = {
38
38
  blockingByList?: string
39
39
  following?: string
40
40
  followedBy?: string
41
+ knownFollowers?: {
42
+ count: number
43
+ followers: string[]
44
+ }
41
45
  }
42
46
 
43
47
  export type ProfileViewerStates = HydrationMap<ProfileViewerState>
44
48
 
49
+ export type KnownFollowers = HydrationMap<ProfileViewerState['knownFollowers']>
50
+
45
51
  export type ProfileAgg = {
46
52
  followers: number
47
53
  follows: number
@@ -171,6 +177,30 @@ export class ActorHydrator {
171
177
  }, new HydrationMap<ProfileViewerState>())
172
178
  }
173
179
 
180
+ async getKnownFollowers(
181
+ dids: string[],
182
+ viewer: string | null,
183
+ ): Promise<KnownFollowers> {
184
+ if (!viewer) return new HydrationMap<ProfileViewerState['knownFollowers']>()
185
+ const { results: knownFollowersResults } =
186
+ await this.dataplane.getFollowsFollowing({
187
+ actorDid: viewer,
188
+ targetDids: dids,
189
+ })
190
+ return dids.reduce((acc, did, i) => {
191
+ const result = knownFollowersResults[i]?.dids
192
+ return acc.set(
193
+ did,
194
+ result && result.length > 0
195
+ ? {
196
+ count: result.length,
197
+ followers: result.slice(0, 5),
198
+ }
199
+ : undefined,
200
+ )
201
+ }, new HydrationMap<ProfileViewerState['knownFollowers']>())
202
+ }
203
+
174
204
  async getProfileAggregates(dids: string[]): Promise<ProfileAggs> {
175
205
  if (!dids.length) return new HydrationMap<ProfileAgg>()
176
206
  const counts = await this.dataplane.getCountsForUsers({ dids })
@@ -13,6 +13,7 @@ import {
13
13
  } from './util'
14
14
  import { AtUri } from '@atproto/syntax'
15
15
  import { ids } from '../lexicon/lexicons'
16
+ import { dedupeStrs } from '@atproto/common'
16
17
 
17
18
  export type Post = RecordInfo<PostRecord> & { violatesThreadGate: boolean }
18
19
  export type Posts = HydrationMap<Post>
@@ -20,6 +21,7 @@ export type Posts = HydrationMap<Post>
20
21
  export type PostViewerState = {
21
22
  like?: string
22
23
  repost?: string
24
+ threadMuted?: boolean
23
25
  }
24
26
 
25
27
  export type PostViewerStates = HydrationMap<PostViewerState>
@@ -57,6 +59,7 @@ export type Threadgate = RecordInfo<ThreadgateRecord>
57
59
  export type Threadgates = HydrationMap<Threadgate>
58
60
 
59
61
  export type ItemRef = { uri: string; cid?: string }
62
+ export type ThreadRef = ItemRef & { threadRoot: string }
60
63
 
61
64
  // @NOTE the feed item types in the protos for author feeds and timelines
62
65
  // technically have additional fields, not supported by the mock dataplane.
@@ -85,11 +88,12 @@ export class FeedHydrator {
85
88
  }
86
89
 
87
90
  async getPostViewerStates(
88
- refs: ItemRef[],
91
+ refs: ThreadRef[],
89
92
  viewer: string,
90
93
  ): Promise<PostViewerStates> {
91
94
  if (!refs.length) return new HydrationMap<PostViewerState>()
92
- const [likes, reposts] = await Promise.all([
95
+ const threadRoots = refs.map((r) => r.threadRoot)
96
+ const [likes, reposts, threadMutesMap] = await Promise.all([
93
97
  this.dataplane.getLikesByActorAndSubjects({
94
98
  actorDid: viewer,
95
99
  refs,
@@ -98,15 +102,31 @@ export class FeedHydrator {
98
102
  actorDid: viewer,
99
103
  refs,
100
104
  }),
105
+ this.getThreadMutes(threadRoots, viewer),
101
106
  ])
102
- return refs.reduce((acc, { uri }, i) => {
107
+ return refs.reduce((acc, { uri, threadRoot }, i) => {
103
108
  return acc.set(uri, {
104
109
  like: parseString(likes.uris[i]),
105
110
  repost: parseString(reposts.uris[i]),
111
+ threadMuted: threadMutesMap.get(threadRoot) ?? false,
106
112
  })
107
113
  }, new HydrationMap<PostViewerState>())
108
114
  }
109
115
 
116
+ private async getThreadMutes(
117
+ threadRoots: string[],
118
+ viewer: string,
119
+ ): Promise<Map<string, boolean>> {
120
+ const deduped = dedupeStrs(threadRoots)
121
+ const threadMutes = await this.dataplane.getThreadMutesOnSubjects({
122
+ actorDid: viewer,
123
+ threadRoots: deduped,
124
+ })
125
+ return deduped.reduce((acc, cur, i) => {
126
+ return acc.set(cur, threadMutes.muted[i] ?? false)
127
+ }, new Map<string, boolean>())
128
+ }
129
+
110
130
  async getPostAggregates(refs: ItemRef[]): Promise<PostAggs> {
111
131
  if (!refs.length) return new HydrationMap<PostAgg>()
112
132
  const counts = await this.dataplane.getInteractionCounts({ refs })
@@ -7,12 +7,14 @@ import { ids } from '../lexicon/lexicons'
7
7
  import { isMain as isEmbedRecord } from '../lexicon/types/app/bsky/embed/record'
8
8
  import { isMain as isEmbedRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia'
9
9
  import { isListRule } from '../lexicon/types/app/bsky/feed/threadgate'
10
+ import { hydrationLogger } from '../logger'
10
11
  import {
11
12
  ActorHydrator,
12
13
  ProfileAggs,
13
14
  Actors,
14
15
  ProfileViewerStates,
15
16
  ProfileViewerState,
17
+ KnownFollowers,
16
18
  } from './actor'
17
19
  import {
18
20
  Follows,
@@ -93,6 +95,7 @@ export type HydrationState = {
93
95
  labelers?: Labelers
94
96
  labelerViewers?: LabelerViewerStates
95
97
  labelerAggs?: LabelerAggs
98
+ knownFollowers?: KnownFollowers
96
99
  }
97
100
 
98
101
  export type PostBlock = { embed: boolean; reply: boolean }
@@ -143,6 +146,7 @@ export class Hydrator {
143
146
  profileViewers?.forEach((item) => {
144
147
  removeNonModListsFromProfileViewer(item, listState)
145
148
  })
149
+
146
150
  return mergeStates(listState, {
147
151
  profileViewers,
148
152
  ctx,
@@ -190,13 +194,29 @@ export class Hydrator {
190
194
  dids: string[],
191
195
  ctx: HydrateCtx,
192
196
  ): Promise<HydrationState> {
197
+ let knownFollowers: KnownFollowers = new HydrationMap()
198
+
199
+ try {
200
+ knownFollowers = await this.actor.getKnownFollowers(dids, ctx.viewer)
201
+ } catch (err) {
202
+ hydrationLogger.error(
203
+ { err },
204
+ 'Failed to get known followers for profiles',
205
+ )
206
+ }
207
+
208
+ const knownFollowersDids = Array.from(knownFollowers.values())
209
+ .filter(Boolean)
210
+ .flatMap((f) => f!.followers)
211
+ const allDids = Array.from(new Set(dids.concat(knownFollowersDids)))
193
212
  const [state, profileAggs] = await Promise.all([
194
- this.hydrateProfiles(dids, ctx),
213
+ this.hydrateProfiles(allDids, ctx),
195
214
  this.actor.getProfileAggregates(dids),
196
215
  ])
197
216
  return {
198
217
  ...state,
199
218
  profileAggs,
219
+ knownFollowers,
200
220
  }
201
221
  }
202
222
 
@@ -314,6 +334,16 @@ export class Hydrator {
314
334
  const posts =
315
335
  mergeManyMaps(postsLayer0, postsLayer1, postsLayer2) ?? postsLayer0
316
336
  const allPostUris = [...posts.keys()]
337
+ const allRefs = [
338
+ ...refs,
339
+ ...postUrisLayer1.map(uriToRef), // supports aggregates on embed #viewRecords
340
+ ...postUrisLayer2.map(uriToRef),
341
+ ]
342
+ const threadRefs = allRefs.map((ref) => ({
343
+ ...ref,
344
+ threadRoot: posts.get(ref.uri)?.record.reply?.root.uri ?? ref.uri,
345
+ }))
346
+
317
347
  const [
318
348
  postAggs,
319
349
  postViewers,
@@ -324,12 +354,10 @@ export class Hydrator {
324
354
  feedGenState,
325
355
  labelerState,
326
356
  ] = await Promise.all([
327
- this.feed.getPostAggregates([
328
- ...refs,
329
- ...postUrisLayer1.map(uriToRef), // supports aggregates on embed #viewRecords
330
- ...postUrisLayer2.map(uriToRef),
331
- ]),
332
- ctx.viewer ? this.feed.getPostViewerStates(refs, ctx.viewer) : undefined,
357
+ this.feed.getPostAggregates(allRefs),
358
+ ctx.viewer
359
+ ? this.feed.getPostViewerStates(threadRefs, ctx.viewer)
360
+ : undefined,
333
361
  this.label.getLabelsForSubjects(allPostUris, ctx.labelers),
334
362
  this.hydratePostBlocks(posts),
335
363
  this.hydrateProfiles(allPostUris.map(didFromUri), ctx),
@@ -856,6 +884,7 @@ export const mergeStates = (
856
884
  labelers: mergeMaps(stateA.labelers, stateB.labelers),
857
885
  labelerAggs: mergeMaps(stateA.labelerAggs, stateB.labelerAggs),
858
886
  labelerViewers: mergeMaps(stateA.labelerViewers, stateB.labelerViewers),
887
+ knownFollowers: mergeMaps(stateA.knownFollowers, stateB.knownFollowers),
859
888
  }
860
889
  }
861
890
 
@@ -110,6 +110,7 @@ import * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteract
110
110
  import * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks'
111
111
  import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers'
112
112
  import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows'
113
+ import * as AppBskyGraphGetKnownFollowers from './types/app/bsky/graph/getKnownFollowers'
113
114
  import * as AppBskyGraphGetList from './types/app/bsky/graph/getList'
114
115
  import * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks'
115
116
  import * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes'
@@ -119,8 +120,10 @@ import * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelatio
119
120
  import * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor'
120
121
  import * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor'
121
122
  import * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList'
123
+ import * as AppBskyGraphMuteThread from './types/app/bsky/graph/muteThread'
122
124
  import * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor'
123
125
  import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList'
126
+ import * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread'
124
127
  import * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices'
125
128
  import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount'
126
129
  import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications'
@@ -1473,6 +1476,17 @@ export class AppBskyGraphNS {
1473
1476
  return this._server.xrpc.method(nsid, cfg)
1474
1477
  }
1475
1478
 
1479
+ getKnownFollowers<AV extends AuthVerifier>(
1480
+ cfg: ConfigOf<
1481
+ AV,
1482
+ AppBskyGraphGetKnownFollowers.Handler<ExtractAuth<AV>>,
1483
+ AppBskyGraphGetKnownFollowers.HandlerReqCtx<ExtractAuth<AV>>
1484
+ >,
1485
+ ) {
1486
+ const nsid = 'app.bsky.graph.getKnownFollowers' // @ts-ignore
1487
+ return this._server.xrpc.method(nsid, cfg)
1488
+ }
1489
+
1476
1490
  getList<AV extends AuthVerifier>(
1477
1491
  cfg: ConfigOf<
1478
1492
  AV,
@@ -1572,6 +1586,17 @@ export class AppBskyGraphNS {
1572
1586
  return this._server.xrpc.method(nsid, cfg)
1573
1587
  }
1574
1588
 
1589
+ muteThread<AV extends AuthVerifier>(
1590
+ cfg: ConfigOf<
1591
+ AV,
1592
+ AppBskyGraphMuteThread.Handler<ExtractAuth<AV>>,
1593
+ AppBskyGraphMuteThread.HandlerReqCtx<ExtractAuth<AV>>
1594
+ >,
1595
+ ) {
1596
+ const nsid = 'app.bsky.graph.muteThread' // @ts-ignore
1597
+ return this._server.xrpc.method(nsid, cfg)
1598
+ }
1599
+
1575
1600
  unmuteActor<AV extends AuthVerifier>(
1576
1601
  cfg: ConfigOf<
1577
1602
  AV,
@@ -1593,6 +1618,17 @@ export class AppBskyGraphNS {
1593
1618
  const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore
1594
1619
  return this._server.xrpc.method(nsid, cfg)
1595
1620
  }
1621
+
1622
+ unmuteThread<AV extends AuthVerifier>(
1623
+ cfg: ConfigOf<
1624
+ AV,
1625
+ AppBskyGraphUnmuteThread.Handler<ExtractAuth<AV>>,
1626
+ AppBskyGraphUnmuteThread.HandlerReqCtx<ExtractAuth<AV>>
1627
+ >,
1628
+ ) {
1629
+ const nsid = 'app.bsky.graph.unmuteThread' // @ts-ignore
1630
+ return this._server.xrpc.method(nsid, cfg)
1631
+ }
1596
1632
  }
1597
1633
 
1598
1634
  export class AppBskyLabelerNS {
@@ -4107,6 +4107,29 @@ export const schemaDict = {
4107
4107
  type: 'string',
4108
4108
  format: 'at-uri',
4109
4109
  },
4110
+ knownFollowers: {
4111
+ type: 'ref',
4112
+ ref: 'lex:app.bsky.actor.defs#knownFollowers',
4113
+ },
4114
+ },
4115
+ },
4116
+ knownFollowers: {
4117
+ type: 'object',
4118
+ description: "The subject's followers whom you also follow",
4119
+ required: ['count', 'followers'],
4120
+ properties: {
4121
+ count: {
4122
+ type: 'integer',
4123
+ },
4124
+ followers: {
4125
+ type: 'array',
4126
+ minLength: 0,
4127
+ maxLength: 5,
4128
+ items: {
4129
+ type: 'ref',
4130
+ ref: 'lex:app.bsky.actor.defs#profileViewBasic',
4131
+ },
4132
+ },
4110
4133
  },
4111
4134
  },
4112
4135
  preferences: {
@@ -5079,6 +5102,9 @@ export const schemaDict = {
5079
5102
  type: 'string',
5080
5103
  format: 'at-uri',
5081
5104
  },
5105
+ threadMuted: {
5106
+ type: 'boolean',
5107
+ },
5082
5108
  replyDisabled: {
5083
5109
  type: 'boolean',
5084
5110
  },
@@ -7101,6 +7127,59 @@ export const schemaDict = {
7101
7127
  },
7102
7128
  },
7103
7129
  },
7130
+ AppBskyGraphGetKnownFollowers: {
7131
+ lexicon: 1,
7132
+ id: 'app.bsky.graph.getKnownFollowers',
7133
+ defs: {
7134
+ main: {
7135
+ type: 'query',
7136
+ description:
7137
+ 'Enumerates accounts which follow a specified account (actor) and are followed by the viewer.',
7138
+ parameters: {
7139
+ type: 'params',
7140
+ required: ['actor'],
7141
+ properties: {
7142
+ actor: {
7143
+ type: 'string',
7144
+ format: 'at-identifier',
7145
+ },
7146
+ limit: {
7147
+ type: 'integer',
7148
+ minimum: 1,
7149
+ maximum: 100,
7150
+ default: 50,
7151
+ },
7152
+ cursor: {
7153
+ type: 'string',
7154
+ },
7155
+ },
7156
+ },
7157
+ output: {
7158
+ encoding: 'application/json',
7159
+ schema: {
7160
+ type: 'object',
7161
+ required: ['subject', 'followers'],
7162
+ properties: {
7163
+ subject: {
7164
+ type: 'ref',
7165
+ ref: 'lex:app.bsky.actor.defs#profileView',
7166
+ },
7167
+ cursor: {
7168
+ type: 'string',
7169
+ },
7170
+ followers: {
7171
+ type: 'array',
7172
+ items: {
7173
+ type: 'ref',
7174
+ ref: 'lex:app.bsky.actor.defs#profileView',
7175
+ },
7176
+ },
7177
+ },
7178
+ },
7179
+ },
7180
+ },
7181
+ },
7182
+ },
7104
7183
  AppBskyGraphGetList: {
7105
7184
  lexicon: 1,
7106
7185
  id: 'app.bsky.graph.getList',
@@ -7599,6 +7678,30 @@ export const schemaDict = {
7599
7678
  },
7600
7679
  },
7601
7680
  },
7681
+ AppBskyGraphMuteThread: {
7682
+ lexicon: 1,
7683
+ id: 'app.bsky.graph.muteThread',
7684
+ defs: {
7685
+ main: {
7686
+ type: 'procedure',
7687
+ description:
7688
+ 'Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.',
7689
+ input: {
7690
+ encoding: 'application/json',
7691
+ schema: {
7692
+ type: 'object',
7693
+ required: ['root'],
7694
+ properties: {
7695
+ root: {
7696
+ type: 'string',
7697
+ format: 'at-uri',
7698
+ },
7699
+ },
7700
+ },
7701
+ },
7702
+ },
7703
+ },
7704
+ },
7602
7705
  AppBskyGraphUnmuteActor: {
7603
7706
  lexicon: 1,
7604
7707
  id: 'app.bsky.graph.unmuteActor',
@@ -7645,6 +7748,29 @@ export const schemaDict = {
7645
7748
  },
7646
7749
  },
7647
7750
  },
7751
+ AppBskyGraphUnmuteThread: {
7752
+ lexicon: 1,
7753
+ id: 'app.bsky.graph.unmuteThread',
7754
+ defs: {
7755
+ main: {
7756
+ type: 'procedure',
7757
+ description: 'Unmutes the specified thread. Requires auth.',
7758
+ input: {
7759
+ encoding: 'application/json',
7760
+ schema: {
7761
+ type: 'object',
7762
+ required: ['root'],
7763
+ properties: {
7764
+ root: {
7765
+ type: 'string',
7766
+ format: 'at-uri',
7767
+ },
7768
+ },
7769
+ },
7770
+ },
7771
+ },
7772
+ },
7773
+ },
7648
7774
  AppBskyLabelerDefs: {
7649
7775
  lexicon: 1,
7650
7776
  id: 'app.bsky.labeler.defs',
@@ -9523,6 +9649,7 @@ export const ids = {
9523
9649
  AppBskyGraphGetBlocks: 'app.bsky.graph.getBlocks',
9524
9650
  AppBskyGraphGetFollowers: 'app.bsky.graph.getFollowers',
9525
9651
  AppBskyGraphGetFollows: 'app.bsky.graph.getFollows',
9652
+ AppBskyGraphGetKnownFollowers: 'app.bsky.graph.getKnownFollowers',
9526
9653
  AppBskyGraphGetList: 'app.bsky.graph.getList',
9527
9654
  AppBskyGraphGetListBlocks: 'app.bsky.graph.getListBlocks',
9528
9655
  AppBskyGraphGetListMutes: 'app.bsky.graph.getListMutes',
@@ -9536,8 +9663,10 @@ export const ids = {
9536
9663
  AppBskyGraphListitem: 'app.bsky.graph.listitem',
9537
9664
  AppBskyGraphMuteActor: 'app.bsky.graph.muteActor',
9538
9665
  AppBskyGraphMuteActorList: 'app.bsky.graph.muteActorList',
9666
+ AppBskyGraphMuteThread: 'app.bsky.graph.muteThread',
9539
9667
  AppBskyGraphUnmuteActor: 'app.bsky.graph.unmuteActor',
9540
9668
  AppBskyGraphUnmuteActorList: 'app.bsky.graph.unmuteActorList',
9669
+ AppBskyGraphUnmuteThread: 'app.bsky.graph.unmuteThread',
9541
9670
  AppBskyLabelerDefs: 'app.bsky.labeler.defs',
9542
9671
  AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',
9543
9672
  AppBskyLabelerService: 'app.bsky.labeler.service',
@@ -133,6 +133,7 @@ export interface ViewerState {
133
133
  blockingByList?: AppBskyGraphDefs.ListViewBasic
134
134
  following?: string
135
135
  followedBy?: string
136
+ knownFollowers?: KnownFollowers
136
137
  [k: string]: unknown
137
138
  }
138
139
 
@@ -148,6 +149,25 @@ export function validateViewerState(v: unknown): ValidationResult {
148
149
  return lexicons.validate('app.bsky.actor.defs#viewerState', v)
149
150
  }
150
151
 
152
+ /** The subject's followers whom you also follow */
153
+ export interface KnownFollowers {
154
+ count: number
155
+ followers: ProfileViewBasic[]
156
+ [k: string]: unknown
157
+ }
158
+
159
+ export function isKnownFollowers(v: unknown): v is KnownFollowers {
160
+ return (
161
+ isObj(v) &&
162
+ hasProp(v, '$type') &&
163
+ v.$type === 'app.bsky.actor.defs#knownFollowers'
164
+ )
165
+ }
166
+
167
+ export function validateKnownFollowers(v: unknown): ValidationResult {
168
+ return lexicons.validate('app.bsky.actor.defs#knownFollowers', v)
169
+ }
170
+
151
171
  export type Preferences = (
152
172
  | AdultContentPref
153
173
  | ContentLabelPref
@@ -49,6 +49,7 @@ export function validatePostView(v: unknown): ValidationResult {
49
49
  export interface ViewerState {
50
50
  repost?: string
51
51
  like?: string
52
+ threadMuted?: boolean
52
53
  replyDisabled?: boolean
53
54
  [k: string]: unknown
54
55
  }