diff --git a/src/core/server/locales/en-US/email.ftl b/src/core/server/locales/en-US/email.ftl index a175cc390..6873c9617 100644 --- a/src/core/server/locales/en-US/email.ftl +++ b/src/core/server/locales/en-US/email.ftl @@ -107,6 +107,20 @@ email-template-notificationOnStaffReply = { $organizationName } - { $storyTitle }

{ $authorUsername } works for { $organizationName } and has replied to your comment: View comment +## On Comment Approved + +email-subject-notificationOnCommentApproved = Your comment on { $organizationName } has been published +email-template-notificationOnCommentApproved = + { $organizationName }

+ Thank you for submitting your comment. Your comment has now been published: View comment + +## On Comment Rejected + +email-subject-notificationOnCommentRejected = Your comment on { $organizationName } was not published +email-template-notificationOnCommentRejected = + { $organizationName }

+ The language used in one of your comments did not comply with our community guidelines, and so the comment has been removed. + # Notification Digest email-subject-notificationDigest = Your latest comment activity at { $organizationName } diff --git a/src/core/server/models/comment/constants.ts b/src/core/server/models/comment/constants.ts index fc80a5703..b64d7a8fb 100644 --- a/src/core/server/models/comment/constants.ts +++ b/src/core/server/models/comment/constants.ts @@ -8,3 +8,8 @@ export const PUBLISHED_STATUSES = [ GQLCOMMENT_STATUS.NONE, GQLCOMMENT_STATUS.APPROVED, ]; + +export const MODERATOR_STATUSES = [ + GQLCOMMENT_STATUS.PREMOD, + GQLCOMMENT_STATUS.SYSTEM_WITHHELD, +]; diff --git a/src/core/server/models/comment/helpers.ts b/src/core/server/models/comment/helpers.ts index 8bace473d..e687ee39c 100644 --- a/src/core/server/models/comment/helpers.ts +++ b/src/core/server/models/comment/helpers.ts @@ -1,7 +1,7 @@ import { GQLCOMMENT_STATUS } from "coral-server/graph/tenant/schema/__generated__/types"; import { Comment } from "./comment"; -import { PUBLISHED_STATUSES } from "./constants"; +import { MODERATOR_STATUSES, PUBLISHED_STATUSES } from "./constants"; import { Revision } from "./revision"; /** @@ -25,6 +25,10 @@ export function hasPublishedStatus(comment: Pick): boolean { return PUBLISHED_STATUSES.includes(comment.status); } +export function hasModeratorStatus(comment: Pick): boolean { + return MODERATOR_STATUSES.includes(comment.status); +} + /** * getLatestRevision will get the latest revision from a Comment. * diff --git a/src/core/server/queue/tasks/mailer/templates/index.ts b/src/core/server/queue/tasks/mailer/templates/index.ts index 293e2a3f1..1227c9f0b 100644 --- a/src/core/server/queue/tasks/mailer/templates/index.ts +++ b/src/core/server/queue/tasks/mailer/templates/index.ts @@ -45,10 +45,24 @@ export type OnFeaturedTemplate = NotificationContext< } >; +export type OnCommentRejectedTemplate = NotificationContext< + "notification/on-comment-rejected", + {} +>; + +export type OnCommentApprovedTemplate = NotificationContext< + "notification/on-comment-approved", + { + commentPermalink: string; + } +>; + export type DigestibleTemplate = | OnReplyTemplate | OnStaffReplyTemplate - | OnFeaturedTemplate; + | OnFeaturedTemplate + | OnCommentRejectedTemplate + | OnCommentApprovedTemplate; type DigestTemplate = NotificationContext< "notification/digest", @@ -187,6 +201,8 @@ type Templates = | OnReplyTemplate | OnStaffReplyTemplate | OnFeaturedTemplate - | DigestTemplate; + | DigestTemplate + | OnCommentRejectedTemplate + | OnCommentApprovedTemplate; export { Templates as EmailTemplate }; diff --git a/src/core/server/queue/tasks/mailer/templates/notification/on-comment-approved.html b/src/core/server/queue/tasks/mailer/templates/notification/on-comment-approved.html new file mode 100644 index 000000000..e1fe7a742 --- /dev/null +++ b/src/core/server/queue/tasks/mailer/templates/notification/on-comment-approved.html @@ -0,0 +1 @@ +{% extends "layouts/notification.html" %} diff --git a/src/core/server/queue/tasks/mailer/templates/notification/on-comment-rejected.html b/src/core/server/queue/tasks/mailer/templates/notification/on-comment-rejected.html new file mode 100644 index 000000000..e1fe7a742 --- /dev/null +++ b/src/core/server/queue/tasks/mailer/templates/notification/on-comment-rejected.html @@ -0,0 +1 @@ +{% extends "layouts/notification.html" %} diff --git a/src/core/server/queue/tasks/mailer/templates/notification/partials/on-comment-approved.html b/src/core/server/queue/tasks/mailer/templates/notification/partials/on-comment-approved.html new file mode 100644 index 000000000..a4481854b --- /dev/null +++ b/src/core/server/queue/tasks/mailer/templates/notification/partials/on-comment-approved.html @@ -0,0 +1,4 @@ +
+ {{ context.organizationName }}

+ Thank you for submitting your comment. Your comment has now been published: View comment +
diff --git a/src/core/server/queue/tasks/mailer/templates/notification/partials/on-comment-rejected.html b/src/core/server/queue/tasks/mailer/templates/notification/partials/on-comment-rejected.html new file mode 100644 index 000000000..3858e10bc --- /dev/null +++ b/src/core/server/queue/tasks/mailer/templates/notification/partials/on-comment-rejected.html @@ -0,0 +1,4 @@ +
+ {{ context.organizationName }}

+ The language used in one of your comments did not comply with our community guidelines, and so the comment has been removed. +
diff --git a/src/core/server/services/notifications/categories/categories.ts b/src/core/server/services/notifications/categories/categories.ts index 0d8163051..a61bd0bdb 100644 --- a/src/core/server/services/notifications/categories/categories.ts +++ b/src/core/server/services/notifications/categories/categories.ts @@ -1,5 +1,6 @@ import { NotificationCategory } from "./category"; import { featured } from "./featured"; +import { moderation } from "./moderation"; import { reply } from "./reply"; import { staffReply } from "./staffReply"; @@ -10,6 +11,7 @@ const categories: NotificationCategory[] = [ ...reply, ...staffReply, ...featured, + ...moderation, ]; export default categories; diff --git a/src/core/server/services/notifications/categories/moderation.ts b/src/core/server/services/notifications/categories/moderation.ts new file mode 100644 index 000000000..a6fed6b85 --- /dev/null +++ b/src/core/server/services/notifications/categories/moderation.ts @@ -0,0 +1,84 @@ +import { CommentStatusUpdatedInput } from "coral-server/graph/tenant/resolvers/Subscription/commentStatusUpdated"; +import { SUBSCRIPTION_CHANNELS } from "coral-server/graph/tenant/resolvers/Subscription/types"; +import { hasModeratorStatus } from "coral-server/models/comment"; + +import { GQLCOMMENT_STATUS } from "coral-server/graph/tenant/schema/__generated__/types"; +import { getURLWithCommentID } from "coral-server/models/story"; +import NotificationContext from "../context"; +import { Notification } from "../notification"; +import { NotificationCategory } from "./category"; + +async function processor( + ctx: NotificationContext, + input: CommentStatusUpdatedInput +): Promise { + // Check to see if this comment was previously in a moderation status. + if (!hasModeratorStatus({ status: input.oldStatus })) { + return null; + } + + // Load the comment in question. + const comment = await ctx.comments.load(input.commentID); + if (!comment) { + return null; + } + + // Get the comment author. + const author = await ctx.users.load(comment.authorID); + if (!author) { + return null; + } + + // Check to see if this user has notifications enabled. + if (!author.notifications.onModeration) { + return null; + } + + // Generate the unsubscribe URL. + const unsubscribeURL = await ctx.generateUnsubscribeURL(author); + + // Check to see which template we should use. + if (comment.status === GQLCOMMENT_STATUS.APPROVED) { + // Get the story that this was written on. + const story = await ctx.stories.load(comment.storyID); + if (!story) { + return null; + } + + return { + userID: author.id, + template: { + name: "notification/on-comment-approved", + context: { + commentPermalink: getURLWithCommentID(story.url, comment.id), + organizationName: ctx.tenant.organization.name, + organizationURL: ctx.tenant.organization.url, + unsubscribeURL, + }, + }, + }; + } else if (comment.status === GQLCOMMENT_STATUS.REJECTED) { + return { + userID: author.id, + template: { + name: "notification/on-comment-rejected", + context: { + organizationName: ctx.tenant.organization.name, + organizationURL: ctx.tenant.organization.url, + unsubscribeURL, + }, + }, + }; + } + + return null; +} + +export const moderation: NotificationCategory[] = [ + { + name: "moderation", + process: processor, + event: SUBSCRIPTION_CHANNELS.COMMENT_STATUS_UPDATED, + digestOrder: 30, + }, +];