@atproto/bsky 0.0.282 → 0.1.1

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 (142) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/api/app/bsky/feed/getFeed.d.ts +8 -0
  3. package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
  4. package/dist/api/app/bsky/feed/getFeed.js +17 -0
  5. package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
  6. package/dist/api/app/bsky/feed/getListFeed.d.ts +2 -1
  7. package/dist/api/app/bsky/feed/getListFeed.d.ts.map +1 -1
  8. package/dist/api/app/bsky/feed/getListFeed.js +17 -4
  9. package/dist/api/app/bsky/feed/getListFeed.js.map +1 -1
  10. package/dist/api/app/bsky/feed/getTimeline.d.ts +2 -1
  11. package/dist/api/app/bsky/feed/getTimeline.d.ts.map +1 -1
  12. package/dist/api/app/bsky/feed/getTimeline.js +17 -4
  13. package/dist/api/app/bsky/feed/getTimeline.js.map +1 -1
  14. package/dist/api/app/bsky/notification/constants.d.ts +37 -0
  15. package/dist/api/app/bsky/notification/constants.d.ts.map +1 -0
  16. package/dist/api/app/bsky/notification/constants.js +24 -0
  17. package/dist/api/app/bsky/notification/constants.js.map +1 -0
  18. package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts +5 -0
  19. package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts.map +1 -0
  20. package/dist/api/app/bsky/notification/getGroupedNotifications.js +349 -0
  21. package/dist/api/app/bsky/notification/getGroupedNotifications.js.map +1 -0
  22. package/dist/api/app/bsky/notification/grouping/algoGravity.d.ts +4 -0
  23. package/dist/api/app/bsky/notification/grouping/algoGravity.d.ts.map +1 -0
  24. package/dist/api/app/bsky/notification/grouping/algoGravity.js +141 -0
  25. package/dist/api/app/bsky/notification/grouping/algoGravity.js.map +1 -0
  26. package/dist/api/app/bsky/notification/grouping/algoLookback.d.ts +4 -0
  27. package/dist/api/app/bsky/notification/grouping/algoLookback.d.ts.map +1 -0
  28. package/dist/api/app/bsky/notification/grouping/algoLookback.js +61 -0
  29. package/dist/api/app/bsky/notification/grouping/algoLookback.js.map +1 -0
  30. package/dist/api/app/bsky/notification/grouping/grouping.d.ts +41 -0
  31. package/dist/api/app/bsky/notification/grouping/grouping.d.ts.map +1 -0
  32. package/dist/api/app/bsky/notification/grouping/grouping.js +54 -0
  33. package/dist/api/app/bsky/notification/grouping/grouping.js.map +1 -0
  34. package/dist/api/app/bsky/notification/grouping/shared.d.ts +8 -0
  35. package/dist/api/app/bsky/notification/grouping/shared.d.ts.map +1 -0
  36. package/dist/api/app/bsky/notification/grouping/shared.js +20 -0
  37. package/dist/api/app/bsky/notification/grouping/shared.js.map +1 -0
  38. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts +3 -0
  39. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts.map +1 -0
  40. package/dist/api/app/bsky/notification/grouping/spotlight.js +114 -0
  41. package/dist/api/app/bsky/notification/grouping/spotlight.js.map +1 -0
  42. package/dist/api/app/bsky/notification/listNotifications.d.ts +0 -7
  43. package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
  44. package/dist/api/app/bsky/notification/listNotifications.js +7 -59
  45. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  46. package/dist/api/app/bsky/notification/updateSeen.d.ts.map +1 -1
  47. package/dist/api/app/bsky/notification/updateSeen.js +4 -3
  48. package/dist/api/app/bsky/notification/updateSeen.js.map +1 -1
  49. package/dist/api/app/bsky/notification/util.d.ts +8 -0
  50. package/dist/api/app/bsky/notification/util.d.ts.map +1 -1
  51. package/dist/api/app/bsky/notification/util.js +55 -0
  52. package/dist/api/app/bsky/notification/util.js.map +1 -1
  53. package/dist/api/index.d.ts.map +1 -1
  54. package/dist/api/index.js +2 -0
  55. package/dist/api/index.js.map +1 -1
  56. package/dist/api/util.d.ts +5 -0
  57. package/dist/api/util.d.ts.map +1 -1
  58. package/dist/api/util.js +4 -1
  59. package/dist/api/util.js.map +1 -1
  60. package/dist/config.d.ts +7 -0
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +19 -0
  63. package/dist/config.js.map +1 -1
  64. package/dist/data-plane/server/db/pagination.d.ts +7 -0
  65. package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
  66. package/dist/data-plane/server/db/pagination.js +18 -2
  67. package/dist/data-plane/server/db/pagination.js.map +1 -1
  68. package/dist/data-plane/server/db/tables/notification.d.ts +2 -1
  69. package/dist/data-plane/server/db/tables/notification.d.ts.map +1 -1
  70. package/dist/data-plane/server/db/tables/notification.js.map +1 -1
  71. package/dist/data-plane/server/indexing/index.d.ts +2 -0
  72. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  73. package/dist/data-plane/server/indexing/plugins/follow.d.ts +6 -9
  74. package/dist/data-plane/server/indexing/plugins/follow.d.ts.map +1 -1
  75. package/dist/data-plane/server/indexing/plugins/follow.js +13 -2
  76. package/dist/data-plane/server/indexing/plugins/follow.js.map +1 -1
  77. package/dist/data-plane/server/indexing/plugins/like.d.ts.map +1 -1
  78. package/dist/data-plane/server/indexing/plugins/like.js +3 -2
  79. package/dist/data-plane/server/indexing/plugins/like.js.map +1 -1
  80. package/dist/data-plane/server/indexing/plugins/post.d.ts.map +1 -1
  81. package/dist/data-plane/server/indexing/plugins/post.js +5 -4
  82. package/dist/data-plane/server/indexing/plugins/post.js.map +1 -1
  83. package/dist/data-plane/server/indexing/plugins/profile.d.ts.map +1 -1
  84. package/dist/data-plane/server/indexing/plugins/profile.js +2 -1
  85. package/dist/data-plane/server/indexing/plugins/profile.js.map +1 -1
  86. package/dist/data-plane/server/indexing/plugins/repost.d.ts.map +1 -1
  87. package/dist/data-plane/server/indexing/plugins/repost.js +3 -2
  88. package/dist/data-plane/server/indexing/plugins/repost.js.map +1 -1
  89. package/dist/data-plane/server/indexing/plugins/verification.d.ts.map +1 -1
  90. package/dist/data-plane/server/indexing/plugins/verification.js +3 -2
  91. package/dist/data-plane/server/indexing/plugins/verification.js.map +1 -1
  92. package/dist/data-plane/server/routes/feeds.d.ts +1 -1
  93. package/dist/data-plane/server/routes/feeds.d.ts.map +1 -1
  94. package/dist/data-plane/server/routes/feeds.js +35 -7
  95. package/dist/data-plane/server/routes/feeds.js.map +1 -1
  96. package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
  97. package/dist/data-plane/server/routes/notifs.js +67 -2
  98. package/dist/data-plane/server/routes/notifs.js.map +1 -1
  99. package/dist/hydration/hydrator.d.ts +2 -0
  100. package/dist/hydration/hydrator.d.ts.map +1 -1
  101. package/dist/hydration/hydrator.js +69 -1
  102. package/dist/hydration/hydrator.js.map +1 -1
  103. package/dist/lexicons/app/bsky/authManageNotifications.defs.d.ts.map +1 -1
  104. package/dist/lexicons/app/bsky/authManageNotifications.defs.js +1 -0
  105. package/dist/lexicons/app/bsky/authManageNotifications.defs.js.map +1 -1
  106. package/dist/lexicons/app/bsky/feed/getListFeed.defs.d.ts +4 -0
  107. package/dist/lexicons/app/bsky/feed/getListFeed.defs.d.ts.map +1 -1
  108. package/dist/lexicons/app/bsky/feed/getListFeed.defs.js +2 -0
  109. package/dist/lexicons/app/bsky/feed/getListFeed.defs.js.map +1 -1
  110. package/dist/lexicons/app/bsky/feed/getTimeline.defs.d.ts +4 -0
  111. package/dist/lexicons/app/bsky/feed/getTimeline.defs.d.ts.map +1 -1
  112. package/dist/lexicons/app/bsky/feed/getTimeline.defs.js +2 -0
  113. package/dist/lexicons/app/bsky/feed/getTimeline.defs.js.map +1 -1
  114. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.d.ts +3 -0
  115. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.d.ts.map +1 -0
  116. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts +299 -0
  117. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts.map +1 -0
  118. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js +231 -0
  119. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js.map +1 -0
  120. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.js +6 -0
  121. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.js.map +1 -0
  122. package/dist/lexicons/app/bsky/notification.d.ts +1 -0
  123. package/dist/lexicons/app/bsky/notification.d.ts.map +1 -1
  124. package/dist/lexicons/app/bsky/notification.js +1 -0
  125. package/dist/lexicons/app/bsky/notification.js.map +1 -1
  126. package/dist/lexicons/tools/ozone/report/defs.defs.d.ts +17 -1
  127. package/dist/lexicons/tools/ozone/report/defs.defs.d.ts.map +1 -1
  128. package/dist/lexicons/tools/ozone/report/defs.defs.js +10 -0
  129. package/dist/lexicons/tools/ozone/report/defs.defs.js.map +1 -1
  130. package/dist/proto/bsky_connect.d.ts +10 -1
  131. package/dist/proto/bsky_connect.d.ts.map +1 -1
  132. package/dist/proto/bsky_connect.js +10 -1
  133. package/dist/proto/bsky_connect.js.map +1 -1
  134. package/dist/proto/bsky_pb.d.ts +120 -25
  135. package/dist/proto/bsky_pb.d.ts.map +1 -1
  136. package/dist/proto/bsky_pb.js +166 -27
  137. package/dist/proto/bsky_pb.js.map +1 -1
  138. package/dist/views/index.d.ts +3 -0
  139. package/dist/views/index.d.ts.map +1 -1
  140. package/dist/views/index.js +133 -0
  141. package/dist/views/index.js.map +1 -1
  142. package/package.json +13 -13
@@ -0,0 +1,61 @@
1
+ import { HOUR, MINUTE } from '@atproto/common';
2
+ import { NotificationFeed } from '../../../../../proto/bsky_pb.js';
3
+ import { MAX_GROUP_SIZE, canGroupNotification, isNotificationRead, } from './shared.js';
4
+ const PARAMS = {
5
+ zones: [
6
+ // @NOTE: Keep in ascending ageMs, null last.
7
+ { ageMs: HOUR, lookbackMs: 30 * MINUTE },
8
+ { ageMs: 12 * HOUR, lookbackMs: HOUR },
9
+ { ageMs: 24 * HOUR, lookbackMs: 4 * HOUR },
10
+ { ageMs: null, lookbackMs: 8 * HOUR },
11
+ ],
12
+ };
13
+ export const buildAlgoLookbackGroups = (items, limit, now, seenAt, feed = NotificationFeed.ALL) => {
14
+ const groups = [];
15
+ const activeGroups = new Map();
16
+ for (const [itemIndex, item] of items.entries()) {
17
+ const canGroup = canGroupNotification(item.raw.reason, feed);
18
+ const key = JSON.stringify([
19
+ item.raw.reason,
20
+ item.groupingKey,
21
+ canGroup ? undefined : item.id,
22
+ ]);
23
+ const active = activeGroups.get(key);
24
+ const age = active ? Math.max(0, now - Date.parse(active.indexedAt)) : 0;
25
+ const zone = active
26
+ ? (PARAMS.zones.find(({ ageMs: maxAgeMs }, index) => maxAgeMs === null ||
27
+ (index === 0 ? age < maxAgeMs : age <= maxAgeMs)) ?? PARAMS.zones.at(-1))
28
+ : undefined;
29
+ const gap = active
30
+ ? Date.parse(active.indexedAt) - Date.parse(item.raw.indexedAt)
31
+ : Number.POSITIVE_INFINITY;
32
+ if (active &&
33
+ active.itemCount < MAX_GROUP_SIZE &&
34
+ zone &&
35
+ gap <= zone.lookbackMs) {
36
+ active.actorDids.push(item.actorDid);
37
+ active.itemCount++;
38
+ active.firstIndexedAt = item.raw.indexedAt;
39
+ active.items.push(item);
40
+ continue;
41
+ }
42
+ if (groups.length >= limit) {
43
+ return { groups, cursor: items[itemIndex - 1]?.raw.indexedAt };
44
+ }
45
+ const group = {
46
+ id: item.id,
47
+ kind: item.raw.reason,
48
+ groupingKey: item.groupingKey,
49
+ actorDids: [item.actorDid],
50
+ itemCount: 1,
51
+ indexedAt: item.raw.indexedAt,
52
+ firstIndexedAt: item.raw.indexedAt,
53
+ isRead: isNotificationRead(item.raw.indexedAt, seenAt),
54
+ items: [item],
55
+ };
56
+ groups.push(group);
57
+ activeGroups.set(key, group);
58
+ }
59
+ return { groups };
60
+ };
61
+ //# sourceMappingURL=algoLookback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algoLookback.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/algoLookback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAMlE,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,aAAa,CAAA;AAWpB,MAAM,MAAM,GAAW;IACrB,KAAK,EAAE;QACL,6CAA6C;QAC7C,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,GAAG,MAAM,EAAE;QACxC,EAAE,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE;QACtC,EAAE,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE;QAC1C,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE;KACtC;CACF,CAAA;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,KAAyB,EACzB,KAAa,EACb,GAAW,EACX,MAAe,EACf,IAAI,GAAG,gBAAgB,CAAC,GAAG,EACX,EAAE;IAClB,MAAM,MAAM,GAAwB,EAAE,CAAA;IACtC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA6B,CAAA;IAEzD,KAAK,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAChD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,MAAM;YACf,IAAI,CAAC,WAAW;YAChB,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE;SAC/B,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACpC,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACxE,MAAM,IAAI,GAAG,MAAM;YACjB,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAChB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,CAC7B,QAAQ,KAAK,IAAI;gBACjB,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,QAAQ,CAAC,CACnD,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC;YAC5B,CAAC,CAAC,SAAS,CAAA;QACb,MAAM,GAAG,GAAG,MAAM;YAChB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YAC/D,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAA;QAE5B,IACE,MAAM;YACN,MAAM,CAAC,SAAS,GAAG,cAAc;YACjC,IAAI;YACJ,GAAG,IAAI,IAAI,CAAC,UAAU,EACtB,CAAC;YACD,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YACpC,MAAM,CAAC,SAAS,EAAE,CAAA;YAClB,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAA;YAC1C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACvB,SAAQ;QACV,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,IAAI,KAAK,EAAE,CAAC;YAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,SAAS,EAAE,CAAA;QAChE,CAAC;QAED,MAAM,KAAK,GAAsB;YAC/B,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM;YACrB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,SAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;YAC1B,SAAS,EAAE,CAAC;YACZ,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS;YAC7B,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS;YAClC,MAAM,EAAE,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC;YACtD,KAAK,EAAE,CAAC,IAAI,CAAC;SACd,CAAA;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClB,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAC9B,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,CAAA;AACnB,CAAC,CAAA","sourcesContent":["import { HOUR, MINUTE } from '@atproto/common'\nimport { NotificationFeed } from '../../../../../proto/bsky_pb.js'\nimport type {\n GroupingResult,\n NotificationGroup,\n NotificationItem,\n} from './grouping.js'\nimport {\n MAX_GROUP_SIZE,\n canGroupNotification,\n isNotificationRead,\n} from './shared.js'\n\ntype Zone = {\n ageMs: number | null\n lookbackMs: number\n}\n\ntype Params = {\n zones: Zone[]\n}\n\nconst PARAMS: Params = {\n zones: [\n // @NOTE: Keep in ascending ageMs, null last.\n { ageMs: HOUR, lookbackMs: 30 * MINUTE },\n { ageMs: 12 * HOUR, lookbackMs: HOUR },\n { ageMs: 24 * HOUR, lookbackMs: 4 * HOUR },\n { ageMs: null, lookbackMs: 8 * HOUR },\n ],\n}\n\nexport const buildAlgoLookbackGroups = (\n items: NotificationItem[],\n limit: number,\n now: number,\n seenAt?: number,\n feed = NotificationFeed.ALL,\n): GroupingResult => {\n const groups: NotificationGroup[] = []\n const activeGroups = new Map<string, NotificationGroup>()\n\n for (const [itemIndex, item] of items.entries()) {\n const canGroup = canGroupNotification(item.raw.reason, feed)\n const key = JSON.stringify([\n item.raw.reason,\n item.groupingKey,\n canGroup ? undefined : item.id,\n ])\n const active = activeGroups.get(key)\n const age = active ? Math.max(0, now - Date.parse(active.indexedAt)) : 0\n const zone = active\n ? (PARAMS.zones.find(\n ({ ageMs: maxAgeMs }, index) =>\n maxAgeMs === null ||\n (index === 0 ? age < maxAgeMs : age <= maxAgeMs),\n ) ?? PARAMS.zones.at(-1)!)\n : undefined\n const gap = active\n ? Date.parse(active.indexedAt) - Date.parse(item.raw.indexedAt)\n : Number.POSITIVE_INFINITY\n\n if (\n active &&\n active.itemCount < MAX_GROUP_SIZE &&\n zone &&\n gap <= zone.lookbackMs\n ) {\n active.actorDids.push(item.actorDid)\n active.itemCount++\n active.firstIndexedAt = item.raw.indexedAt\n active.items.push(item)\n continue\n }\n\n if (groups.length >= limit) {\n return { groups, cursor: items[itemIndex - 1]?.raw.indexedAt }\n }\n\n const group: NotificationGroup = {\n id: item.id,\n kind: item.raw.reason,\n groupingKey: item.groupingKey,\n actorDids: [item.actorDid],\n itemCount: 1,\n indexedAt: item.raw.indexedAt,\n firstIndexedAt: item.raw.indexedAt,\n isRead: isNotificationRead(item.raw.indexedAt, seenAt),\n items: [item],\n }\n groups.push(group)\n activeGroups.set(key, group)\n }\n\n return { groups }\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { AtUriString, DatetimeString, DidString } from '@atproto/lex';
2
+ import type { NotificationsV2Algorithm } from '../../../../../config.js';
3
+ import { type Notification, NotificationFeed } from '../../../../../proto/bsky_pb.js';
4
+ import { type AppviewNotificationReason, type DataplaneNotificationReason, NOTIFICATION_REASON } from '../constants.js';
5
+ type DataplaneNotificationReasonWithSubject = typeof NOTIFICATION_REASON.LIKE | typeof NOTIFICATION_REASON.REPOST | typeof NOTIFICATION_REASON.LIKE_VIA_REPOST | typeof NOTIFICATION_REASON.REPOST_VIA_REPOST | typeof NOTIFICATION_REASON.REPLY | typeof NOTIFICATION_REASON.QUOTE | typeof NOTIFICATION_REASON.STARTERPACK_JOINED;
6
+ export type RawNotification = {
7
+ recipientDid: DidString;
8
+ uri: AtUriString;
9
+ indexedAt: DatetimeString;
10
+ } & ({
11
+ reason: DataplaneNotificationReasonWithSubject;
12
+ reasonSubject: AtUriString;
13
+ } | {
14
+ reason: Exclude<DataplaneNotificationReason, DataplaneNotificationReasonWithSubject>;
15
+ reasonSubject: undefined;
16
+ });
17
+ export type NotificationItem = {
18
+ raw: RawNotification;
19
+ id: string;
20
+ groupingKey: string;
21
+ actorDid: DidString;
22
+ };
23
+ export type NotificationGroup = {
24
+ id: string;
25
+ kind: AppviewNotificationReason;
26
+ groupingKey: string;
27
+ actorDids: DidString[];
28
+ itemCount: number;
29
+ indexedAt: DatetimeString;
30
+ firstIndexedAt: DatetimeString;
31
+ isRead: boolean;
32
+ items: NotificationItem[];
33
+ };
34
+ export type GroupingResult = {
35
+ groups: NotificationGroup[];
36
+ cursor?: string;
37
+ };
38
+ export declare function parseRawNotification(notification: Notification): RawNotification | undefined;
39
+ export declare const buildGroups: (items: NotificationItem[], limit: number, now: number, seenAt: number | undefined, algorithm: NotificationsV2Algorithm, feed?: NotificationFeed) => GroupingResult;
40
+ export {};
41
+ //# sourceMappingURL=grouping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grouping.d.ts","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/grouping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC1E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AACxE,OAAO,EACL,KAAK,YAAY,EACjB,gBAAgB,EACjB,MAAM,iCAAiC,CAAA;AACxC,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,mBAAmB,EACpB,MAAM,iBAAiB,CAAA;AAMxB,KAAK,sCAAsC,GACvC,OAAO,mBAAmB,CAAC,IAAI,GAC/B,OAAO,mBAAmB,CAAC,MAAM,GACjC,OAAO,mBAAmB,CAAC,eAAe,GAC1C,OAAO,mBAAmB,CAAC,iBAAiB,GAC5C,OAAO,mBAAmB,CAAC,KAAK,GAChC,OAAO,mBAAmB,CAAC,KAAK,GAChC,OAAO,mBAAmB,CAAC,kBAAkB,CAAA;AAKjD,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,SAAS,CAAA;IACvB,GAAG,EAAE,WAAW,CAAA;IAChB,SAAS,EAAE,cAAc,CAAA;CAC1B,GAAG,CACA;IACE,MAAM,EAAE,sCAAsC,CAAA;IAC9C,aAAa,EAAE,WAAW,CAAA;CAC3B,GACD;IACE,MAAM,EAAE,OAAO,CACb,2BAA2B,EAC3B,sCAAsC,CACvC,CAAA;IACD,aAAa,EAAE,SAAS,CAAA;CACzB,CACJ,CAAA;AAID,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,eAAe,CAAA;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAID,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,yBAAyB,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,SAAS,EAAE,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,cAAc,CAAA;IACzB,cAAc,EAAE,cAAc,CAAA;IAC9B,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,gBAAgB,EAAE,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,YAAY,GACzB,eAAe,GAAG,SAAS,CAsC7B;AAED,eAAO,MAAM,WAAW,UACf,gBAAgB,EAAE,SAClB,MAAM,OACR,MAAM,UACH,MAAM,GAAG,SAAS,aACf,wBAAwB,8BAElC,cAQF,CAAA"}
@@ -0,0 +1,54 @@
1
+ import { NotificationFeed, } from '../../../../../proto/bsky_pb.js';
2
+ import { NOTIFICATION_REASON, } from '../constants.js';
3
+ import { buildAlgoGravityGroups } from './algoGravity.js';
4
+ import { buildAlgoLookbackGroups } from './algoLookback.js';
5
+ export function parseRawNotification(notification) {
6
+ const { recipientDid, uri, reason, reasonSubject, timestamp } = notification;
7
+ if (!recipientDid || !uri || !timestamp)
8
+ return;
9
+ const indexedAt = timestamp.toJson();
10
+ switch (reason) {
11
+ case NOTIFICATION_REASON.LIKE:
12
+ case NOTIFICATION_REASON.REPOST:
13
+ case NOTIFICATION_REASON.LIKE_VIA_REPOST:
14
+ case NOTIFICATION_REASON.REPOST_VIA_REPOST:
15
+ case NOTIFICATION_REASON.REPLY:
16
+ case NOTIFICATION_REASON.QUOTE:
17
+ case NOTIFICATION_REASON.STARTERPACK_JOINED:
18
+ if (!reasonSubject)
19
+ return;
20
+ return {
21
+ recipientDid: recipientDid,
22
+ uri: uri,
23
+ reason,
24
+ reasonSubject: reasonSubject,
25
+ indexedAt,
26
+ };
27
+ case NOTIFICATION_REASON.CONTACT_MATCH:
28
+ case NOTIFICATION_REASON.FOLLOW:
29
+ case NOTIFICATION_REASON.FOLLOW_BACK:
30
+ case NOTIFICATION_REASON.MENTION:
31
+ case NOTIFICATION_REASON.SUBSCRIBED_POST:
32
+ case NOTIFICATION_REASON.UNVERIFIED:
33
+ case NOTIFICATION_REASON.VERIFIED:
34
+ return {
35
+ recipientDid: recipientDid,
36
+ uri: uri,
37
+ reason,
38
+ reasonSubject: undefined,
39
+ indexedAt,
40
+ };
41
+ default:
42
+ return;
43
+ }
44
+ }
45
+ export const buildGroups = (items, limit, now, seenAt, algorithm, feed = NotificationFeed.ALL) => {
46
+ switch (algorithm) {
47
+ case 'algoLookback':
48
+ return buildAlgoLookbackGroups(items, limit, now, seenAt, feed);
49
+ case 'algoGravity':
50
+ default:
51
+ return buildAlgoGravityGroups(items, limit, now, seenAt, feed);
52
+ }
53
+ };
54
+ //# sourceMappingURL=grouping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grouping.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/grouping.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,gBAAgB,GACjB,MAAM,iCAAiC,CAAA;AACxC,OAAO,EAGL,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AA8D3D,MAAM,UAAU,oBAAoB,CAClC,YAA0B;IAE1B,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,YAAY,CAAA;IAC5E,IAAI,CAAC,YAAY,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS;QAAE,OAAM;IAC/C,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,EAA8B,CAAA;IAEhE,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,mBAAmB,CAAC,IAAI,CAAC;QAC9B,KAAK,mBAAmB,CAAC,MAAM,CAAC;QAChC,KAAK,mBAAmB,CAAC,eAAe,CAAC;QACzC,KAAK,mBAAmB,CAAC,iBAAiB,CAAC;QAC3C,KAAK,mBAAmB,CAAC,KAAK,CAAC;QAC/B,KAAK,mBAAmB,CAAC,KAAK,CAAC;QAC/B,KAAK,mBAAmB,CAAC,kBAAkB;YACzC,IAAI,CAAC,aAAa;gBAAE,OAAM;YAC1B,OAAO;gBACL,YAAY,EAAE,YAAyB;gBACvC,GAAG,EAAE,GAAkB;gBACvB,MAAM;gBACN,aAAa,EAAE,aAA4B;gBAC3C,SAAS;aACV,CAAA;QACH,KAAK,mBAAmB,CAAC,aAAa,CAAC;QACvC,KAAK,mBAAmB,CAAC,MAAM,CAAC;QAChC,KAAK,mBAAmB,CAAC,WAAW,CAAC;QACrC,KAAK,mBAAmB,CAAC,OAAO,CAAC;QACjC,KAAK,mBAAmB,CAAC,eAAe,CAAC;QACzC,KAAK,mBAAmB,CAAC,UAAU,CAAC;QACpC,KAAK,mBAAmB,CAAC,QAAQ;YAC/B,OAAO;gBACL,YAAY,EAAE,YAAyB;gBACvC,GAAG,EAAE,GAAkB;gBACvB,MAAM;gBACN,aAAa,EAAE,SAAS;gBACxB,SAAS;aACV,CAAA;QACH;YACE,OAAM;IACV,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,KAAyB,EACzB,KAAa,EACb,GAAW,EACX,MAA0B,EAC1B,SAAmC,EACnC,IAAI,GAAG,gBAAgB,CAAC,GAAG,EACX,EAAE;IAClB,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,cAAc;YACjB,OAAO,uBAAuB,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;QACjE,KAAK,aAAa,CAAC;QACnB;YACE,OAAO,sBAAsB,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;IAClE,CAAC;AACH,CAAC,CAAA","sourcesContent":["import type { AtUriString, DatetimeString, DidString } from '@atproto/lex'\nimport type { NotificationsV2Algorithm } from '../../../../../config.js'\nimport {\n type Notification,\n NotificationFeed,\n} from '../../../../../proto/bsky_pb.js'\nimport {\n type AppviewNotificationReason,\n type DataplaneNotificationReason,\n NOTIFICATION_REASON,\n} from '../constants.js'\nimport { buildAlgoGravityGroups } from './algoGravity.js'\nimport { buildAlgoLookbackGroups } from './algoLookback.js'\n\n// Notification reasons that require reasonSubject to be non-empty.\n// It should be the AT-URI of a record belonging to the notification recipient\ntype DataplaneNotificationReasonWithSubject =\n | typeof NOTIFICATION_REASON.LIKE\n | typeof NOTIFICATION_REASON.REPOST\n | typeof NOTIFICATION_REASON.LIKE_VIA_REPOST\n | typeof NOTIFICATION_REASON.REPOST_VIA_REPOST\n | typeof NOTIFICATION_REASON.REPLY\n | typeof NOTIFICATION_REASON.QUOTE\n | typeof NOTIFICATION_REASON.STARTERPACK_JOINED\n\n// A dataplane notification that has passed our checks.\n// We just don't use the underlying proto type because\n// it has optional fields that we validate and ensure are present with this type.\nexport type RawNotification = {\n recipientDid: DidString\n uri: AtUriString\n indexedAt: DatetimeString\n} & (\n | {\n reason: DataplaneNotificationReasonWithSubject\n reasonSubject: AtUriString\n }\n | {\n reason: Exclude<\n DataplaneNotificationReason,\n DataplaneNotificationReasonWithSubject\n >\n reasonSubject: undefined\n }\n)\n\n// NotificationItem contains the raw notification plus computed data from it,\n// that will be used to calculate its grouping.\nexport type NotificationItem = {\n raw: RawNotification\n id: string\n groupingKey: string\n actorDid: DidString\n}\n\n// NotificationGroup is the grouping algorithm’s result: the notification items it grouped together,\n// plus their count and timestamps, before Views builds the lexicon response.\nexport type NotificationGroup = {\n id: string\n kind: AppviewNotificationReason\n groupingKey: string\n actorDids: DidString[]\n itemCount: number\n indexedAt: DatetimeString\n firstIndexedAt: DatetimeString\n isRead: boolean\n items: NotificationItem[]\n}\n\nexport type GroupingResult = {\n groups: NotificationGroup[]\n cursor?: string\n}\n\nexport function parseRawNotification(\n notification: Notification,\n): RawNotification | undefined {\n const { recipientDid, uri, reason, reasonSubject, timestamp } = notification\n if (!recipientDid || !uri || !timestamp) return\n const indexedAt = timestamp.toJson() as string as DatetimeString\n\n switch (reason) {\n case NOTIFICATION_REASON.LIKE:\n case NOTIFICATION_REASON.REPOST:\n case NOTIFICATION_REASON.LIKE_VIA_REPOST:\n case NOTIFICATION_REASON.REPOST_VIA_REPOST:\n case NOTIFICATION_REASON.REPLY:\n case NOTIFICATION_REASON.QUOTE:\n case NOTIFICATION_REASON.STARTERPACK_JOINED:\n if (!reasonSubject) return\n return {\n recipientDid: recipientDid as DidString,\n uri: uri as AtUriString,\n reason,\n reasonSubject: reasonSubject as AtUriString,\n indexedAt,\n }\n case NOTIFICATION_REASON.CONTACT_MATCH:\n case NOTIFICATION_REASON.FOLLOW:\n case NOTIFICATION_REASON.FOLLOW_BACK:\n case NOTIFICATION_REASON.MENTION:\n case NOTIFICATION_REASON.SUBSCRIBED_POST:\n case NOTIFICATION_REASON.UNVERIFIED:\n case NOTIFICATION_REASON.VERIFIED:\n return {\n recipientDid: recipientDid as DidString,\n uri: uri as AtUriString,\n reason,\n reasonSubject: undefined,\n indexedAt,\n }\n default:\n return\n }\n}\n\nexport const buildGroups = (\n items: NotificationItem[],\n limit: number,\n now: number,\n seenAt: number | undefined,\n algorithm: NotificationsV2Algorithm,\n feed = NotificationFeed.ALL,\n): GroupingResult => {\n switch (algorithm) {\n case 'algoLookback':\n return buildAlgoLookbackGroups(items, limit, now, seenAt, feed)\n case 'algoGravity':\n default:\n return buildAlgoGravityGroups(items, limit, now, seenAt, feed)\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { NotificationFeed } from '../../../../../proto/bsky_pb.js';
2
+ import { type DataplaneNotificationReason } from '../constants.js';
3
+ import type { NotificationGroup } from './grouping.js';
4
+ export declare const MAX_GROUP_SIZE = 200;
5
+ export declare const compareNotificationGroupsNewestFirst: (left: NotificationGroup, right: NotificationGroup) => number;
6
+ export declare const canGroupNotification: (kind: DataplaneNotificationReason, feed: NotificationFeed) => boolean;
7
+ export declare const isNotificationRead: (indexedAt: string, seenAt: number | undefined) => boolean;
8
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAClE,OAAO,EACL,KAAK,2BAA2B,EAEjC,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEtD,eAAO,MAAM,cAAc,MAAM,CAAA;AAGjC,eAAO,MAAM,oCAAoC,SACzC,iBAAiB,SAChB,iBAAiB,KACvB,MAAkE,CAAA;AAWrE,eAAO,MAAM,oBAAoB,SACzB,2BAA2B,QAC3B,gBAAgB,KACrB,OAOA,CAAA;AAEH,eAAO,MAAM,kBAAkB,cAClB,MAAM,UACT,MAAM,GAAG,SAAS,YAC+B,CAAA"}
@@ -0,0 +1,20 @@
1
+ import { NotificationFeed } from '../../../../../proto/bsky_pb.js';
2
+ import { NOTIFICATION_REASON, } from '../constants.js';
3
+ export const MAX_GROUP_SIZE = 200;
4
+ // Notification timestamps never collide; no tie-breaker is needed.
5
+ export const compareNotificationGroupsNewestFirst = (left, right) => Date.parse(right.indexedAt) - Date.parse(left.indexedAt);
6
+ const GROUPABLE_KINDS = new Set([
7
+ NOTIFICATION_REASON.FOLLOW,
8
+ NOTIFICATION_REASON.LIKE,
9
+ NOTIFICATION_REASON.LIKE_VIA_REPOST,
10
+ NOTIFICATION_REASON.REPOST,
11
+ NOTIFICATION_REASON.REPOST_VIA_REPOST,
12
+ NOTIFICATION_REASON.SUBSCRIBED_POST,
13
+ ]);
14
+ export const canGroupNotification = (kind, feed) => GROUPABLE_KINDS.has(kind) &&
15
+ !((kind === NOTIFICATION_REASON.FOLLOW &&
16
+ feed === NotificationFeed.FOLLOWERS) ||
17
+ (kind === NOTIFICATION_REASON.SUBSCRIBED_POST &&
18
+ feed === NotificationFeed.ACTIVITY));
19
+ export const isNotificationRead = (indexedAt, seenAt) => seenAt !== undefined && Date.parse(indexedAt) < seenAt;
20
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAClE,OAAO,EAEL,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AAGxB,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAA;AAEjC,mEAAmE;AACnE,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAClD,IAAuB,EACvB,KAAwB,EAChB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAErE,MAAM,eAAe,GAAG,IAAI,GAAG,CAA8B;IAC3D,mBAAmB,CAAC,MAAM;IAC1B,mBAAmB,CAAC,IAAI;IACxB,mBAAmB,CAAC,eAAe;IACnC,mBAAmB,CAAC,MAAM;IAC1B,mBAAmB,CAAC,iBAAiB;IACrC,mBAAmB,CAAC,eAAe;CACpC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAiC,EACjC,IAAsB,EACb,EAAE,CACX,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;IACzB,CAAC,CACC,CAAC,IAAI,KAAK,mBAAmB,CAAC,MAAM;QAClC,IAAI,KAAK,gBAAgB,CAAC,SAAS,CAAC;QACtC,CAAC,IAAI,KAAK,mBAAmB,CAAC,eAAe;YAC3C,IAAI,KAAK,gBAAgB,CAAC,QAAQ,CAAC,CACtC,CAAA;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,SAAiB,EACjB,MAA0B,EAC1B,EAAE,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,MAAM,CAAA","sourcesContent":["import { NotificationFeed } from '../../../../../proto/bsky_pb.js'\nimport {\n type DataplaneNotificationReason,\n NOTIFICATION_REASON,\n} from '../constants.js'\nimport type { NotificationGroup } from './grouping.js'\n\nexport const MAX_GROUP_SIZE = 200\n\n// Notification timestamps never collide; no tie-breaker is needed.\nexport const compareNotificationGroupsNewestFirst = (\n left: NotificationGroup,\n right: NotificationGroup,\n): number => Date.parse(right.indexedAt) - Date.parse(left.indexedAt)\n\nconst GROUPABLE_KINDS = new Set<DataplaneNotificationReason>([\n NOTIFICATION_REASON.FOLLOW,\n NOTIFICATION_REASON.LIKE,\n NOTIFICATION_REASON.LIKE_VIA_REPOST,\n NOTIFICATION_REASON.REPOST,\n NOTIFICATION_REASON.REPOST_VIA_REPOST,\n NOTIFICATION_REASON.SUBSCRIBED_POST,\n])\n\nexport const canGroupNotification = (\n kind: DataplaneNotificationReason,\n feed: NotificationFeed,\n): boolean =>\n GROUPABLE_KINDS.has(kind) &&\n !(\n (kind === NOTIFICATION_REASON.FOLLOW &&\n feed === NotificationFeed.FOLLOWERS) ||\n (kind === NOTIFICATION_REASON.SUBSCRIBED_POST &&\n feed === NotificationFeed.ACTIVITY)\n )\n\nexport const isNotificationRead = (\n indexedAt: string,\n seenAt: number | undefined,\n) => seenAt !== undefined && Date.parse(indexedAt) < seenAt\n"]}
@@ -0,0 +1,3 @@
1
+ import type { GroupingResult, NotificationGroup, NotificationItem } from './grouping.js';
2
+ export declare const buildSpotlight: (items: NotificationItem[], groups: NotificationGroup[], cursor: string | undefined, limit: number, seenAt: number | undefined) => GroupingResult;
3
+ //# sourceMappingURL=spotlight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spotlight.d.ts","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/spotlight.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,eAAe,CAAA;AAsBtB,eAAO,MAAM,cAAc,UAClB,gBAAgB,EAAE,UACjB,iBAAiB,EAAE,UACnB,MAAM,GAAG,SAAS,SACnB,MAAM,UACL,MAAM,GAAG,SAAS,KACzB,cAuEF,CAAA"}
@@ -0,0 +1,114 @@
1
+ import { mapDefined } from '@atproto/common';
2
+ import { AtUri } from '@atproto/syntax';
3
+ import { app } from '../../../../../lexicons/index.js';
4
+ import { APPVIEW_NOTIFICATION_REASON, NOTIFICATION_REASON, } from '../constants.js';
5
+ import { MAX_GROUP_SIZE, compareNotificationGroupsNewestFirst, isNotificationRead, } from './shared.js';
6
+ const MULTI_POST_LIKE_RANGES = [
7
+ { maxItems: 200, requiredPosts: 4 },
8
+ { maxItems: 500, requiredPosts: 8 },
9
+ ];
10
+ export const buildSpotlight = (items, groups, cursor, limit, seenAt) => {
11
+ const noSpotlightPage = { groups, cursor };
12
+ const itemCount = groups.reduce((count, group) => count + group.items.length, 0);
13
+ // Grouping consumes a continuous raw prefix, even when groups interleave.
14
+ // items contains all fetched notifications, but grouping may stop before consuming them all;
15
+ // itemCount counts notifications included in the ordinary groups, and pageItems contains the notifications on the page.
16
+ const pageItems = items.slice(0, itemCount);
17
+ const spotlight = selectMultiPostLikeSpotlight(pageItems);
18
+ if (!spotlight)
19
+ return noSpotlightPage;
20
+ const selectedItems = new Set(spotlight.items);
21
+ const positions = new Map(pageItems.map((item, index) => [item, index]));
22
+ const proposedGroups = mapDefined([
23
+ ...groups.map((group) => ({
24
+ // Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.
25
+ kind: group.kind,
26
+ groupingKey: group.groupingKey,
27
+ items: group.items.filter((item) => !selectedItems.has(item)),
28
+ })),
29
+ spotlight,
30
+ ], (group) => {
31
+ const newest = group.items[0];
32
+ const oldest = group.items.at(-1);
33
+ if (!newest || !oldest)
34
+ return;
35
+ // Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.
36
+ return {
37
+ kind: group.kind,
38
+ groupingKey: group.groupingKey,
39
+ id: newest.id,
40
+ items: group.items,
41
+ actorDids: group.items.map((item) => item.actorDid),
42
+ itemCount: group.items.length,
43
+ indexedAt: newest.raw.indexedAt,
44
+ firstIndexedAt: oldest.raw.indexedAt,
45
+ isRead: isNotificationRead(newest.raw.indexedAt, seenAt),
46
+ };
47
+ }).sort(compareNotificationGroupsNewestFirst);
48
+ let retainedItems = pageItems;
49
+ const extraGroup = proposedGroups[limit];
50
+ if (extraGroup) {
51
+ const cutoff = positions.get(extraGroup.items[0]);
52
+ retainedItems = pageItems.slice(0, cutoff);
53
+ cursor = pageItems[cutoff - 1].raw.indexedAt;
54
+ // Trimming every group at one raw position preserves the page prefix.
55
+ for (const group of proposedGroups) {
56
+ group.items = group.items.filter((item) => positions.get(item) < cutoff);
57
+ group.actorDids = group.items.map((item) => item.actorDid);
58
+ group.itemCount = group.items.length;
59
+ const oldest = group.items.at(-1);
60
+ if (oldest)
61
+ group.firstIndexedAt = oldest.raw.indexedAt;
62
+ }
63
+ }
64
+ const spotlightItems = retainedItems.filter((item) => selectedItems.has(item));
65
+ if (!spotlight.isEligibleAfterTrimming(spotlightItems, retainedItems)) {
66
+ return noSpotlightPage;
67
+ }
68
+ return {
69
+ cursor,
70
+ groups: proposedGroups.filter((group) => group.items.length > 0),
71
+ };
72
+ };
73
+ const selectMultiPostLikeSpotlight = (items) => {
74
+ const range = MULTI_POST_LIKE_RANGES.find(({ maxItems }) => items.length <= maxItems);
75
+ if (!range)
76
+ return;
77
+ // Eligibility uses the ordinary page's volume, including after trimming.
78
+ const { requiredPosts } = range;
79
+ const likesByTopLiker = getLikeNotificationsByTopLiker(items);
80
+ if (likesByTopLiker.length < requiredPosts)
81
+ return;
82
+ const actorDid = likesByTopLiker[0].actorDid;
83
+ return {
84
+ kind: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE,
85
+ groupingKey: actorDid,
86
+ items: likesByTopLiker.slice(0, MAX_GROUP_SIZE),
87
+ isEligibleAfterTrimming: (spotlightItems, retainedItems) =>
88
+ // Check if after trimming it still satisfies the requirements.
89
+ spotlightItems.length >= requiredPosts &&
90
+ getLikeNotificationsByTopLiker(retainedItems)[0]?.actorDid === actorDid,
91
+ };
92
+ };
93
+ const getLikeNotificationsByTopLiker = (items) => {
94
+ const likesByActor = new Map();
95
+ for (const item of items) {
96
+ if (item.raw.reason !== NOTIFICATION_REASON.LIKE ||
97
+ new AtUri(item.raw.reasonSubject).collection !== app.bsky.feed.post.$type) {
98
+ continue;
99
+ }
100
+ const likes = likesByActor.get(item.actorDid);
101
+ if (likes)
102
+ likes.push(item);
103
+ else
104
+ likesByActor.set(item.actorDid, [item]);
105
+ }
106
+ // Each actor likes a post once; insertion order breaks ties by newest like.
107
+ let top = [];
108
+ for (const likes of likesByActor.values()) {
109
+ if (likes.length > top.length)
110
+ top = likes;
111
+ }
112
+ return top;
113
+ };
114
+ //# sourceMappingURL=spotlight.js.map
@@ -0,0 +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 MULTI_POST_LIKE_RANGES = [\n { maxItems: 200, requiredPosts: 4 },\n { maxItems: 500, requiredPosts: 8 },\n]\n\ntype SpotlightCandidate = Pick<\n NotificationGroup,\n 'kind' | 'groupingKey' | 'items'\n> & {\n isEligibleAfterTrimming: (\n spotlightItems: NotificationItem[],\n retainedItems: NotificationItem[],\n ) => boolean\n}\n\nexport const buildSpotlight = (\n items: NotificationItem[],\n groups: NotificationGroup[],\n cursor: string | undefined,\n limit: number,\n seenAt: number | undefined,\n): GroupingResult => {\n const noSpotlightPage = { groups, cursor }\n const itemCount = groups.reduce(\n (count, group) => count + group.items.length,\n 0,\n )\n\n // 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 spotlight = selectMultiPostLikeSpotlight(pageItems)\n if (!spotlight) return noSpotlightPage\n\n const selectedItems = new Set(spotlight.items)\n const positions = new Map(pageItems.map((item, index) => [item, index]))\n const proposedGroups = mapDefined(\n [\n ...groups.map((group) => ({\n // 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 spotlight,\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 // 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 let retainedItems = pageItems\n const extraGroup = proposedGroups[limit]\n if (extraGroup) {\n const cutoff = positions.get(extraGroup.items[0]!)!\n retainedItems = pageItems.slice(0, cutoff)\n cursor = pageItems[cutoff - 1]!.raw.indexedAt\n // Trimming every group at one raw position preserves the page prefix.\n for (const group of proposedGroups) {\n group.items = group.items.filter((item) => positions.get(item)! < cutoff)\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 spotlightItems = retainedItems.filter((item) => selectedItems.has(item))\n if (!spotlight.isEligibleAfterTrimming(spotlightItems, retainedItems)) {\n return noSpotlightPage\n }\n\n return {\n cursor,\n groups: proposedGroups.filter((group) => group.items.length > 0),\n }\n}\n\nconst selectMultiPostLikeSpotlight = (\n items: NotificationItem[],\n): SpotlightCandidate | undefined => {\n const range = MULTI_POST_LIKE_RANGES.find(\n ({ maxItems }) => items.length <= maxItems,\n )\n if (!range) return\n\n // Eligibility uses the ordinary page's volume, including after trimming.\n const { requiredPosts } = range\n const likesByTopLiker = getLikeNotificationsByTopLiker(items)\n if (likesByTopLiker.length < requiredPosts) return\n const actorDid = likesByTopLiker[0]!.actorDid\n return {\n kind: APPVIEW_NOTIFICATION_REASON.MULTI_POST_LIKE,\n groupingKey: actorDid,\n items: likesByTopLiker.slice(0, MAX_GROUP_SIZE),\n isEligibleAfterTrimming: (spotlightItems, retainedItems) =>\n // Check if after trimming it still satisfies the requirements.\n spotlightItems.length >= requiredPosts &&\n getLikeNotificationsByTopLiker(retainedItems)[0]?.actorDid === actorDid,\n }\n}\n\nconst getLikeNotificationsByTopLiker = (\n items: NotificationItem[],\n): NotificationItem[] => {\n const likesByActor = new Map<DidString, NotificationItem[]>()\n for (const item of items) {\n if (\n item.raw.reason !== NOTIFICATION_REASON.LIKE ||\n new AtUri(item.raw.reasonSubject).collection !== app.bsky.feed.post.$type\n ) {\n continue\n }\n const likes = likesByActor.get(item.actorDid)\n if (likes) likes.push(item)\n else likesByActor.set(item.actorDid, [item])\n }\n\n // Each actor likes a post once; insertion order breaks ties by newest like.\n let top: NotificationItem[] = []\n for (const likes of likesByActor.values()) {\n if (likes.length > top.length) top = likes\n }\n return top\n}\n"]}
@@ -1,11 +1,4 @@
1
1
  import { type Server } from '@atproto/xrpc-server';
2
2
  import type { AppContext } from '../../../../context.js';
3
3
  export default function (server: Server, ctx: AppContext): void;
4
- /**
5
- * Applies a configurable delay to the datetime string of a cursor,
6
- * effectively allowing for a delay on listing the notifications.
7
- * This is useful to allow time for services to process notifications
8
- * before they are listed to the user.
9
- */
10
- export declare const delayCursor: (cursorStr: string | undefined, delayMs: number) => string;
11
4
  //# sourceMappingURL=listNotifications.d.ts.map
@@ -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;AAmBxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QAuCvD;AAwBD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,cACX,MAAM,GAAG,SAAS,WACpB,MAAM,KACd,MAMF,CAAA"}
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"}
@@ -3,8 +3,8 @@ import { InvalidRequestError } from '@atproto/xrpc-server';
3
3
  import { app } from '../../../../lexicons/index.js';
4
4
  import { createPipeline, } from '../../../../pipeline.js';
5
5
  import { uriToDid as didFromUri } from '../../../../util/uris.js';
6
- import { isPostRecordType } from '../../../../views/types.js';
7
6
  import { fillPage, resHeaders } from '../../../util.js';
7
+ import { delayCursor, shouldFilterForNeedsReview, shouldFilterHiddenThreadTag, shouldFilterReplyByThreadgate, } from './util.js';
8
8
  export default function (server, ctx) {
9
9
  const listNotifications = createPipeline(skeleton, hydration, noBlockOrMutesOrNeedsFiltering, presentation);
10
10
  server.add(app.bsky.notification.listNotifications, {
@@ -48,21 +48,6 @@ const paginateNotifications = async (opts) => {
48
48
  cursor: res.cursor,
49
49
  };
50
50
  };
51
- /**
52
- * Applies a configurable delay to the datetime string of a cursor,
53
- * effectively allowing for a delay on listing the notifications.
54
- * This is useful to allow time for services to process notifications
55
- * before they are listed to the user.
56
- */
57
- export const delayCursor = (cursorStr, delayMs) => {
58
- const nowMinusDelay = Date.now() - delayMs;
59
- if (cursorStr === undefined)
60
- return new Date(nowMinusDelay).toISOString();
61
- const cursor = new Date(cursorStr).getTime();
62
- if (isNaN(cursor))
63
- return cursorStr;
64
- return new Date(Math.min(cursor, nowMinusDelay)).toISOString();
65
- };
66
51
  const skeleton = async (input) => {
67
52
  const { params, ctx } = input;
68
53
  const originalCursor = params.cursor;
@@ -102,51 +87,14 @@ const noBlockOrMutesOrNeedsFiltering = (input) => {
102
87
  ctx.views.viewerMuteExists(did, hydration)) {
103
88
  return false;
104
89
  }
105
- // Filter out hidden replies only if the viewer owns
106
- // the threadgate and they hid the reply.
107
- if (item.reason === 'reply') {
108
- const post = hydration.posts?.get(uri);
109
- if (post) {
110
- const rootPostUri = isPostRecordType(post.record)
111
- ? post.record.reply?.root.uri
112
- : undefined;
113
- const isRootPostByViewer = rootPostUri && didFromUri(rootPostUri) === params.hydrateCtx?.viewer;
114
- const isHiddenByThreadgate = isRootPostByViewer
115
- ? ctx.views.replyIsHiddenByThreadgate(uri, rootPostUri, hydration)
116
- : false;
117
- if (isHiddenByThreadgate) {
118
- return false;
119
- }
120
- }
90
+ if (shouldFilterReplyByThreadgate(item.reason, uri, params.hydrateCtx.viewer, hydration, ctx.views)) {
91
+ return false;
121
92
  }
122
- // Filter out notifications from users that have thread hide tags and are from people they
123
- // are not following
124
- if (item.reason === 'reply' ||
125
- item.reason === 'quote' ||
126
- item.reason === 'mention') {
127
- const post = hydration.posts?.get(uri);
128
- if (post) {
129
- for (const [tag] of post.tags.entries()) {
130
- if (ctx.cfg.threadTagsHide.has(tag)) {
131
- if (!hydration.profileViewers?.get(did)?.following) {
132
- return false;
133
- }
134
- else {
135
- break;
136
- }
137
- }
138
- }
139
- }
93
+ if (shouldFilterHiddenThreadTag(item.reason, uri, did, hydration, ctx.cfg.threadTagsHide)) {
94
+ return false;
140
95
  }
141
- // Filter out notifications from users that need review unless moots
142
- if (item.reason === 'reply' ||
143
- item.reason === 'quote' ||
144
- item.reason === 'mention' ||
145
- item.reason === 'like' ||
146
- item.reason === 'follow') {
147
- if (!ctx.views.viewerSeesNeedsReview({ did, uri }, hydration)) {
148
- return false;
149
- }
96
+ if (shouldFilterForNeedsReview(item.reason, did, uri, hydration, ctx.views)) {
97
+ return false;
150
98
  }
151
99
  return true;
152
100
  });
@@ -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 { isPostRecordType } from '../../../../views/types.js'\nimport { fillPage, resHeaders } 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\n/**\n * Applies a configurable delay to the datetime string of a cursor,\n * effectively allowing for a delay on listing the notifications.\n * This is useful to allow time for services to process notifications\n * before they are listed to the user.\n */\nexport const delayCursor = (\n cursorStr: string | undefined,\n delayMs: number,\n): string => {\n const nowMinusDelay = Date.now() - delayMs\n if (cursorStr === undefined) return new Date(nowMinusDelay).toISOString()\n const cursor = new Date(cursorStr).getTime()\n if (isNaN(cursor)) return cursorStr\n return new Date(Math.min(cursor, nowMinusDelay)).toISOString()\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 // Filter out hidden replies only if the viewer owns\n // the threadgate and they hid the reply.\n if (item.reason === 'reply') {\n const post = hydration.posts?.get(uri)\n if (post) {\n const rootPostUri = isPostRecordType(post.record)\n ? post.record.reply?.root.uri\n : undefined\n const isRootPostByViewer =\n rootPostUri && didFromUri(rootPostUri) === params.hydrateCtx?.viewer\n const isHiddenByThreadgate = isRootPostByViewer\n ? ctx.views.replyIsHiddenByThreadgate(uri, rootPostUri, hydration)\n : false\n if (isHiddenByThreadgate) {\n return false\n }\n }\n }\n // Filter out notifications from users that have thread hide tags and are from people they\n // are not following\n if (\n item.reason === 'reply' ||\n item.reason === 'quote' ||\n item.reason === 'mention'\n ) {\n const post = hydration.posts?.get(uri)\n if (post) {\n for (const [tag] of post.tags.entries()) {\n if (ctx.cfg.threadTagsHide.has(tag)) {\n if (!hydration.profileViewers?.get(did)?.following) {\n return false\n } else {\n break\n }\n }\n }\n }\n }\n // Filter out notifications from users that need review unless moots\n if (\n item.reason === 'reply' ||\n item.reason === 'quote' ||\n item.reason === 'mention' ||\n item.reason === 'like' ||\n item.reason === 'follow'\n ) {\n if (!ctx.views.viewerSeesNeedsReview({ did, uri }, hydration)) {\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({ 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 +1 @@
1
- {"version":3,"file":"updateSeen.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/updateSeen.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAGxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QA2BvD"}
1
+ {"version":3,"file":"updateSeen.d.ts","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/updateSeen.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAIxD,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,QA2BvD"}
@@ -2,6 +2,7 @@ import { Struct, Timestamp } from '@bufbuild/protobuf';
2
2
  import * as MurmurhashModule from 'murmurhash';
3
3
  const Murmurhash = ((m) => m.default ?? m)(MurmurhashModule);
4
4
  import { app } from '../../../../lexicons/index.js';
5
+ import { MARK_READ_GENERIC } from './constants.js';
5
6
  export default function (server, ctx) {
6
7
  server.add(app.bsky.notification.updateSeen, {
7
8
  auth: ctx.authVerifier.standard,
@@ -18,10 +19,10 @@ export default function (server, ctx) {
18
19
  clientControlled: true,
19
20
  recipientDid: viewer,
20
21
  alwaysDeliver: false,
21
- collapseKey: 'mark-read-generic',
22
+ collapseKey: MARK_READ_GENERIC,
22
23
  timestamp: Timestamp.fromDate(new Date()),
23
24
  additional: Struct.fromJson({
24
- reason: 'mark-read-generic',
25
+ reason: MARK_READ_GENERIC,
25
26
  }),
26
27
  },
27
28
  ],
@@ -31,7 +32,7 @@ export default function (server, ctx) {
31
32
  });
32
33
  }
33
34
  function getNotifId(viewer, seenAt) {
34
- const key = ['mark-read-generic', viewer, seenAt.getTime().toString()].join('::');
35
+ const key = [MARK_READ_GENERIC, viewer, seenAt.getTime().toString()].join('::');
35
36
  return Murmurhash.v3(key).toString(16);
36
37
  }
37
38
  //# sourceMappingURL=updateSeen.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"updateSeen.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/updateSeen.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,gBAAgB,MAAM,YAAY,CAAA;AAC9C,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAA;AAG5D,OAAO,EAAE,GAAG,EAAE,MAAM,+BAA+B,CAAA;AAEnD,MAAM,CAAC,OAAO,WAAW,MAAc,EAAE,GAAe;IACtD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE;QAC3C,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,QAAQ;QAC/B,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAA;YACnC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC1C,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;YAC5C,MAAM,OAAO,CAAC,GAAG,CAAC;gBAChB,GAAG,CAAC,WAAW,CAAC,sBAAsB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;gBACvE,GAAG,CAAC,aAAa,EAAE,iBAAiB,CAAC;oBACnC,aAAa,EAAE;wBACb;4BACE,EAAE,EAAE,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC;4BAC9B,gBAAgB,EAAE,IAAI;4BACtB,YAAY,EAAE,MAAM;4BACpB,aAAa,EAAE,KAAK;4BACpB,WAAW,EAAE,mBAAmB;4BAChC,SAAS,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC;4BACzC,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC;gCAC1B,MAAM,EAAE,mBAAmB;6BAC5B,CAAC;yBACH;qBACF;iBACF,CAAC;aACH,CAAC,CAAA;QACJ,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,MAAc,EAAE,MAAY;IAC9C,MAAM,GAAG,GAAG,CAAC,mBAAmB,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CACzE,IAAI,CACL,CAAA;IAED,OAAO,UAAU,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AACxC,CAAC","sourcesContent":["import { Struct, Timestamp } from '@bufbuild/protobuf'\nimport * as MurmurhashModule from 'murmurhash'\nconst Murmurhash = ((m) => m.default ?? m)(MurmurhashModule)\nimport type { Server } from '@atproto/xrpc-server'\nimport type { AppContext } from '../../../../context.js'\nimport { app } from '../../../../lexicons/index.js'\n\nexport default function (server: Server, ctx: AppContext) {\n server.add(app.bsky.notification.updateSeen, {\n auth: ctx.authVerifier.standard,\n handler: async ({ input, auth }) => {\n const viewer = auth.credentials.iss\n const seenAt = new Date(input.body.seenAt)\n const timestamp = Timestamp.fromDate(seenAt)\n await Promise.all([\n ctx.bsyncClient.fanoutNotificationSeen({ actorDid: viewer, timestamp }),\n ctx.courierClient?.pushNotifications({\n notifications: [\n {\n id: getNotifId(viewer, seenAt),\n clientControlled: true,\n recipientDid: viewer,\n alwaysDeliver: false,\n collapseKey: 'mark-read-generic',\n timestamp: Timestamp.fromDate(new Date()),\n additional: Struct.fromJson({\n reason: 'mark-read-generic',\n }),\n },\n ],\n }),\n ])\n },\n })\n}\n\nfunction getNotifId(viewer: string, seenAt: Date) {\n const key = ['mark-read-generic', viewer, seenAt.getTime().toString()].join(\n '::',\n )\n\n return Murmurhash.v3(key).toString(16)\n}\n"]}
1
+ {"version":3,"file":"updateSeen.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/updateSeen.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,gBAAgB,MAAM,YAAY,CAAA;AAC9C,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAA;AAG5D,OAAO,EAAE,GAAG,EAAE,MAAM,+BAA+B,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAElD,MAAM,CAAC,OAAO,WAAW,MAAc,EAAE,GAAe;IACtD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE;QAC3C,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,QAAQ;QAC/B,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAA;YACnC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC1C,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;YAC5C,MAAM,OAAO,CAAC,GAAG,CAAC;gBAChB,GAAG,CAAC,WAAW,CAAC,sBAAsB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;gBACvE,GAAG,CAAC,aAAa,EAAE,iBAAiB,CAAC;oBACnC,aAAa,EAAE;wBACb;4BACE,EAAE,EAAE,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC;4BAC9B,gBAAgB,EAAE,IAAI;4BACtB,YAAY,EAAE,MAAM;4BACpB,aAAa,EAAE,KAAK;4BACpB,WAAW,EAAE,iBAAiB;4BAC9B,SAAS,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC;4BACzC,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC;gCAC1B,MAAM,EAAE,iBAAiB;6BAC1B,CAAC;yBACH;qBACF;iBACF,CAAC;aACH,CAAC,CAAA;QACJ,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,MAAc,EAAE,MAAY;IAC9C,MAAM,GAAG,GAAG,CAAC,iBAAiB,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CACvE,IAAI,CACL,CAAA;IAED,OAAO,UAAU,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AACxC,CAAC","sourcesContent":["import { Struct, Timestamp } from '@bufbuild/protobuf'\nimport * as MurmurhashModule from 'murmurhash'\nconst Murmurhash = ((m) => m.default ?? m)(MurmurhashModule)\nimport type { Server } from '@atproto/xrpc-server'\nimport type { AppContext } from '../../../../context.js'\nimport { app } from '../../../../lexicons/index.js'\nimport { MARK_READ_GENERIC } from './constants.js'\n\nexport default function (server: Server, ctx: AppContext) {\n server.add(app.bsky.notification.updateSeen, {\n auth: ctx.authVerifier.standard,\n handler: async ({ input, auth }) => {\n const viewer = auth.credentials.iss\n const seenAt = new Date(input.body.seenAt)\n const timestamp = Timestamp.fromDate(seenAt)\n await Promise.all([\n ctx.bsyncClient.fanoutNotificationSeen({ actorDid: viewer, timestamp }),\n ctx.courierClient?.pushNotifications({\n notifications: [\n {\n id: getNotifId(viewer, seenAt),\n clientControlled: true,\n recipientDid: viewer,\n alwaysDeliver: false,\n collapseKey: MARK_READ_GENERIC,\n timestamp: Timestamp.fromDate(new Date()),\n additional: Struct.fromJson({\n reason: MARK_READ_GENERIC,\n }),\n },\n ],\n }),\n ])\n },\n })\n}\n\nfunction getNotifId(viewer: string, seenAt: Date) {\n const key = [MARK_READ_GENERIC, viewer, seenAt.getTime().toString()].join(\n '::',\n )\n\n return Murmurhash.v3(key).toString(16)\n}\n"]}