@atproto/bsky 0.4.0 → 0.5.0

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 (264) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/api/app/bsky/actor/getProfile.d.ts.map +1 -1
  3. package/dist/api/app/bsky/actor/getProfile.js +1 -0
  4. package/dist/api/app/bsky/actor/getProfile.js.map +1 -1
  5. package/dist/api/app/bsky/actor/getProfiles.d.ts.map +1 -1
  6. package/dist/api/app/bsky/actor/getProfiles.js +1 -0
  7. package/dist/api/app/bsky/actor/getProfiles.js.map +1 -1
  8. package/dist/api/app/bsky/actor/getSuggestions.d.ts.map +1 -1
  9. package/dist/api/app/bsky/actor/getSuggestions.js +1 -0
  10. package/dist/api/app/bsky/actor/getSuggestions.js.map +1 -1
  11. package/dist/api/app/bsky/actor/searchActors.d.ts.map +1 -1
  12. package/dist/api/app/bsky/actor/searchActors.js +1 -0
  13. package/dist/api/app/bsky/actor/searchActors.js.map +1 -1
  14. package/dist/api/app/bsky/actor/searchActorsTypeahead.d.ts.map +1 -1
  15. package/dist/api/app/bsky/actor/searchActorsTypeahead.js +1 -0
  16. package/dist/api/app/bsky/actor/searchActorsTypeahead.js.map +1 -1
  17. package/dist/api/app/bsky/bookmark/getBookmarks.d.ts.map +1 -1
  18. package/dist/api/app/bsky/bookmark/getBookmarks.js +1 -0
  19. package/dist/api/app/bsky/bookmark/getBookmarks.js.map +1 -1
  20. package/dist/api/app/bsky/contact/getMatches.d.ts.map +1 -1
  21. package/dist/api/app/bsky/contact/getMatches.js +1 -0
  22. package/dist/api/app/bsky/contact/getMatches.js.map +1 -1
  23. package/dist/api/app/bsky/contact/importContacts.d.ts.map +1 -1
  24. package/dist/api/app/bsky/contact/importContacts.js +1 -0
  25. package/dist/api/app/bsky/contact/importContacts.js.map +1 -1
  26. package/dist/api/app/bsky/feed/getActorFeeds.d.ts.map +1 -1
  27. package/dist/api/app/bsky/feed/getActorFeeds.js +5 -1
  28. package/dist/api/app/bsky/feed/getActorFeeds.js.map +1 -1
  29. package/dist/api/app/bsky/feed/getActorLikes.d.ts.map +1 -1
  30. package/dist/api/app/bsky/feed/getActorLikes.js +1 -0
  31. package/dist/api/app/bsky/feed/getActorLikes.js.map +1 -1
  32. package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
  33. package/dist/api/app/bsky/feed/getAuthorFeed.js +1 -0
  34. package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
  35. package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
  36. package/dist/api/app/bsky/feed/getFeed.js +1 -0
  37. package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
  38. package/dist/api/app/bsky/feed/getFeedGenerator.d.ts.map +1 -1
  39. package/dist/api/app/bsky/feed/getFeedGenerator.js +5 -1
  40. package/dist/api/app/bsky/feed/getFeedGenerator.js.map +1 -1
  41. package/dist/api/app/bsky/feed/getFeedGenerators.d.ts.map +1 -1
  42. package/dist/api/app/bsky/feed/getFeedGenerators.js +5 -1
  43. package/dist/api/app/bsky/feed/getFeedGenerators.js.map +1 -1
  44. package/dist/api/app/bsky/feed/getLikes.d.ts.map +1 -1
  45. package/dist/api/app/bsky/feed/getLikes.js +1 -0
  46. package/dist/api/app/bsky/feed/getLikes.js.map +1 -1
  47. package/dist/api/app/bsky/feed/getListFeed.d.ts.map +1 -1
  48. package/dist/api/app/bsky/feed/getListFeed.js +1 -0
  49. package/dist/api/app/bsky/feed/getListFeed.js.map +1 -1
  50. package/dist/api/app/bsky/feed/getPostThread.d.ts.map +1 -1
  51. package/dist/api/app/bsky/feed/getPostThread.js +1 -0
  52. package/dist/api/app/bsky/feed/getPostThread.js.map +1 -1
  53. package/dist/api/app/bsky/feed/getPosts.d.ts.map +1 -1
  54. package/dist/api/app/bsky/feed/getPosts.js +5 -1
  55. package/dist/api/app/bsky/feed/getPosts.js.map +1 -1
  56. package/dist/api/app/bsky/feed/getQuotes.d.ts.map +1 -1
  57. package/dist/api/app/bsky/feed/getQuotes.js +1 -0
  58. package/dist/api/app/bsky/feed/getQuotes.js.map +1 -1
  59. package/dist/api/app/bsky/feed/getRepostedBy.d.ts.map +1 -1
  60. package/dist/api/app/bsky/feed/getRepostedBy.js +1 -0
  61. package/dist/api/app/bsky/feed/getRepostedBy.js.map +1 -1
  62. package/dist/api/app/bsky/feed/getSuggestedFeeds.d.ts.map +1 -1
  63. package/dist/api/app/bsky/feed/getSuggestedFeeds.js +5 -1
  64. package/dist/api/app/bsky/feed/getSuggestedFeeds.js.map +1 -1
  65. package/dist/api/app/bsky/feed/getTimeline.d.ts.map +1 -1
  66. package/dist/api/app/bsky/feed/getTimeline.js +1 -0
  67. package/dist/api/app/bsky/feed/getTimeline.js.map +1 -1
  68. package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
  69. package/dist/api/app/bsky/feed/searchPosts.js +1 -0
  70. package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
  71. package/dist/api/app/bsky/feed/searchPostsV2.d.ts.map +1 -1
  72. package/dist/api/app/bsky/feed/searchPostsV2.js +1 -0
  73. package/dist/api/app/bsky/feed/searchPostsV2.js.map +1 -1
  74. package/dist/api/app/bsky/graph/getActorStarterPacks.d.ts.map +1 -1
  75. package/dist/api/app/bsky/graph/getActorStarterPacks.js +1 -0
  76. package/dist/api/app/bsky/graph/getActorStarterPacks.js.map +1 -1
  77. package/dist/api/app/bsky/graph/getBlocks.d.ts.map +1 -1
  78. package/dist/api/app/bsky/graph/getBlocks.js +5 -1
  79. package/dist/api/app/bsky/graph/getBlocks.js.map +1 -1
  80. package/dist/api/app/bsky/graph/getFollowers.d.ts.map +1 -1
  81. package/dist/api/app/bsky/graph/getFollowers.js +1 -0
  82. package/dist/api/app/bsky/graph/getFollowers.js.map +1 -1
  83. package/dist/api/app/bsky/graph/getFollows.d.ts.map +1 -1
  84. package/dist/api/app/bsky/graph/getFollows.js +1 -0
  85. package/dist/api/app/bsky/graph/getFollows.js.map +1 -1
  86. package/dist/api/app/bsky/graph/getKnownFollowers.d.ts.map +1 -1
  87. package/dist/api/app/bsky/graph/getKnownFollowers.js +1 -0
  88. package/dist/api/app/bsky/graph/getKnownFollowers.js.map +1 -1
  89. package/dist/api/app/bsky/graph/getList.d.ts.map +1 -1
  90. package/dist/api/app/bsky/graph/getList.js +1 -0
  91. package/dist/api/app/bsky/graph/getList.js.map +1 -1
  92. package/dist/api/app/bsky/graph/getListBlocks.d.ts.map +1 -1
  93. package/dist/api/app/bsky/graph/getListBlocks.js +5 -1
  94. package/dist/api/app/bsky/graph/getListBlocks.js.map +1 -1
  95. package/dist/api/app/bsky/graph/getListMutes.d.ts.map +1 -1
  96. package/dist/api/app/bsky/graph/getListMutes.js +5 -1
  97. package/dist/api/app/bsky/graph/getListMutes.js.map +1 -1
  98. package/dist/api/app/bsky/graph/getLists.d.ts.map +1 -1
  99. package/dist/api/app/bsky/graph/getLists.js +1 -0
  100. package/dist/api/app/bsky/graph/getLists.js.map +1 -1
  101. package/dist/api/app/bsky/graph/getListsWithMembership.d.ts.map +1 -1
  102. package/dist/api/app/bsky/graph/getListsWithMembership.js +1 -0
  103. package/dist/api/app/bsky/graph/getListsWithMembership.js.map +1 -1
  104. package/dist/api/app/bsky/graph/getMutes.d.ts.map +1 -1
  105. package/dist/api/app/bsky/graph/getMutes.js +5 -1
  106. package/dist/api/app/bsky/graph/getMutes.js.map +1 -1
  107. package/dist/api/app/bsky/graph/getStarterPack.d.ts.map +1 -1
  108. package/dist/api/app/bsky/graph/getStarterPack.js +1 -0
  109. package/dist/api/app/bsky/graph/getStarterPack.js.map +1 -1
  110. package/dist/api/app/bsky/graph/getStarterPacks.d.ts.map +1 -1
  111. package/dist/api/app/bsky/graph/getStarterPacks.js +1 -0
  112. package/dist/api/app/bsky/graph/getStarterPacks.js.map +1 -1
  113. package/dist/api/app/bsky/graph/getStarterPacksWithMembership.d.ts.map +1 -1
  114. package/dist/api/app/bsky/graph/getStarterPacksWithMembership.js +1 -0
  115. package/dist/api/app/bsky/graph/getStarterPacksWithMembership.js.map +1 -1
  116. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
  117. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js +1 -0
  118. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
  119. package/dist/api/app/bsky/graph/searchStarterPacks.d.ts.map +1 -1
  120. package/dist/api/app/bsky/graph/searchStarterPacks.js +1 -0
  121. package/dist/api/app/bsky/graph/searchStarterPacks.js.map +1 -1
  122. package/dist/api/app/bsky/graph/searchStarterPacksV2.d.ts.map +1 -1
  123. package/dist/api/app/bsky/graph/searchStarterPacksV2.js +1 -0
  124. package/dist/api/app/bsky/graph/searchStarterPacksV2.js.map +1 -1
  125. package/dist/api/app/bsky/labeler/getServices.d.ts.map +1 -1
  126. package/dist/api/app/bsky/labeler/getServices.js +1 -0
  127. package/dist/api/app/bsky/labeler/getServices.js.map +1 -1
  128. package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts.map +1 -1
  129. package/dist/api/app/bsky/notification/getGroupedNotifications.js +29 -19
  130. package/dist/api/app/bsky/notification/getGroupedNotifications.js.map +1 -1
  131. package/dist/api/app/bsky/notification/grouping/algoGravity.d.ts +4 -3
  132. package/dist/api/app/bsky/notification/grouping/algoGravity.d.ts.map +1 -1
  133. package/dist/api/app/bsky/notification/grouping/algoGravity.js +53 -42
  134. package/dist/api/app/bsky/notification/grouping/algoGravity.js.map +1 -1
  135. package/dist/api/app/bsky/notification/grouping/algoLookback.d.ts +4 -3
  136. package/dist/api/app/bsky/notification/grouping/algoLookback.d.ts.map +1 -1
  137. package/dist/api/app/bsky/notification/grouping/algoLookback.js +56 -42
  138. package/dist/api/app/bsky/notification/grouping/algoLookback.js.map +1 -1
  139. package/dist/api/app/bsky/notification/grouping/grouping.d.ts +12 -2
  140. package/dist/api/app/bsky/notification/grouping/grouping.d.ts.map +1 -1
  141. package/dist/api/app/bsky/notification/grouping/grouping.js +20 -6
  142. package/dist/api/app/bsky/notification/grouping/grouping.js.map +1 -1
  143. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts +2 -2
  144. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts.map +1 -1
  145. package/dist/api/app/bsky/notification/grouping/spotlight.js +35 -22
  146. package/dist/api/app/bsky/notification/grouping/spotlight.js.map +1 -1
  147. package/dist/api/app/bsky/notification/listActivitySubscriptions.d.ts.map +1 -1
  148. package/dist/api/app/bsky/notification/listActivitySubscriptions.js +1 -0
  149. package/dist/api/app/bsky/notification/listActivitySubscriptions.js.map +1 -1
  150. package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
  151. package/dist/api/app/bsky/notification/listNotifications.js +5 -1
  152. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  153. package/dist/api/app/bsky/unspecced/atmosphere-grouping.d.ts +16 -0
  154. package/dist/api/app/bsky/unspecced/atmosphere-grouping.d.ts.map +1 -0
  155. package/dist/api/app/bsky/unspecced/atmosphere-grouping.js +124 -0
  156. package/dist/api/app/bsky/unspecced/atmosphere-grouping.js.map +1 -0
  157. package/dist/api/app/bsky/unspecced/atmosphere-hydration.d.ts +32 -0
  158. package/dist/api/app/bsky/unspecced/atmosphere-hydration.d.ts.map +1 -0
  159. package/dist/api/app/bsky/unspecced/atmosphere-hydration.js +201 -0
  160. package/dist/api/app/bsky/unspecced/atmosphere-hydration.js.map +1 -0
  161. package/dist/api/app/bsky/unspecced/atmosphere-pagination.d.ts +6 -0
  162. package/dist/api/app/bsky/unspecced/atmosphere-pagination.d.ts.map +1 -0
  163. package/dist/api/app/bsky/unspecced/atmosphere-pagination.js +164 -0
  164. package/dist/api/app/bsky/unspecced/atmosphere-pagination.js.map +1 -0
  165. package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.d.ts.map +1 -1
  166. package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.js +120 -55
  167. package/dist/api/app/bsky/unspecced/getAtmosphereExploreTab.js.map +1 -1
  168. package/dist/api/app/bsky/unspecced/getAtmosphereTimeline.d.ts +4 -0
  169. package/dist/api/app/bsky/unspecced/getAtmosphereTimeline.d.ts.map +1 -0
  170. package/dist/api/app/bsky/unspecced/getAtmosphereTimeline.js +30 -0
  171. package/dist/api/app/bsky/unspecced/getAtmosphereTimeline.js.map +1 -0
  172. package/dist/api/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.d.ts.map +1 -1
  173. package/dist/api/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.js +4 -7
  174. package/dist/api/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.js.map +1 -1
  175. package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.d.ts.map +1 -1
  176. package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.js +1 -0
  177. package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.js.map +1 -1
  178. package/dist/api/app/bsky/unspecced/getPostThreadOtherV2.d.ts.map +1 -1
  179. package/dist/api/app/bsky/unspecced/getPostThreadOtherV2.js +1 -0
  180. package/dist/api/app/bsky/unspecced/getPostThreadOtherV2.js.map +1 -1
  181. package/dist/api/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -1
  182. package/dist/api/app/bsky/unspecced/getPostThreadV2.js +1 -0
  183. package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
  184. package/dist/api/app/bsky/unspecced/getSuggestedFeeds.d.ts.map +1 -1
  185. package/dist/api/app/bsky/unspecced/getSuggestedFeeds.js +4 -7
  186. package/dist/api/app/bsky/unspecced/getSuggestedFeeds.js.map +1 -1
  187. package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.d.ts.map +1 -1
  188. package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js +1 -0
  189. package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js.map +1 -1
  190. package/dist/api/app/bsky/unspecced/getSuggestedStarterPacks.d.ts.map +1 -1
  191. package/dist/api/app/bsky/unspecced/getSuggestedStarterPacks.js +4 -7
  192. package/dist/api/app/bsky/unspecced/getSuggestedStarterPacks.js.map +1 -1
  193. package/dist/api/app/bsky/unspecced/getSuggestedUsers.d.ts.map +1 -1
  194. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js +2 -19
  195. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js.map +1 -1
  196. package/dist/api/app/bsky/unspecced/getSuggestedUsersForDiscover.d.ts.map +1 -1
  197. package/dist/api/app/bsky/unspecced/getSuggestedUsersForDiscover.js +1 -0
  198. package/dist/api/app/bsky/unspecced/getSuggestedUsersForDiscover.js.map +1 -1
  199. package/dist/api/app/bsky/unspecced/getSuggestedUsersForExplore.d.ts.map +1 -1
  200. package/dist/api/app/bsky/unspecced/getSuggestedUsersForExplore.js +1 -0
  201. package/dist/api/app/bsky/unspecced/getSuggestedUsersForExplore.js.map +1 -1
  202. package/dist/api/app/bsky/unspecced/getSuggestedUsersForSeeMore.d.ts.map +1 -1
  203. package/dist/api/app/bsky/unspecced/getSuggestedUsersForSeeMore.js +1 -0
  204. package/dist/api/app/bsky/unspecced/getSuggestedUsersForSeeMore.js.map +1 -1
  205. package/dist/api/app/bsky/unspecced/getTrendingTopics.d.ts.map +1 -1
  206. package/dist/api/app/bsky/unspecced/getTrendingTopics.js +6 -2
  207. package/dist/api/app/bsky/unspecced/getTrendingTopics.js.map +1 -1
  208. package/dist/api/app/bsky/unspecced/getTrends.d.ts.map +1 -1
  209. package/dist/api/app/bsky/unspecced/getTrends.js +6 -2
  210. package/dist/api/app/bsky/unspecced/getTrends.js.map +1 -1
  211. package/dist/api/index.d.ts.map +1 -1
  212. package/dist/api/index.js +2 -0
  213. package/dist/api/index.js.map +1 -1
  214. package/dist/api/internal/bsky/actor/getProfiles.d.ts.map +1 -1
  215. package/dist/api/internal/bsky/actor/getProfiles.js +1 -0
  216. package/dist/api/internal/bsky/actor/getProfiles.js.map +1 -1
  217. package/dist/config.d.ts +0 -4
  218. package/dist/config.d.ts.map +1 -1
  219. package/dist/config.js +0 -10
  220. package/dist/config.js.map +1 -1
  221. package/dist/context.d.ts +0 -2
  222. package/dist/context.d.ts.map +1 -1
  223. package/dist/context.js +0 -3
  224. package/dist/context.js.map +1 -1
  225. package/dist/feature-gates/gates.d.ts +0 -4
  226. package/dist/feature-gates/gates.d.ts.map +1 -1
  227. package/dist/feature-gates/gates.js +0 -4
  228. package/dist/feature-gates/gates.js.map +1 -1
  229. package/dist/hydration/external.d.ts +8 -2
  230. package/dist/hydration/external.d.ts.map +1 -1
  231. package/dist/hydration/external.js +2 -0
  232. package/dist/hydration/external.js.map +1 -1
  233. package/dist/hydration/hydrator.d.ts +6 -2
  234. package/dist/hydration/hydrator.d.ts.map +1 -1
  235. package/dist/hydration/hydrator.js +96 -29
  236. package/dist/hydration/hydrator.js.map +1 -1
  237. package/dist/index.d.ts.map +1 -1
  238. package/dist/index.js +4 -19
  239. package/dist/index.js.map +1 -1
  240. package/dist/lexicons/app/bsky/unspecced/getAtmosphereExploreTab.defs.d.ts +29 -0
  241. package/dist/lexicons/app/bsky/unspecced/getAtmosphereExploreTab.defs.d.ts.map +1 -1
  242. package/dist/lexicons/app/bsky/unspecced/getAtmosphereExploreTab.defs.js +2 -1
  243. package/dist/lexicons/app/bsky/unspecced/getAtmosphereExploreTab.defs.js.map +1 -1
  244. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.d.ts +3 -0
  245. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.d.ts.map +1 -0
  246. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.defs.d.ts +85 -0
  247. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.defs.d.ts.map +1 -0
  248. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.defs.js +29 -0
  249. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.defs.js.map +1 -0
  250. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.js +6 -0
  251. package/dist/lexicons/app/bsky/unspecced/getAtmosphereTimeline.js.map +1 -0
  252. package/dist/lexicons/app/bsky/unspecced.d.ts +1 -0
  253. package/dist/lexicons/app/bsky/unspecced.d.ts.map +1 -1
  254. package/dist/lexicons/app/bsky/unspecced.js +1 -0
  255. package/dist/lexicons/app/bsky/unspecced.js.map +1 -1
  256. package/dist/telemetry/events.d.ts +5 -4
  257. package/dist/telemetry/events.d.ts.map +1 -1
  258. package/dist/telemetry/events.js +13 -2
  259. package/dist/telemetry/events.js.map +1 -1
  260. package/dist/views/external.d.ts +5 -0
  261. package/dist/views/external.d.ts.map +1 -1
  262. package/dist/views/external.js +38 -11
  263. package/dist/views/external.js.map +1 -1
  264. package/package.json +6 -6
@@ -4,6 +4,7 @@ import { app } from '../../../../../lexicons/index.js';
4
4
  import { APPVIEW_NOTIFICATION_REASON, NOTIFICATION_REASON, } from '../constants.js';
5
5
  import { MAX_GROUP_SIZE, compareNotificationGroupsNewestFirst, isNotificationRead, } from './shared.js';
6
6
  const MAX_SPOTLIGHTS = 3;
7
+ const TIME_SPAN_MULTIPLIER = 2;
7
8
  const MULTI_POST_RANGES = [
8
9
  { maxItems: 200, requiredPosts: 4 },
9
10
  { maxItems: 500, requiredPosts: 8 },
@@ -15,7 +16,7 @@ const SPOTLIGHT_KINDS = {
15
16
  [NOTIFICATION_REASON.REPOST_VIA_REPOST]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST_VIA_REPOST,
16
17
  };
17
18
  const SPOTLIGHT_REASONS = Object.keys(SPOTLIGHT_KINDS);
18
- export function buildSpotlight(items, groups, cursor, limit, seenAt) {
19
+ export const buildSpotlight = (items, groups, cursor, limit, now, seenAt, splitBursts) => {
19
20
  // @NOTE Try one spotlight at a time, keeping the last page where all spotlights pass validation.
20
21
  let lastGoodPage = { groups, cursor };
21
22
  const itemCount = groups.reduce((count, group) => count + group.items.length, 0);
@@ -24,8 +25,10 @@ export function buildSpotlight(items, groups, cursor, limit, seenAt) {
24
25
  // itemCount counts notifications included in the ordinary groups, and pageItems contains the notifications on the page.
25
26
  const pageItems = items.slice(0, itemCount);
26
27
  const positions = new Map(pageItems.map((item, index) => [item, index]));
27
- // @NOTE Rank each reason's top actor by full count, then recency, before capping spotlight size.
28
- const candidates = mapDefined(SPOTLIGHT_REASONS, (reason) => selectSpotlight(pageItems, reason))
28
+ const subjectCollections = new Map();
29
+ // @NOTE `candidates` are at most 1 candidate spotlight (the best one, with most items, then more recent) per reason.
30
+ // E.g.: Alice's likes, Bob's reposts, Bob's likes via reposts.
31
+ const candidates = mapDefined(SPOTLIGHT_REASONS, (reason) => selectSpotlight(pageItems, reason, now, splitBursts, subjectCollections))
29
32
  .sort((left, right) => right.items.length - left.items.length ||
30
33
  positions.get(left.items[0]) - positions.get(right.items[0]))
31
34
  .map((candidate) => ({
@@ -74,7 +77,8 @@ export function buildSpotlight(items, groups, cursor, limit, seenAt) {
74
77
  const cutoff = positions.get(extraGroup.items[0]);
75
78
  retainedItems = lastGoodItems.slice(0, cutoff);
76
79
  nextCursor = pageItems[cutoff - 1].raw.indexedAt;
77
- // @NOTE Trimming every group at one raw position preserves the page prefix.
80
+ // @NOTE Every returned page must represent a contiguous prefix of raw notifications,
81
+ // regardless of how spotlights regroup them.
78
82
  for (const group of proposedGroups) {
79
83
  group.items = group.items.filter((item) => positions.get(item) < cutoff);
80
84
  group.actorDids = group.items.map((item) => item.actorDid);
@@ -85,8 +89,10 @@ export function buildSpotlight(items, groups, cursor, limit, seenAt) {
85
89
  }
86
90
  }
87
91
  const retained = new Set(retainedItems);
88
- // @NOTE 3. Reject the attempt if any spotlight fails, preserving the previously accepted page and cursor.
89
- const allEligible = [...acceptedSpotlights, candidate].every((spotlight) => spotlight.isEligibleAfterTrimming(spotlight.items.filter((item) => retained.has(item)), retainedItems));
92
+ // @NOTE 3. Reject adding this spotlight if trimming leaves it or an already accepted spotlight
93
+ // below its minimum size; keep the previous page and cursor.
94
+ const allEligible = [...acceptedSpotlights, candidate].every((spotlight) => spotlight.items.filter((item) => retained.has(item)).length >=
95
+ spotlight.requiredPosts);
90
96
  if (!allEligible)
91
97
  continue;
92
98
  // @NOTE 4. Save the valid page, cursor, and remaining notifications for the next attempt.
@@ -98,14 +104,14 @@ export function buildSpotlight(items, groups, cursor, limit, seenAt) {
98
104
  acceptedSpotlights.push(candidate);
99
105
  }
100
106
  return lastGoodPage;
101
- }
102
- function selectSpotlight(items, reason) {
107
+ };
108
+ const selectSpotlight = (items, reason, now, splitBursts, subjectCollections) => {
103
109
  const range = MULTI_POST_RANGES.find(({ maxItems }) => items.length <= maxItems);
104
110
  if (!range)
105
111
  return;
106
112
  // @NOTE Eligibility uses the ordinary page's volume, including after trimming.
107
113
  const { requiredPosts } = range;
108
- const topItems = getNotificationsByTopActor(items, reason);
114
+ const topItems = getTopBurst(items, reason, now, splitBursts, subjectCollections);
109
115
  if (topItems.length < requiredPosts)
110
116
  return;
111
117
  const actorDid = topItems[0].actorDid;
@@ -113,36 +119,43 @@ function selectSpotlight(items, reason) {
113
119
  kind: SPOTLIGHT_KINDS[reason],
114
120
  groupingKey: actorDid,
115
121
  items: topItems,
116
- isEligibleAfterTrimming: (spotlightItems, retainedItems) =>
117
- // @NOTE Check if after trimming it still satisfies the requirements.
118
- spotlightItems.length >= requiredPosts &&
119
- getNotificationsByTopActor(retainedItems, reason)[0]?.actorDid ===
120
- actorDid,
122
+ requiredPosts,
121
123
  };
122
- }
123
- function getNotificationsByTopActor(items, reason) {
124
+ };
125
+ const getTopBurst = (items, reason, now, splitBursts, subjectCollections) => {
124
126
  const collection = reason === NOTIFICATION_REASON.LIKE_VIA_REPOST ||
125
127
  reason === NOTIFICATION_REASON.REPOST_VIA_REPOST
126
128
  ? app.bsky.feed.repost.$type
127
129
  : app.bsky.feed.post.$type;
128
130
  const itemsByActor = new Map();
129
131
  for (const item of items) {
130
- if (item.raw.reason !== reason ||
131
- new AtUri(item.raw.reasonSubject).collection !== collection) {
132
+ if (item.raw.reason !== reason)
132
133
  continue;
134
+ const subjectUri = item.raw.reasonSubject;
135
+ let subjectCollection = subjectCollections.get(subjectUri);
136
+ if (subjectCollection === undefined) {
137
+ subjectCollection = new AtUri(subjectUri).collection;
138
+ subjectCollections.set(subjectUri, subjectCollection);
133
139
  }
140
+ if (subjectCollection !== collection)
141
+ continue;
134
142
  const actorItems = itemsByActor.get(item.actorDid);
135
143
  if (actorItems)
136
144
  actorItems.push(item);
137
145
  else
138
146
  itemsByActor.set(item.actorDid, [item]);
139
147
  }
140
- // @NOTE Each actor likes or reposts a post once per reason; insertion order breaks ties by newest notification.
148
+ // @NOTE Each actor likes or reposts a post once per reason.
141
149
  let top = [];
142
150
  for (const actorItems of itemsByActor.values()) {
143
- if (actorItems.length > top.length)
144
- top = actorItems;
151
+ for (const burst of splitBursts(actorItems, now, TIME_SPAN_MULTIPLIER)) {
152
+ if (burst.length > top.length ||
153
+ (burst.length === top.length &&
154
+ burst[0].indexedTime > top[0].indexedTime)) {
155
+ top = burst;
156
+ }
157
+ }
145
158
  }
146
159
  return top;
147
- }
160
+ };
148
161
  //# sourceMappingURL=spotlight.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"spotlight.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/spotlight.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { DidString } from '@atproto/lex'\nimport { AtUri } from '@atproto/syntax'\nimport { app } from '../../../../../lexicons/index.js'\nimport {\n APPVIEW_NOTIFICATION_REASON,\n NOTIFICATION_REASON,\n} from '../constants.js'\nimport type {\n GroupingResult,\n NotificationGroup,\n NotificationItem,\n} from './grouping.js'\nimport {\n MAX_GROUP_SIZE,\n compareNotificationGroupsNewestFirst,\n isNotificationRead,\n} from './shared.js'\n\nconst MAX_SPOTLIGHTS = 3\nconst MULTI_POST_RANGES = [\n { maxItems: 200, requiredPosts: 4 },\n { maxItems: 500, requiredPosts: 8 },\n]\nconst SPOTLIGHT_KINDS = {\n [NOTIFICATION_REASON.LIKE]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE,\n [NOTIFICATION_REASON.REPOST]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST,\n [NOTIFICATION_REASON.LIKE_VIA_REPOST]:\n APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE_VIA_REPOST,\n [NOTIFICATION_REASON.REPOST_VIA_REPOST]:\n APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST_VIA_REPOST,\n} as const\ntype SpotlightReason = keyof typeof SPOTLIGHT_KINDS\nconst SPOTLIGHT_REASONS = Object.keys(SPOTLIGHT_KINDS) as SpotlightReason[]\n\ntype SpotlightCandidate = Pick<\n NotificationGroup,\n 'kind' | 'groupingKey' | 'items'\n> & {\n isEligibleAfterTrimming: (\n spotlightItems: NotificationItem[],\n retainedItems: NotificationItem[],\n ) => boolean\n}\n\nexport function buildSpotlight(\n items: NotificationItem[],\n groups: NotificationGroup[],\n cursor: string | undefined,\n limit: number,\n seenAt: number | undefined,\n): GroupingResult {\n // @NOTE Try one spotlight at a time, keeping the last page where all spotlights pass validation.\n let lastGoodPage: GroupingResult = { groups, cursor }\n const itemCount = groups.reduce(\n (count, group) => count + group.items.length,\n 0,\n )\n // @NOTE Grouping consumes a continuous raw prefix, even when groups interleave.\n // items contains all fetched notifications, but grouping may stop before consuming them all;\n // itemCount counts notifications included in the ordinary groups, and pageItems contains the notifications on the page.\n const pageItems = items.slice(0, itemCount)\n const positions = new Map(pageItems.map((item, index) => [item, index]))\n // @NOTE Rank each reason's top actor by full count, then recency, before capping spotlight size.\n const candidates = mapDefined(SPOTLIGHT_REASONS, (reason) =>\n selectSpotlight(pageItems, reason),\n )\n .sort(\n (left, right) =>\n right.items.length - left.items.length ||\n positions.get(left.items[0]!)! - positions.get(right.items[0]!)!,\n )\n .map((candidate) => ({\n ...candidate,\n items: candidate.items.slice(0, MAX_GROUP_SIZE),\n }))\n\n let lastGoodItems = pageItems\n const acceptedSpotlights: SpotlightCandidate[] = []\n for (const candidate of candidates) {\n if (acceptedSpotlights.length === MAX_SPOTLIGHTS) break\n // @NOTE 1. Tentatively add this spotlight using only notifications retained on the last accepted page.\n const candidateItems = candidate.items.filter(\n (item) => positions.get(item)! < lastGoodItems.length,\n )\n const selectedItems = new Set(candidateItems)\n const proposedGroups = mapDefined(\n [\n ...lastGoodPage.groups.map((group) => ({\n // @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n kind: group.kind,\n groupingKey: group.groupingKey,\n items: group.items.filter((item) => !selectedItems.has(item)),\n })),\n { ...candidate, items: candidateItems },\n ],\n (group): NotificationGroup | undefined => {\n const newest = group.items[0]\n const oldest = group.items.at(-1)\n if (!newest || !oldest) return\n\n // @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n return {\n kind: group.kind,\n groupingKey: group.groupingKey,\n id: newest.id,\n items: group.items,\n actorDids: group.items.map((item) => item.actorDid),\n itemCount: group.items.length,\n indexedAt: newest.raw.indexedAt,\n firstIndexedAt: oldest.raw.indexedAt,\n isRead: isNotificationRead(newest.raw.indexedAt, seenAt),\n }\n },\n ).sort(compareNotificationGroupsNewestFirst)\n\n // @NOTE 2. Trim the tentative page to the group limit without changing the saved state.\n let retainedItems = lastGoodItems\n let nextCursor = lastGoodPage.cursor\n const extraGroup = proposedGroups[limit]\n if (extraGroup) {\n const cutoff = positions.get(extraGroup.items[0]!)!\n retainedItems = lastGoodItems.slice(0, cutoff)\n nextCursor = pageItems[cutoff - 1]!.raw.indexedAt\n // @NOTE Trimming every group at one raw position preserves the page prefix.\n for (const group of proposedGroups) {\n group.items = group.items.filter(\n (item) => positions.get(item)! < cutoff,\n )\n group.actorDids = group.items.map((item) => item.actorDid)\n group.itemCount = group.items.length\n const oldest = group.items.at(-1)\n if (oldest) group.firstIndexedAt = oldest.raw.indexedAt\n }\n }\n\n const retained = new Set(retainedItems)\n // @NOTE 3. Reject the attempt if any spotlight fails, preserving the previously accepted page and cursor.\n const allEligible = [...acceptedSpotlights, candidate].every((spotlight) =>\n spotlight.isEligibleAfterTrimming(\n spotlight.items.filter((item) => retained.has(item)),\n retainedItems,\n ),\n )\n if (!allEligible) continue\n\n // @NOTE 4. Save the valid page, cursor, and remaining notifications for the next attempt.\n lastGoodPage = {\n cursor: nextCursor,\n groups: proposedGroups.filter((group) => group.items.length > 0),\n }\n lastGoodItems = retainedItems\n acceptedSpotlights.push(candidate)\n }\n\n return lastGoodPage\n}\n\nfunction selectSpotlight(\n items: NotificationItem[],\n reason: SpotlightReason,\n): SpotlightCandidate | undefined {\n const range = MULTI_POST_RANGES.find(\n ({ maxItems }) => items.length <= maxItems,\n )\n if (!range) return\n\n // @NOTE Eligibility uses the ordinary page's volume, including after trimming.\n const { requiredPosts } = range\n const topItems = getNotificationsByTopActor(items, reason)\n if (topItems.length < requiredPosts) return\n const actorDid = topItems[0]!.actorDid\n return {\n kind: SPOTLIGHT_KINDS[reason],\n groupingKey: actorDid,\n items: topItems,\n isEligibleAfterTrimming: (spotlightItems, retainedItems) =>\n // @NOTE Check if after trimming it still satisfies the requirements.\n spotlightItems.length >= requiredPosts &&\n getNotificationsByTopActor(retainedItems, reason)[0]?.actorDid ===\n actorDid,\n }\n}\n\nfunction getNotificationsByTopActor(\n items: NotificationItem[],\n reason: SpotlightReason,\n): NotificationItem[] {\n const collection =\n reason === NOTIFICATION_REASON.LIKE_VIA_REPOST ||\n reason === NOTIFICATION_REASON.REPOST_VIA_REPOST\n ? app.bsky.feed.repost.$type\n : app.bsky.feed.post.$type\n const itemsByActor = new Map<DidString, NotificationItem[]>()\n for (const item of items) {\n if (\n item.raw.reason !== reason ||\n new AtUri(item.raw.reasonSubject).collection !== collection\n ) {\n continue\n }\n const actorItems = itemsByActor.get(item.actorDid)\n if (actorItems) actorItems.push(item)\n else itemsByActor.set(item.actorDid, [item])\n }\n\n // @NOTE Each actor likes or reposts a post once per reason; insertion order breaks ties by newest notification.\n let top: NotificationItem[] = []\n for (const actorItems of itemsByActor.values()) {\n if (actorItems.length > top.length) top = actorItems\n }\n return top\n}\n"]}
1
+ {"version":3,"file":"spotlight.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/spotlight.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { AtUriString, DidString } from '@atproto/lex'\nimport { AtUri } from '@atproto/syntax'\nimport { app } from '../../../../../lexicons/index.js'\nimport {\n APPVIEW_NOTIFICATION_REASON,\n NOTIFICATION_REASON,\n} from '../constants.js'\nimport type {\n BurstSplitter,\n GroupingResult,\n NotificationGroup,\n NotificationItem,\n} from './grouping.js'\nimport {\n MAX_GROUP_SIZE,\n compareNotificationGroupsNewestFirst,\n isNotificationRead,\n} from './shared.js'\n\nconst MAX_SPOTLIGHTS = 3\nconst TIME_SPAN_MULTIPLIER = 2\nconst MULTI_POST_RANGES = [\n { maxItems: 200, requiredPosts: 4 },\n { maxItems: 500, requiredPosts: 8 },\n]\nconst SPOTLIGHT_KINDS = {\n [NOTIFICATION_REASON.LIKE]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE,\n [NOTIFICATION_REASON.REPOST]: APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST,\n [NOTIFICATION_REASON.LIKE_VIA_REPOST]:\n APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE_VIA_REPOST,\n [NOTIFICATION_REASON.REPOST_VIA_REPOST]:\n APPVIEW_NOTIFICATION_REASON.MULTI_POST_REPOST_VIA_REPOST,\n} as const\ntype SpotlightReason = keyof typeof SPOTLIGHT_KINDS\nconst SPOTLIGHT_REASONS = Object.keys(SPOTLIGHT_KINDS) as SpotlightReason[]\n\ntype SpotlightCandidate = Pick<\n NotificationGroup,\n 'kind' | 'groupingKey' | 'items'\n> & {\n requiredPosts: number\n}\n\nexport const buildSpotlight = (\n items: NotificationItem[],\n groups: NotificationGroup[],\n cursor: string | undefined,\n limit: number,\n now: number,\n seenAt: number | undefined,\n splitBursts: BurstSplitter,\n): GroupingResult => {\n // @NOTE Try one spotlight at a time, keeping the last page where all spotlights pass validation.\n let lastGoodPage: GroupingResult = { groups, cursor }\n const itemCount = groups.reduce(\n (count, group) => count + group.items.length,\n 0,\n )\n // @NOTE Grouping consumes a continuous raw prefix, even when groups interleave.\n // items contains all fetched notifications, but grouping may stop before consuming them all;\n // itemCount counts notifications included in the ordinary groups, and pageItems contains the notifications on the page.\n const pageItems = items.slice(0, itemCount)\n const positions = new Map(pageItems.map((item, index) => [item, index]))\n const subjectCollections = new Map<AtUriString, string>()\n\n // @NOTE `candidates` are at most 1 candidate spotlight (the best one, with most items, then more recent) per reason.\n // E.g.: Alice's likes, Bob's reposts, Bob's likes via reposts.\n const candidates = mapDefined(SPOTLIGHT_REASONS, (reason) =>\n selectSpotlight(pageItems, reason, now, splitBursts, subjectCollections),\n )\n .sort(\n (left, right) =>\n right.items.length - left.items.length ||\n positions.get(left.items[0]!)! - positions.get(right.items[0]!)!,\n )\n .map((candidate) => ({\n ...candidate,\n items: candidate.items.slice(0, MAX_GROUP_SIZE),\n }))\n\n let lastGoodItems = pageItems\n const acceptedSpotlights: SpotlightCandidate[] = []\n for (const candidate of candidates) {\n if (acceptedSpotlights.length === MAX_SPOTLIGHTS) break\n // @NOTE 1. Tentatively add this spotlight using only notifications retained on the last accepted page.\n const candidateItems = candidate.items.filter(\n (item) => positions.get(item)! < lastGoodItems.length,\n )\n const selectedItems = new Set(candidateItems)\n const proposedGroups = mapDefined(\n [\n ...lastGoodPage.groups.map((group) => ({\n // @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n kind: group.kind,\n groupingKey: group.groupingKey,\n items: group.items.filter((item) => !selectedItems.has(item)),\n })),\n { ...candidate, items: candidateItems },\n ],\n (group): NotificationGroup | undefined => {\n const newest = group.items[0]\n const oldest = group.items.at(-1)\n if (!newest || !oldest) return\n\n // @NOTE Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n return {\n kind: group.kind,\n groupingKey: group.groupingKey,\n id: newest.id,\n items: group.items,\n actorDids: group.items.map((item) => item.actorDid),\n itemCount: group.items.length,\n indexedAt: newest.raw.indexedAt,\n firstIndexedAt: oldest.raw.indexedAt,\n isRead: isNotificationRead(newest.raw.indexedAt, seenAt),\n }\n },\n ).sort(compareNotificationGroupsNewestFirst)\n\n // @NOTE 2. Trim the tentative page to the group limit without changing the saved state.\n let retainedItems = lastGoodItems\n let nextCursor = lastGoodPage.cursor\n const extraGroup = proposedGroups[limit]\n if (extraGroup) {\n const cutoff = positions.get(extraGroup.items[0]!)!\n retainedItems = lastGoodItems.slice(0, cutoff)\n nextCursor = pageItems[cutoff - 1]!.raw.indexedAt\n // @NOTE Every returned page must represent a contiguous prefix of raw notifications,\n // regardless of how spotlights regroup them.\n for (const group of proposedGroups) {\n group.items = group.items.filter(\n (item) => positions.get(item)! < cutoff,\n )\n group.actorDids = group.items.map((item) => item.actorDid)\n group.itemCount = group.items.length\n const oldest = group.items.at(-1)\n if (oldest) group.firstIndexedAt = oldest.raw.indexedAt\n }\n }\n\n const retained = new Set(retainedItems)\n // @NOTE 3. Reject adding this spotlight if trimming leaves it or an already accepted spotlight\n // below its minimum size; keep the previous page and cursor.\n const allEligible = [...acceptedSpotlights, candidate].every(\n (spotlight) =>\n spotlight.items.filter((item) => retained.has(item)).length >=\n spotlight.requiredPosts,\n )\n if (!allEligible) continue\n\n // @NOTE 4. Save the valid page, cursor, and remaining notifications for the next attempt.\n lastGoodPage = {\n cursor: nextCursor,\n groups: proposedGroups.filter((group) => group.items.length > 0),\n }\n lastGoodItems = retainedItems\n acceptedSpotlights.push(candidate)\n }\n\n return lastGoodPage\n}\n\nconst selectSpotlight = (\n items: NotificationItem[],\n reason: SpotlightReason,\n now: number,\n splitBursts: BurstSplitter,\n subjectCollections: Map<AtUriString, string>,\n): SpotlightCandidate | undefined => {\n const range = MULTI_POST_RANGES.find(\n ({ maxItems }) => items.length <= maxItems,\n )\n if (!range) return\n\n // @NOTE Eligibility uses the ordinary page's volume, including after trimming.\n const { requiredPosts } = range\n const topItems = getTopBurst(\n items,\n reason,\n now,\n splitBursts,\n subjectCollections,\n )\n if (topItems.length < requiredPosts) return\n const actorDid = topItems[0]!.actorDid\n return {\n kind: SPOTLIGHT_KINDS[reason],\n groupingKey: actorDid,\n items: topItems,\n requiredPosts,\n }\n}\n\nconst getTopBurst = (\n items: NotificationItem[],\n reason: SpotlightReason,\n now: number,\n splitBursts: BurstSplitter,\n subjectCollections: Map<AtUriString, string>,\n): NotificationItem[] => {\n const collection =\n reason === NOTIFICATION_REASON.LIKE_VIA_REPOST ||\n reason === NOTIFICATION_REASON.REPOST_VIA_REPOST\n ? app.bsky.feed.repost.$type\n : app.bsky.feed.post.$type\n const itemsByActor = new Map<DidString, NotificationItem[]>()\n for (const item of items) {\n if (item.raw.reason !== reason) continue\n const subjectUri = item.raw.reasonSubject\n let subjectCollection = subjectCollections.get(subjectUri)\n if (subjectCollection === undefined) {\n subjectCollection = new AtUri(subjectUri).collection\n subjectCollections.set(subjectUri, subjectCollection)\n }\n if (subjectCollection !== collection) continue\n const actorItems = itemsByActor.get(item.actorDid)\n if (actorItems) actorItems.push(item)\n else itemsByActor.set(item.actorDid, [item])\n }\n\n // @NOTE Each actor likes or reposts a post once per reason.\n let top: NotificationItem[] = []\n for (const actorItems of itemsByActor.values()) {\n for (const burst of splitBursts(actorItems, now, TIME_SPAN_MULTIPLIER)) {\n if (\n burst.length > top.length ||\n (burst.length === top.length &&\n burst[0]!.indexedTime > top[0]!.indexedTime)\n ) {\n top = burst\n }\n }\n }\n return top\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"listActivitySubscriptions.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listActivitySubscriptions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAgBxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QAmCvD"}
1
+ {"version":3,"file":"listActivitySubscriptions.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listActivitySubscriptions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAgBxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QAoCvD"}
@@ -10,6 +10,7 @@ export default function (server, ctx) {
10
10
  const viewer = auth.credentials.iss;
11
11
  const labelers = ctx.reqLabelers(req);
12
12
  const hydrateCtx = await ctx.hydrator.createContext({
13
+ xrpcEndpoint: app.bsky.notification.listActivitySubscriptions.$lxm,
13
14
  labelers,
14
15
  viewer,
15
16
  });
@@ -1 +1 @@
1
- {"version":3,"file":"listActivitySubscriptions.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listActivitySubscriptions.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { DidString } from '@atproto/syntax'\nimport type { Server } from '@atproto/xrpc-server'\nimport type { AppContext } from '../../../../context.js'\nimport type {\n HydrateCtxWithViewer,\n Hydrator,\n} from '../../../../hydration/hydrator.js'\nimport { app } from '../../../../lexicons/index.js'\nimport {\n type HydrationFnInput,\n type PresentationFnInput,\n type RulesFnInput,\n type SkeletonFnInput,\n createPipeline,\n} from '../../../../pipeline.js'\nimport type { Views } from '../../../../views/index.js'\nimport { clearlyBadCursor, fillPage, resHeaders } from '../../../util.js'\n\nexport default function (server: Server, ctx: AppContext) {\n const listActivitySubscriptions = createPipeline(\n skeleton,\n hydration,\n noBlocks,\n presentation,\n )\n server.add(app.bsky.notification.listActivitySubscriptions, {\n auth: ctx.authVerifier.standard,\n handler: async ({ params, auth, req }) => {\n const viewer = auth.credentials.iss\n const labelers = ctx.reqLabelers(req)\n const hydrateCtx = await ctx.hydrator.createContext({\n labelers,\n viewer,\n })\n\n const result = await fillPage({\n cursor: params.cursor,\n limit: params.limit,\n fetch: ({ cursor, limit }) =>\n listActivitySubscriptions(\n { ...params, cursor, limit, hydrateCtx },\n ctx,\n ),\n items: (r) => r.subscriptions,\n })\n\n return {\n encoding: 'application/json',\n body: result,\n headers: resHeaders({ labelers: hydrateCtx.labelers }),\n }\n },\n })\n}\n\nconst skeleton = async (\n input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n const { params, ctx } = input\n const actorDid = params.hydrateCtx.viewer\n if (clearlyBadCursor(params.cursor)) {\n return { actorDid, dids: [] }\n }\n const { dids, cursor } =\n await ctx.hydrator.dataplane.getActivitySubscriptionDids({\n actorDid: params.hydrateCtx.viewer,\n limit: params.limit,\n cursor: params.cursor,\n })\n return {\n actorDid,\n dids: dids as DidString[],\n cursor,\n }\n}\n\nconst hydration = async (\n input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n const { ctx, params, skeleton } = input\n const { dids } = skeleton\n return ctx.hydrator.hydrateProfiles(dids, params.hydrateCtx, {\n activitySubscriptions: true,\n })\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n const { skeleton, hydration, ctx } = input\n skeleton.dids = skeleton.dids.filter(\n (did) => !ctx.views.viewerBlockExists(did, hydration),\n )\n return skeleton\n}\n\nconst presentation = (\n input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n const { ctx, hydration, skeleton } = input\n const { dids, cursor } = skeleton\n const subscriptions = mapDefined(dids, (did) => {\n return ctx.views.profile(did, hydration)\n })\n return { subscriptions, cursor }\n}\n\ntype Context = {\n hydrator: Hydrator\n views: Views\n}\n\ntype Params = app.bsky.notification.listActivitySubscriptions.$Params & {\n hydrateCtx: HydrateCtxWithViewer\n}\n\ntype SkeletonState = {\n actorDid: DidString\n dids: DidString[]\n cursor?: string\n}\n"]}
1
+ {"version":3,"file":"listActivitySubscriptions.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listActivitySubscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAQ5C,OAAO,EAAE,GAAG,EAAE,MAAM,+BAA+B,CAAA;AACnD,OAAO,EAKL,cAAc,GACf,MAAM,yBAAyB,CAAA;AAEhC,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAEzE,MAAM,CAAC,OAAO,WAAW,MAAc,EAAE,GAAe;IACtD,MAAM,yBAAyB,GAAG,cAAc,CAC9C,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,YAAY,CACb,CAAA;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,yBAAyB,EAAE;QAC1D,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,QAAQ;QAC/B,OAAO,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;YACvC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAA;YACnC,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;YACrC,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC;gBAClD,YAAY,EAAE,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,yBAAyB,CAAC,IAAI;gBAClE,QAAQ;gBACR,MAAM;aACP,CAAC,CAAA;YAEF,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC;gBAC5B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAC3B,yBAAyB,CACvB,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,EACxC,GAAG,CACJ;gBACH,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa;aAC9B,CAAC,CAAA;YAEF,OAAO;gBACL,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC;aACvD,CAAA;QACH,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,QAAQ,GAAG,KAAK,EACpB,KAAuC,EACf,EAAE;IAC1B,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,CAAA;IAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAA;IACzC,IAAI,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;IAC/B,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GACpB,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,2BAA2B,CAAC;QACvD,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM;QAClC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC,CAAA;IACJ,OAAO;QACL,QAAQ;QACR,IAAI,EAAE,IAAmB;QACzB,MAAM;KACP,CAAA;AACH,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,KAAK,EACrB,KAAuD,EACvD,EAAE;IACF,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IACvC,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAA;IACzB,OAAO,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE;QAC3D,qBAAqB,EAAE,IAAI;KAC5B,CAAC,CAAA;AACJ,CAAC,CAAA;AAED,MAAM,QAAQ,GAAG,CAAC,KAAmD,EAAE,EAAE;IACvE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,KAAK,CAAA;IAC1C,QAAQ,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAClC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,EAAE,SAAS,CAAC,CACtD,CAAA;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,CACnB,KAA0D,EAC1D,EAAE;IACF,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IAC1C,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAA;IACjC,MAAM,aAAa,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE;QAC7C,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IACF,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,CAAA;AAClC,CAAC,CAAA","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { DidString } from '@atproto/syntax'\nimport type { Server } from '@atproto/xrpc-server'\nimport type { AppContext } from '../../../../context.js'\nimport type {\n HydrateCtxWithViewer,\n Hydrator,\n} from '../../../../hydration/hydrator.js'\nimport { app } from '../../../../lexicons/index.js'\nimport {\n type HydrationFnInput,\n type PresentationFnInput,\n type RulesFnInput,\n type SkeletonFnInput,\n createPipeline,\n} from '../../../../pipeline.js'\nimport type { Views } from '../../../../views/index.js'\nimport { clearlyBadCursor, fillPage, resHeaders } from '../../../util.js'\n\nexport default function (server: Server, ctx: AppContext) {\n const listActivitySubscriptions = createPipeline(\n skeleton,\n hydration,\n noBlocks,\n presentation,\n )\n server.add(app.bsky.notification.listActivitySubscriptions, {\n auth: ctx.authVerifier.standard,\n handler: async ({ params, auth, req }) => {\n const viewer = auth.credentials.iss\n const labelers = ctx.reqLabelers(req)\n const hydrateCtx = await ctx.hydrator.createContext({\n xrpcEndpoint: app.bsky.notification.listActivitySubscriptions.$lxm,\n labelers,\n viewer,\n })\n\n const result = await fillPage({\n cursor: params.cursor,\n limit: params.limit,\n fetch: ({ cursor, limit }) =>\n listActivitySubscriptions(\n { ...params, cursor, limit, hydrateCtx },\n ctx,\n ),\n items: (r) => r.subscriptions,\n })\n\n return {\n encoding: 'application/json',\n body: result,\n headers: resHeaders({ labelers: hydrateCtx.labelers }),\n }\n },\n })\n}\n\nconst skeleton = async (\n input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n const { params, ctx } = input\n const actorDid = params.hydrateCtx.viewer\n if (clearlyBadCursor(params.cursor)) {\n return { actorDid, dids: [] }\n }\n const { dids, cursor } =\n await ctx.hydrator.dataplane.getActivitySubscriptionDids({\n actorDid: params.hydrateCtx.viewer,\n limit: params.limit,\n cursor: params.cursor,\n })\n return {\n actorDid,\n dids: dids as DidString[],\n cursor,\n }\n}\n\nconst hydration = async (\n input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n const { ctx, params, skeleton } = input\n const { dids } = skeleton\n return ctx.hydrator.hydrateProfiles(dids, params.hydrateCtx, {\n activitySubscriptions: true,\n })\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n const { skeleton, hydration, ctx } = input\n skeleton.dids = skeleton.dids.filter(\n (did) => !ctx.views.viewerBlockExists(did, hydration),\n )\n return skeleton\n}\n\nconst presentation = (\n input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n const { ctx, hydration, skeleton } = input\n const { dids, cursor } = skeleton\n const subscriptions = mapDefined(dids, (did) => {\n return ctx.views.profile(did, hydration)\n })\n return { subscriptions, cursor }\n}\n\ntype Context = {\n hydrator: Hydrator\n views: Views\n}\n\ntype Params = app.bsky.notification.listActivitySubscriptions.$Params & {\n hydrateCtx: HydrateCtxWithViewer\n}\n\ntype SkeletonState = {\n actorDid: DidString\n dids: DidString[]\n cursor?: string\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"listNotifications.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listNotifications.ts"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAEvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAwBxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QAuCvD"}
1
+ {"version":3,"file":"listNotifications.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listNotifications.ts"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAEvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAwBxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QA2CvD"}
@@ -15,7 +15,11 @@ export default function (server, ctx) {
15
15
  }
16
16
  const viewer = auth.credentials.iss;
17
17
  const labelers = ctx.reqLabelers(req);
18
- const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer });
18
+ const hydrateCtx = await ctx.hydrator.createContext({
19
+ labelers,
20
+ viewer,
21
+ xrpcEndpoint: app.bsky.notification.listNotifications.$lxm,
22
+ });
19
23
  const lastSeenRes = await ctx.hydrator.dataplane.getNotificationSeen({
20
24
  actorDid: viewer,
21
25
  });
@@ -1 +1 @@
1
- {"version":3,"file":"listNotifications.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listNotifications.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { AtUriString, DatetimeString } from '@atproto/syntax'\nimport { InvalidRequestError, type Server } from '@atproto/xrpc-server'\nimport type { ServerConfig } from '../../../../config.js'\nimport type { AppContext } from '../../../../context.js'\nimport type {\n HydrateCtxWithViewer,\n Hydrator,\n} from '../../../../hydration/hydrator.js'\nimport { app } from '../../../../lexicons/index.js'\nimport {\n type HydrationFnInput,\n type PresentationFnInput,\n type RulesFnInput,\n type SkeletonFnInput,\n createPipeline,\n} from '../../../../pipeline.js'\nimport type { Notification } from '../../../../proto/bsky_pb.js'\nimport { uriToDid as didFromUri } from '../../../../util/uris.js'\nimport type { Views } from '../../../../views/index.js'\nimport { fillPage, resHeaders } from '../../../util.js'\nimport {\n delayCursor,\n shouldFilterForNeedsReview,\n shouldFilterHiddenThreadTag,\n shouldFilterReplyByThreadgate,\n} from './util.js'\n\nexport default function (server: Server, ctx: AppContext) {\n const listNotifications = createPipeline(\n skeleton,\n hydration,\n noBlockOrMutesOrNeedsFiltering,\n presentation,\n )\n server.add(app.bsky.notification.listNotifications, {\n auth: ctx.authVerifier.standard,\n handler: async ({ params, auth, req }) => {\n if (params.seenAt) {\n throw new InvalidRequestError('The seenAt parameter is unsupported')\n }\n const viewer = auth.credentials.iss\n const labelers = ctx.reqLabelers(req)\n const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n\n const lastSeenRes = await ctx.hydrator.dataplane.getNotificationSeen({\n actorDid: viewer,\n })\n const lastSeen = lastSeenRes.timestamp?.toDate()\n\n const result = await fillPage({\n cursor: params.cursor,\n limit: params.limit,\n fetch: ({ cursor, limit }) =>\n listNotifications(\n { ...params, cursor, limit, hydrateCtx, lastSeen },\n ctx,\n ),\n items: (r) => r.notifications,\n })\n return {\n encoding: 'application/json',\n body: result,\n headers: resHeaders({ labelers: hydrateCtx.labelers }),\n }\n },\n })\n}\n\nconst paginateNotifications = async (opts: {\n ctx: Context\n reasons?: string[]\n cursor?: string\n limit: number\n viewer: string\n}) => {\n const { ctx, reasons, limit, viewer } = opts\n\n const res = await ctx.hydrator.dataplane.getNotifications({\n actorDid: viewer,\n cursor: opts.cursor,\n limit,\n })\n return {\n notifications: reasons\n ? res.notifications.filter((notif) => reasons.includes(notif.reason))\n : res.notifications,\n cursor: res.cursor,\n }\n}\n\nconst skeleton = async (\n input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n const { params, ctx } = input\n const originalCursor = params.cursor\n const delayedCursor = delayCursor(\n originalCursor,\n ctx.cfg.notificationsDelayMs,\n )\n const viewer = params.hydrateCtx.viewer\n const res = await paginateNotifications({\n ctx,\n reasons: params.reasons,\n cursor: delayedCursor,\n limit: params.limit,\n viewer,\n })\n // @NOTE for the first page of results if there's no last-seen time, consider top notification unread\n // rather than all notifications. bit of a hack to be more graceful when seen times are out of sync.\n let lastSeenDate = params.lastSeen\n if (!lastSeenDate && !originalCursor) {\n lastSeenDate = res.notifications.at(0)?.timestamp?.toDate()\n }\n return {\n notifs: res.notifications,\n cursor: res.cursor,\n lastSeenNotifs: lastSeenDate\n ? (lastSeenDate.toISOString() as DatetimeString)\n : undefined,\n }\n}\n\nconst hydration = async (\n input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n const { skeleton, params, ctx } = input\n return ctx.hydrator.hydrateNotifications(skeleton.notifs, params.hydrateCtx)\n}\n\nconst noBlockOrMutesOrNeedsFiltering = (\n input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n const { skeleton, hydration, ctx, params } = input\n skeleton.notifs = skeleton.notifs.filter((item) => {\n const uri = item.uri as AtUriString\n const did = didFromUri(uri)\n if (\n ctx.views.viewerBlockExists(did, hydration) ||\n ctx.views.viewerMuteExists(did, hydration)\n ) {\n return false\n }\n\n if (\n shouldFilterReplyByThreadgate(\n item.reason,\n uri,\n params.hydrateCtx.viewer,\n hydration,\n ctx.views,\n )\n ) {\n return false\n }\n\n if (\n shouldFilterHiddenThreadTag(\n item.reason,\n uri,\n did,\n hydration,\n ctx.cfg.threadTagsHide,\n )\n ) {\n return false\n }\n\n if (\n shouldFilterForNeedsReview(item.reason, did, uri, hydration, ctx.views)\n ) {\n return false\n }\n\n return true\n })\n return skeleton\n}\n\nconst presentation = (\n input: PresentationFnInput<Context, Params, SkeletonState>,\n): app.bsky.notification.listNotifications.$OutputBody => {\n const { skeleton, hydration, ctx } = input\n const { notifs, lastSeenNotifs, cursor } = skeleton\n const notifications = mapDefined(notifs, (notif) =>\n ctx.views.notification(notif, lastSeenNotifs, hydration),\n )\n return {\n notifications,\n cursor,\n seenAt: skeleton.lastSeenNotifs,\n }\n}\n\ntype Context = {\n hydrator: Hydrator\n views: Views\n cfg: ServerConfig\n}\n\ntype Params = app.bsky.notification.listNotifications.$Params & {\n hydrateCtx: HydrateCtxWithViewer\n lastSeen?: Date\n}\n\ntype SkeletonState = {\n notifs: Notification[]\n lastSeenNotifs?: DatetimeString\n cursor?: string\n}\n"]}
1
+ {"version":3,"file":"listNotifications.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/listNotifications.ts"],"names":[],"mappings":"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","sourcesContent":["import { mapDefined } from '@atproto/common'\nimport type { AtUriString, DatetimeString } from '@atproto/syntax'\nimport { InvalidRequestError, type Server } from '@atproto/xrpc-server'\nimport type { ServerConfig } from '../../../../config.js'\nimport type { AppContext } from '../../../../context.js'\nimport type {\n HydrateCtxWithViewer,\n Hydrator,\n} from '../../../../hydration/hydrator.js'\nimport { app } from '../../../../lexicons/index.js'\nimport {\n type HydrationFnInput,\n type PresentationFnInput,\n type RulesFnInput,\n type SkeletonFnInput,\n createPipeline,\n} from '../../../../pipeline.js'\nimport type { Notification } from '../../../../proto/bsky_pb.js'\nimport { uriToDid as didFromUri } from '../../../../util/uris.js'\nimport type { Views } from '../../../../views/index.js'\nimport { fillPage, resHeaders } from '../../../util.js'\nimport {\n delayCursor,\n shouldFilterForNeedsReview,\n shouldFilterHiddenThreadTag,\n shouldFilterReplyByThreadgate,\n} from './util.js'\n\nexport default function (server: Server, ctx: AppContext) {\n const listNotifications = createPipeline(\n skeleton,\n hydration,\n noBlockOrMutesOrNeedsFiltering,\n presentation,\n )\n server.add(app.bsky.notification.listNotifications, {\n auth: ctx.authVerifier.standard,\n handler: async ({ params, auth, req }) => {\n if (params.seenAt) {\n throw new InvalidRequestError('The seenAt parameter is unsupported')\n }\n const viewer = auth.credentials.iss\n const labelers = ctx.reqLabelers(req)\n const hydrateCtx = await ctx.hydrator.createContext({\n labelers,\n viewer,\n xrpcEndpoint: app.bsky.notification.listNotifications.$lxm,\n })\n\n const lastSeenRes = await ctx.hydrator.dataplane.getNotificationSeen({\n actorDid: viewer,\n })\n const lastSeen = lastSeenRes.timestamp?.toDate()\n\n const result = await fillPage({\n cursor: params.cursor,\n limit: params.limit,\n fetch: ({ cursor, limit }) =>\n listNotifications(\n { ...params, cursor, limit, hydrateCtx, lastSeen },\n ctx,\n ),\n items: (r) => r.notifications,\n })\n return {\n encoding: 'application/json',\n body: result,\n headers: resHeaders({ labelers: hydrateCtx.labelers }),\n }\n },\n })\n}\n\nconst paginateNotifications = async (opts: {\n ctx: Context\n reasons?: string[]\n cursor?: string\n limit: number\n viewer: string\n}) => {\n const { ctx, reasons, limit, viewer } = opts\n\n const res = await ctx.hydrator.dataplane.getNotifications({\n actorDid: viewer,\n cursor: opts.cursor,\n limit,\n })\n return {\n notifications: reasons\n ? res.notifications.filter((notif) => reasons.includes(notif.reason))\n : res.notifications,\n cursor: res.cursor,\n }\n}\n\nconst skeleton = async (\n input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n const { params, ctx } = input\n const originalCursor = params.cursor\n const delayedCursor = delayCursor(\n originalCursor,\n ctx.cfg.notificationsDelayMs,\n )\n const viewer = params.hydrateCtx.viewer\n const res = await paginateNotifications({\n ctx,\n reasons: params.reasons,\n cursor: delayedCursor,\n limit: params.limit,\n viewer,\n })\n // @NOTE for the first page of results if there's no last-seen time, consider top notification unread\n // rather than all notifications. bit of a hack to be more graceful when seen times are out of sync.\n let lastSeenDate = params.lastSeen\n if (!lastSeenDate && !originalCursor) {\n lastSeenDate = res.notifications.at(0)?.timestamp?.toDate()\n }\n return {\n notifs: res.notifications,\n cursor: res.cursor,\n lastSeenNotifs: lastSeenDate\n ? (lastSeenDate.toISOString() as DatetimeString)\n : undefined,\n }\n}\n\nconst hydration = async (\n input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n const { skeleton, params, ctx } = input\n return ctx.hydrator.hydrateNotifications(skeleton.notifs, params.hydrateCtx)\n}\n\nconst noBlockOrMutesOrNeedsFiltering = (\n input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n const { skeleton, hydration, ctx, params } = input\n skeleton.notifs = skeleton.notifs.filter((item) => {\n const uri = item.uri as AtUriString\n const did = didFromUri(uri)\n if (\n ctx.views.viewerBlockExists(did, hydration) ||\n ctx.views.viewerMuteExists(did, hydration)\n ) {\n return false\n }\n\n if (\n shouldFilterReplyByThreadgate(\n item.reason,\n uri,\n params.hydrateCtx.viewer,\n hydration,\n ctx.views,\n )\n ) {\n return false\n }\n\n if (\n shouldFilterHiddenThreadTag(\n item.reason,\n uri,\n did,\n hydration,\n ctx.cfg.threadTagsHide,\n )\n ) {\n return false\n }\n\n if (\n shouldFilterForNeedsReview(item.reason, did, uri, hydration, ctx.views)\n ) {\n return false\n }\n\n return true\n })\n return skeleton\n}\n\nconst presentation = (\n input: PresentationFnInput<Context, Params, SkeletonState>,\n): app.bsky.notification.listNotifications.$OutputBody => {\n const { skeleton, hydration, ctx } = input\n const { notifs, lastSeenNotifs, cursor } = skeleton\n const notifications = mapDefined(notifs, (notif) =>\n ctx.views.notification(notif, lastSeenNotifs, hydration),\n )\n return {\n notifications,\n cursor,\n seenAt: skeleton.lastSeenNotifs,\n }\n}\n\ntype Context = {\n hydrator: Hydrator\n views: Views\n cfg: ServerConfig\n}\n\ntype Params = app.bsky.notification.listNotifications.$Params & {\n hydrateCtx: HydrateCtxWithViewer\n lastSeen?: Date\n}\n\ntype SkeletonState = {\n notifs: Notification[]\n lastSeenNotifs?: DatetimeString\n cursor?: string\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { AtUriString } from '@atproto/lex';
2
+ import { app } from '../../../../lexicons/index.js';
3
+ type ActivityItem = app.bsky.unspecced.getAtmosphereTimeline.ActivityItem;
4
+ type ActivityGroup = app.bsky.unspecced.getAtmosphereTimeline.ActivityGroup;
5
+ export type TimelineActivity = {
6
+ item: ActivityItem;
7
+ sortedAt: number;
8
+ target?: AtUriString;
9
+ groupable: boolean;
10
+ };
11
+ /** Select the timeline gap and grouping window using the newer activity's age. */
12
+ export declare function atmosphereWindow(ageMs: number): number;
13
+ /** Attach article reactions, then group remaining activities at their newest position. */
14
+ export declare function groupAtmosphereActivities(activities: TimelineActivity[], now: number): (ActivityItem | ActivityGroup)[];
15
+ export {};
16
+ //# sourceMappingURL=atmosphere-grouping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atmosphere-grouping.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/unspecced/atmosphere-grouping.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,GAAG,EAAe,MAAM,+BAA+B,CAAA;AAGhE,KAAK,YAAY,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC,YAAY,CAAA;AACzE,KAAK,aAAa,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC,aAAa,CAAA;AAE3E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,0FAA0F;AAC1F,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,gBAAgB,EAAE,EAC9B,GAAG,EAAE,MAAM,GACV,CAAC,YAAY,GAAG,aAAa,CAAC,EAAE,CAmGlC"}
@@ -0,0 +1,124 @@
1
+ import { HOUR } from '@atproto/common';
2
+ import { app, place, site } from '../../../../lexicons/index.js';
3
+ import { uriToDid } from '../../../../util/uris.js';
4
+ /** Select the timeline gap and grouping window using the newer activity's age. */
5
+ export function atmosphereWindow(ageMs) {
6
+ if (ageMs < HOUR)
7
+ return HOUR / 2;
8
+ if (ageMs <= 12 * HOUR)
9
+ return HOUR;
10
+ if (ageMs <= 24 * HOUR)
11
+ return 4 * HOUR;
12
+ return 8 * HOUR;
13
+ }
14
+ /** Attach article reactions, then group remaining activities at their newest position. */
15
+ export function groupAtmosphereActivities(activities, now) {
16
+ const consumed = new Set();
17
+ const output = new Map();
18
+ const articles = new Map();
19
+ const related = new Map();
20
+ for (const [index, { item }] of activities.entries()) {
21
+ if (item.collection === site.standard.document.$type) {
22
+ articles.set(item.record.uri, index);
23
+ }
24
+ }
25
+ for (const [index, { item, target }] of activities.entries()) {
26
+ if (!target ||
27
+ (item.collection !== site.standard.graph.recommend.$type &&
28
+ item.collection !== app.bsky.feed.post.$type)) {
29
+ continue;
30
+ }
31
+ const article = articles.get(target);
32
+ if (article === undefined)
33
+ continue;
34
+ const attachments = related.get(article);
35
+ if (attachments)
36
+ attachments.push(item);
37
+ else
38
+ related.set(article, [item]);
39
+ consumed.add(index);
40
+ }
41
+ const itemAt = (index) => {
42
+ const item = activities[index].item;
43
+ const attachments = related.get(index);
44
+ return attachments ? { ...item, relatedActivity: attachments } : item;
45
+ };
46
+ const group = (groupBy) => {
47
+ const buckets = new Map();
48
+ for (const [index, activity] of activities.entries()) {
49
+ if (consumed.has(index))
50
+ continue;
51
+ const value = groupBy === 'subject'
52
+ ? activity.target
53
+ : authorGroupable(activity)
54
+ ? uriToDid(activity.item.record.uri)
55
+ : undefined;
56
+ if (!value)
57
+ continue;
58
+ const key = `${activity.item.collection}:${value}`;
59
+ const bucket = buckets.get(key);
60
+ if (bucket)
61
+ bucket.push(index);
62
+ else
63
+ buckets.set(key, [index]);
64
+ }
65
+ for (const indices of buckets.values()) {
66
+ let start = 0;
67
+ while (start < indices.length) {
68
+ const firstIndex = indices[start];
69
+ const first = activities[firstIndex];
70
+ const window = atmosphereWindow(now - first.sortedAt);
71
+ let end = start + 1;
72
+ while (end < indices.length &&
73
+ first.sortedAt - activities[indices[end]].sortedAt <= window) {
74
+ end++;
75
+ }
76
+ if (end - start >= 2) {
77
+ const items = [];
78
+ for (let offset = start; offset < end; offset++) {
79
+ const index = indices[offset];
80
+ items.push(itemAt(index));
81
+ consumed.add(index);
82
+ }
83
+ output.set(firstIndex, app.bsky.unspecced.getAtmosphereTimeline.activityGroup.$build({
84
+ id: `${groupBy}:${first.item.record.uri}`,
85
+ groupBy,
86
+ collection: first.item.collection,
87
+ indexedAt: first.item.record.indexedAt,
88
+ items,
89
+ ...(groupBy === 'subject'
90
+ ? { subject: first.target }
91
+ : { actor: uriToDid(first.item.record.uri) }),
92
+ }));
93
+ }
94
+ start = end;
95
+ }
96
+ }
97
+ };
98
+ group('subject');
99
+ group('actor');
100
+ const feed = [];
101
+ for (let index = 0; index < activities.length; index++) {
102
+ const grouped = output.get(index);
103
+ if (grouped)
104
+ feed.push(grouped);
105
+ else if (!consumed.has(index))
106
+ feed.push(itemAt(index));
107
+ }
108
+ return feed;
109
+ }
110
+ function authorGroupable({ item, groupable }) {
111
+ if (!groupable || item.collection === app.bsky.feed.post.$type)
112
+ return false;
113
+ if (item.collection === site.standard.document.$type &&
114
+ item.viewerState.subscribed) {
115
+ return false;
116
+ }
117
+ if (item.collection === place.stream.livestream.$type) {
118
+ return !!(item.view &&
119
+ app.bsky.embed.external.viewLivestream.$isTypeOf(item.view) &&
120
+ item.view.endedAt);
121
+ }
122
+ return true;
123
+ }
124
+ //# sourceMappingURL=atmosphere-grouping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atmosphere-grouping.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/unspecced/atmosphere-grouping.ts"],"names":[],"mappings":"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","sourcesContent":["import { HOUR } from '@atproto/common'\nimport type { AtUriString } from '@atproto/lex'\nimport { app, place, site } from '../../../../lexicons/index.js'\nimport { uriToDid } from '../../../../util/uris.js'\n\ntype ActivityItem = app.bsky.unspecced.getAtmosphereTimeline.ActivityItem\ntype ActivityGroup = app.bsky.unspecced.getAtmosphereTimeline.ActivityGroup\n\nexport type TimelineActivity = {\n item: ActivityItem\n sortedAt: number\n target?: AtUriString\n groupable: boolean\n}\n\n/** Select the timeline gap and grouping window using the newer activity's age. */\nexport function atmosphereWindow(ageMs: number): number {\n if (ageMs < HOUR) return HOUR / 2\n if (ageMs <= 12 * HOUR) return HOUR\n if (ageMs <= 24 * HOUR) return 4 * HOUR\n return 8 * HOUR\n}\n\n/** Attach article reactions, then group remaining activities at their newest position. */\nexport function groupAtmosphereActivities(\n activities: TimelineActivity[],\n now: number,\n): (ActivityItem | ActivityGroup)[] {\n const consumed = new Set<number>()\n const output = new Map<number, ActivityItem | ActivityGroup>()\n const articles = new Map<AtUriString, number>()\n const related = new Map<number, ActivityItem[]>()\n\n for (const [index, { item }] of activities.entries()) {\n if (item.collection === site.standard.document.$type) {\n articles.set(item.record.uri, index)\n }\n }\n for (const [index, { item, target }] of activities.entries()) {\n if (\n !target ||\n (item.collection !== site.standard.graph.recommend.$type &&\n item.collection !== app.bsky.feed.post.$type)\n ) {\n continue\n }\n const article = articles.get(target)\n if (article === undefined) continue\n const attachments = related.get(article)\n if (attachments) attachments.push(item)\n else related.set(article, [item])\n consumed.add(index)\n }\n\n const itemAt = (index: number): ActivityItem => {\n const item = activities[index].item\n const attachments = related.get(index)\n return attachments ? { ...item, relatedActivity: attachments } : item\n }\n\n const group = (groupBy: 'subject' | 'actor') => {\n const buckets = new Map<string, number[]>()\n for (const [index, activity] of activities.entries()) {\n if (consumed.has(index)) continue\n const value =\n groupBy === 'subject'\n ? activity.target\n : authorGroupable(activity)\n ? uriToDid(activity.item.record.uri)\n : undefined\n if (!value) continue\n const key = `${activity.item.collection}:${value}`\n const bucket = buckets.get(key)\n if (bucket) bucket.push(index)\n else buckets.set(key, [index])\n }\n\n for (const indices of buckets.values()) {\n let start = 0\n while (start < indices.length) {\n const firstIndex = indices[start]\n const first = activities[firstIndex]\n const window = atmosphereWindow(now - first.sortedAt)\n let end = start + 1\n while (\n end < indices.length &&\n first.sortedAt - activities[indices[end]].sortedAt <= window\n ) {\n end++\n }\n if (end - start >= 2) {\n const items: ActivityItem[] = []\n for (let offset = start; offset < end; offset++) {\n const index = indices[offset]\n items.push(itemAt(index))\n consumed.add(index)\n }\n output.set(\n firstIndex,\n app.bsky.unspecced.getAtmosphereTimeline.activityGroup.$build({\n id: `${groupBy}:${first.item.record.uri}`,\n groupBy,\n collection: first.item.collection,\n indexedAt: first.item.record.indexedAt,\n items,\n ...(groupBy === 'subject'\n ? { subject: first.target }\n : { actor: uriToDid(first.item.record.uri) }),\n }),\n )\n }\n start = end\n }\n }\n }\n\n group('subject')\n group('actor')\n\n const feed: (ActivityItem | ActivityGroup)[] = []\n for (let index = 0; index < activities.length; index++) {\n const grouped = output.get(index)\n if (grouped) feed.push(grouped)\n else if (!consumed.has(index)) feed.push(itemAt(index))\n }\n return feed\n}\n\nfunction authorGroupable({ item, groupable }: TimelineActivity): boolean {\n if (!groupable || item.collection === app.bsky.feed.post.$type) return false\n if (\n item.collection === site.standard.document.$type &&\n item.viewerState.subscribed\n ) {\n return false\n }\n if (item.collection === place.stream.livestream.$type) {\n return !!(\n item.view &&\n app.bsky.embed.external.viewLivestream.$isTypeOf(item.view) &&\n item.view.endedAt\n )\n }\n return true\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import { type AtUriString, type DidString } from '@atproto/lex';
2
+ import { type HydrateCtxWithViewer, type Hydrator } from '../../../../hydration/hydrator.js';
3
+ import { app } from '../../../../lexicons/index.js';
4
+ import type { Views } from '../../../../views/index.js';
5
+ import type { TimelineActivity } from './atmosphere-grouping.js';
6
+ /** Request-scoped hydration and visibility for incrementally fetched roots. */
7
+ export declare class AtmosphereTimelineHydrator {
8
+ private readonly ctx;
9
+ private readonly hydrateCtx;
10
+ private readonly now;
11
+ private state;
12
+ private readonly seen;
13
+ private readonly seenPosts;
14
+ constructor(ctx: {
15
+ hydrator: Hydrator;
16
+ views: Views;
17
+ }, hydrateCtx: HydrateCtxWithViewer, now: number);
18
+ /** Hydrate new roots and their dependencies without repeating prior lookups. */
19
+ hydrate(uris: AtUriString[]): Promise<void>;
20
+ /** Return only an already-loaded, visible author profile for overflow. */
21
+ profile(did: DidString): app.bsky.actor.defs.ProfileViewBasic | undefined;
22
+ /** Recheck current dependencies and moderation without building a view. */
23
+ isVisible(uri: AtUriString): boolean;
24
+ /** Compose a renderable root and the target used by final-page grouping. */
25
+ activity(uri: AtUriString, sortedAt: number): TimelineActivity | undefined;
26
+ private selectActivity;
27
+ private visibleAuthor;
28
+ private hasUnmutedAuthor;
29
+ private visibleRecord;
30
+ private visibleExternal;
31
+ }
32
+ //# sourceMappingURL=atmosphere-hydration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atmosphere-hydration.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/unspecced/atmosphere-hydration.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,SAAS,EAGf,MAAM,cAAc,CAAA;AAQrB,OAAO,EACL,KAAK,oBAAoB,EAEzB,KAAK,QAAQ,EAEd,MAAM,mCAAmC,CAAA;AAE1C,OAAO,EAAE,GAAG,EAAuB,MAAM,+BAA+B,CAAA;AAMxE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAA;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAEhE,+EAA+E;AAC/E,qBAAa,0BAA0B;IAMnC,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IAEnD,YACmB,GAAG,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EACzC,UAAU,EAAE,oBAAoB,EAChC,GAAG,EAAE,MAAM,EAM7B;IAED,gFAAgF;IAC1E,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2ChD;IAED,0EAA0E;IAC1E,OAAO,CAAC,GAAG,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAGxE;IAED,2EAA2E;IAC3E,SAAS,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAEnC;IAED,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAyBzE;IAED,OAAO,CAAC,cAAc;IA+FtB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,eAAe;CASxB"}