@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,40 @@
1
+ import { idSchema } from "../types/schemas/shared.js";
2
+ import { noopLogger } from "../shared/logger.js";
3
+ import { z } from "zod";
4
+ //#region src/discussion/jobs.ts
5
+ const POST_PUBLISH_JOB = "discussion.post.publish";
6
+ const DIGEST_OPEN_JOB = "discussion.digest.open";
7
+ const postPublishPayload = z.object({
8
+ orgId: idSchema,
9
+ postId: idSchema
10
+ }).strict();
11
+ function discussionJobs(deps) {
12
+ const logger = deps.logger ?? noopLogger;
13
+ return [{
14
+ job: POST_PUBLISH_JOB,
15
+ async run(payload, fire) {
16
+ const parsed = postPublishPayload.safeParse(payload);
17
+ if (!parsed.success) {
18
+ logger.error("scheduled post payload refused", {
19
+ job: POST_PUBLISH_JOB,
20
+ fireId: fire.id
21
+ });
22
+ return;
23
+ }
24
+ await deps.posts.publishScheduled(parsed.data.orgId, parsed.data.postId, fire.startedAt);
25
+ }
26
+ }, {
27
+ job: DIGEST_OPEN_JOB,
28
+ async run(_payload, fire) {
29
+ await deps.digests.openDue(fire.startedAt);
30
+ }
31
+ }];
32
+ }
33
+ const discussionRecurringJobs = [{
34
+ job: DIGEST_OPEN_JOB,
35
+ cron: "0 * * * *"
36
+ }];
37
+ //#endregion
38
+ export { DIGEST_OPEN_JOB, POST_PUBLISH_JOB, discussionJobs, discussionRecurringJobs };
39
+
40
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.js","names":[],"sources":["../../src/discussion/jobs.ts"],"sourcesContent":["import { z } from 'zod';\nimport { idSchema } from '../types/schemas/index.js';\nimport type { Logger, RecurringJob, ScheduledJob } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { DigestService, PostService } from './ports.js';\n\nexport const POST_PUBLISH_JOB = 'discussion.post.publish';\nexport const DIGEST_OPEN_JOB = 'discussion.digest.open';\n\nconst postPublishPayload = z.object({ orgId: idSchema, postId: idSchema }).strict();\n\nexport interface DiscussionJobDeps {\n posts: Pick<PostService, 'publishScheduled'>;\n digests: Pick<DigestService, 'openDue'>;\n logger?: Logger;\n}\n\nexport function discussionJobs(deps: DiscussionJobDeps): ScheduledJob[] {\n const logger = deps.logger ?? noopLogger;\n return [\n {\n job: POST_PUBLISH_JOB,\n async run(payload, fire) {\n const parsed = postPublishPayload.safeParse(payload);\n if (!parsed.success) {\n logger.error('scheduled post payload refused', {\n job: POST_PUBLISH_JOB,\n fireId: fire.id,\n });\n return;\n }\n await deps.posts.publishScheduled(parsed.data.orgId, parsed.data.postId, fire.startedAt);\n },\n },\n {\n job: DIGEST_OPEN_JOB,\n async run(_payload, fire) {\n await deps.digests.openDue(fire.startedAt);\n },\n },\n ];\n}\n\nexport const discussionRecurringJobs: RecurringJob[] = [\n { job: DIGEST_OPEN_JOB, cron: '0 * * * *' },\n];\n"],"mappings":";;;;AAMA,MAAa,mBAAmB;AAChC,MAAa,kBAAkB;AAE/B,MAAM,qBAAqB,EAAE,OAAO;CAAE,OAAO;CAAU,QAAQ;AAAS,CAAC,CAAC,CAAC,OAAO;AAQlF,SAAgB,eAAe,MAAyC;CACtE,MAAM,SAAS,KAAK,UAAU;CAC9B,OAAO,CACL;EACE,KAAK;EACL,MAAM,IAAI,SAAS,MAAM;GACvB,MAAM,SAAS,mBAAmB,UAAU,OAAO;GACnD,IAAI,CAAC,OAAO,SAAS;IACnB,OAAO,MAAM,kCAAkC;KAC7C,KAAK;KACL,QAAQ,KAAK;IACf,CAAC;IACD;GACF;GACA,MAAM,KAAK,MAAM,iBAAiB,OAAO,KAAK,OAAO,OAAO,KAAK,QAAQ,KAAK,SAAS;EACzF;CACF,GACA;EACE,KAAK;EACL,MAAM,IAAI,UAAU,MAAM;GACxB,MAAM,KAAK,QAAQ,QAAQ,KAAK,SAAS;EAC3C;CACF,CACF;AACF;AAEA,MAAa,0BAA0C,CACrD;CAAE,KAAK;CAAiB,MAAM;AAAY,CAC5C"}
@@ -0,0 +1,11 @@
1
+ import { MentionItemType, MentionView } from "../types/schemas/discussion.js";
2
+ import "../types/index.js";
3
+ import { AuthorProfileReader, MentionsRepository } from "./ports.js";
4
+ //#region src/discussion/mentions.d.ts
5
+ declare function mentionTokens(...texts: string[]): string[];
6
+ declare function nameMentions(text: string, nameOf: (orgUserId: string) => Promise<string>): Promise<string>;
7
+ declare function keepAllowed(tokens: string[], allowed: string[]): string[];
8
+ declare function mentionViews(mentions: Pick<MentionsRepository, 'forItems'>, profiles: AuthorProfileReader, orgId: string, itemType: MentionItemType, itemIds: string[]): Promise<Record<string, MentionView[]>>;
9
+ //#endregion
10
+ export { keepAllowed, mentionTokens, mentionViews, nameMentions };
11
+ //# sourceMappingURL=mentions.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { authorView } from "./authors.js";
2
+ //#region src/discussion/mentions.ts
3
+ const TOKEN = /<@([A-Za-z0-9_-]+)>/g;
4
+ const UNNAMED = "A member";
5
+ function mentionTokens(...texts) {
6
+ const found = /* @__PURE__ */ new Set();
7
+ for (const text of texts) for (const match of text.matchAll(TOKEN)) found.add(match[1]);
8
+ return [...found];
9
+ }
10
+ async function nameMentions(text, nameOf) {
11
+ const names = /* @__PURE__ */ new Map();
12
+ for (const orgUserId of mentionTokens(text)) names.set(orgUserId, await nameOf(orgUserId));
13
+ return text.replace(TOKEN, (_token, orgUserId) => names.get(orgUserId) ?? UNNAMED);
14
+ }
15
+ function keepAllowed(tokens, allowed) {
16
+ const kept = new Set(allowed);
17
+ return tokens.filter((token) => kept.has(token));
18
+ }
19
+ async function mentionViews(mentions, profiles, orgId, itemType, itemIds) {
20
+ if (itemIds.length === 0) return {};
21
+ const byItem = await mentions.forItems(orgId, itemType, [...new Set(itemIds)]);
22
+ const people = [...new Set(Object.values(byItem).flat())];
23
+ const records = people.length > 0 ? await profiles.authorsOf(orgId, people) : {};
24
+ return Object.fromEntries(Object.entries(byItem).map(([itemId, orgUserIds]) => [itemId, orgUserIds.flatMap((orgUserId) => {
25
+ const record = records[orgUserId];
26
+ return record ? [{
27
+ orgUserId,
28
+ name: authorView(orgId, record, null).displayName || UNNAMED
29
+ }] : [];
30
+ })]));
31
+ }
32
+ //#endregion
33
+ export { keepAllowed, mentionTokens, mentionViews, nameMentions };
34
+
35
+ //# sourceMappingURL=mentions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mentions.js","names":[],"sources":["../../src/discussion/mentions.ts"],"sourcesContent":["import type { MentionItemType, MentionView } from '../types/index.js';\nimport { authorView } from './authors.js';\nimport type { AuthorProfileReader, MentionsRepository } from './ports.js';\n\nconst TOKEN = /<@([A-Za-z0-9_-]+)>/g;\nconst UNNAMED = 'A member';\n\nexport function mentionTokens(...texts: string[]): string[] {\n const found = new Set<string>();\n for (const text of texts) {\n for (const match of text.matchAll(TOKEN)) {\n found.add(match[1]!);\n }\n }\n return [...found];\n}\n\nexport async function nameMentions(\n text: string,\n nameOf: (orgUserId: string) => Promise<string>,\n): Promise<string> {\n const names = new Map<string, string>();\n for (const orgUserId of mentionTokens(text)) {\n names.set(orgUserId, await nameOf(orgUserId));\n }\n return text.replace(TOKEN, (_token, orgUserId: string) => names.get(orgUserId) ?? UNNAMED);\n}\n\nexport function keepAllowed(tokens: string[], allowed: string[]): string[] {\n const kept = new Set(allowed);\n return tokens.filter((token) => kept.has(token));\n}\n\nexport async function mentionViews(\n mentions: Pick<MentionsRepository, 'forItems'>,\n profiles: AuthorProfileReader,\n orgId: string,\n itemType: MentionItemType,\n itemIds: string[],\n): Promise<Record<string, MentionView[]>> {\n if (itemIds.length === 0) {\n return {};\n }\n const byItem = await mentions.forItems(orgId, itemType, [...new Set(itemIds)]);\n const people = [...new Set(Object.values(byItem).flat())];\n const records = people.length > 0 ? await profiles.authorsOf(orgId, people) : {};\n return Object.fromEntries(\n Object.entries(byItem).map(([itemId, orgUserIds]) => [\n itemId,\n orgUserIds.flatMap((orgUserId) => {\n const record = records[orgUserId];\n return record\n ? [{ orgUserId, name: authorView(orgId, record, null).displayName || UNNAMED }]\n : [];\n }),\n ]),\n );\n}\n"],"mappings":";;AAIA,MAAM,QAAQ;AACd,MAAM,UAAU;AAEhB,SAAgB,cAAc,GAAG,OAA2B;CAC1D,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,QAAQ,OACjB,KAAK,MAAM,SAAS,KAAK,SAAS,KAAK,GACrC,MAAM,IAAI,MAAM,EAAG;CAGvB,OAAO,CAAC,GAAG,KAAK;AAClB;AAEA,eAAsB,aACpB,MACA,QACiB;CACjB,MAAM,wBAAQ,IAAI,IAAoB;CACtC,KAAK,MAAM,aAAa,cAAc,IAAI,GACxC,MAAM,IAAI,WAAW,MAAM,OAAO,SAAS,CAAC;CAE9C,OAAO,KAAK,QAAQ,QAAQ,QAAQ,cAAsB,MAAM,IAAI,SAAS,KAAK,OAAO;AAC3F;AAEA,SAAgB,YAAY,QAAkB,SAA6B;CACzE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,OAAO,QAAQ,UAAU,KAAK,IAAI,KAAK,CAAC;AACjD;AAEA,eAAsB,aACpB,UACA,UACA,OACA,UACA,SACwC;CACxC,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;CAEV,MAAM,SAAS,MAAM,SAAS,SAAS,OAAO,UAAU,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,CAAC;CAC7E,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;CACxD,MAAM,UAAU,OAAO,SAAS,IAAI,MAAM,SAAS,UAAU,OAAO,MAAM,IAAI,CAAC;CAC/E,OAAO,OAAO,YACZ,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,QAAQ,gBAAgB,CACnD,QACA,WAAW,SAAS,cAAc;EAChC,MAAM,SAAS,QAAQ;EACvB,OAAO,SACH,CAAC;GAAE;GAAW,MAAM,WAAW,OAAO,QAAQ,IAAI,CAAC,CAAC,eAAe;EAAQ,CAAC,IAC5E,CAAC;CACP,CAAC,CACH,CAAC,CACH;AACF"}
@@ -1,11 +1,13 @@
1
1
  import { JsonValue, Page } from "../types/shared.js";
2
2
  import { Activity, SpaceDetail } from "../types/schemas/content.js";
3
3
  import { Role } from "../types/schemas/organizations.js";
4
- import { Chat, ChatLastMessage, ChatUser, CommentListItem, CommentView, CommentsConfig, FlaggedComment, ListCommentsQuery, Message, MessageFlag, MessageFlagResolution, MessageView, Post, PostFlag, PostFlagResolution, PostStatus, PostView, ReactionCounts, ReactionEmoji } from "../types/schemas/discussion.js";
4
+ import { Chat, ChatLastMessage, ChatUser, CommentListItem, CommentView, CommentsConfig, DigestRun, FlaggedComment, ListCommentsQuery, MentionItemType, Message, MessageFlag, MessageFlagResolution, MessageView, Post, PostFlag, PostFlagResolution, PostStatus, PostView, ReactionCounts, ReactionEmoji, Subscription, SubscriptionItemType, SubscriptionView } from "../types/schemas/discussion.js";
5
5
  import { Comment, CommentReport } from "../types/discussion.js";
6
+ import { SubjectRef } from "../types/notifications.js";
6
7
  import "../types/index.js";
7
8
  import { OutboxAppender, UnitOfWork } from "../shared/ports.js";
8
9
  import { Actor } from "../shared/actor.js";
10
+ import { CommunityDigest } from "../content/ports.js";
9
11
  import "../content/index.js";
10
12
  import "../organizations/index.js";
11
13
  import "./model.js";
@@ -51,6 +53,13 @@ interface AuthorRecord {
51
53
  imageAssetOrgId: string | null;
52
54
  imageAssetId: string | null;
53
55
  }
56
+ interface ItemTitle {
57
+ title: string;
58
+ excerpt?: string;
59
+ }
60
+ interface ItemTitles {
61
+ titlesFor(orgId: string, actor: Actor, refs: SubjectRef[]): Promise<Record<string, ItemTitle>>;
62
+ }
54
63
  interface AuthorProfileReader {
55
64
  authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>>;
56
65
  }
@@ -88,7 +97,11 @@ interface DiscussionService {
88
97
  reportComment(orgId: string, commentId: string, actor: Actor, reason: string): Promise<CommentReport>;
89
98
  /** Plain read of one comment — no rules applied. */
90
99
  getComment(orgId: string, commentId: string): Promise<Comment | null>;
91
- sendDigest(orgId: string, now: Date): Promise<void>;
100
+ commenterIds(orgId: string, target: {
101
+ postId: string;
102
+ } | {
103
+ activityId: string;
104
+ }): Promise<string[]>;
92
105
  }
93
106
  interface ContentAccessReader {
94
107
  hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean>;
@@ -99,13 +112,18 @@ interface ActivityGetter {
99
112
  interface DiscussionRepository extends PostCommentReader, AuthorProfileReader {
100
113
  insertComment(orgId: string, comment: Comment): Promise<Comment>;
101
114
  findComment(orgId: string, id: string): Promise<Comment | null>;
115
+ commenterIds(orgId: string, target: {
116
+ postId: string;
117
+ } | {
118
+ activityId: string;
119
+ }): Promise<string[]>;
102
120
  updateComment(orgId: string, id: string, patch: Partial<Pick<Comment, 'body' | 'status' | 'removedBy' | 'updatedAt'>>): Promise<Comment | null>;
103
121
  /** Every comment on the activity, oldest first, including removed ones. */
104
122
  listByActivity(orgId: string, activityId: string): Promise<Comment[]>;
105
123
  /** A page of the post's top-level threads the viewer can see, oldest first, with their replies. `total` counts threads. */
106
124
  listThreadsByPost(orgId: string, postId: string, query: PostThreadsQuery): Promise<Page<Comment>>;
107
125
  reactionsOf(orgId: string, commentIds: string[], viewerId: string): Promise<Record<string, CommentReactions>>;
108
- setReaction(orgId: string, commentId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<void>;
126
+ setReaction(orgId: string, commentId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
109
127
  /** Returns null when this person has already reported this comment. */
110
128
  insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null>;
111
129
  /** Every unresolved report against the given comments. */
@@ -121,6 +139,7 @@ interface DiscussionRepository extends PostCommentReader, AuthorProfileReader {
121
139
  * entry commit in one transaction. */
122
140
  interface DiscussionWriteScope {
123
141
  discussion: DiscussionRepository;
142
+ mentions: MentionsRepository;
124
143
  outbox: OutboxAppender;
125
144
  }
126
145
  type DiscussionUnitOfWork = UnitOfWork<DiscussionWriteScope>;
@@ -175,12 +194,22 @@ interface PostCommentReader {
175
194
  listOpenFlagsForSpace(orgId: string, spaceId: string, limit: number): Promise<FlaggedComment[]>;
176
195
  publishedCommentCounts(orgId: string, postIds: string[]): Promise<Record<string, number>>;
177
196
  }
197
+ interface PublishedPost {
198
+ postId: string;
199
+ spaceId: string;
200
+ title: string;
201
+ orgUserId: string;
202
+ commentCount: number;
203
+ }
178
204
  interface PostService {
179
205
  visibleTitles(orgId: string, actor: Actor, postIds: string[]): Promise<Record<string, string>>;
206
+ publishedBetween(orgId: string, communityId: string, from: Date, to: Date): Promise<PublishedPost[]>;
180
207
  create(orgId: string, actor: Actor, input: CreatePostInput): Promise<PostView>;
181
208
  update(orgId: string, postId: string, actor: Actor, patch: UpdatePostInput): Promise<PostView>;
182
209
  submit(orgId: string, postId: string, actor: Actor): Promise<PostView>;
183
210
  schedule(orgId: string, postId: string, actor: Actor, scheduledAt: Date): Promise<PostView>;
211
+ unschedule(orgId: string, postId: string, actor: Actor): Promise<PostView>;
212
+ publishScheduled(orgId: string, postId: string, now: Date): Promise<void>;
184
213
  approve(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
185
214
  remove(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
186
215
  restore(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
@@ -190,8 +219,6 @@ interface PostService {
190
219
  openComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;
191
220
  setReactions(orgId: string, postId: string, actor: Actor, enabled: boolean): Promise<PostView>;
192
221
  setReaction(orgId: string, postId: string, actor: Actor, emoji: ReactionEmoji | null): Promise<PostReactions>;
193
- follow(orgId: string, postId: string, actor: Actor): Promise<void>;
194
- unfollow(orgId: string, postId: string, actor: Actor): Promise<void>;
195
222
  bookmark(orgId: string, postId: string, actor: Actor): Promise<void>;
196
223
  unbookmark(orgId: string, postId: string, actor: Actor): Promise<void>;
197
224
  flag(orgId: string, postId: string, actor: Actor, reason: string): Promise<PostFlag>;
@@ -208,29 +235,26 @@ interface PostService {
208
235
  space: SpaceDetail;
209
236
  }>;
210
237
  find(orgId: string, postId: string): Promise<Post | null>;
211
- followerIds(orgId: string, postId: string): Promise<string[]>;
212
238
  moderationQueue(orgId: string, actor: Actor, communityId: string, spaceId: string): Promise<SpaceModerationQueue>;
213
- publishDue(orgId: string, now: Date): Promise<void>;
214
- hasDue(orgId: string, now: Date): Promise<boolean>;
239
+ }
240
+ interface PostGuard {
241
+ status: PostStatus;
242
+ scheduledAt?: Date;
243
+ dueBy?: Date;
215
244
  }
216
245
  interface PostRepository {
217
246
  insert(orgId: string, post: Post): Promise<Post>;
218
247
  update(orgId: string, id: string, patch: PostPatch): Promise<Post | null>;
248
+ updateIf(orgId: string, id: string, guard: PostGuard, patch: PostPatch): Promise<Post | null>;
219
249
  find(orgId: string, id: string): Promise<Post | null>;
220
250
  findMany(orgId: string, ids: string[]): Promise<Post[]>;
251
+ publishedBetween(orgId: string, communityId: string, from: Date, to: Date): Promise<PublishedPost[]>;
221
252
  listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>>;
222
253
  listByAuthor(orgId: string, orgUserId: string, spaceIds: string[], query: PageQuery): Promise<Page<Post>>;
223
254
  listBookmarked(orgId: string, orgUserId: string, spaceIds: string[], query: BookmarkQuery): Promise<Page<Post>>;
224
255
  listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]>;
225
- /** Due scheduled posts in id order, after `afterId` when given. */
226
- listDue(orgId: string, now: Date, limit: number, afterId?: string): Promise<Post[]>;
227
- hasDue(orgId: string, now: Date): Promise<boolean>;
228
256
  reactionsOf(orgId: string, postIds: string[], viewerId: string): Promise<Record<string, PostReactions>>;
229
- setReaction(orgId: string, postId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<void>;
230
- follow(orgId: string, postId: string, orgUserId: string): Promise<void>;
231
- unfollow(orgId: string, postId: string, orgUserId: string): Promise<void>;
232
- followerIds(orgId: string, postId: string): Promise<string[]>;
233
- followedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;
257
+ setReaction(orgId: string, postId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
234
258
  bookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
235
259
  unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
236
260
  bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;
@@ -245,9 +269,25 @@ interface PostRepository {
245
269
  }
246
270
  interface PostWriteScope {
247
271
  posts: PostRepository;
272
+ mentions: MentionsRepository;
248
273
  outbox: OutboxAppender;
249
274
  }
250
275
  type PostUnitOfWork = UnitOfWork<PostWriteScope>;
276
+ interface DigestScheduleReader {
277
+ communitiesWithDigest(): Promise<CommunityDigest[]>;
278
+ }
279
+ interface DigestRunRepository {
280
+ listRuns(): Promise<DigestRun[]>;
281
+ advance(orgId: string, communityId: string, dueAt: Date): Promise<boolean>;
282
+ }
283
+ interface DigestWriteScope {
284
+ digests: DigestRunRepository;
285
+ outbox: OutboxAppender;
286
+ }
287
+ type DigestUnitOfWork = UnitOfWork<DigestWriteScope>;
288
+ interface DigestService {
289
+ openDue(now: Date): Promise<void>;
290
+ }
251
291
  interface StartDirectInput {
252
292
  communityId: string;
253
293
  orgUserId: string;
@@ -267,11 +307,19 @@ interface ChatListItem {
267
307
  chat: Chat;
268
308
  participantIds: string[];
269
309
  lastReadMessageId: string | null;
270
- muted: boolean;
271
- mutedUntil: Date | null;
310
+ subscription: SubscriptionView | null;
272
311
  unread: number;
273
312
  lastMessage: ChatLastMessage | null;
274
313
  }
314
+ type ChatInboxRow = Omit<ChatListItem, 'subscription'>;
315
+ interface ChatUnread {
316
+ count: number;
317
+ latest: {
318
+ messageId: string;
319
+ senderOrgUserId: string;
320
+ body: string;
321
+ } | null;
322
+ }
275
323
  interface MessageReactions {
276
324
  reactions: ReactionCounts;
277
325
  viewerReaction?: ReactionEmoji;
@@ -300,8 +348,6 @@ interface ChatService {
300
348
  deleteMessage(orgId: string, actor: Actor, messageId: string): Promise<MessageView>;
301
349
  setReaction(orgId: string, actor: Actor, messageId: string, emoji: ReactionEmoji | null): Promise<MessageReactions>;
302
350
  markRead(orgId: string, actor: Actor, chatId: string, messageId: string): Promise<void>;
303
- mute(orgId: string, actor: Actor, chatId: string, until?: Date): Promise<void>;
304
- unmute(orgId: string, actor: Actor, chatId: string): Promise<void>;
305
351
  block(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;
306
352
  unblock(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;
307
353
  flagMessage(orgId: string, actor: Actor, messageId: string, reason: string): Promise<MessageFlag>;
@@ -314,6 +360,8 @@ interface ChatService {
314
360
  participants: ChatUser[];
315
361
  } | null>;
316
362
  latestMessageBody(orgId: string, chatId: string): Promise<string | null>;
363
+ unreadInChat(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;
364
+ findMessage(orgId: string, messageId: string): Promise<Message | null>;
317
365
  isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;
318
366
  titlesFor(orgId: string, actor: Actor, chatIds: string[]): Promise<Record<string, string>>;
319
367
  messages(orgId: string, actor: Actor, chatId: string, query: Pick<FeedQuery, 'page' | 'pageSize'>): Promise<Page<MessageView>>;
@@ -328,12 +376,12 @@ interface ChatRepository {
328
376
  insertChat(orgId: string, chat: Chat): Promise<Chat>;
329
377
  updateChat(orgId: string, id: string, patch: Partial<Pick<Chat, 'status' | 'lockedAt' | 'lockedBy'>>): Promise<Chat | null>;
330
378
  findChat(orgId: string, id: string): Promise<Chat | null>;
331
- listForUser(orgId: string, communityId: string, orgUserId: string, query: PageQuery): Promise<Page<ChatListItem>>;
379
+ listForUser(orgId: string, communityId: string, orgUserId: string, query: PageQuery): Promise<Page<ChatInboxRow>>;
332
380
  participants(orgId: string, chatId: string): Promise<ChatUser[]>;
333
381
  join(orgId: string, user: ChatUser): Promise<void>;
334
382
  markLeft(orgId: string, chatId: string, orgUserId: string, leftAt: Date): Promise<void>;
335
383
  setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<void>;
336
- setMuted(orgId: string, chatId: string, orgUserId: string, muted: boolean, mutedUntil: Date | null): Promise<void>;
384
+ unreadFor(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;
337
385
  insertMessage(orgId: string, message: Message): Promise<Message>;
338
386
  updateMessage(orgId: string, id: string, patch: Partial<Pick<Message, 'body' | 'editedAt' | 'deletedAt'>>): Promise<Message | null>;
339
387
  findMessage(orgId: string, id: string): Promise<Message | null>;
@@ -341,7 +389,7 @@ interface ChatRepository {
341
389
  attach(orgId: string, communityId: string, messageId: string, assetIds: string[]): Promise<void>;
342
390
  attachmentsOf(orgId: string, messageIds: string[]): Promise<Record<string, string[]>>;
343
391
  blockedWith(orgId: string, communityId: string, orgUserId: string, otherIds: string[]): Promise<string[]>;
344
- setReaction(orgId: string, communityId: string, messageId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<void>;
392
+ setReaction(orgId: string, communityId: string, messageId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
345
393
  reactionsOf(orgId: string, messageIds: string[], viewerId: string): Promise<Record<string, MessageReactions>>;
346
394
  block(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
347
395
  unblock(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
@@ -365,9 +413,44 @@ interface ChatRepository {
365
413
  }
366
414
  interface ChatWriteScope {
367
415
  chats: ChatRepository;
416
+ mentions: MentionsRepository;
368
417
  outbox: OutboxAppender;
369
418
  }
370
419
  type ChatUnitOfWork = UnitOfWork<ChatWriteScope>;
420
+ type SubscriptionInput = {
421
+ state: 'following';
422
+ } | {
423
+ state: 'muted';
424
+ mutedUntil?: Date | null;
425
+ } | {
426
+ state: 'none';
427
+ };
428
+ type MuteInput = Exclude<SubscriptionInput, {
429
+ state: 'following';
430
+ }>;
431
+ interface SubscriptionService {
432
+ setPost(orgId: string, actor: Actor, postId: string, input: SubscriptionInput): Promise<void>;
433
+ setSpace(orgId: string, actor: Actor, spaceId: string, input: MuteInput): Promise<void>;
434
+ setChat(orgId: string, actor: Actor, chatId: string, input: MuteInput): Promise<void>;
435
+ viewerSubscriptions(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemIds: string[], at: Date): Promise<Record<string, SubscriptionView>>;
436
+ followerIds(orgId: string, postId: string): Promise<string[]>;
437
+ mutedAmong(orgId: string, item: SubjectRef, orgUserIds: string[], at: Date): Promise<string[]>;
438
+ itemSubscriptions(orgId: string, item: SubjectRef): Promise<Subscription[]>;
439
+ }
440
+ interface SubscriptionsRepository {
441
+ upsert(orgId: string, subscription: Subscription): Promise<void>;
442
+ remove(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemId: string): Promise<void>;
443
+ forViewer(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemIds: string[]): Promise<Subscription[]>;
444
+ ofItem(orgId: string, itemType: SubscriptionItemType, itemId: string): Promise<Subscription[]>;
445
+ followerIds(orgId: string, postId: string): Promise<string[]>;
446
+ mutedAmong(orgId: string, itemType: SubscriptionItemType, itemId: string, orgUserIds: string[], at: Date): Promise<string[]>;
447
+ }
448
+ interface MentionsRepository {
449
+ ofItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<string[]>;
450
+ forItems(orgId: string, itemType: MentionItemType, itemIds: string[]): Promise<Record<string, string[]>>;
451
+ sync(orgId: string, itemType: MentionItemType, itemId: string, orgUserIds: string[], at: Date): Promise<string[]>;
452
+ removeItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<void>;
453
+ }
371
454
  //#endregion
372
- export { ActivityComments, ActivityGetter, type Actor, AuthorProfileReader, AuthorRecord, AuthorRoleReader, BookmarkQuery, ChatListItem, ChatRepository, ChatService, ChatSummary, ChatUnitOfWork, ChatWriteScope, CommentReactions, CommentWithContext, ContentAccessReader, CreatePostInput, DiscussionRepository, DiscussionService, DiscussionUnitOfWork, DiscussionWriteScope, FeedQuery, MODERATION_QUEUE_LIMIT, MessageQueue, MessageQueueEntry, MessageReactions, PageQuery, PostCommentInput, PostCommentReader, PostMode, PostPatch, PostReactions, PostRepository, PostService, PostThreadsQuery, PostUnitOfWork, PostWriteScope, SendMessageInput, SpaceModerationQueue, StartDirectInput, StartGroupInput, UpdatePostInput };
455
+ export { ActivityComments, ActivityGetter, type Actor, AuthorProfileReader, AuthorRecord, AuthorRoleReader, BookmarkQuery, ChatInboxRow, ChatListItem, ChatRepository, ChatService, ChatSummary, ChatUnitOfWork, ChatUnread, ChatWriteScope, CommentReactions, CommentWithContext, ContentAccessReader, CreatePostInput, DigestRunRepository, DigestScheduleReader, DigestService, DigestUnitOfWork, DigestWriteScope, DiscussionRepository, DiscussionService, DiscussionUnitOfWork, DiscussionWriteScope, FeedQuery, ItemTitle, ItemTitles, MODERATION_QUEUE_LIMIT, MentionsRepository, MessageQueue, MessageQueueEntry, MessageReactions, MuteInput, PageQuery, PostCommentInput, PostCommentReader, PostGuard, PostMode, PostPatch, PostReactions, PostRepository, PostService, PostThreadsQuery, PostUnitOfWork, PostWriteScope, PublishedPost, SendMessageInput, SpaceModerationQueue, StartDirectInput, StartGroupInput, SubscriptionInput, SubscriptionService, SubscriptionsRepository, UpdatePostInput };
373
456
  //# sourceMappingURL=ports.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ports.js","names":[],"sources":["../../src/discussion/ports.ts"],"sourcesContent":["// discussion context — ports.\n// Inbound: the use cases the service implements.\n// Outbound: the persistence contract the repository fulfils.\nimport type {\n Activity,\n Chat,\n ChatLastMessage,\n ChatUser,\n FlaggedComment,\n JsonValue,\n Message,\n MessageFlag,\n MessageFlagResolution,\n MessageView,\n Post,\n PostFlag,\n PostFlagResolution,\n PostStatus,\n PostView,\n} from '../types/index.js';\nimport type { SpaceDetail } from '../content/index.js';\nimport type { OutboxAppender, UnitOfWork } from '../shared/ports.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { Role } from '../organizations/index.js';\nimport type { Comment, CommentReport } from './model.js';\nimport type {\n CommentsConfig,\n CommentListItem,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionCounts,\n ReactionEmoji,\n} from './types.js';\n\n/** An activity's comments as this reader may see them, with the config that\n * decided it. */\nexport interface ActivityComments {\n config: CommentsConfig;\n comments: CommentView[];\n}\n\nexport interface CommentReactions {\n reactions: ReactionCounts;\n viewerReaction?: ReactionEmoji;\n}\n\nexport interface CommentWithContext {\n comment: Comment;\n courseId: string;\n activityTitle: string;\n}\n\nexport type { Actor } from '../shared/actor.js';\nexport type PostCommentInput = {\n actor: Actor;\n parentId: string | null;\n body: string;\n} & ({ activityId: string; postId?: undefined } | { postId: string; activityId?: undefined });\n\n/** A profile row as the repository loads it, keyed by `org_users.id`. `email`\n * is stripped before comments are served and kept only for the staff list. */\nexport interface AuthorRecord {\n id: string;\n firstName: string | null;\n lastName: string | null;\n email: string;\n image: string | null;\n role: Role;\n displayName: string | null;\n headline: string | null;\n imageAssetOrgId: string | null;\n imageAssetId: string | null;\n}\n\nexport interface AuthorProfileReader {\n authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>>;\n}\n\nexport interface DiscussionService {\n /** Post a root comment or a reply. Lands pending where review is required and\n * the poster is not staff. */\n postComment(orgId: string, input: PostCommentInput): Promise<CommentView>;\n /** Author-only. Throws ForbiddenError for anyone else, staff included. */\n edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView>;\n /** The author, or anyone whose role is staff. */\n remove(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string,\n ): Promise<CommentView>;\n /** Staff only. Returns the comment to published. */\n restore(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string,\n ): Promise<CommentView>;\n /** Staff only. Publishes a pending comment. */\n approve(orgId: string, commentId: string, actor: Actor): Promise<Comment>;\n /** Make a comment published, whichever state it is in. Staff only. */\n publish(orgId: string, commentId: string, actor: Actor): Promise<Comment>;\n\n /** The activity's comments as this reader may see them. */\n activityComments(orgId: string, activityId: string, actor: Actor): Promise<ActivityComments>;\n\n /** One page of top-level comments, oldest first, each with its replies. */\n postComments(\n orgId: string,\n postId: string,\n actor: Actor,\n query: PageQuery,\n ): Promise<Page<CommentView>>;\n\n /**\n * The org's comments, filtered. Staff-scoped: rows carry the author's email\n * and a removed comment's body, neither of which a learner may see, so the\n * edge admits only staff.\n *\n * No rule is applied beyond the filters — this is what exists, not what\n * someone is allowed to read.\n */\n listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>>;\n\n setReaction(\n orgId: string,\n commentId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<CommentReactions>;\n\n resolveReports(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string,\n ): Promise<void>;\n\n /** Accepted even on locked comments — a locked activity can still hold\n * something a moderator needs to see. */\n reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport>;\n\n /** Plain read of one comment — no rules applied. */\n getComment(orgId: string, commentId: string): Promise<Comment | null>;\n\n sendDigest(orgId: string, now: Date): Promise<void>;\n}\n\nexport interface ContentAccessReader {\n hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean>;\n}\n\nexport interface ActivityGetter {\n getActivity(orgId: string, activityId: string): Promise<Activity | null>;\n}\n\nexport interface DiscussionRepository extends PostCommentReader, AuthorProfileReader {\n insertComment(orgId: string, comment: Comment): Promise<Comment>;\n findComment(orgId: string, id: string): Promise<Comment | null>;\n updateComment(\n orgId: string,\n id: string,\n patch: Partial<Pick<Comment, 'body' | 'status' | 'removedBy' | 'updatedAt'>>,\n ): Promise<Comment | null>;\n /** Every comment on the activity, oldest first, including removed ones. */\n listByActivity(orgId: string, activityId: string): Promise<Comment[]>;\n /** A page of the post's top-level threads the viewer can see, oldest first, with their replies. `total` counts threads. */\n listThreadsByPost(orgId: string, postId: string, query: PostThreadsQuery): Promise<Page<Comment>>;\n\n reactionsOf(\n orgId: string,\n commentIds: string[],\n viewerId: string,\n ): Promise<Record<string, CommentReactions>>;\n setReaction(\n orgId: string,\n commentId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<void>;\n\n /** Returns null when this person has already reported this comment. */\n insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null>;\n /** Every unresolved report against the given comments. */\n listOpenReports(orgId: string, commentIds: string[]): Promise<CommentReport[]>;\n resolveReportsFor(\n orgId: string,\n commentId: string,\n resolved: {\n resolvedAt: Date;\n resolvedBy: string;\n resolution: 'dismissed' | 'removed';\n },\n ): Promise<void>;\n\n listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentWithContext>>;\n}\n\n/** Writes that emit events run through this scope so the row and the outbox\n * entry commit in one transaction. */\nexport interface DiscussionWriteScope {\n discussion: DiscussionRepository;\n outbox: OutboxAppender;\n}\nexport type DiscussionUnitOfWork = UnitOfWork<DiscussionWriteScope>;\n\nexport interface AuthorRoleReader {\n roleOf(orgId: string, orgUserId: string): Promise<Role | null>;\n rolesOf(orgId: string, orgUserIds: string[]): Promise<Record<string, Role>>;\n}\n\nexport type PostMode = 'draft' | 'submit' | 'schedule';\n\nexport interface CreatePostInput {\n communityId: string;\n spaceId: string;\n title: string;\n body: string;\n topicId: string | null;\n extra: JsonValue;\n coverAssetId: string | null;\n mode: PostMode;\n scheduledAt: Date | null;\n}\n\nexport interface UpdatePostInput {\n title?: string;\n body?: string;\n topicId?: string | null;\n extra?: JsonValue;\n coverAssetId?: string | null;\n}\n\nexport type PostPatch = Partial<\n Pick<\n Post,\n | 'title'\n | 'body'\n | 'topicId'\n | 'extra'\n | 'coverAssetId'\n | 'status'\n | 'scheduledAt'\n | 'publishedAt'\n | 'removedBy'\n | 'pinned'\n | 'commentsClosed'\n | 'reactionsEnabled'\n >\n>;\n\nexport interface FeedQuery {\n page: number;\n pageSize: number;\n topicId?: string;\n}\n\nexport interface PostReactions {\n reactions: ReactionCounts;\n viewerReaction?: ReactionEmoji;\n}\n\nexport type PageQuery = Pick<FeedQuery, 'page' | 'pageSize'>;\n\nexport interface PostThreadsQuery extends PageQuery {\n viewerId: string;\n seesHeld: boolean;\n}\n\nexport interface BookmarkQuery extends PageQuery {\n visibleStatuses: PostStatus[];\n}\n\nexport const MODERATION_QUEUE_LIMIT = 100;\n\nexport interface SpaceModerationQueue {\n pending: PostView[];\n flags: PostFlag[];\n commentFlags: FlaggedComment[];\n}\n\nexport interface PostCommentReader {\n listOpenFlagsForSpace(orgId: string, spaceId: string, limit: number): Promise<FlaggedComment[]>;\n publishedCommentCounts(orgId: string, postIds: string[]): Promise<Record<string, number>>;\n}\n\nexport interface PostService {\n visibleTitles(orgId: string, actor: Actor, postIds: string[]): Promise<Record<string, string>>;\n create(orgId: string, actor: Actor, input: CreatePostInput): Promise<PostView>;\n update(orgId: string, postId: string, actor: Actor, patch: UpdatePostInput): Promise<PostView>;\n submit(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n schedule(orgId: string, postId: string, actor: Actor, scheduledAt: Date): Promise<PostView>;\n approve(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;\n remove(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;\n restore(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;\n pin(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n unpin(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n closeComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n openComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n setReactions(orgId: string, postId: string, actor: Actor, enabled: boolean): Promise<PostView>;\n setReaction(\n orgId: string,\n postId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<PostReactions>;\n follow(orgId: string, postId: string, actor: Actor): Promise<void>;\n unfollow(orgId: string, postId: string, actor: Actor): Promise<void>;\n bookmark(orgId: string, postId: string, actor: Actor): Promise<void>;\n unbookmark(orgId: string, postId: string, actor: Actor): Promise<void>;\n flag(orgId: string, postId: string, actor: Actor, reason: string): Promise<PostFlag>;\n resolveFlags(\n orgId: string,\n postId: string,\n actor: Actor,\n resolution: Extract<PostFlagResolution, 'dismissed'>,\n communityId?: string,\n ): Promise<void>;\n spaceFeed(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n query: FeedQuery,\n ): Promise<Page<PostView>>;\n memberFeed(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: Pick<FeedQuery, 'page' | 'pageSize'>,\n ): Promise<Page<PostView>>;\n listOwn(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: PageQuery,\n ): Promise<Page<PostView>>;\n listBookmarked(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: PageQuery,\n ): Promise<Page<PostView>>;\n /** Signed URL of the post's cover, for whoever can read the post. */\n coverFile(orgId: string, actor: Actor, postId: string): Promise<string | null>;\n getPost(orgId: string, actor: Actor, postId: string): Promise<PostView>;\n visible(orgId: string, actor: Actor, postId: string): Promise<{ post: Post; space: SpaceDetail }>;\n find(orgId: string, postId: string): Promise<Post | null>;\n followerIds(orgId: string, postId: string): Promise<string[]>;\n moderationQueue(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceModerationQueue>;\n publishDue(orgId: string, now: Date): Promise<void>;\n hasDue(orgId: string, now: Date): Promise<boolean>;\n}\n\nexport interface PostRepository {\n insert(orgId: string, post: Post): Promise<Post>;\n update(orgId: string, id: string, patch: PostPatch): Promise<Post | null>;\n find(orgId: string, id: string): Promise<Post | null>;\n findMany(orgId: string, ids: string[]): Promise<Post[]>;\n listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>>;\n listByAuthor(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: PageQuery,\n ): Promise<Page<Post>>;\n listBookmarked(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: BookmarkQuery,\n ): Promise<Page<Post>>;\n listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]>;\n /** Due scheduled posts in id order, after `afterId` when given. */\n listDue(orgId: string, now: Date, limit: number, afterId?: string): Promise<Post[]>;\n hasDue(orgId: string, now: Date): Promise<boolean>;\n reactionsOf(\n orgId: string,\n postIds: string[],\n viewerId: string,\n ): Promise<Record<string, PostReactions>>;\n setReaction(\n orgId: string,\n postId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<void>;\n follow(orgId: string, postId: string, orgUserId: string): Promise<void>;\n unfollow(orgId: string, postId: string, orgUserId: string): Promise<void>;\n followerIds(orgId: string, postId: string): Promise<string[]>;\n followedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;\n bookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;\n unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;\n bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;\n insertFlag(orgId: string, flag: PostFlag): Promise<PostFlag | null>;\n findFlag(orgId: string, postId: string, orgUserId: string): Promise<PostFlag | null>;\n listOpenFlags(orgId: string, spaceId: string, limit: number): Promise<PostFlag[]>;\n resolveFlags(\n orgId: string,\n postId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: PostFlagResolution },\n ): Promise<void>;\n}\n\nexport interface PostWriteScope {\n posts: PostRepository;\n outbox: OutboxAppender;\n}\nexport type PostUnitOfWork = UnitOfWork<PostWriteScope>;\n\nexport interface StartDirectInput {\n communityId: string;\n orgUserId: string;\n body: string | null;\n assetIds: string[];\n}\n\nexport interface StartGroupInput {\n communityId: string;\n orgUserIds: string[];\n}\n\nexport interface SendMessageInput {\n body: string;\n replyToId: string | null;\n assetIds: string[];\n}\n\nexport interface ChatListItem {\n chat: Chat;\n participantIds: string[];\n lastReadMessageId: string | null;\n muted: boolean;\n mutedUntil: Date | null;\n unread: number;\n lastMessage: ChatLastMessage | null;\n}\n\nexport interface MessageReactions {\n reactions: ReactionCounts;\n viewerReaction?: ReactionEmoji;\n}\n\nexport interface ChatSummary {\n chat: Chat;\n participantIds: string[];\n}\n\nexport interface MessageQueueEntry {\n message: MessageView;\n flags: MessageFlag[];\n chat: Chat;\n participantIds: string[];\n}\n\nexport interface MessageQueue {\n entries: MessageQueueEntry[];\n}\n\nexport interface ChatService {\n startDirect(orgId: string, actor: Actor, input: StartDirectInput): Promise<Chat>;\n startGroup(orgId: string, actor: Actor, input: StartGroupInput): Promise<Chat>;\n addParticipant(orgId: string, actor: Actor, chatId: string, orgUserId: string): Promise<Chat>;\n leave(orgId: string, actor: Actor, chatId: string): Promise<Chat>;\n accept(orgId: string, actor: Actor, chatId: string): Promise<Chat>;\n send(orgId: string, actor: Actor, chatId: string, input: SendMessageInput): Promise<MessageView>;\n editMessage(orgId: string, actor: Actor, messageId: string, body: string): Promise<MessageView>;\n deleteMessage(orgId: string, actor: Actor, messageId: string): Promise<MessageView>;\n setReaction(\n orgId: string,\n actor: Actor,\n messageId: string,\n emoji: ReactionEmoji | null,\n ): Promise<MessageReactions>;\n markRead(orgId: string, actor: Actor, chatId: string, messageId: string): Promise<void>;\n mute(orgId: string, actor: Actor, chatId: string, until?: Date): Promise<void>;\n unmute(orgId: string, actor: Actor, chatId: string): Promise<void>;\n block(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;\n unblock(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;\n flagMessage(orgId: string, actor: Actor, messageId: string, reason: string): Promise<MessageFlag>;\n resolveMessageFlags(\n orgId: string,\n actor: Actor,\n messageId: string,\n resolution: Extract<MessageFlagResolution, 'dismissed'>,\n communityId?: string,\n ): Promise<void>;\n lock(orgId: string, actor: Actor, chatId: string, communityId?: string): Promise<Chat>;\n unlock(orgId: string, actor: Actor, chatId: string, communityId?: string): Promise<Chat>;\n listChats(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: PageQuery,\n ): Promise<Page<ChatListItem>>;\n chatSummary(\n orgId: string,\n chatId: string,\n ): Promise<{ chat: Chat; participants: ChatUser[] } | null>;\n latestMessageBody(orgId: string, chatId: string): Promise<string | null>;\n isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;\n titlesFor(orgId: string, actor: Actor, chatIds: string[]): Promise<Record<string, string>>;\n messages(\n orgId: string,\n actor: Actor,\n chatId: string,\n query: Pick<FeedQuery, 'page' | 'pageSize'>,\n ): Promise<Page<MessageView>>;\n /** Signed URL of a message attachment: for the chat's participants while the\n * message stands (staff also after deletion), and for staff while it is flagged. */\n attachmentFile(\n orgId: string,\n actor: Actor,\n chatId: string,\n messageId: string,\n assetId: string,\n ): Promise<string | null>;\n messageQueue(orgId: string, actor: Actor, communityId: string): Promise<MessageQueue>;\n}\n\nexport interface ChatRepository {\n lockDirectPair(\n orgId: string,\n communityId: string,\n orgUserId: string,\n otherOrgUserId: string,\n ): Promise<void>;\n findDirect(\n orgId: string,\n communityId: string,\n orgUserId: string,\n otherOrgUserId: string,\n ): Promise<Chat | null>;\n insertChat(orgId: string, chat: Chat): Promise<Chat>;\n updateChat(\n orgId: string,\n id: string,\n patch: Partial<Pick<Chat, 'status' | 'lockedAt' | 'lockedBy'>>,\n ): Promise<Chat | null>;\n findChat(orgId: string, id: string): Promise<Chat | null>;\n listForUser(\n orgId: string,\n communityId: string,\n orgUserId: string,\n query: PageQuery,\n ): Promise<Page<ChatListItem>>;\n participants(orgId: string, chatId: string): Promise<ChatUser[]>;\n join(orgId: string, user: ChatUser): Promise<void>;\n markLeft(orgId: string, chatId: string, orgUserId: string, leftAt: Date): Promise<void>;\n setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<void>;\n setMuted(\n orgId: string,\n chatId: string,\n orgUserId: string,\n muted: boolean,\n mutedUntil: Date | null,\n ): Promise<void>;\n insertMessage(orgId: string, message: Message): Promise<Message>;\n updateMessage(\n orgId: string,\n id: string,\n patch: Partial<Pick<Message, 'body' | 'editedAt' | 'deletedAt'>>,\n ): Promise<Message | null>;\n findMessage(orgId: string, id: string): Promise<Message | null>;\n listMessages(\n orgId: string,\n chatId: string,\n query: Pick<FeedQuery, 'page' | 'pageSize'>,\n ): Promise<Page<Message>>;\n attach(orgId: string, communityId: string, messageId: string, assetIds: string[]): Promise<void>;\n attachmentsOf(orgId: string, messageIds: string[]): Promise<Record<string, string[]>>;\n blockedWith(\n orgId: string,\n communityId: string,\n orgUserId: string,\n otherIds: string[],\n ): Promise<string[]>;\n setReaction(\n orgId: string,\n communityId: string,\n messageId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<void>;\n reactionsOf(\n orgId: string,\n messageIds: string[],\n viewerId: string,\n ): Promise<Record<string, MessageReactions>>;\n block(\n orgId: string,\n communityId: string,\n blockerOrgUserId: string,\n blockedOrgUserId: string,\n ): Promise<void>;\n unblock(\n orgId: string,\n communityId: string,\n blockerOrgUserId: string,\n blockedOrgUserId: string,\n ): Promise<void>;\n isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;\n anyBlockedAmong(orgId: string, communityId: string, orgUserIds: string[]): Promise<boolean>;\n insertFlag(orgId: string, flag: MessageFlag): Promise<MessageFlag | null>;\n findFlag(orgId: string, messageId: string, orgUserId: string): Promise<MessageFlag | null>;\n listOpenFlags(orgId: string, communityId: string, limit?: number): Promise<MessageFlag[]>;\n messagesByIds(orgId: string, ids: string[]): Promise<Message[]>;\n chatSummaries(orgId: string, chatIds: string[]): Promise<ChatSummary[]>;\n resolveFlags(\n orgId: string,\n messageId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: MessageFlagResolution },\n ): Promise<void>;\n resolveOpenFlagsForChat(\n orgId: string,\n chatId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: MessageFlagResolution },\n ): Promise<void>;\n}\n\nexport interface ChatWriteScope {\n chats: ChatRepository;\n outbox: OutboxAppender;\n}\nexport type ChatUnitOfWork = UnitOfWork<ChatWriteScope>;\n"],"mappings":";AAuRA,MAAa,yBAAyB"}
1
+ {"version":3,"file":"ports.js","names":[],"sources":["../../src/discussion/ports.ts"],"sourcesContent":["// discussion context — ports.\n// Inbound: the use cases the service implements.\n// Outbound: the persistence contract the repository fulfils.\nimport type {\n Activity,\n Chat,\n ChatLastMessage,\n ChatUser,\n DigestRun,\n FlaggedComment,\n JsonValue,\n MentionItemType,\n Message,\n MessageFlag,\n MessageFlagResolution,\n MessageView,\n Post,\n PostFlag,\n PostFlagResolution,\n PostStatus,\n PostView,\n SubjectRef,\n Subscription,\n SubscriptionItemType,\n SubscriptionView,\n} from '../types/index.js';\nimport type { CommunityDigest, SpaceDetail } from '../content/index.js';\nimport type { OutboxAppender, UnitOfWork } from '../shared/ports.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { Role } from '../organizations/index.js';\nimport type { Comment, CommentReport } from './model.js';\nimport type {\n CommentsConfig,\n CommentListItem,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionCounts,\n ReactionEmoji,\n} from './types.js';\n\n/** An activity's comments as this reader may see them, with the config that\n * decided it. */\nexport interface ActivityComments {\n config: CommentsConfig;\n comments: CommentView[];\n}\n\nexport interface CommentReactions {\n reactions: ReactionCounts;\n viewerReaction?: ReactionEmoji;\n}\n\nexport interface CommentWithContext {\n comment: Comment;\n courseId: string;\n activityTitle: string;\n}\n\nexport type { Actor } from '../shared/actor.js';\nexport type PostCommentInput = {\n actor: Actor;\n parentId: string | null;\n body: string;\n} & ({ activityId: string; postId?: undefined } | { postId: string; activityId?: undefined });\n\n/** A profile row as the repository loads it, keyed by `org_users.id`. `email`\n * is stripped before comments are served and kept only for the staff list. */\nexport interface AuthorRecord {\n id: string;\n firstName: string | null;\n lastName: string | null;\n email: string;\n image: string | null;\n role: Role;\n displayName: string | null;\n headline: string | null;\n imageAssetOrgId: string | null;\n imageAssetId: string | null;\n}\n\nexport interface ItemTitle {\n title: string;\n excerpt?: string;\n}\n\nexport interface ItemTitles {\n titlesFor(orgId: string, actor: Actor, refs: SubjectRef[]): Promise<Record<string, ItemTitle>>;\n}\n\nexport interface AuthorProfileReader {\n authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>>;\n}\n\nexport interface DiscussionService {\n /** Post a root comment or a reply. Lands pending where review is required and\n * the poster is not staff. */\n postComment(orgId: string, input: PostCommentInput): Promise<CommentView>;\n /** Author-only. Throws ForbiddenError for anyone else, staff included. */\n edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView>;\n /** The author, or anyone whose role is staff. */\n remove(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string,\n ): Promise<CommentView>;\n /** Staff only. Returns the comment to published. */\n restore(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string,\n ): Promise<CommentView>;\n /** Staff only. Publishes a pending comment. */\n approve(orgId: string, commentId: string, actor: Actor): Promise<Comment>;\n /** Make a comment published, whichever state it is in. Staff only. */\n publish(orgId: string, commentId: string, actor: Actor): Promise<Comment>;\n\n /** The activity's comments as this reader may see them. */\n activityComments(orgId: string, activityId: string, actor: Actor): Promise<ActivityComments>;\n\n /** One page of top-level comments, oldest first, each with its replies. */\n postComments(\n orgId: string,\n postId: string,\n actor: Actor,\n query: PageQuery,\n ): Promise<Page<CommentView>>;\n\n /**\n * The org's comments, filtered. Staff-scoped: rows carry the author's email\n * and a removed comment's body, neither of which a learner may see, so the\n * edge admits only staff.\n *\n * No rule is applied beyond the filters — this is what exists, not what\n * someone is allowed to read.\n */\n listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>>;\n\n setReaction(\n orgId: string,\n commentId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<CommentReactions>;\n\n resolveReports(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string,\n ): Promise<void>;\n\n /** Accepted even on locked comments — a locked activity can still hold\n * something a moderator needs to see. */\n reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport>;\n\n /** Plain read of one comment — no rules applied. */\n getComment(orgId: string, commentId: string): Promise<Comment | null>;\n commenterIds(\n orgId: string,\n target: { postId: string } | { activityId: string },\n ): Promise<string[]>;\n}\n\nexport interface ContentAccessReader {\n hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean>;\n}\n\nexport interface ActivityGetter {\n getActivity(orgId: string, activityId: string): Promise<Activity | null>;\n}\n\nexport interface DiscussionRepository extends PostCommentReader, AuthorProfileReader {\n insertComment(orgId: string, comment: Comment): Promise<Comment>;\n findComment(orgId: string, id: string): Promise<Comment | null>;\n commenterIds(\n orgId: string,\n target: { postId: string } | { activityId: string },\n ): Promise<string[]>;\n updateComment(\n orgId: string,\n id: string,\n patch: Partial<Pick<Comment, 'body' | 'status' | 'removedBy' | 'updatedAt'>>,\n ): Promise<Comment | null>;\n /** Every comment on the activity, oldest first, including removed ones. */\n listByActivity(orgId: string, activityId: string): Promise<Comment[]>;\n /** A page of the post's top-level threads the viewer can see, oldest first, with their replies. `total` counts threads. */\n listThreadsByPost(orgId: string, postId: string, query: PostThreadsQuery): Promise<Page<Comment>>;\n\n reactionsOf(\n orgId: string,\n commentIds: string[],\n viewerId: string,\n ): Promise<Record<string, CommentReactions>>;\n setReaction(\n orgId: string,\n commentId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<boolean>;\n\n /** Returns null when this person has already reported this comment. */\n insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null>;\n /** Every unresolved report against the given comments. */\n listOpenReports(orgId: string, commentIds: string[]): Promise<CommentReport[]>;\n resolveReportsFor(\n orgId: string,\n commentId: string,\n resolved: {\n resolvedAt: Date;\n resolvedBy: string;\n resolution: 'dismissed' | 'removed';\n },\n ): Promise<void>;\n\n listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentWithContext>>;\n}\n\n/** Writes that emit events run through this scope so the row and the outbox\n * entry commit in one transaction. */\nexport interface DiscussionWriteScope {\n discussion: DiscussionRepository;\n mentions: MentionsRepository;\n outbox: OutboxAppender;\n}\nexport type DiscussionUnitOfWork = UnitOfWork<DiscussionWriteScope>;\n\nexport interface AuthorRoleReader {\n roleOf(orgId: string, orgUserId: string): Promise<Role | null>;\n rolesOf(orgId: string, orgUserIds: string[]): Promise<Record<string, Role>>;\n}\n\nexport type PostMode = 'draft' | 'submit' | 'schedule';\n\nexport interface CreatePostInput {\n communityId: string;\n spaceId: string;\n title: string;\n body: string;\n topicId: string | null;\n extra: JsonValue;\n coverAssetId: string | null;\n mode: PostMode;\n scheduledAt: Date | null;\n}\n\nexport interface UpdatePostInput {\n title?: string;\n body?: string;\n topicId?: string | null;\n extra?: JsonValue;\n coverAssetId?: string | null;\n}\n\nexport type PostPatch = Partial<\n Pick<\n Post,\n | 'title'\n | 'body'\n | 'topicId'\n | 'extra'\n | 'coverAssetId'\n | 'status'\n | 'scheduledAt'\n | 'publishedAt'\n | 'removedBy'\n | 'pinned'\n | 'commentsClosed'\n | 'reactionsEnabled'\n >\n>;\n\nexport interface FeedQuery {\n page: number;\n pageSize: number;\n topicId?: string;\n}\n\nexport interface PostReactions {\n reactions: ReactionCounts;\n viewerReaction?: ReactionEmoji;\n}\n\nexport type PageQuery = Pick<FeedQuery, 'page' | 'pageSize'>;\n\nexport interface PostThreadsQuery extends PageQuery {\n viewerId: string;\n seesHeld: boolean;\n}\n\nexport interface BookmarkQuery extends PageQuery {\n visibleStatuses: PostStatus[];\n}\n\nexport const MODERATION_QUEUE_LIMIT = 100;\n\nexport interface SpaceModerationQueue {\n pending: PostView[];\n flags: PostFlag[];\n commentFlags: FlaggedComment[];\n}\n\nexport interface PostCommentReader {\n listOpenFlagsForSpace(orgId: string, spaceId: string, limit: number): Promise<FlaggedComment[]>;\n publishedCommentCounts(orgId: string, postIds: string[]): Promise<Record<string, number>>;\n}\n\nexport interface PublishedPost {\n postId: string;\n spaceId: string;\n title: string;\n orgUserId: string;\n commentCount: number;\n}\n\nexport interface PostService {\n visibleTitles(orgId: string, actor: Actor, postIds: string[]): Promise<Record<string, string>>;\n publishedBetween(\n orgId: string,\n communityId: string,\n from: Date,\n to: Date,\n ): Promise<PublishedPost[]>;\n create(orgId: string, actor: Actor, input: CreatePostInput): Promise<PostView>;\n update(orgId: string, postId: string, actor: Actor, patch: UpdatePostInput): Promise<PostView>;\n submit(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n schedule(orgId: string, postId: string, actor: Actor, scheduledAt: Date): Promise<PostView>;\n unschedule(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n publishScheduled(orgId: string, postId: string, now: Date): Promise<void>;\n approve(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;\n remove(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;\n restore(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;\n pin(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n unpin(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n closeComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n openComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;\n setReactions(orgId: string, postId: string, actor: Actor, enabled: boolean): Promise<PostView>;\n setReaction(\n orgId: string,\n postId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<PostReactions>;\n bookmark(orgId: string, postId: string, actor: Actor): Promise<void>;\n unbookmark(orgId: string, postId: string, actor: Actor): Promise<void>;\n flag(orgId: string, postId: string, actor: Actor, reason: string): Promise<PostFlag>;\n resolveFlags(\n orgId: string,\n postId: string,\n actor: Actor,\n resolution: Extract<PostFlagResolution, 'dismissed'>,\n communityId?: string,\n ): Promise<void>;\n spaceFeed(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n query: FeedQuery,\n ): Promise<Page<PostView>>;\n memberFeed(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: Pick<FeedQuery, 'page' | 'pageSize'>,\n ): Promise<Page<PostView>>;\n listOwn(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: PageQuery,\n ): Promise<Page<PostView>>;\n listBookmarked(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: PageQuery,\n ): Promise<Page<PostView>>;\n /** Signed URL of the post's cover, for whoever can read the post. */\n coverFile(orgId: string, actor: Actor, postId: string): Promise<string | null>;\n getPost(orgId: string, actor: Actor, postId: string): Promise<PostView>;\n visible(orgId: string, actor: Actor, postId: string): Promise<{ post: Post; space: SpaceDetail }>;\n find(orgId: string, postId: string): Promise<Post | null>;\n moderationQueue(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceModerationQueue>;\n}\n\nexport interface PostGuard {\n status: PostStatus;\n scheduledAt?: Date;\n dueBy?: Date;\n}\n\nexport interface PostRepository {\n insert(orgId: string, post: Post): Promise<Post>;\n update(orgId: string, id: string, patch: PostPatch): Promise<Post | null>;\n updateIf(orgId: string, id: string, guard: PostGuard, patch: PostPatch): Promise<Post | null>;\n find(orgId: string, id: string): Promise<Post | null>;\n findMany(orgId: string, ids: string[]): Promise<Post[]>;\n publishedBetween(\n orgId: string,\n communityId: string,\n from: Date,\n to: Date,\n ): Promise<PublishedPost[]>;\n listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>>;\n listByAuthor(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: PageQuery,\n ): Promise<Page<Post>>;\n listBookmarked(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: BookmarkQuery,\n ): Promise<Page<Post>>;\n listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]>;\n reactionsOf(\n orgId: string,\n postIds: string[],\n viewerId: string,\n ): Promise<Record<string, PostReactions>>;\n setReaction(\n orgId: string,\n postId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<boolean>;\n bookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;\n unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;\n bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;\n insertFlag(orgId: string, flag: PostFlag): Promise<PostFlag | null>;\n findFlag(orgId: string, postId: string, orgUserId: string): Promise<PostFlag | null>;\n listOpenFlags(orgId: string, spaceId: string, limit: number): Promise<PostFlag[]>;\n resolveFlags(\n orgId: string,\n postId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: PostFlagResolution },\n ): Promise<void>;\n}\n\nexport interface PostWriteScope {\n posts: PostRepository;\n mentions: MentionsRepository;\n outbox: OutboxAppender;\n}\nexport type PostUnitOfWork = UnitOfWork<PostWriteScope>;\n\nexport interface DigestScheduleReader {\n communitiesWithDigest(): Promise<CommunityDigest[]>;\n}\n\nexport interface DigestRunRepository {\n listRuns(): Promise<DigestRun[]>;\n advance(orgId: string, communityId: string, dueAt: Date): Promise<boolean>;\n}\n\nexport interface DigestWriteScope {\n digests: DigestRunRepository;\n outbox: OutboxAppender;\n}\nexport type DigestUnitOfWork = UnitOfWork<DigestWriteScope>;\n\nexport interface DigestService {\n openDue(now: Date): Promise<void>;\n}\n\nexport interface StartDirectInput {\n communityId: string;\n orgUserId: string;\n body: string | null;\n assetIds: string[];\n}\n\nexport interface StartGroupInput {\n communityId: string;\n orgUserIds: string[];\n}\n\nexport interface SendMessageInput {\n body: string;\n replyToId: string | null;\n assetIds: string[];\n}\n\nexport interface ChatListItem {\n chat: Chat;\n participantIds: string[];\n lastReadMessageId: string | null;\n subscription: SubscriptionView | null;\n unread: number;\n lastMessage: ChatLastMessage | null;\n}\n\nexport type ChatInboxRow = Omit<ChatListItem, 'subscription'>;\n\nexport interface ChatUnread {\n count: number;\n latest: { messageId: string; senderOrgUserId: string; body: string } | null;\n}\n\nexport interface MessageReactions {\n reactions: ReactionCounts;\n viewerReaction?: ReactionEmoji;\n}\n\nexport interface ChatSummary {\n chat: Chat;\n participantIds: string[];\n}\n\nexport interface MessageQueueEntry {\n message: MessageView;\n flags: MessageFlag[];\n chat: Chat;\n participantIds: string[];\n}\n\nexport interface MessageQueue {\n entries: MessageQueueEntry[];\n}\n\nexport interface ChatService {\n startDirect(orgId: string, actor: Actor, input: StartDirectInput): Promise<Chat>;\n startGroup(orgId: string, actor: Actor, input: StartGroupInput): Promise<Chat>;\n addParticipant(orgId: string, actor: Actor, chatId: string, orgUserId: string): Promise<Chat>;\n leave(orgId: string, actor: Actor, chatId: string): Promise<Chat>;\n accept(orgId: string, actor: Actor, chatId: string): Promise<Chat>;\n send(orgId: string, actor: Actor, chatId: string, input: SendMessageInput): Promise<MessageView>;\n editMessage(orgId: string, actor: Actor, messageId: string, body: string): Promise<MessageView>;\n deleteMessage(orgId: string, actor: Actor, messageId: string): Promise<MessageView>;\n setReaction(\n orgId: string,\n actor: Actor,\n messageId: string,\n emoji: ReactionEmoji | null,\n ): Promise<MessageReactions>;\n markRead(orgId: string, actor: Actor, chatId: string, messageId: string): Promise<void>;\n block(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;\n unblock(orgId: string, actor: Actor, communityId: string, orgUserId: string): Promise<void>;\n flagMessage(orgId: string, actor: Actor, messageId: string, reason: string): Promise<MessageFlag>;\n resolveMessageFlags(\n orgId: string,\n actor: Actor,\n messageId: string,\n resolution: Extract<MessageFlagResolution, 'dismissed'>,\n communityId?: string,\n ): Promise<void>;\n lock(orgId: string, actor: Actor, chatId: string, communityId?: string): Promise<Chat>;\n unlock(orgId: string, actor: Actor, chatId: string, communityId?: string): Promise<Chat>;\n listChats(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: PageQuery,\n ): Promise<Page<ChatListItem>>;\n chatSummary(\n orgId: string,\n chatId: string,\n ): Promise<{ chat: Chat; participants: ChatUser[] } | null>;\n latestMessageBody(orgId: string, chatId: string): Promise<string | null>;\n unreadInChat(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;\n findMessage(orgId: string, messageId: string): Promise<Message | null>;\n isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;\n titlesFor(orgId: string, actor: Actor, chatIds: string[]): Promise<Record<string, string>>;\n messages(\n orgId: string,\n actor: Actor,\n chatId: string,\n query: Pick<FeedQuery, 'page' | 'pageSize'>,\n ): Promise<Page<MessageView>>;\n /** Signed URL of a message attachment: for the chat's participants while the\n * message stands (staff also after deletion), and for staff while it is flagged. */\n attachmentFile(\n orgId: string,\n actor: Actor,\n chatId: string,\n messageId: string,\n assetId: string,\n ): Promise<string | null>;\n messageQueue(orgId: string, actor: Actor, communityId: string): Promise<MessageQueue>;\n}\n\nexport interface ChatRepository {\n lockDirectPair(\n orgId: string,\n communityId: string,\n orgUserId: string,\n otherOrgUserId: string,\n ): Promise<void>;\n findDirect(\n orgId: string,\n communityId: string,\n orgUserId: string,\n otherOrgUserId: string,\n ): Promise<Chat | null>;\n insertChat(orgId: string, chat: Chat): Promise<Chat>;\n updateChat(\n orgId: string,\n id: string,\n patch: Partial<Pick<Chat, 'status' | 'lockedAt' | 'lockedBy'>>,\n ): Promise<Chat | null>;\n findChat(orgId: string, id: string): Promise<Chat | null>;\n listForUser(\n orgId: string,\n communityId: string,\n orgUserId: string,\n query: PageQuery,\n ): Promise<Page<ChatInboxRow>>;\n participants(orgId: string, chatId: string): Promise<ChatUser[]>;\n join(orgId: string, user: ChatUser): Promise<void>;\n markLeft(orgId: string, chatId: string, orgUserId: string, leftAt: Date): Promise<void>;\n setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<void>;\n unreadFor(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;\n insertMessage(orgId: string, message: Message): Promise<Message>;\n updateMessage(\n orgId: string,\n id: string,\n patch: Partial<Pick<Message, 'body' | 'editedAt' | 'deletedAt'>>,\n ): Promise<Message | null>;\n findMessage(orgId: string, id: string): Promise<Message | null>;\n listMessages(\n orgId: string,\n chatId: string,\n query: Pick<FeedQuery, 'page' | 'pageSize'>,\n ): Promise<Page<Message>>;\n attach(orgId: string, communityId: string, messageId: string, assetIds: string[]): Promise<void>;\n attachmentsOf(orgId: string, messageIds: string[]): Promise<Record<string, string[]>>;\n blockedWith(\n orgId: string,\n communityId: string,\n orgUserId: string,\n otherIds: string[],\n ): Promise<string[]>;\n setReaction(\n orgId: string,\n communityId: string,\n messageId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<boolean>;\n reactionsOf(\n orgId: string,\n messageIds: string[],\n viewerId: string,\n ): Promise<Record<string, MessageReactions>>;\n block(\n orgId: string,\n communityId: string,\n blockerOrgUserId: string,\n blockedOrgUserId: string,\n ): Promise<void>;\n unblock(\n orgId: string,\n communityId: string,\n blockerOrgUserId: string,\n blockedOrgUserId: string,\n ): Promise<void>;\n isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;\n anyBlockedAmong(orgId: string, communityId: string, orgUserIds: string[]): Promise<boolean>;\n insertFlag(orgId: string, flag: MessageFlag): Promise<MessageFlag | null>;\n findFlag(orgId: string, messageId: string, orgUserId: string): Promise<MessageFlag | null>;\n listOpenFlags(orgId: string, communityId: string, limit?: number): Promise<MessageFlag[]>;\n messagesByIds(orgId: string, ids: string[]): Promise<Message[]>;\n chatSummaries(orgId: string, chatIds: string[]): Promise<ChatSummary[]>;\n resolveFlags(\n orgId: string,\n messageId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: MessageFlagResolution },\n ): Promise<void>;\n resolveOpenFlagsForChat(\n orgId: string,\n chatId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: MessageFlagResolution },\n ): Promise<void>;\n}\n\nexport interface ChatWriteScope {\n chats: ChatRepository;\n mentions: MentionsRepository;\n outbox: OutboxAppender;\n}\nexport type ChatUnitOfWork = UnitOfWork<ChatWriteScope>;\n\nexport type SubscriptionInput =\n { state: 'following' } | { state: 'muted'; mutedUntil?: Date | null } | { state: 'none' };\n\nexport type MuteInput = Exclude<SubscriptionInput, { state: 'following' }>;\n\nexport interface SubscriptionService {\n setPost(orgId: string, actor: Actor, postId: string, input: SubscriptionInput): Promise<void>;\n setSpace(orgId: string, actor: Actor, spaceId: string, input: MuteInput): Promise<void>;\n setChat(orgId: string, actor: Actor, chatId: string, input: MuteInput): Promise<void>;\n viewerSubscriptions(\n orgId: string,\n orgUserId: string,\n itemType: SubscriptionItemType,\n itemIds: string[],\n at: Date,\n ): Promise<Record<string, SubscriptionView>>;\n followerIds(orgId: string, postId: string): Promise<string[]>;\n mutedAmong(orgId: string, item: SubjectRef, orgUserIds: string[], at: Date): Promise<string[]>;\n itemSubscriptions(orgId: string, item: SubjectRef): Promise<Subscription[]>;\n}\n\nexport interface SubscriptionsRepository {\n upsert(orgId: string, subscription: Subscription): Promise<void>;\n remove(\n orgId: string,\n orgUserId: string,\n itemType: SubscriptionItemType,\n itemId: string,\n ): Promise<void>;\n forViewer(\n orgId: string,\n orgUserId: string,\n itemType: SubscriptionItemType,\n itemIds: string[],\n ): Promise<Subscription[]>;\n ofItem(orgId: string, itemType: SubscriptionItemType, itemId: string): Promise<Subscription[]>;\n followerIds(orgId: string, postId: string): Promise<string[]>;\n mutedAmong(\n orgId: string,\n itemType: SubscriptionItemType,\n itemId: string,\n orgUserIds: string[],\n at: Date,\n ): Promise<string[]>;\n}\n\nexport interface MentionsRepository {\n ofItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<string[]>;\n forItems(\n orgId: string,\n itemType: MentionItemType,\n itemIds: string[],\n ): Promise<Record<string, string[]>>;\n sync(\n orgId: string,\n itemType: MentionItemType,\n itemId: string,\n orgUserIds: string[],\n at: Date,\n ): Promise<string[]>;\n removeItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<void>;\n}\n"],"mappings":";AA6SA,MAAa,yBAAyB"}
@@ -3,7 +3,7 @@ import { Logger } from "../types/ports.js";
3
3
  import { SpaceDetail } from "../types/schemas/content.js";
4
4
  import { Post, PostFlag, PostFlagResolution, PostView, ReactionEmoji } from "../types/schemas/discussion.js";
5
5
  import "../types/index.js";
6
- import "../shared/ports.js";
6
+ import { Scheduler } from "../shared/ports.js";
7
7
  import { FileUrls } from "../assets/file-urls.js";
8
8
  import { AssetFiles, MemberImageReader } from "../assets/ports.js";
9
9
  import "../assets/index.js";
@@ -13,7 +13,7 @@ import "../content/index.js";
13
13
  import { CommunityAccessReader } from "../entitlements/ports.js";
14
14
  import "../entitlements/index.js";
15
15
  import "./types.js";
16
- import { AuthorProfileReader, AuthorRoleReader, CreatePostInput, FeedQuery, PageQuery, PostCommentReader, PostReactions, PostRepository, PostService, PostUnitOfWork, SpaceModerationQueue, UpdatePostInput } from "./ports.js";
16
+ import { AuthorProfileReader, AuthorRoleReader, CreatePostInput, FeedQuery, MentionsRepository, PageQuery, PostCommentReader, PostReactions, PostRepository, PostService, PostUnitOfWork, PublishedPost, SpaceModerationQueue, SubscriptionsRepository, UpdatePostInput } from "./ports.js";
17
17
  //#region src/discussion/posts.d.ts
18
18
  type PostServiceParams = {
19
19
  repo: PostRepository;
@@ -22,9 +22,12 @@ type PostServiceParams = {
22
22
  authors: AuthorRoleReader;
23
23
  profiles: AuthorProfileReader;
24
24
  comments: PostCommentReader;
25
+ subscriptions: SubscriptionsRepository;
26
+ mentions: MentionsRepository;
25
27
  images: MemberImageReader & Pick<AssetFiles, 'signFile'>;
26
28
  fileUrls: FileUrls;
27
29
  uow: PostUnitOfWork;
30
+ scheduler: Pick<Scheduler, 'at' | 'cancel'>;
28
31
  logger?: Logger;
29
32
  };
30
33
  declare class PostServiceImpl implements PostService {
@@ -34,16 +37,24 @@ declare class PostServiceImpl implements PostService {
34
37
  private readonly authors;
35
38
  private readonly profiles;
36
39
  private readonly comments;
40
+ private readonly subscriptions;
41
+ private readonly mentions;
37
42
  private readonly images;
38
43
  private readonly fileUrls;
39
44
  private readonly uow;
45
+ private readonly scheduler;
40
46
  private readonly logger;
41
47
  constructor(params: PostServiceParams);
42
48
  visibleTitles(orgId: string, actor: Actor, postIds: string[]): Promise<Record<string, string>>;
49
+ publishedBetween(orgId: string, communityId: string, from: Date, to: Date): Promise<PublishedPost[]>;
43
50
  create(orgId: string, actor: Actor, input: CreatePostInput): Promise<PostView>;
44
51
  update(orgId: string, postId: string, actor: Actor, patch: UpdatePostInput): Promise<PostView>;
45
52
  submit(orgId: string, postId: string, actor: Actor): Promise<PostView>;
46
53
  schedule(orgId: string, postId: string, actor: Actor, scheduledAt: Date): Promise<PostView>;
54
+ unschedule(orgId: string, postId: string, actor: Actor): Promise<PostView>;
55
+ publishScheduled(orgId: string, postId: string, now: Date): Promise<void>;
56
+ private arm;
57
+ private disarm;
47
58
  approve(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
48
59
  remove(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
49
60
  restore(orgId: string, postId: string, actor: Actor, communityId?: string): Promise<PostView>;
@@ -53,8 +64,6 @@ declare class PostServiceImpl implements PostService {
53
64
  openComments(orgId: string, postId: string, actor: Actor): Promise<PostView>;
54
65
  setReactions(orgId: string, postId: string, actor: Actor, enabled: boolean): Promise<PostView>;
55
66
  setReaction(orgId: string, postId: string, actor: Actor, emoji: ReactionEmoji | null): Promise<PostReactions>;
56
- follow(orgId: string, postId: string, actor: Actor): Promise<void>;
57
- unfollow(orgId: string, postId: string, actor: Actor): Promise<void>;
58
67
  bookmark(orgId: string, postId: string, actor: Actor): Promise<void>;
59
68
  unbookmark(orgId: string, postId: string, actor: Actor): Promise<void>;
60
69
  flag(orgId: string, postId: string, actor: Actor, reason: string): Promise<PostFlag>;
@@ -72,10 +81,6 @@ declare class PostServiceImpl implements PostService {
72
81
  space: SpaceDetail;
73
82
  }>;
74
83
  find(orgId: string, postId: string): Promise<Post | null>;
75
- followerIds(orgId: string, postId: string): Promise<string[]>;
76
- hasDue(orgId: string, now: Date): Promise<boolean>;
77
- publishDue(orgId: string, now: Date): Promise<void>;
78
- private submitDue;
79
84
  private submittable;
80
85
  private feedPage;
81
86
  private viewOf;
@@ -96,6 +101,11 @@ declare class PostServiceImpl implements PostService {
96
101
  private topicFits;
97
102
  private assertTopic;
98
103
  private patchChanges;
104
+ private mentionable;
105
+ private openersOf;
106
+ private landed;
107
+ private published;
108
+ private updated;
99
109
  private meta;
100
110
  }
101
111
  //#endregion