From 143adc7c6f5dba57822a9454635be04aa8fcd3d9 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 21 Sep 2017 23:39:04 +0700 Subject: [PATCH] Live update modqueue on new comments --- .../Moderation/components/Moderation.js | 1 + .../Moderation/components/ModerationQueue.css | 0 .../Moderation/components/ModerationQueue.js | 99 +++++++++++++- .../routes/Moderation/components/ViewMore.css | 21 +++ .../routes/Moderation/components/ViewMore.js | 24 ++++ .../Moderation/containers/Moderation.js | 123 ++++++++---------- graph/mutators/comment.js | 6 +- graph/setupFunctions.js | 22 +++- graph/typeDefs.graphql | 3 +- 9 files changed, 225 insertions(+), 74 deletions(-) create mode 100644 client/coral-admin/src/routes/Moderation/components/ModerationQueue.css create mode 100644 client/coral-admin/src/routes/Moderation/components/ViewMore.css create mode 100644 client/coral-admin/src/routes/Moderation/components/ViewMore.js diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index 98a5d5837..dcbb7510e 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -132,6 +132,7 @@ export default class Moderation extends Component { activeTab={activeTab} /> nodes.some((node) => node.id === id); + +// resetCursors will return the id cursors of the first and second comment of +// the current comment list. The cursors are used to dertermine which +// comments to show. The spare cursor functions as a backup in case one +// of the comments gets deleted. +function resetCursors(state, props) { + if (props.comments && props.comments.length) { + const idCursors = [props.comments[0].id]; + if (props.comments[1]) { + idCursors.push(props.comments[1].id); + } + return {idCursors}; + } + return {idCursors: []}; +} + +// invalidateCursor is called whenever a comment is removed which is referenced +// by one of the 2 id cursors. It returns a new set of id cursors calculated +// using the help of the backup cursor. +function invalidateCursor(invalidated, state, props) { + const alt = invalidated === 1 ? 0 : 1; + const idCursors = []; + if (state.idCursors[alt]) { + idCursors.push(state.idCursors[alt]); + const index = props.comments.findIndex((node) => node.id === idCursors[0]); + const nextInLine = props.comments[index + 1]; + if (nextInLine) { + idCursors.push(nextInLine.id); + } + } + return {idCursors}; +} + class ModerationQueue extends React.Component { isLoadingMore = false; @@ -38,6 +73,9 @@ class ModerationQueue extends React.Component { constructor(props) { super(props); + this.state = { + ...resetCursors(this.state, props), + }; } componentDidUpdate (prev) { @@ -51,6 +89,59 @@ class ModerationQueue extends React.Component { } } + componentWillReceiveProps(next) { + const {comments: prevComments} = this.props; + const {comments: nextComments} = next; + + if (!prevComments && nextComments) { + this.setState(resetCursors); + return; + } + + if ( + prevComments && nextComments && + nextComments.length < prevComments.length + ) { + + // Invalidate first cursor if referenced comment was removed. + if (this.state.idCursors[0] && !hasComment(nextComments, this.state.idCursors[0])) { + this.setState(invalidateCursor(0, this.state, next)); + } + + // Invalidate second cursor if referenced comment was removed. + if (this.state.idCursors[1] && !hasComment(nextComments, this.state.idCursors[1])) { + this.setState(invalidateCursor(1, this.state, next)); + } + } + } + + viewNewComments = () => { + this.setState(resetCursors); + }; + + // getVisibileComments returns a list containing comments + // which comes after the `idCursor`. + getVisibleComments() { + const {comments} = this.props; + const idCursor = this.state.idCursors[0]; + + if (!comments) { + return []; + } + + const view = []; + let pastCursor = false; + comments.forEach((comment) => { + if (comment.id === idCursor) { + pastCursor = true; + } + if (pastCursor) { + view.push(comment); + } + }); + return view; + } + render () { const { comments, @@ -62,8 +153,14 @@ class ModerationQueue extends React.Component { ...props } = this.props; + const view = this.getVisibleComments(); + return (
+ { - comments.map((comment, i) => { + view.map((comment, i) => { const status = comment.action_summaries ? 'FLAGGED' : comment.status; return +
+ { + count > 0 && + } +
; + +ViewMore.propTypes = { + viewMore: PropTypes.func.isRequired, + count: PropTypes.number.isRequired, + className: PropTypes.string +}; + +export default ViewMore; diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index baf6b8dfd..48b4a9364 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -82,50 +82,56 @@ class ModerationContainer extends Component { } subscribeToUpdates(variables = this.props.data.variables) { - const sub1 = this.props.data.subscribeToMore({ - document: COMMENT_ACCEPTED_SUBSCRIPTION, - variables, - updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => { - const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const notifyText = this.props.auth.user.id === user.id - ? '' - : t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notifyText); + const parameters = [ + { + document: COMMENT_ADDED_SUBSCRIPTION, + variables, + updateQuery: (prev, {subscriptionData: {data: {commentAdded: comment}}}) => { + return this.handleCommentChange(prev, comment); + }, }, - }); - - const sub2 = this.props.data.subscribeToMore({ - document: COMMENT_REJECTED_SUBSCRIPTION, - variables, - updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => { - const user = comment.status_history[comment.status_history.length - 1].assigned_by; - const notifyText = this.props.auth.user.id === user.id - ? '' - : t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notifyText); + { + document: COMMENT_ACCEPTED_SUBSCRIPTION, + variables, + updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => { + const user = comment.status_history[comment.status_history.length - 1].assigned_by; + const notifyText = this.props.auth.user.id === user.id + ? '' + : t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); + }, }, - }); - - const sub3 = this.props.data.subscribeToMore({ - document: COMMENT_EDITED_SUBSCRIPTION, - variables, - updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => { - const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notifyText); + { + document: COMMENT_REJECTED_SUBSCRIPTION, + variables, + updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => { + const user = comment.status_history[comment.status_history.length - 1].assigned_by; + const notifyText = this.props.auth.user.id === user.id + ? '' + : t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); + }, }, - }); - - const sub4 = this.props.data.subscribeToMore({ - document: COMMENT_FLAGGED_SUBSCRIPTION, - variables, - updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => { - const user = comment.actions[comment.actions.length - 1].user; - const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)); - return this.handleCommentChange(prev, comment, notifyText); + { + document: COMMENT_EDITED_SUBSCRIPTION, + variables, + updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => { + const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); + }, }, - }); + { + document: COMMENT_FLAGGED_SUBSCRIPTION, + variables, + updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => { + const user = comment.actions[comment.actions.length - 1].user; + const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); + }, + }, + ]; - this.subscriptions.push(sub1, sub2, sub3, sub4); + this.subscriptions = parameters.map((param) => this.props.data.subscribeToMore(param)); } unsubscribe() { @@ -204,12 +210,9 @@ class ModerationContainer extends Component { // Not found. return ; } - if (asset === undefined || asset.id !== assetId) { + } - // Still loading. - return ; - } - } else if (asset !== undefined || !('premodCount' in root)) { + if(data.loading) { // loading. return ; @@ -240,6 +243,14 @@ class ModerationContainer extends Component { />; } } +const COMMENT_ADDED_SUBSCRIPTION = gql` + subscription CommentAdded($asset_id: ID){ + commentAdded(asset_id: $asset_id, statuses: null){ + ...${getDefinitionName(Comment.fragments.comment)} + } + } + ${Comment.fragments.comment} +`; const COMMENT_EDITED_SUBSCRIPTION = gql` subscription CommentEdited($asset_id: ID){ @@ -369,29 +380,6 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql` }, }); -const withQueueCountPolling = withQuery(({queueConfig}) => gql` - query CoralAdmin_ModerationCountPoll($asset_id: ID) { - ${Object.keys(queueConfig).map((queue) => ` - ${queue}Count: commentCount(query: { - ${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''} - ${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''} - ${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''} - asset_id: $asset_id, - }) - `)} - } -`, { - options: (props) => { - const id = getAssetId(props); - return { - pollInterval: 5000, - variables: { - asset_id: id - } - }; - } -}); - const mapStateToProps = (state) => ({ moderation: state.moderation, settings: state.settings, @@ -419,6 +407,5 @@ export default compose( withQueueConfig(baseQueueConfig), connect(mapStateToProps, mapDispatchToProps), withSetCommentStatus, - withQueueCountPolling, withModQueueQuery, )(ModerationContainer); diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index a1315d847..de3345257 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -182,11 +182,11 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu Comments.parentCountByAssetID.incr(asset_id); } Comments.countByAssetID.incr(asset_id); - - // Publish the newly added comment via the subscription. - pubsub.publish('commentAdded', comment); } + // Publish the newly added comment via the subscription. + pubsub.publish('commentAdded', comment); + return comment; }; diff --git a/graph/setupFunctions.js b/graph/setupFunctions.js index 273aedfa2..42dc15a9f 100644 --- a/graph/setupFunctions.js +++ b/graph/setupFunctions.js @@ -26,10 +26,30 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu commentAdded: (options, args) => ({ commentAdded: { filter: (comment, context) => { + + // Only priviledged users can subscribe to all assets. if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) { return false; } - return !args.asset_id || comment.asset_id === args.asset_id; + + // If user scubsscribes for statuses other than NONE and/or ACCEPTED statuses, it needs + // special priviledges. + if ( + (!args.statuses || args.statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) && + (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED)) + ) { + return false; + } + + if (args.asset_id && comment.asset_id !== args.asset_id) { + return false; + } + + if (args.statuses && !args.statuses.includes(comment.status)) { + return false; + } + + return true; } }, }), diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 70167bb67..ca2a39871 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1356,7 +1356,8 @@ type Subscription { # Get an update whenever a comment was added. # `asset_id` is required except for users with the `ADMIN` or `MODERATOR` role. - commentAdded(asset_id: ID): Comment + # Non privileged user can only subscribe to 'NONE' and/or 'ACCEPTED' statuses. + commentAdded(asset_id: ID, statuses: [COMMENT_STATUS!] = [NONE, ACCEPTED]): Comment # Get an update whenever a comment was edited. # `asset_id` is required except for users with the `ADMIN` or `MODERATOR` role.