diff --git a/client/coral-admin/src/graphql/utils.js b/client/coral-admin/src/graphql/utils.js index d42029df4..62126f1af 100644 --- a/client/coral-admin/src/graphql/utils.js +++ b/client/coral-admin/src/graphql/utils.js @@ -90,10 +90,12 @@ export function handleCommentStatusChange(root, comment, {sort, notify, user, ac const nextQueues = getCommentQueues(comment); queues.forEach((queue) => { - if (nextQueues.indexOf(queue) >= 0 && !queueHasComment(next, queue, comment.id)) { - next = addCommentToQueue(next, queue, comment, sort); - if (notify && activeQueue === queue && isCommentInCursor(next, queue, comment, sort)) { - showNotification(queue, comment, user); + if (nextQueues.indexOf(queue) >= 0) { + if (!queueHasComment(next, queue, comment.id)) { + next = addCommentToQueue(next, queue, comment, sort); + if (notify && activeQueue === queue && isCommentInCursor(next, queue, comment, sort)) { + showNotification(queue, comment, user); + } } } else if(queueHasComment(next, queue, comment.id)){ next = removeCommentFromQueue(next, queue, comment.id); @@ -102,10 +104,27 @@ export function handleCommentStatusChange(root, comment, {sort, notify, user, ac } } - // TODO: All notification + if ( + queue === 'all' + && queueHasComment(next, queue, comment.id) + && notify + && activeQueue === queue + ) { + showNotification(queue, comment, user); + } + // TODO: Flagged notification // TODO: Edited notification }); return next; } +export function handleCommentEdit(root, comment, {sort, activeQueue}) { + if ( + queueHasComment(root, activeQueue, comment.id) + ) { + const text = `${comment.user.username} edited comment to "${truncate(comment.body, 50)}"`; + notification.info(text); + } + return handleCommentStatusChange(root, comment, {sort, activeQueue}); +} diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 814b87c82..18f3aa057 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -13,6 +13,8 @@ import ActionButton from 'coral-admin/src/components/ActionButton'; import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; import cn from 'classnames'; +import {murmur3} from 'murmurhash-js'; +import {CSSTransitionGroup} from 'react-transition-group'; const linkify = new Linkify(); @@ -91,6 +93,11 @@ class Comment extends React.Component { {timeago(comment.created_at || Date.now() - props.index * 60 * 1000)} + { + (comment.editing && comment.editing.edited) + ?  (Edited) + : null + } {props.currentUserId !== comment.user.id && {t('modqueue.moderate')}} -
-

- - {' '} - - {t('comment.view_context')} - -

- -
- {links - ? - Contains Link - - : null} -
- {actions.map((action, i) => { - const active = - (action === 'REJECT' && comment.status === 'REJECTED') || - (action === 'APPROVE' && comment.status === 'ACCEPTED'); - return ( - - (comment.status === 'ACCEPTED' - ? null - : props.acceptComment({commentId: comment.id}))} - rejectComment={() => - (comment.status === 'REJECTED' - ? null - : props.rejectComment({commentId: comment.id}))} - /> - ); - })} -
+ +
+

+ + {' '} + + {t('comment.view_context')} + +

+
+ {links + ? + Contains Link + + : null} +
+ {actions.map((action, i) => { + const active = + (action === 'REJECT' && comment.status === 'REJECTED') || + (action === 'APPROVE' && comment.status === 'ACCEPTED'); + return ( + + (comment.status === 'ACCEPTED' + ? null + : props.acceptComment({commentId: comment.id}))} + rejectComment={() => + (comment.status === 'REJECTED' + ? null + : props.rejectComment({commentId: comment.id}))} + /> + ); + })} +
+ +
-
+
0) { + this.loadMore(); + } + } + componentDidUpdate (prev) { const {comments, commentCount} = this.props; @@ -66,8 +74,7 @@ class ModerationQueue extends React.Component { transitionLeaveTimeout={1000} > { - comments.length - ? comments.map((comment, i) => { + comments.map((comment, i) => { const status = comment.action_summaries ? 'FLAGGED' : comment.status; return ; }) - : {t('modqueue.empty_queue')} } + {comments.length === 0 &&

{t('modqueue.empty_queue')}

} + { + updateQuery: (prev, {subscriptionData: {data: {commentStatusChanged: {user, comment}}}}) => { const extraParams = this.props.auth.user.id === user.id ? {} : { notify: true, user, activeQueue: this.activeTab, - previous, }; return handleCommentStatusChange(prev, comment, { sort: this.props.moderation.sortOrder, @@ -57,14 +56,25 @@ class ModerationContainer extends Component { }); }, }); + + const sub2 = this.props.data.subscribeToMore({ + document: COMMENTS_EDITED_SUBSCRIPTION, + variables: { + asset_id: this.props.data.variables.asset_id, + }, + updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => { + return handleCommentEdit(prev, commentEdited, { + activeQueue: this.activeTab, + }); + }, + }); + + this.subscriptions.push(sub1, sub2); } unsubscribe() { - if (!this.unsubscribe) { - return; - } - this.unsubscribe(); - this.unsubscribe = null; + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions = []; } resubscribe() { @@ -196,6 +206,23 @@ class ModerationContainer extends Component { } } +const COMMENTS_EDITED_SUBSCRIPTION = gql` + subscription CommentEdited($asset_id: ID){ + commentEdited(asset_id: $asset_id){ + id + body + status + editing { + edited + } + user { + id + username + } + } + } +`; + const STATUS_CHANGED_SUBSCRIPTION = gql` subscription CommentStatusChanged($asset_id: ID){ commentStatusChanged(asset_id: $asset_id){ @@ -224,9 +251,6 @@ const STATUS_CHANGED_SUBSCRIPTION = gql` } } } - previous { - status - } } } `; diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index e11eaf2b9..d9dd47a9a 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -370,11 +370,7 @@ const edit = async (context, {id, asset_id, edit: {body}}) => { // Publish the edited comment via the subscription. context.pubsub.publish('commentEdited', comment); - - // Publish the comment status change via the subscription. - context.pubsub.publish('commentStatusChanged', comment); } - return comment; }; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 087ec94bc..83886f0d1 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -31,14 +31,16 @@ const RootMutation = { stopIgnoringUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.stopIgnoringUser({id})); }, - setCommentStatus: async (_, {id, status}, {loaders: {Comments}, mutators: {Comment}, user, pubsub}) => { - const previous = await Comments.get.load(id); + setCommentStatus: async (_, {id, status}, {mutators: {Comment}, user, pubsub}) => { const comment = await Comment.setStatus({id, status}); - // Publish the comment status change via the subscription. - pubsub.publish('commentStatusChanged', {user, comment, previous}); + if (pubsub) { - return wrapResponse(null)(Comment.setStatus({id, status})); + // Publish the comment status change via the subscription. + pubsub.publish('commentStatusChanged', {user, comment}); + } + + return wrapResponse(null)(comment); }, addTag(_, {tag}, {mutators: {Tag}}) { return wrapResponse(null)(Tag.add(tag)); diff --git a/graph/subscriptions.js b/graph/subscriptions.js index 2192dbdcf..9b3df6b0e 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -12,6 +12,7 @@ const {deserializeUser} = require('../services/subscriptions'); const { SUBSCRIBE_COMMENT_STATUS, + SUBSCRIBE_ALL_COMMENT_EDITS, } = require('../perms/constants'); /** @@ -31,7 +32,12 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu }), commentEdited: (options, args) => ({ commentEdited: { - filter: (comment) => comment.asset_id === args.asset_id + filter: (comment, context) => { + if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_EDITS))) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + } }, }), commentStatusChanged: (options, args) => ({ diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index c1dfa0b7d..2bd7c0476 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -940,12 +940,11 @@ type RootMutation { type CommentStatusChangedUpdate { user: User comment: Comment - previous: Comment } type Subscription { commentAdded(asset_id: ID!): Comment - commentEdited(asset_id: ID!): Comment + commentEdited(asset_id: ID): Comment commentStatusChanged(asset_id: ID): CommentStatusChangedUpdate } diff --git a/package.json b/package.json index 92bcd2be4..5724e96ca 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "mongoose": "^4.9.8", "morgan": "^1.8.1", "ms": "^2.0.0", + "murmurhash-js": "^1.0.0", "natural": "^0.5.0", "node-emoji": "^1.5.1", "node-fetch": "^1.6.3", diff --git a/perms/constants.js b/perms/constants.js index 1578946cf..0b14932f3 100644 --- a/perms/constants.js +++ b/perms/constants.js @@ -25,4 +25,6 @@ module.exports = { // subscriptions SUBSCRIBE_COMMENT_STATUS: 'SUBSCRIBE_COMMENT_STATUS', + SUBSCRIBE_ALL_COMMENT_FLAGS: 'SUBSCRIBE_ALL_COMMENT_FLAGS', + SUBSCRIBE_ALL_COMMENT_EDITS: 'SUBSCRIBE_ALL_COMMENT_EDITS', }; diff --git a/perms/subscriptionReducer.js b/perms/subscriptionReducer.js index 4ce817ea5..e2570b272 100644 --- a/perms/subscriptionReducer.js +++ b/perms/subscriptionReducer.js @@ -5,6 +5,10 @@ module.exports = (user, perm) => { switch (perm) { case types.SUBSCRIBE_COMMENT_STATUS: return check(user, ['ADMIN', 'MODERATOR']); + case types.SUBSCRIBE_ALL_COMMENT_EDITS: + return check(user, ['ADMIN', 'MODERATOR']); + case types.SUBSCRIBE_ALL_COMMENT_FLAGS: + return check(user, ['ADMIN', 'MODERATOR']); default: break; } diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index 78bc45dfd..243b8915b 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -132,8 +132,11 @@ function getReactionConfig(reaction) { return Action.create({item_id, item_type: 'COMMENTS', action_type: REACTION}) .then((action) => { - // The comment is needed to allow better filtering e.g. by asset_id. - pubsub.publish(`${reaction}ActionCreated`, {action, comment}); + if (pubsub) { + + // The comment is needed to allow better filtering e.g. by asset_id. + pubsub.publish(`${reaction}ActionCreated`, {action, comment}); + } return Promise.resolve(action); }) .catch((err) => { @@ -155,8 +158,11 @@ function getReactionConfig(reaction) { } return Comments.get.load(action.item_id).then((comment) => { - // The comment is needed to allow better filtering e.g. by asset_id. - pubsub.publish(`${reaction}ActionDeleted`, {action, comment}); + if (pubsub) { + + // The comment is needed to allow better filtering e.g. by asset_id. + pubsub.publish(`${reaction}ActionDeleted`, {action, comment}); + } return Promise.resolve(action); }); }); diff --git a/yarn.lock b/yarn.lock index 6013a3e49..1633c27d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5387,6 +5387,10 @@ muri@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/muri/-/muri-1.2.1.tgz#ec7ea5ce6ca6a523eb1ab35bacda5fa816c9aa3c" +murmurhash-js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" + mute-stream@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.4.tgz#a9219960a6d5d5d046597aee51252c6655f7177e"