From 3658a804b0013380eef3aace54e2dcf31cf4d00d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 15 Jun 2017 01:08:25 +0700 Subject: [PATCH] Implement live updates for mod actions --- .../src/components/ToastContainer.css | 4 +- client/coral-admin/src/graphql/index.js | 54 --- client/coral-admin/src/graphql/utils.js | 62 ++++ client/coral-admin/src/index.js | 4 +- .../routes/Moderation/components/Comment.js | 336 +++++++++--------- .../Moderation/components/Moderation.js | 3 +- .../Moderation/components/ModerationQueue.js | 22 +- .../routes/Moderation/components/styles.css | 18 + .../Moderation/containers/Moderation.js | 88 +++++ client/coral-admin/src/services/client.js | 22 +- client/coral-admin/src/services/store.js | 6 +- client/coral-framework/services/client.js | 3 +- graph/mutators/comment.js | 5 +- graph/resolvers/root_mutation.js | 8 +- graph/resolvers/subscription.js | 5 +- graph/subscriptions.js | 14 + graph/typeDefs.graphql | 8 + package.json | 1 - perms/constants.js | 5 +- perms/index.js | 4 +- perms/subscriptionReducer.js | 11 + services/comments.js | 4 + 22 files changed, 432 insertions(+), 255 deletions(-) create mode 100644 client/coral-admin/src/graphql/utils.js create mode 100644 perms/subscriptionReducer.js diff --git a/client/coral-admin/src/components/ToastContainer.css b/client/coral-admin/src/components/ToastContainer.css index 319872ceb..57ddf3134 100644 --- a/client/coral-admin/src/components/ToastContainer.css +++ b/client/coral-admin/src/components/ToastContainer.css @@ -133,7 +133,7 @@ animation-fill-mode: both; } .toastify { - z-index: 999; + z-index: 99999; position: fixed; padding: 4px; width: 350px; @@ -197,7 +197,7 @@ border-radius: 2px; box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1), 0 3px 20px 0 rgba(0, 0, 0, 0.05); } .toastify-content--info { - background: #2488cb; } + background: #404040; } .toastify-content--success { background: #008577; } .toastify-content--warning { diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 59a6d7c8f..915fe2989 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -1,6 +1,4 @@ import {add} from 'coral-framework/services/graphqlRegistry'; -import update from 'immutability-helper'; -const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected']; const extension = { mutations: { @@ -10,58 +8,6 @@ const extension = { RejectUsername: () => ({ refetchQueries: ['CoralAdmin_Community'], }), - SetCommentStatus: ({variables: {commentId, status}}) => ({ - updateQueries: { - CoralAdmin_Moderation: (prev) => { - const comment = queues.reduce((comment, queue) => { - return comment ? comment : prev[queue].nodes.find((c) => c.id === commentId); - }, null); - - let acceptedNodes = prev.accepted.nodes; - let acceptedCount = prev.acceptedCount; - let rejectedNodes = prev.rejected.nodes; - let rejectedCount = prev.rejectedCount; - - if (status !== comment.status) { - if (status === 'ACCEPTED') { - comment.status = 'ACCEPTED'; - acceptedCount++; - acceptedNodes = [comment, ...acceptedNodes]; - } - else if (status === 'REJECTED') { - comment.status = 'REJECTED'; - rejectedCount++; - rejectedNodes = [comment, ...rejectedNodes]; - } - } - - const premodNodes = prev.premod.nodes.filter((c) => c.id !== commentId); - const flaggedNodes = prev.flagged.nodes.filter((c) => c.id !== commentId); - const premodCount = premodNodes.length < prev.premod.nodes.length ? prev.premodCount - 1 : prev.premodCount; - const flaggedCount = flaggedNodes.length < prev.flagged.nodes.length ? prev.flaggedCount - 1 : prev.flaggedCount; - - if (status === 'REJECTED') { - acceptedNodes = prev.accepted.nodes.filter((c) => c.id !== commentId); - acceptedCount = acceptedNodes.length < prev.accepted.nodes.length ? prev.acceptedCount - 1 : prev.acceptedCount; - } - else if (status === 'ACCEPTED') { - rejectedNodes = prev.rejected.nodes.filter((c) => c.id !== commentId); - rejectedCount = rejectedNodes.length < prev.rejected.nodes.length ? prev.rejectedCount - 1 : prev.rejectedCount; - } - - return update(prev, { - premodCount: {$set: Math.max(0, premodCount)}, - flaggedCount: {$set: Math.max(0, flaggedCount)}, - acceptedCount: {$set: Math.max(0, acceptedCount)}, - rejectedCount: {$set: Math.max(0, rejectedCount)}, - premod: {nodes: {$set: premodNodes}}, - flagged: {nodes: {$set: flaggedNodes}}, - accepted: {nodes: {$set: acceptedNodes}}, - rejected: {nodes: {$set: rejectedNodes}}, - }); - } - } - }), }, }; diff --git a/client/coral-admin/src/graphql/utils.js b/client/coral-admin/src/graphql/utils.js new file mode 100644 index 000000000..7c8bd8875 --- /dev/null +++ b/client/coral-admin/src/graphql/utils.js @@ -0,0 +1,62 @@ +import update from 'immutability-helper'; + +export function findCommentInModQueues(root, id, queues = ['all', 'premod', 'flagged', 'accepted', 'rejected']) { + return queues.reduce((comment, queue) => { + return comment ? comment : root[queue].nodes.find((c) => c.id === id); + }, null); +} + +export function handleCommentStatusChange(root, {id, status}, previousStatus) { + const comment = findCommentInModQueues(root, id); + if (!previousStatus && comment) { + previousStatus = comment.status; + } + + if (status === previousStatus) { + return root; + } + + let acceptedNodes = root.accepted.nodes; + let acceptedCount = root.acceptedCount; + let rejectedNodes = root.rejected.nodes; + let rejectedCount = root.rejectedCount; + + if (status === 'ACCEPTED') { + acceptedCount++; + if (comment) { + acceptedNodes = [{...comment, status}, ...acceptedNodes]; + } + } + else if (status === 'REJECTED') { + rejectedCount++; + if (comment) { + rejectedNodes = [{...comment, status}, ...rejectedNodes]; + } + } + + const premodNodes = root.premod.nodes.filter((c) => c.id !== id); + const flaggedNodes = root.flagged.nodes.filter((c) => c.id !== id); + const premodCount = premodNodes.length < root.premod.nodes.length ? root.premodCount - 1 : root.premodCount; + const flaggedCount = flaggedNodes.length < root.flagged.nodes.length ? root.flaggedCount - 1 : root.flaggedCount; + + if (status === 'REJECTED') { + acceptedNodes = root.accepted.nodes.filter((c) => c.id !== id); + acceptedCount = acceptedNodes.length < root.accepted.nodes.length ? root.acceptedCount - 1 : root.acceptedCount; + } + else if (status === 'ACCEPTED') { + rejectedNodes = root.rejected.nodes.filter((c) => c.id !== id); + rejectedCount = rejectedNodes.length < root.rejected.nodes.length ? root.rejectedCount - 1 : root.rejectedCount; + } + + return update(root, { + premodCount: {$set: Math.max(0, premodCount)}, + flaggedCount: {$set: Math.max(0, flaggedCount)}, + acceptedCount: {$set: Math.max(0, acceptedCount)}, + rejectedCount: {$set: Math.max(0, rejectedCount)}, + premod: {nodes: {$set: premodNodes}}, + flagged: {nodes: {$set: flaggedNodes}}, + accepted: {nodes: {$set: acceptedNodes}}, + rejected: {nodes: {$set: rejectedNodes}}, + }); +} + diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 8cd7685f0..2f2e75275 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -2,7 +2,7 @@ import React from 'react'; import {render} from 'react-dom'; import {ApolloProvider} from 'react-apollo'; -import {client} from './services/client'; +import {getClient} from './services/client'; import store from './services/store'; import App from './components/App'; @@ -15,7 +15,7 @@ loadPluginsTranslations(); injectPluginsReducers(); render( - + , document.querySelector('#root') diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index 9d1019b5e..814b87c82 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -12,194 +12,202 @@ import {getActionSummary} from 'coral-framework/utils'; 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'; const linkify = new Linkify(); import t, {timeago} from 'coral-framework/services/i18n'; -const Comment = ({ - actions = [], - comment, - viewUserDetail, - suspectWords, - bannedWords, - minimal, - selected, - toggleSelect, - ...props -}) => { - const links = linkify.getMatches(comment.body); - const linkText = links ? links.map((link) => link.raw) : []; - const flagActionSummaries = getActionSummary('FlagActionSummary', comment); - const flagActions = - comment.actions && - comment.actions.filter((a) => a.__typename === 'FlagAction'); - let commentType = ''; - if (comment.status === 'PREMOD') { - commentType = 'premod'; - } else if (flagActions && flagActions.length) { - commentType = 'flagged'; - } +class Comment extends React.Component { - // since words are checked against word boundaries on the backend, - // should be the behavior on the front end as well. - // currently the highlighter plugin does not support out of the box. - const searchWords = [...suspectWords, ...bannedWords] - .filter((w) => { - return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body); - }) - .concat(linkText); + render() { + const { + actions = [], + comment, + viewUserDetail, + suspectWords, + bannedWords, + minimal, + selected, + toggleSelect, + className, + ...props + } = this.props; - let selectionStateCSS; - if (minimal) { - selectionStateCSS = selected ? styles.minimalSelection : ''; - } else { - selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; - } + const links = linkify.getMatches(comment.body); + const linkText = links ? links.map((link) => link.raw) : []; + const flagActionSummaries = getActionSummary('FlagActionSummary', comment); + const flagActions = + comment.actions && + comment.actions.filter((a) => a.__typename === 'FlagAction'); + let commentType = ''; + if (comment.status === 'PREMOD') { + commentType = 'premod'; + } else if (flagActions && flagActions.length) { + commentType = 'flagged'; + } - return ( -
  • -
    -
    -
    - { - !minimal && ( - viewUserDetail(comment.user.id)}> - {comment.user.name} - - ) - } - { - minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && ( - toggleSelect(e.target.value, e.target.checked)} /> - ) - } - - {timeago(comment.created_at || Date.now() - props.index * 60 * 1000)} - - {props.currentUserId !== comment.user.id && - - props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}> - Suspend User - props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}> - Ban User - - - } - -
    - {comment.user.status === 'banned' - ? - - {t('comment.banned_user')} + // since words are checked against word boundaries on the backend, + // should be the behavior on the front end as well. + // currently the highlighter plugin does not support out of the box. + const searchWords = [...suspectWords, ...bannedWords] + .filter((w) => { + return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body); + }) + .concat(linkText); + + let selectionStateCSS; + if (minimal) { + selectionStateCSS = selected ? styles.minimalSelection : ''; + } else { + selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; + } + + return ( +
  • +
    +
    +
    + { + !minimal && ( + viewUserDetail(comment.user.id)}> + {comment.user.name} + + ) + } + { + minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && ( + toggleSelect(e.target.value, e.target.checked)} /> + ) + } + + {timeago(comment.created_at || Date.now() - props.index * 60 * 1000)} - : null} - -
    -
    - Story: {comment.asset.title} - {!props.currentAsset && - {t('modqueue.moderate')}} -
    -
    -

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

    - -
    - {links - ? - Contains Link + {props.currentUserId !== comment.user.id && + + props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}> + Suspend User + props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}> + Ban User + + + } + +
    + {comment.user.status === 'banned' + ? + + {t('comment.banned_user')} : 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}))} - /> - ); - })} -
    + +
    +
    + Story: {comment.asset.title} + {!props.currentAsset && + {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}))} + /> + ); + })} +
    + +
    -
    - - {flagActions && flagActions.length - ? - : null} -
  • - ); -}; + + {flagActions && flagActions.length + ? + : null} + + ); + } +} Comment.propTypes = { minimal: PropTypes.bool, viewUserDetail: PropTypes.func.isRequired, acceptComment: PropTypes.func.isRequired, rejectComment: PropTypes.func.isRequired, + className: PropTypes.string, suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired, currentAsset: PropTypes.object, diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index c788da993..1c1aa7690 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -92,9 +92,8 @@ export default class Moderation extends Component { } render () { - const {root, moderation, settings, assets, viewUserDetail, hideUserDetail, ...props} = this.props; + const {root, moderation, settings, assets, viewUserDetail, hideUserDetail, activeTab, ...props} = this.props; const providedAssetId = this.props.params.id; - const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path; let asset; diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index 490c2a985..399fc48ed 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -6,6 +6,7 @@ import EmptyCard from '../../../components/EmptyCard'; import {actionsMap} from '../helpers/moderationQueueActionsMap'; import LoadMore from './LoadMore'; import t from 'coral-framework/services/i18n'; +import {CSSTransitionGroup} from 'react-transition-group'; class ModerationQueue extends React.Component { @@ -43,12 +44,27 @@ class ModerationQueue extends React.Component { commentCount, singleView, viewUserDetail, + activeTab, ...props } = this.props; return (
    -
      + { comments.length ? comments.map((comment, i) => { @@ -56,7 +72,7 @@ class ModerationQueue extends React.Component { return {t('modqueue.empty_queue')} } -
    + length) ? `${s.substring(0, length)}...` : s; +} + class ModerationContainer extends Component { + unsubscribe = null; + + get activeTab() { return this.props.route.path === ':id' ? 'premod' : this.props.route.path; } + + subscribeToUpdates() { + this.unsubscribe = this.props.data.subscribeToMore({ + document: STATUS_CHANGED_SUBSCRIPTION, + variables: { + asset_id: this.props.data.variables.asset_id, + }, + updateQuery: (prev, {subscriptionData: {data: {commentStatusChanged: {user, comment, previous}}}}) => { + const activeTab = this.activeTab; + + // Status changed was caused by a different user. + if (user && user.id !== this.props.auth.user.id) { + if (findCommentInModQueues(prev, comment.id) && ( + activeTab === 'all' && findCommentInModQueues(prev, comment.id, ['all']) + || activeTab === 'premod' && previous.status === 'PREMOD' + || activeTab === 'flagged' && findCommentInModQueues(prev, comment.id, ['flagged']) + || comment.status === 'ACCEPTED' && activeTab === 'accepted' + || comment.status !== 'ACCEPTED' && previous.status === 'ACCEPTED' && activeTab === 'accepted' + || comment.status === 'REJECTED' && activeTab === 'rejected' + || comment.status !== 'REJECTED' && previous.status === 'REJECTED' && activeTab === 'rejected' + ) + ) { + const text = `${user.username} ${comment.status.toLowerCase()} comment "${truncate(comment.body, 50)}"`; + notification.info(text); + } + } + return handleCommentStatusChange(prev, comment, previous.status, user); + }, + }); + } + + unsubscribe() { + if (!this.unsubscribe) { + return; + } + this.unsubscribe(); + this.unsubscribe = null; + } + + resubscribe() { + this.unsubscribe(); + this.subscribeToUpdates(); + } + componentWillMount() { this.props.fetchSettings(); + this.subscribeToUpdates(); + } + + componentWillUnmount() { + this.unsubscribe(); } componentWillReceiveProps(nextProps) { @@ -40,6 +97,11 @@ class ModerationContainer extends Component { if(!isEqual(nextProps.root.assets, this.props.root.assets)) { updateAssets(nextProps.root.assets); } + + // Resubscribe when we change between assets. + if(this.props.data.variables.asset_id !== nextProps.data.variables.asset_id) { + this.resubscribe(); + } } suspendUser = async (args) => { @@ -113,6 +175,9 @@ class ModerationContainer extends Component { return update(prev, { [tab]: { nodes: {$push: comments.nodes}, + hasNextPage: {$set: comments.hasNextPage}, + startCursor: {$set: comments.startCursor}, + endCursor: {$set: comments.endCursor}, }, }); } @@ -137,10 +202,30 @@ class ModerationContainer extends Component { acceptComment={this.acceptComment} rejectComment={this.rejectComment} suspendUser={this.suspendUser} + activeTab={this.activeTab} />; } } +const STATUS_CHANGED_SUBSCRIPTION = gql` + subscription CommentStatusChanged($asset_id: ID){ + commentStatusChanged(asset_id: $asset_id){ + user { + id + username + } + comment { + id + status + body + } + previous { + status + } + } + } +`; + const LOAD_MORE_QUERY = gql` query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) { comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) { @@ -153,6 +238,9 @@ const LOAD_MORE_QUERY = gql` } } } + hasNextPage + startCursor + endCursor } } ${Comment.fragments.comment} diff --git a/client/coral-admin/src/services/client.js b/client/coral-admin/src/services/client.js index df97066bc..934de2c9e 100644 --- a/client/coral-admin/src/services/client.js +++ b/client/coral-admin/src/services/client.js @@ -1,20 +1,6 @@ -import ApolloClient, {addTypename} from 'apollo-client'; -import {networkInterface} from 'coral-framework/services/transport'; +import {getClient as getFrameworkClient} from 'coral-framework/services/client'; import fragmentMatcher from './fragmentMatcher'; -export const client = new ApolloClient({ - fragmentMatcher, - connectToDevTools: true, - addTypename: true, - queryTransformer: addTypename, - dataIdFromObject: (result) => { - if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle - return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle - } - return null; - }, - networkInterface -}); - -export default client; - +export function getClient() { + return getFrameworkClient({fragmentMatcher}); +} diff --git a/client/coral-admin/src/services/store.js b/client/coral-admin/src/services/store.js index 9815a0f70..06cb3c758 100644 --- a/client/coral-admin/src/services/store.js +++ b/client/coral-admin/src/services/store.js @@ -1,10 +1,10 @@ import {createStore, combineReducers, applyMiddleware, compose} from 'redux'; import thunk from 'redux-thunk'; import mainReducer from '../reducers'; -import {client} from './client'; +import {getClient} from './client'; const middlewares = [ - applyMiddleware(client.middleware()), + applyMiddleware(getClient().middleware()), applyMiddleware(thunk) ]; @@ -16,7 +16,7 @@ if (window.devToolsExtension) { const coralReducers = { ...mainReducer, - apollo: client.reducer() + apollo: getClient().reducer() }; const store = createStore( diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index 26d8e391f..307646db0 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -26,7 +26,7 @@ export function resetWebsocket() { }); } -export function getClient() { +export function getClient(options = {}) { if (client) { return client; } @@ -56,6 +56,7 @@ export function getClient() { ); client = new ApolloClient({ + ...options, connectToDevTools: true, addTypename: true, queryTransformer: addTypename, diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 247eb264e..e11eaf2b9 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -326,7 +326,7 @@ const createPublicComment = async (context, commentInput) => { * @param {String} id identifier of the comment (uuid) * @param {String} status the new status of the comment */ -const setStatus = async ({user, loaders: {Comments}}, {id, status}) => { +const setStatus = async ({user, loaders: {Comments}, pubsub}, {id, status}) => { let comment = await CommentsService.pushStatus(id, status, user ? user.id : null); // If the loaders are present, clear the caches for these values because we @@ -370,6 +370,9 @@ 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 f192405cc..087ec94bc 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -31,7 +31,13 @@ const RootMutation = { stopIgnoringUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.stopIgnoringUser({id})); }, - setCommentStatus(_, {id, status}, {mutators: {Comment}}) { + setCommentStatus: async (_, {id, status}, {loaders: {Comments}, mutators: {Comment}, user, pubsub}) => { + const previous = await Comments.get.load(id); + const comment = await Comment.setStatus({id, status}); + + // Publish the comment status change via the subscription. + pubsub.publish('commentStatusChanged', {user, comment, previous}); + return wrapResponse(null)(Comment.setStatus({id, status})); }, addTag(_, {tag}, {mutators: {Tag}}) { diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js index a593c1eb6..8a989cc81 100644 --- a/graph/resolvers/subscription.js +++ b/graph/resolvers/subscription.js @@ -4,7 +4,10 @@ const Subscription = { }, commentEdited(comment) { return comment; - } + }, + commentStatusChanged(data) { + return data; + }, }; module.exports = Subscription; diff --git a/graph/subscriptions.js b/graph/subscriptions.js index 7d97d3521..2192dbdcf 100644 --- a/graph/subscriptions.js +++ b/graph/subscriptions.js @@ -10,6 +10,10 @@ const plugins = require('../services/plugins'); const {deserializeUser} = require('../services/subscriptions'); +const { + SUBSCRIBE_COMMENT_STATUS, +} = require('../perms/constants'); + /** * Plugin support requires that we merge in existing setupFunctions with our new * plugin based ones. This allows plugins to extend existing setupFunctions as well @@ -30,6 +34,16 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu filter: (comment) => comment.asset_id === args.asset_id }, }), + commentStatusChanged: (options, args) => ({ + commentStatusChanged: { + filter: ({comment}, context) => { + if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_STATUS)) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + } + }, + }), }); /** diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 117b7e077..c1dfa0b7d 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -936,9 +936,17 @@ type RootMutation { ## Subscriptions ################################################################################ +# Response to ignoreUser mutation +type CommentStatusChangedUpdate { + user: User + comment: Comment + previous: Comment +} + type Subscription { commentAdded(asset_id: ID!): Comment commentEdited(asset_id: ID!): Comment + commentStatusChanged(asset_id: ID): CommentStatusChangedUpdate } ################################################################################ diff --git a/package.json b/package.json index f6a969de9..92bcd2be4 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,6 @@ "regenerator": "^0.8.46", "selenium-standalone": "^5.11.2", "style-loader": "^0.16.0", - "subscriptions-transport-ws": "^0.5.5-alpha.0", "supertest": "^2.0.1", "timeago.js": "^2.0.3", "webpack": "^2.3.1" diff --git a/perms/constants.js b/perms/constants.js index 2b5b907ac..1578946cf 100644 --- a/perms/constants.js +++ b/perms/constants.js @@ -21,5 +21,8 @@ module.exports = { SEARCH_ACTIONS: 'SEARCH_ACTIONS', SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS', SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS', - SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS' + SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS', + + // subscriptions + SUBSCRIBE_COMMENT_STATUS: 'SUBSCRIBE_COMMENT_STATUS', }; diff --git a/perms/index.js b/perms/index.js index f0e14c5fc..be6e7a2cd 100644 --- a/perms/index.js +++ b/perms/index.js @@ -2,11 +2,13 @@ const constants = require('./constants'); const root = require('./rootReducer'); const queries = require('./queryReducer'); const mutations = require('./mutationReducer'); +const subscriptions = require('./subscriptionReducer'); const reducers = [ root, queries, - mutations + mutations, + subscriptions, ]; // this will make 'reducer' a key in this array. hm. diff --git a/perms/subscriptionReducer.js b/perms/subscriptionReducer.js new file mode 100644 index 000000000..4ce817ea5 --- /dev/null +++ b/perms/subscriptionReducer.js @@ -0,0 +1,11 @@ +const {check} = require('./utils'); +const types = require('./constants'); + +module.exports = (user, perm) => { + switch (perm) { + case types.SUBSCRIBE_COMMENT_STATUS: + return check(user, ['ADMIN', 'MODERATOR']); + default: + break; + } +}; diff --git a/services/comments.js b/services/comments.js index 3f18d55e3..af7593a8e 100644 --- a/services/comments.js +++ b/services/comments.js @@ -215,6 +215,10 @@ module.exports = class CommentsService { } }, $set: {status} + }, { + + // return modified comment. + new: true, }); }