diff --git a/.gitignore b/.gitignore index 24022b9d7..aaf49c933 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ plugins/* !plugins/talk-plugin-moderation-actions !plugins/talk-plugin-notifications !plugins/talk-plugin-notifications-category-featured +!plugins/talk-plugin-notifications-category-moderation-actions !plugins/talk-plugin-notifications-category-reply !plugins/talk-plugin-notifications-category-staff !plugins/talk-plugin-notifications-digest-daily diff --git a/plugins/talk-plugin-notifications-category-moderation-actions/README.md b/plugins/talk-plugin-notifications-category-moderation-actions/README.md new file mode 100644 index 000000000..62485037b --- /dev/null +++ b/plugins/talk-plugin-notifications-category-moderation-actions/README.md @@ -0,0 +1,19 @@ +--- +title: talk-plugin-notifications-moderation-actions +permalink: /plugin/talk-plugin-notifications-moderation-actions/ +layout: plugin +plugin: + name: talk-plugin-notifications-moderation-actions + depends: + - name: talk-plugin-notifications + provides: + - Server + - Client +--- + +When a comment that is initially withheld from publication and is then +approved or rejected, the user will receive a notification email. + +## Configuration: + +- `TALK_MODERATION_NOTIFICATION_TYPES`. This plugin requires values to be set. Available options: `APPROVED`, `REJECTED` as a single string (comma separated). diff --git a/plugins/talk-plugin-notifications-category-moderation-actions/client/.eslintrc.json b/plugins/talk-plugin-notifications-category-moderation-actions/client/.eslintrc.json new file mode 100644 index 000000000..c8a6db18a --- /dev/null +++ b/plugins/talk-plugin-notifications-category-moderation-actions/client/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk/client" +} diff --git a/plugins/talk-plugin-notifications-category-moderation-actions/client/index.js b/plugins/talk-plugin-notifications-category-moderation-actions/client/index.js new file mode 100644 index 000000000..b00b6cadd --- /dev/null +++ b/plugins/talk-plugin-notifications-category-moderation-actions/client/index.js @@ -0,0 +1,14 @@ +import translations from './translations.yml'; +import { t } from 'plugin-api/beta/client/services'; +import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories'; + +const SettingsToggle = createSettingsToggle('onModeration', () => + t('talk-plugin-notifications-category-moderation-actions.toggle_description') +); + +export default { + slots: { + notificationSettings: [SettingsToggle], + }, + translations, +}; diff --git a/plugins/talk-plugin-notifications-category-moderation-actions/client/translations.yml b/plugins/talk-plugin-notifications-category-moderation-actions/client/translations.yml new file mode 100644 index 000000000..aa1cea3ac --- /dev/null +++ b/plugins/talk-plugin-notifications-category-moderation-actions/client/translations.yml @@ -0,0 +1,3 @@ +en: + talk-plugin-notifications-category-moderation-actions: + toggle_description: My pending comments have been reviewed diff --git a/plugins/talk-plugin-notifications-category-moderation-actions/config.js b/plugins/talk-plugin-notifications-category-moderation-actions/config.js new file mode 100644 index 000000000..be850bbc8 --- /dev/null +++ b/plugins/talk-plugin-notifications-category-moderation-actions/config.js @@ -0,0 +1,8 @@ +const MODERATION_NOTIFICATION_TYPES = + (process.env.TALK_MODERATION_NOTIFICATION_TYPES && + process.env.TALK_MODERATION_NOTIFICATION_TYPES.split(',')) || + []; + +module.exports = { + MODERATION_NOTIFICATION_TYPES, +}; diff --git a/plugins/talk-plugin-notifications-category-moderation-actions/index.js b/plugins/talk-plugin-notifications-category-moderation-actions/index.js new file mode 100644 index 000000000..92406550a --- /dev/null +++ b/plugins/talk-plugin-notifications-category-moderation-actions/index.js @@ -0,0 +1,153 @@ +const { get } = require('lodash'); +const path = require('path'); + +const { MODERATION_NOTIFICATION_TYPES } = require('./config'); + +const PENDING_STATUS_TYPES = ['PREMOD', 'SYSTEM_WITHHELD']; +const AVAILABLE_NOTIFICATION_TYPES = ['APPROVED', 'REJECTED']; + +const handle = async (ctx, comment) => { + const commentID = get(comment, 'id', null); + if (commentID === null) { + ctx.log.info('could not get comment id'); + return; + } + + // Check to see if this was a pending comment. + const commentHistory = get(comment, 'status_history', []); + let wasPending = false; + + // Check for last status before current one + if (commentHistory.length >= 2) { + const previousStatus = commentHistory[commentHistory.length - 2]; + if (PENDING_STATUS_TYPES.includes(previousStatus.type)) { + wasPending = true; + } + } + + if (!wasPending) { + ctx.log.info('comment was not pending'); + return; + } + + // Execute the graph request. + const commentQl = await ctx.graphql( + ` + query GetAuthorUserMetadata($comment_id: ID!) { + comment(id: $comment_id) { + id + user { + id + notificationSettings { + onModeration + } + } + } + } + `, + { comment_id: commentID } + ); + if (commentQl.errors) { + ctx.log.error( + { err: commentQl.errors }, + 'could not query for author metadata' + ); + return; + } + + // Check if the user has notifications enabled. + const enabled = get( + commentQl, + 'data.comment.user.notificationSettings.onModeration', + false + ); + if (!enabled) { + return; + } + + const userID = get(commentQl, 'data.comment.user.id', null); + if (!userID) { + ctx.log.info('could not get comment user id'); + return; + } + + // The user does have notifications for moderated comments enabled, queue the + // notification to be sent. + return { userID, date: comment.created_at, context: comment.id }; +}; + +const hydrate = async (ctx, category, context) => { + const comment = await ctx.graphql( + ` + query GetNotificationData($context: ID!) { + comment(id: $context) { + id + asset { + title + url + } + status_history { + type + } + } + } + `, + { context } + ); + if (comment.errors) { + throw comment.errors; + } + + const commentData = get(comment, 'data.comment'); + + const headline = get(commentData, 'asset.title', null); + const assetURL = get(commentData, 'asset.url', null); + const permalink = `${assetURL}?commentId=${commentData.id}`; + return [headline, permalink]; +}; + +const handlers = { + approved: { + handle, + category: 'moderation-actions.approved', + event: 'commentAccepted', + hydrate, + digestOrder: 10, + }, + rejected: { + handle, + category: 'moderation-actions.rejected', + event: 'commentRejected', + hydrate, + digestOrder: 10, + }, +}; + +const notifications = []; +MODERATION_NOTIFICATION_TYPES.forEach(type => { + if (AVAILABLE_NOTIFICATION_TYPES.includes(type)) { + notifications.push(handlers[type.toLowerCase()]); + } else { + console.error(`Unkown moderation notification type: ${type}`); + } +}); + +module.exports = { + typeDefs: ` + type NotificationSettings { + onModeration: Boolean! + } + + input NotificationSettingsInput { + onModeration: Boolean + } + `, + resolvers: { + NotificationSettings: { + // onModeration returns false by default if not specified. + onModeration: settings => get(settings, 'onModeration', false), + }, + }, + translations: path.join(__dirname, 'translations.yml'), + notifications, +}; diff --git a/plugins/talk-plugin-notifications-category-moderation-actions/translations.yml b/plugins/talk-plugin-notifications-category-moderation-actions/translations.yml new file mode 100644 index 000000000..2f967e4b9 --- /dev/null +++ b/plugins/talk-plugin-notifications-category-moderation-actions/translations.yml @@ -0,0 +1,10 @@ +en: + talk-plugin-notifications: + categories: + moderation-actions: + approved: + subject: "Your comment on {0} has been published" + body: "{0}\nThank you for submitting your comment. Your comment has now been published: {1}" + rejected: + subject: "Your comment on {0} was not published" + body: "{0}\nThe language used in one of your comments did not comply with our community guidelines, and so the comment has been removed."