@headless-lms/core 0.3.0 → 0.4.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 (205) hide show
  1. package/dist/assets/events.d.ts +9 -9
  2. package/dist/assets/index.d.ts +3 -2
  3. package/dist/assets/index.js +2 -1
  4. package/dist/assets/jobs.d.ts +15 -0
  5. package/dist/assets/jobs.js +30 -0
  6. package/dist/assets/jobs.js.map +1 -0
  7. package/dist/assets/ports.d.ts +4 -3
  8. package/dist/assets/service.d.ts +0 -1
  9. package/dist/assets/service.js +0 -4
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js +2 -63
  12. package/dist/automations/actions.js.map +1 -1
  13. package/dist/automations/catalog.d.ts +1 -2
  14. package/dist/automations/catalog.js +69 -62
  15. package/dist/automations/catalog.js.map +1 -1
  16. package/dist/automations/community-derivations.js +4 -4
  17. package/dist/automations/community-derivations.js.map +1 -1
  18. package/dist/automations/community-mail.d.ts +3 -17
  19. package/dist/automations/community-mail.js +1 -21
  20. package/dist/automations/community-mail.js.map +1 -1
  21. package/dist/automations/events.d.ts +0 -10
  22. package/dist/automations/index.d.ts +11 -4
  23. package/dist/automations/index.js +8 -1
  24. package/dist/automations/notifications/debounce.d.ts +25 -0
  25. package/dist/automations/notifications/debounce.js +93 -0
  26. package/dist/automations/notifications/debounce.js.map +1 -0
  27. package/dist/automations/notifications/defaults.d.ts +8 -0
  28. package/dist/automations/notifications/defaults.js +316 -0
  29. package/dist/automations/notifications/defaults.js.map +1 -0
  30. package/dist/automations/notifications/delivery.d.ts +28 -0
  31. package/dist/automations/notifications/delivery.js +88 -0
  32. package/dist/automations/notifications/delivery.js.map +1 -0
  33. package/dist/automations/notifications/processor.d.ts +64 -0
  34. package/dist/automations/notifications/processor.js +270 -0
  35. package/dist/automations/notifications/processor.js.map +1 -0
  36. package/dist/automations/notifications/rules/content.d.ts +6 -0
  37. package/dist/automations/notifications/rules/content.js +39 -0
  38. package/dist/automations/notifications/rules/content.js.map +1 -0
  39. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  40. package/dist/automations/notifications/rules/discussion.js +678 -0
  41. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  42. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  43. package/dist/automations/notifications/rules/entitlements.js +249 -0
  44. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  45. package/dist/automations/notifications/rules/index.d.ts +6 -0
  46. package/dist/automations/notifications/rules/index.js +15 -0
  47. package/dist/automations/notifications/rules/index.js.map +1 -0
  48. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  49. package/dist/automations/notifications/rules/organizations.js +28 -0
  50. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  51. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  52. package/dist/automations/notifications/rules/recipients.js +99 -0
  53. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  54. package/dist/automations/notifications/service.d.ts +27 -0
  55. package/dist/automations/notifications/service.js +45 -0
  56. package/dist/automations/notifications/service.js.map +1 -0
  57. package/dist/automations/notifications/types.d.ts +7 -0
  58. package/dist/automations/notifications/types.js +473 -0
  59. package/dist/automations/notifications/types.js.map +1 -0
  60. package/dist/automations/ports.d.ts +119 -5
  61. package/dist/automations/service.d.ts +10 -13
  62. package/dist/automations/service.js +21 -83
  63. package/dist/automations/service.js.map +1 -1
  64. package/dist/content/events.d.ts +130 -80
  65. package/dist/content/events.js +11 -0
  66. package/dist/content/events.js.map +1 -1
  67. package/dist/content/index.d.ts +6 -4
  68. package/dist/content/index.js +3 -1
  69. package/dist/content/model.d.ts +2 -2
  70. package/dist/content/notification-categories.d.ts +36 -0
  71. package/dist/content/notification-categories.js +64 -0
  72. package/dist/content/notification-categories.js.map +1 -0
  73. package/dist/content/ports.d.ts +25 -6
  74. package/dist/content/service.d.ts +4 -4
  75. package/dist/content/service.js +24 -8
  76. package/dist/content/service.js.map +1 -1
  77. package/dist/content/titles.d.ts +22 -0
  78. package/dist/content/titles.js +69 -0
  79. package/dist/content/titles.js.map +1 -0
  80. package/dist/content/types.d.ts +2 -2
  81. package/dist/discussion/chats.d.ts +9 -4
  82. package/dist/discussion/chats.js +80 -47
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/digests.d.ts +24 -0
  85. package/dist/discussion/digests.js +104 -0
  86. package/dist/discussion/digests.js.map +1 -0
  87. package/dist/discussion/events.d.ts +194 -28
  88. package/dist/discussion/events.js +74 -17
  89. package/dist/discussion/events.js.map +1 -1
  90. package/dist/discussion/index.d.ts +7 -2
  91. package/dist/discussion/index.js +6 -1
  92. package/dist/discussion/jobs.d.ts +16 -0
  93. package/dist/discussion/jobs.js +40 -0
  94. package/dist/discussion/jobs.js.map +1 -0
  95. package/dist/discussion/mentions.d.ts +11 -0
  96. package/dist/discussion/mentions.js +35 -0
  97. package/dist/discussion/mentions.js.map +1 -0
  98. package/dist/discussion/ports.d.ts +107 -24
  99. package/dist/discussion/ports.js.map +1 -1
  100. package/dist/discussion/posts.d.ts +18 -8
  101. package/dist/discussion/posts.js +207 -155
  102. package/dist/discussion/posts.js.map +1 -1
  103. package/dist/discussion/service.d.ts +17 -8
  104. package/dist/discussion/service.js +105 -41
  105. package/dist/discussion/service.js.map +1 -1
  106. package/dist/discussion/subscriptions.d.ts +37 -0
  107. package/dist/discussion/subscriptions.js +99 -0
  108. package/dist/discussion/subscriptions.js.map +1 -0
  109. package/dist/discussion/titles.d.ts +26 -0
  110. package/dist/discussion/titles.js +84 -0
  111. package/dist/discussion/titles.js.map +1 -0
  112. package/dist/entitlements/events.d.ts +20 -20
  113. package/dist/entitlements/index.d.ts +2 -1
  114. package/dist/entitlements/index.js +2 -1
  115. package/dist/entitlements/jobs.d.ts +12 -0
  116. package/dist/entitlements/jobs.js +18 -0
  117. package/dist/entitlements/jobs.js.map +1 -0
  118. package/dist/entitlements/ports.d.ts +7 -3
  119. package/dist/entitlements/service.d.ts +5 -2
  120. package/dist/entitlements/service.js +23 -3
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +30 -0
  123. package/dist/identity/devices.js +107 -0
  124. package/dist/identity/devices.js.map +1 -0
  125. package/dist/identity/events.d.ts +2 -2
  126. package/dist/identity/index.d.ts +5 -4
  127. package/dist/identity/index.js +3 -2
  128. package/dist/identity/model.d.ts +2 -2
  129. package/dist/identity/notification-settings.d.ts +28 -0
  130. package/dist/identity/notification-settings.js +113 -0
  131. package/dist/identity/notification-settings.js.map +1 -0
  132. package/dist/identity/ports.d.ts +78 -54
  133. package/dist/integrations/events.d.ts +3 -3
  134. package/dist/organizations/events.d.ts +32 -32
  135. package/dist/organizations/index.d.ts +2 -1
  136. package/dist/organizations/index.js +2 -1
  137. package/dist/organizations/notification-categories.d.ts +32 -0
  138. package/dist/organizations/notification-categories.js +51 -0
  139. package/dist/organizations/notification-categories.js.map +1 -0
  140. package/dist/organizations/ports.d.ts +3 -0
  141. package/dist/organizations/service.d.ts +2 -0
  142. package/dist/organizations/service.js +7 -0
  143. package/dist/organizations/service.js.map +1 -1
  144. package/dist/progress/events.d.ts +14 -14
  145. package/dist/reporting/member/index.d.ts +2 -2
  146. package/dist/reporting/member/model.d.ts +56 -23
  147. package/dist/reporting/member/ports.d.ts +9 -6
  148. package/dist/reporting/member/service.d.ts +33 -14
  149. package/dist/reporting/member/service.js +138 -60
  150. package/dist/reporting/member/service.js.map +1 -1
  151. package/dist/shared/errors.d.ts +13 -1
  152. package/dist/shared/errors.js +18 -1
  153. package/dist/shared/errors.js.map +1 -1
  154. package/dist/shared/id.d.ts +3 -0
  155. package/dist/shared/id.js +4 -1
  156. package/dist/shared/id.js.map +1 -1
  157. package/dist/shared/mailer.d.ts +11 -1
  158. package/dist/shared/mailer.js +32 -5
  159. package/dist/shared/mailer.js.map +1 -1
  160. package/dist/shared/notification-categories.d.ts +16 -0
  161. package/dist/shared/notification-categories.js +94 -0
  162. package/dist/shared/notification-categories.js.map +1 -0
  163. package/dist/shared/notification-settings.d.ts +14 -0
  164. package/dist/shared/notification-settings.js +61 -0
  165. package/dist/shared/notification-settings.js.map +1 -0
  166. package/dist/shared/ports.d.ts +48 -2
  167. package/dist/shared/settings.d.ts +2 -1
  168. package/dist/shared/settings.js +1 -0
  169. package/dist/shared/settings.js.map +1 -1
  170. package/dist/types/automations.d.ts +8 -2
  171. package/dist/types/content.d.ts +2 -2
  172. package/dist/types/discussion.d.ts +2 -2
  173. package/dist/types/email-templates.d.ts +97 -3
  174. package/dist/types/identity.d.ts +2 -2
  175. package/dist/types/index.d.ts +7 -6
  176. package/dist/types/notifications.d.ts +157 -0
  177. package/dist/types/notifications.js +1 -0
  178. package/dist/types/schemas/automations.d.ts +1 -10
  179. package/dist/types/schemas/automations.js +1 -3
  180. package/dist/types/schemas/automations.js.map +1 -1
  181. package/dist/types/schemas/content.d.ts +62 -1
  182. package/dist/types/schemas/content.js +25 -3
  183. package/dist/types/schemas/content.js.map +1 -1
  184. package/dist/types/schemas/discussion.d.ts +85 -12
  185. package/dist/types/schemas/discussion.js +47 -13
  186. package/dist/types/schemas/discussion.js.map +1 -1
  187. package/dist/types/schemas/identity.d.ts +1 -85
  188. package/dist/types/schemas/identity.js +1 -42
  189. package/dist/types/schemas/identity.js.map +1 -1
  190. package/dist/types/schemas/index.d.ts +6 -5
  191. package/dist/types/schemas/index.js +6 -5
  192. package/dist/types/schemas/notifications.d.ts +95 -0
  193. package/dist/types/schemas/notifications.js +56 -0
  194. package/dist/types/schemas/notifications.js.map +1 -0
  195. package/dist/types/schemas/progress.d.ts +3 -3
  196. package/package.json +1 -1
  197. package/dist/automations/community-notifications.d.ts +0 -16
  198. package/dist/automations/community-notifications.js +0 -84
  199. package/dist/automations/community-notifications.js.map +0 -1
  200. package/dist/automations/seeds.d.ts +0 -17
  201. package/dist/automations/seeds.js +0 -235
  202. package/dist/automations/seeds.js.map +0 -1
  203. package/dist/identity/notifications.d.ts +0 -27
  204. package/dist/identity/notifications.js +0 -101
  205. package/dist/identity/notifications.js.map +0 -1
@@ -0,0 +1,22 @@
1
+ import { Activity } from "../types/schemas/content.js";
2
+ import { SubjectRef } from "../types/notifications.js";
3
+ import "../types/index.js";
4
+ import { Actor } from "../shared/actor.js";
5
+ import { BundlesService, ContentService, ItemTitle, ItemTitleAccess, ItemTitles } from "./ports.js";
6
+ //#region src/content/titles.d.ts
7
+ declare function activityTitle(activity: Pick<Activity, 'settings'>): string;
8
+ type TitleReads = Pick<ContentService, 'getCourse' | 'getActivity' | 'getDownload' | 'getSpace' | 'getCommunity'> & Pick<BundlesService, 'getBundle' | 'listBundleItems'>;
9
+ declare class ContentItemTitles implements ItemTitles {
10
+ private readonly deps;
11
+ constructor(deps: {
12
+ content: TitleReads;
13
+ access: ItemTitleAccess;
14
+ });
15
+ titlesFor(orgId: string, actor: Actor, refs: SubjectRef[]): Promise<Record<string, ItemTitle>>;
16
+ private titleOf;
17
+ private opensCourse;
18
+ private holdsBundle;
19
+ }
20
+ //#endregion
21
+ export { ContentItemTitles, activityTitle };
22
+ //# sourceMappingURL=titles.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { isStaffRole } from "../organizations/roles.js";
2
+ //#region src/content/titles.ts
3
+ function activityTitle(activity) {
4
+ const title = activity.settings?.title;
5
+ return typeof title === "string" ? title : "";
6
+ }
7
+ function isPublishedActivity(activity) {
8
+ return activity.settings?.published !== false;
9
+ }
10
+ var ContentItemTitles = class {
11
+ deps;
12
+ constructor(deps) {
13
+ this.deps = deps;
14
+ }
15
+ async titlesFor(orgId, actor, refs) {
16
+ const unique = new Map(refs.map((ref) => [`${ref.type}:${ref.id}`, ref]));
17
+ const entries = await Promise.all([...unique].map(async ([key, ref]) => {
18
+ const title = await this.titleOf(orgId, actor, ref);
19
+ return title === null ? null : [key, { title }];
20
+ }));
21
+ return Object.fromEntries(entries.filter((entry) => entry !== null));
22
+ }
23
+ async titleOf(orgId, actor, ref) {
24
+ const staff = isStaffRole(actor.role);
25
+ switch (ref.type) {
26
+ case "course": {
27
+ const course = await this.deps.content.getCourse(orgId, ref.id);
28
+ return course && (staff || await this.opensCourse(orgId, actor, course)) ? course.title : null;
29
+ }
30
+ case "activity": {
31
+ const activity = await this.deps.content.getActivity(orgId, ref.id);
32
+ if (!activity) return null;
33
+ if (staff) return activityTitle(activity);
34
+ const course = await this.deps.content.getCourse(orgId, activity.courseId);
35
+ return course && isPublishedActivity(activity) && await this.opensCourse(orgId, actor, course) ? activityTitle(activity) : null;
36
+ }
37
+ case "download": {
38
+ const download = await this.deps.content.getDownload(orgId, ref.id);
39
+ if (!download) return null;
40
+ return staff || download.status === "published" && await this.deps.access.hasAccess(orgId, actor.id, download.id) ? download.title : null;
41
+ }
42
+ case "bundle": {
43
+ const bundle = await this.deps.content.getBundle(orgId, ref.id);
44
+ if (!bundle) return null;
45
+ return staff || await this.holdsBundle(orgId, actor, ref.id) ? bundle.name : null;
46
+ }
47
+ case "community": {
48
+ const community = await this.deps.content.getCommunity(orgId, ref.id);
49
+ return community && await this.deps.access.isCommunityMember(orgId, actor, ref.id) ? community.title : null;
50
+ }
51
+ case "space": {
52
+ const space = await this.deps.content.getSpace(orgId, ref.id);
53
+ return space && await this.deps.access.canOpenSpace(orgId, actor, ref.id) ? space.name : null;
54
+ }
55
+ default: return null;
56
+ }
57
+ }
58
+ async opensCourse(orgId, actor, course) {
59
+ return course.status === "published" && this.deps.access.hasAccess(orgId, actor.id, course.id);
60
+ }
61
+ async holdsBundle(orgId, actor, bundleId) {
62
+ const items = await this.deps.content.listBundleItems(orgId, bundleId);
63
+ return (await Promise.all(items.map((item) => this.deps.access.hasAccess(orgId, actor.id, item.contentId)))).some(Boolean);
64
+ }
65
+ };
66
+ //#endregion
67
+ export { ContentItemTitles, activityTitle };
68
+
69
+ //# sourceMappingURL=titles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"titles.js","names":[],"sources":["../../src/content/titles.ts"],"sourcesContent":["import { isStaffRole } from '../organizations/index.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { Activity, SubjectRef } from '../types/index.js';\nimport type {\n BundlesService,\n ContentService,\n ItemTitle,\n ItemTitleAccess,\n ItemTitles,\n} from './ports.js';\n\nexport function activityTitle(activity: Pick<Activity, 'settings'>): string {\n const title = (activity.settings as { title?: unknown } | null)?.title;\n return typeof title === 'string' ? title : '';\n}\n\nfunction isPublishedActivity(activity: Pick<Activity, 'settings'>): boolean {\n return (activity.settings as { published?: boolean } | null)?.published !== false;\n}\n\ntype TitleReads = Pick<\n ContentService,\n 'getCourse' | 'getActivity' | 'getDownload' | 'getSpace' | 'getCommunity'\n> &\n Pick<BundlesService, 'getBundle' | 'listBundleItems'>;\n\nexport class ContentItemTitles implements ItemTitles {\n constructor(\n private readonly deps: {\n content: TitleReads;\n access: ItemTitleAccess;\n },\n ) {}\n\n async titlesFor(\n orgId: string,\n actor: Actor,\n refs: SubjectRef[],\n ): Promise<Record<string, ItemTitle>> {\n const unique = new Map(refs.map((ref) => [`${ref.type}:${ref.id}`, ref]));\n const entries = await Promise.all(\n [...unique].map(async ([key, ref]): Promise<[string, ItemTitle] | null> => {\n const title = await this.titleOf(orgId, actor, ref);\n return title === null ? null : [key, { title }];\n }),\n );\n return Object.fromEntries(\n entries.filter((entry): entry is [string, ItemTitle] => entry !== null),\n );\n }\n\n private async titleOf(orgId: string, actor: Actor, ref: SubjectRef): Promise<string | null> {\n const staff = isStaffRole(actor.role);\n switch (ref.type) {\n case 'course': {\n const course = await this.deps.content.getCourse(orgId, ref.id);\n return course && (staff || (await this.opensCourse(orgId, actor, course)))\n ? course.title\n : null;\n }\n case 'activity': {\n const activity = await this.deps.content.getActivity(orgId, ref.id);\n if (!activity) {\n return null;\n }\n if (staff) {\n return activityTitle(activity);\n }\n const course = await this.deps.content.getCourse(orgId, activity.courseId);\n return course &&\n isPublishedActivity(activity) &&\n (await this.opensCourse(orgId, actor, course))\n ? activityTitle(activity)\n : null;\n }\n case 'download': {\n const download = await this.deps.content.getDownload(orgId, ref.id);\n if (!download) {\n return null;\n }\n return staff ||\n (download.status === 'published' &&\n (await this.deps.access.hasAccess(orgId, actor.id, download.id)))\n ? download.title\n : null;\n }\n case 'bundle': {\n const bundle = await this.deps.content.getBundle(orgId, ref.id);\n if (!bundle) {\n return null;\n }\n return staff || (await this.holdsBundle(orgId, actor, ref.id)) ? bundle.name : null;\n }\n case 'community': {\n const community = await this.deps.content.getCommunity(orgId, ref.id);\n return community && (await this.deps.access.isCommunityMember(orgId, actor, ref.id))\n ? community.title\n : null;\n }\n case 'space': {\n const space = await this.deps.content.getSpace(orgId, ref.id);\n return space && (await this.deps.access.canOpenSpace(orgId, actor, ref.id))\n ? space.name\n : null;\n }\n default:\n return null;\n }\n }\n\n private async opensCourse(\n orgId: string,\n actor: Actor,\n course: { id: string; status: string },\n ): Promise<boolean> {\n return course.status === 'published' && this.deps.access.hasAccess(orgId, actor.id, course.id);\n }\n\n private async holdsBundle(orgId: string, actor: Actor, bundleId: string): Promise<boolean> {\n const items = await this.deps.content.listBundleItems(orgId, bundleId);\n const held = await Promise.all(\n items.map((item) => this.deps.access.hasAccess(orgId, actor.id, item.contentId)),\n );\n return held.some(Boolean);\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { Page } from "../types/shared.js";
2
- import { AddDownloadAssetInput, CreateBundleInput, CreateCommunityInput, CreateCourseInput, CreateDownloadInput, CreateSpaceInput, ListBundlesQuery, ListCommunitiesQuery, ListCoursesQuery, ListDownloadsQuery, PatchDmSettingsInput, ReorderDownloadAssetsInput, SetSpaceTopicsInput, UpdateBundleInput, UpdateCommunityInput, UpdateCourseInput, UpdateDownloadInput, UpdateSpaceInput } from "../types/schemas/content.js";
2
+ import { AddDownloadAssetInput, CreateBundleInput, CreateCommunityInput, CreateCourseInput, CreateDownloadInput, CreateSpaceInput, ListBundlesQuery, ListCommunitiesQuery, ListCoursesQuery, ListDownloadsQuery, PatchCommunitySettingsInput, PatchDmSettingsInput, ReorderDownloadAssetsInput, SetSpaceTopicsInput, UpdateBundleInput, UpdateCommunityInput, UpdateCourseInput, UpdateDownloadInput, UpdateSpaceInput } from "../types/schemas/content.js";
3
3
  import "../types/index.js";
4
- export type { AddDownloadAssetInput, CreateBundleInput, CreateCommunityInput, CreateCourseInput, CreateDownloadInput, CreateSpaceInput, ListBundlesQuery, ListCommunitiesQuery, ListCoursesQuery, ListDownloadsQuery, Page, PatchDmSettingsInput, ReorderDownloadAssetsInput, SetSpaceTopicsInput, UpdateBundleInput, UpdateCommunityInput, UpdateCourseInput, UpdateDownloadInput, UpdateSpaceInput };
4
+ export type { AddDownloadAssetInput, CreateBundleInput, CreateCommunityInput, CreateCourseInput, CreateDownloadInput, CreateSpaceInput, ListBundlesQuery, ListCommunitiesQuery, ListCoursesQuery, ListDownloadsQuery, Page, PatchCommunitySettingsInput, PatchDmSettingsInput, ReorderDownloadAssetsInput, SetSpaceTopicsInput, UpdateBundleInput, UpdateCommunityInput, UpdateCourseInput, UpdateDownloadInput, UpdateSpaceInput };
@@ -1,6 +1,6 @@
1
1
  import { Page } from "../types/shared.js";
2
2
  import { Logger } from "../types/ports.js";
3
- import { Chat, ChatUser, MessageFlag, MessageFlagResolution, MessageView, ReactionEmoji } from "../types/schemas/discussion.js";
3
+ import { Chat, ChatUser, Message, MessageFlag, MessageFlagResolution, MessageView, ReactionEmoji } from "../types/schemas/discussion.js";
4
4
  import "../types/index.js";
5
5
  import "../shared/ports.js";
6
6
  import { FileUrls } from "../assets/file-urls.js";
@@ -11,7 +11,7 @@ import { CommunityReader } from "../content/ports.js";
11
11
  import "../content/index.js";
12
12
  import { CommunityAccessReader } from "../entitlements/ports.js";
13
13
  import "../entitlements/index.js";
14
- import { AuthorProfileReader, AuthorRoleReader, ChatListItem, ChatRepository, ChatService, ChatUnitOfWork, MessageQueue, MessageReactions, PageQuery, SendMessageInput, StartDirectInput, StartGroupInput } from "./ports.js";
14
+ import { AuthorProfileReader, AuthorRoleReader, ChatListItem, ChatRepository, ChatService, ChatUnitOfWork, ChatUnread, MentionsRepository, MessageQueue, MessageReactions, PageQuery, SendMessageInput, StartDirectInput, StartGroupInput, SubscriptionsRepository } from "./ports.js";
15
15
  //#region src/discussion/chats.d.ts
16
16
  type ChatServiceParams = {
17
17
  repo: ChatRepository;
@@ -19,6 +19,8 @@ type ChatServiceParams = {
19
19
  content: CommunityReader;
20
20
  authors: AuthorRoleReader;
21
21
  profiles: AuthorProfileReader;
22
+ subscriptions: SubscriptionsRepository;
23
+ mentions: MentionsRepository;
22
24
  images: MemberImageReader & Pick<AssetFiles, 'signFile'>;
23
25
  fileUrls: FileUrls;
24
26
  uow: ChatUnitOfWork;
@@ -30,6 +32,8 @@ declare class ChatServiceImpl implements ChatService {
30
32
  private readonly content;
31
33
  private readonly authors;
32
34
  private readonly profiles;
35
+ private readonly subscriptions;
36
+ private readonly mentions;
33
37
  private readonly images;
34
38
  private readonly fileUrls;
35
39
  private readonly uow;
@@ -45,8 +49,6 @@ declare class ChatServiceImpl implements ChatService {
45
49
  deleteMessage(orgId: string, actor: Actor, messageId: string): Promise<MessageView>;
46
50
  setReaction(orgId: string, actor: Actor, messageId: string, emoji: ReactionEmoji | null): Promise<MessageReactions>;
47
51
  markRead(orgId: string, actor: Actor, chatId: string, messageId: string): Promise<void>;
48
- mute(orgId: string, actor: Actor, chatId: string, until?: Date): Promise<void>;
49
- unmute(orgId: string, actor: Actor, chatId: string): Promise<void>;
50
52
  block(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;
51
53
  unblock(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;
52
54
  flagMessage(orgId: string, actor: Actor, messageId: string, reason: string): Promise<MessageFlag>;
@@ -65,6 +67,8 @@ declare class ChatServiceImpl implements ChatService {
65
67
  participants: ChatUser[];
66
68
  } | null>;
67
69
  titlesFor(orgId: string, actor: Actor, chatIds: string[]): Promise<Record<string, string>>;
70
+ unreadInChat(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;
71
+ findMessage(orgId: string, messageId: string): Promise<Message | null>;
68
72
  latestMessageBody(orgId: string, chatId: string): Promise<string | null>;
69
73
  isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;
70
74
  private setLock;
@@ -74,6 +78,7 @@ declare class ChatServiceImpl implements ChatService {
74
78
  private personRole;
75
79
  private personRoles;
76
80
  private assertMaySend;
81
+ private mentionable;
77
82
  private writeMessage;
78
83
  private assertMayMessage;
79
84
  private assertGroup;
@@ -4,6 +4,8 @@ import { noopLogger } from "../shared/logger.js";
4
4
  import { isStaffRole } from "../organizations/roles.js";
5
5
  import { discussionEvents } from "./events.js";
6
6
  import { authorView } from "./authors.js";
7
+ import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
8
+ import { activeSubscription } from "./subscriptions.js";
7
9
  import "./ports.js";
8
10
  //#region src/discussion/chats.ts
9
11
  var ChatServiceImpl = class {
@@ -12,6 +14,8 @@ var ChatServiceImpl = class {
12
14
  content;
13
15
  authors;
14
16
  profiles;
17
+ subscriptions;
18
+ mentions;
15
19
  images;
16
20
  fileUrls;
17
21
  uow;
@@ -22,6 +26,8 @@ var ChatServiceImpl = class {
22
26
  this.content = params.content;
23
27
  this.authors = params.authors;
24
28
  this.profiles = params.profiles;
29
+ this.subscriptions = params.subscriptions;
30
+ this.mentions = params.mentions;
25
31
  this.images = params.images;
26
32
  this.fileUrls = params.fileUrls;
27
33
  this.uow = params.uow;
@@ -39,7 +45,7 @@ var ChatServiceImpl = class {
39
45
  replyToId: null,
40
46
  assetIds: input.assetIds
41
47
  };
42
- const saved = await this.uow.run(async ({ chats, outbox }) => {
48
+ const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
43
49
  await chats.lockDirectPair(orgId, input.communityId, actor.id, input.orgUserId);
44
50
  const existing = await chats.findDirect(orgId, input.communityId, actor.id, input.orgUserId);
45
51
  if (existing) {
@@ -51,11 +57,11 @@ var ChatServiceImpl = class {
51
57
  } : user);
52
58
  if (first) await this.assertMaySend(chats, orgId, actor, existing, present, settings, first);
53
59
  if (rejoin) await chats.join(orgId, this.memberRow(orgId, existing.communityId, existing.id, actor.id, /* @__PURE__ */ new Date()));
54
- const written = first ? await this.writeMessage(chats, orgId, actor, existing, first) : null;
60
+ const written = first ? await this.writeMessage(chats, mentions, orgId, actor, existing, present, first) : null;
55
61
  const chat = written?.chat ?? existing;
56
62
  const events = [];
57
63
  if (rejoin || written?.opened) events.push(this.updatedEvent(orgId, actor, chat, present));
58
- if (written) events.push(this.sentEvent(orgId, actor, written.chat, written.message));
64
+ if (written) events.push(this.sentEvent(orgId, actor, written.chat, written.message, written.mentioned));
59
65
  if (events.length > 0) await outbox.append(events);
60
66
  return {
61
67
  chat,
@@ -83,8 +89,8 @@ var ChatServiceImpl = class {
83
89
  for (const user of users) await chats.join(orgId, user);
84
90
  const events = [this.createdEvent(orgId, actor, chat, users)];
85
91
  if (first) {
86
- const written = await this.writeMessage(chats, orgId, actor, chat, first);
87
- events.push(this.sentEvent(orgId, actor, written.chat, written.message));
92
+ const written = await this.writeMessage(chats, mentions, orgId, actor, chat, users, first);
93
+ events.push(this.sentEvent(orgId, actor, written.chat, written.message, written.mentioned));
88
94
  }
89
95
  await outbox.append(events);
90
96
  return {
@@ -227,14 +233,14 @@ var ChatServiceImpl = class {
227
233
  await this.requireActor(orgId, actor, preview.communityId);
228
234
  const settings = await this.requireSettings(orgId, preview.communityId);
229
235
  if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
230
- const saved = await this.uow.run(async ({ chats, outbox }) => {
236
+ const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
231
237
  const chat = await this.mustChat(chats, orgId, chatId);
232
238
  const users = await chats.participants(orgId, chatId);
233
239
  await this.assertMaySend(chats, orgId, actor, chat, users, settings, input);
234
- const written = await this.writeMessage(chats, orgId, actor, chat, input);
240
+ const written = await this.writeMessage(chats, mentions, orgId, actor, chat, users, input);
235
241
  const events = [];
236
242
  if (written.opened) events.push(this.updatedEvent(orgId, actor, written.chat, users));
237
- events.push(this.sentEvent(orgId, actor, written.chat, written.message));
243
+ events.push(this.sentEvent(orgId, actor, written.chat, written.message, written.mentioned));
238
244
  await outbox.append(events);
239
245
  return written.message;
240
246
  });
@@ -252,11 +258,30 @@ var ChatServiceImpl = class {
252
258
  if (message.orgUserId !== actor.id) throw new ForbiddenError("You can change only your own message");
253
259
  if (message.deletedAt) throw new ConflictError("This message was deleted");
254
260
  if (message.body === body) return this.viewOf(orgId, actor, message);
255
- const saved = await this.repo.updateMessage(orgId, messageId, {
256
- body,
257
- editedAt: /* @__PURE__ */ new Date()
261
+ const now = /* @__PURE__ */ new Date();
262
+ const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
263
+ const next = await chats.updateMessage(orgId, messageId, {
264
+ body,
265
+ editedAt: now
266
+ });
267
+ if (!next) throw new NotFoundError("Message", messageId);
268
+ const chat = await this.mustChat(chats, orgId, next.chatId);
269
+ const users = await chats.participants(orgId, next.chatId);
270
+ const candidates = this.mentionable(body, users);
271
+ const added = await mentions.sync(orgId, "message", messageId, candidates, now);
272
+ await outbox.append([discussionEvents.messageUpdated.make({
273
+ orgId,
274
+ data: {
275
+ ...next,
276
+ mentionedOrgUserIds: keepAllowed(candidates, added)
277
+ },
278
+ metadata: {
279
+ ...this.meta(actor),
280
+ chatStatus: chat.status
281
+ }
282
+ })]);
283
+ return next;
258
284
  });
259
- if (!saved) throw new NotFoundError("Message", messageId);
260
285
  this.logger.info("message edited", {
261
286
  orgId,
262
287
  messageId,
@@ -273,9 +298,10 @@ var ChatServiceImpl = class {
273
298
  }
274
299
  if (message.deletedAt) throw new ConflictError("This message was deleted");
275
300
  const now = /* @__PURE__ */ new Date();
276
- const saved = await this.uow.run(async ({ chats, outbox }) => {
301
+ const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
277
302
  const next = await chats.updateMessage(orgId, messageId, { deletedAt: now });
278
303
  if (!next) throw new NotFoundError("Message", messageId);
304
+ await mentions.removeItem(orgId, "message", messageId);
279
305
  await chats.resolveFlags(orgId, messageId, {
280
306
  resolvedAt: now,
281
307
  resolvedBy: actor.id,
@@ -299,7 +325,20 @@ var ChatServiceImpl = class {
299
325
  const message = await this.mustMessage(this.repo, orgId, messageId);
300
326
  await this.requireActor(orgId, actor, message.communityId);
301
327
  await this.requireActive(this.repo, orgId, message.chatId, actor.id);
302
- await this.repo.setReaction(orgId, message.communityId, messageId, actor.id, emoji);
328
+ await this.uow.run(async ({ chats, outbox }) => {
329
+ if (!await chats.setReaction(orgId, message.communityId, messageId, actor.id, emoji) || emoji === null) return;
330
+ await outbox.append([discussionEvents.messageReactionAdded.make({
331
+ orgId,
332
+ data: {
333
+ messageId,
334
+ chatId: message.chatId,
335
+ communityId: message.communityId,
336
+ orgUserId: actor.id,
337
+ reaction: emoji
338
+ },
339
+ metadata: this.meta(actor)
340
+ })]);
341
+ });
303
342
  this.logger.debug("message reaction set", {
304
343
  orgId,
305
344
  messageId
@@ -318,26 +357,6 @@ var ChatServiceImpl = class {
318
357
  messageId
319
358
  });
320
359
  }
321
- async mute(orgId, actor, chatId, until) {
322
- const chat = await this.mustChat(this.repo, orgId, chatId);
323
- await this.requireActor(orgId, actor, chat.communityId);
324
- await this.requireActive(this.repo, orgId, chat.id, actor.id);
325
- await this.repo.setMuted(orgId, chatId, actor.id, true, until ?? null);
326
- this.logger.debug("chat muted", {
327
- orgId,
328
- chatId
329
- });
330
- }
331
- async unmute(orgId, actor, chatId) {
332
- const chat = await this.mustChat(this.repo, orgId, chatId);
333
- await this.requireActor(orgId, actor, chat.communityId);
334
- await this.requireActive(this.repo, orgId, chat.id, actor.id);
335
- await this.repo.setMuted(orgId, chatId, actor.id, false, null);
336
- this.logger.debug("chat unmuted", {
337
- orgId,
338
- chatId
339
- });
340
- }
341
360
  async block(orgId, actor, communityId, orgUserId) {
342
361
  if (actor.id === orgUserId) throw new InvalidInputError("You cannot block yourself");
343
362
  await this.requireSettings(orgId, communityId);
@@ -434,12 +453,11 @@ var ChatServiceImpl = class {
434
453
  await this.requireActor(orgId, actor, communityId);
435
454
  const now = /* @__PURE__ */ new Date();
436
455
  const page = await this.repo.listForUser(orgId, communityId, actor.id, query);
456
+ const subscribed = await this.subscriptions.forViewer(orgId, actor.id, "chat", page.rows.map((item) => item.chat.id));
457
+ const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
437
458
  const items = page.rows.map((item) => ({
438
459
  ...item,
439
- ...item.muted && item.mutedUntil !== null && item.mutedUntil <= now ? {
440
- muted: false,
441
- mutedUntil: null
442
- } : {},
460
+ subscription: activeSubscription(subscriptionOf.get(item.chat.id), now),
443
461
  lastMessage: item.lastMessage && item.lastMessage.deletedAt !== null && !isStaffRole(actor.role) ? {
444
462
  ...item.lastMessage,
445
463
  body: ""
@@ -526,6 +544,12 @@ var ChatServiceImpl = class {
526
544
  return summary.chat.kind === "group" ? [[summary.chat.id, "Group chat"]] : [];
527
545
  }));
528
546
  }
547
+ unreadInChat(orgId, chatId, orgUserId) {
548
+ return this.repo.unreadFor(orgId, chatId, orgUserId);
549
+ }
550
+ findMessage(orgId, messageId) {
551
+ return this.repo.findMessage(orgId, messageId);
552
+ }
529
553
  async latestMessageBody(orgId, chatId) {
530
554
  return (await this.repo.listMessages(orgId, chatId, {
531
555
  page: 1,
@@ -612,15 +636,21 @@ var ChatServiceImpl = class {
612
636
  }
613
637
  await this.requireOwnedImages(orgId, actor.id, input.assetIds);
614
638
  }
615
- async writeMessage(chats, orgId, actor, chat, input) {
639
+ mentionable(body, users) {
640
+ return keepAllowed(mentionTokens(body), users.filter((user) => user.leftAt === null).map((user) => user.orgUserId));
641
+ }
642
+ async writeMessage(chats, mentions, orgId, actor, chat, users, input) {
616
643
  const opened = chat.kind === "direct" && chat.status === "pending" && chat.createdBy !== actor.id;
617
644
  const current = opened ? await this.mustUpdate(chats, orgId, chat.id, { status: "open" }) : chat;
618
645
  const message = await chats.insertMessage(orgId, this.newMessage(orgId, current, actor, input, /* @__PURE__ */ new Date()));
619
646
  if (input.assetIds.length > 0) await chats.attach(orgId, current.communityId, message.id, input.assetIds);
647
+ const mentioned = this.mentionable(input.body, users);
648
+ await mentions.sync(orgId, "message", message.id, mentioned, message.createdAt);
620
649
  return {
621
650
  chat: current,
622
651
  message,
623
- opened
652
+ opened,
653
+ mentioned
624
654
  };
625
655
  }
626
656
  assertMayMessage(settings, actor, role) {
@@ -686,8 +716,6 @@ var ChatServiceImpl = class {
686
716
  joinedAt: now,
687
717
  leftAt: null,
688
718
  lastReadMessageId: null,
689
- muted: false,
690
- mutedUntil: null,
691
719
  createdAt: now,
692
720
  updatedAt: now
693
721
  };
@@ -745,10 +773,13 @@ var ChatServiceImpl = class {
745
773
  metadata: this.meta(actor)
746
774
  });
747
775
  }
748
- sentEvent(orgId, actor, chat, message) {
776
+ sentEvent(orgId, actor, chat, message, mentionedOrgUserIds) {
749
777
  return discussionEvents.messageSent.make({
750
778
  orgId,
751
- data: message,
779
+ data: {
780
+ ...message,
781
+ mentionedOrgUserIds
782
+ },
752
783
  metadata: {
753
784
  ...this.meta(actor),
754
785
  chatStatus: chat.status
@@ -762,10 +793,11 @@ var ChatServiceImpl = class {
762
793
  async withSenders(orgId, actor, messages) {
763
794
  if (messages.length === 0) return [];
764
795
  const messageIds = messages.map((message) => message.id);
765
- const [records, attached, reactions] = await Promise.all([
796
+ const [records, attached, reactions, mentioned] = await Promise.all([
766
797
  this.profiles.authorsOf(orgId, [...new Set(messages.map((message) => message.orgUserId))]),
767
798
  this.repo.attachmentsOf(orgId, messageIds),
768
- this.repo.reactionsOf(orgId, messageIds, actor.id)
799
+ this.repo.reactionsOf(orgId, messageIds, actor.id),
800
+ mentionViews(this.mentions, this.profiles, orgId, "message", messageIds)
769
801
  ]);
770
802
  const shown = (message) => message.deletedAt !== null && !isStaffRole(actor.role) ? [] : attached[message.id] ?? [];
771
803
  return messages.map((message) => {
@@ -780,7 +812,8 @@ var ChatServiceImpl = class {
780
812
  url: this.fileUrls.messageAttachment(message.chatId, message.id, assetId)
781
813
  })),
782
814
  reactions: counts?.reactions ?? {},
783
- viewerReaction: counts?.viewerReaction ?? null
815
+ viewerReaction: counts?.viewerReaction ?? null,
816
+ mentions: message.deletedAt !== null && !isStaffRole(actor.role) ? [] : mentioned[message.id] ?? []
784
817
  };
785
818
  });
786
819
  }