diff --git a/client/coral-admin/src/routes/Moderation/containers/Comment.js b/client/coral-admin/src/routes/Moderation/containers/Comment.js index 9f7bfc959..35f11a3fb 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Comment.js +++ b/client/coral-admin/src/routes/Moderation/containers/Comment.js @@ -59,6 +59,9 @@ export default withFragments({ editing { edited } + status_history { + type + } hasParent ${getSlotFragmentSpreads(slots, 'comment')} ...${getDefinitionName(CommentLabels.fragments.comment)} diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index 200e5f6ae..1b75d23cc 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -111,6 +111,17 @@ class ModerationContainer extends Component { return this.handleCommentChange(prev, comment, notifyText); }, }, + { + document: COMMENT_RESET_SUBSCRIPTION, + variables, + updateQuery: (prev, {subscriptionData: {data: {commentReset: 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_reset', user.username, prepareNotificationText(comment.body)); + return this.handleCommentChange(prev, comment, notifyText); + }, + }, { document: COMMENT_EDITED_SUBSCRIPTION, variables, @@ -299,6 +310,23 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql` ${Comment.fragments.comment} `; +const COMMENT_RESET_SUBSCRIPTION = gql` + subscription CommentReset($asset_id: ID){ + commentReset(asset_id: $asset_id){ + ...${getDefinitionName(Comment.fragments.comment)} + status_history { + type + created_at + assigned_by { + id + username + } + } + } + } + ${Comment.fragments.comment} +`; + const LOAD_MORE_QUERY = gql` query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) { comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) { diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index b9957bbce..f5bead3b8 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -158,6 +158,7 @@ } .content { + word-wrap: break-word; } .footer { diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 0d9323687..7ae36b5fa 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -182,6 +182,7 @@ export default class Comment extends React.Component { editableUntil: PropTypes.string, }) }).isRequired, + setCommentStatus: PropTypes.func.isRequired, // edit a comment, passed (id, asset_id, { body }) editComment: PropTypes.func, @@ -343,7 +344,12 @@ export default class Comment extends React.Component { } = this.props; if (!highlighted && this.commentIsRejected(comment)) { - return ; + return { + this.props.setCommentStatus({ + commentId: comment.id, + status: comment.status_history[comment.status_history.length - 2].type, + }); + }}/>; } if (this.commentIsIgnored(comment)) { diff --git a/client/coral-embed-stream/src/components/CommentTombstone.css b/client/coral-embed-stream/src/components/CommentTombstone.css index 6119a5cd8..a8caeec7a 100644 --- a/client/coral-embed-stream/src/components/CommentTombstone.css +++ b/client/coral-embed-stream/src/components/CommentTombstone.css @@ -3,4 +3,10 @@ text-align: center; padding: 1em; color: #3E4F71; +} + +.undo { + cursor: pointer; + text-decoration: underline; + margin-left: 5px; } \ No newline at end of file diff --git a/client/coral-embed-stream/src/components/CommentTombstone.js b/client/coral-embed-stream/src/components/CommentTombstone.js index cc3362563..4347d4277 100644 --- a/client/coral-embed-stream/src/components/CommentTombstone.js +++ b/client/coral-embed-stream/src/components/CommentTombstone.js @@ -24,6 +24,9 @@ class CommentTombstone extends React.Component {

{this.getCopy()} + {this.props.action === 'reject' && + {t('comment.undo_reject')} + }

); @@ -32,6 +35,7 @@ class CommentTombstone extends React.Component { CommentTombstone.propTypes = { action: PropTypes.string, + onUndo: PropTypes.func, }; export default CommentTombstone; diff --git a/client/coral-embed-stream/src/containers/Comment.js b/client/coral-embed-stream/src/containers/Comment.js index 694314bd7..26fc872d5 100644 --- a/client/coral-embed-stream/src/containers/Comment.js +++ b/client/coral-embed-stream/src/containers/Comment.js @@ -3,6 +3,7 @@ import React from 'react'; import Comment from '../components/Comment'; import {withFragments} from 'coral-framework/hocs'; import {getSlotFragmentSpreads} from 'coral-framework/utils'; +import {withSetCommentStatus} from 'coral-framework/graphql/mutations'; import {THREADING_LEVEL} from '../constants/stream'; import hoistStatics from 'recompose/hoistStatics'; import {nest} from '../graphql/utils'; @@ -75,6 +76,9 @@ const singleCommentFragment = gql` id username } + status_history { + type + } action_summaries { __typename count @@ -130,6 +134,7 @@ const withCommentFragments = withFragments({ const enhance = compose( withAnimateEnter, withCommentFragments, + withSetCommentStatus, ); export default enhance(Comment); diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 3f6fd4832..d18cdc435 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -102,6 +102,9 @@ export default { } } } + status_history { + type + } action_summaries { count current_user { @@ -182,6 +185,7 @@ export default { editableUntil: new Date().toISOString(), edited: false, }, + status_history: [], id: `pending-${uuid()}`, } } diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 0fe0d8896..e1acd82f4 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -3,6 +3,7 @@ import cn from 'classnames'; import styles from './Slot.css'; import {connect} from 'react-redux'; import omit from 'lodash/omit'; +import kebabCase from 'lodash/kebabCase'; import PropTypes from 'prop-types'; import isEqual from 'lodash/isEqual'; import {getShallowChanges} from 'coral-framework/utils'; @@ -58,6 +59,7 @@ class Slot extends React.Component { childFactory, defaultComponent: DefaultComponent, queryData, + fill, } = this.props; const {plugins} = this.context; let children = this.getChildren(); @@ -72,7 +74,7 @@ class Slot extends React.Component { } return ( - + {children} ); @@ -85,12 +87,22 @@ Slot.defaultProps = { Slot.propTypes = { fill: PropTypes.string.isRequired, + inline: PropTypes.bool, + className: PropTypes.string, + reduxState: PropTypes.object, + defaultComponent: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + ]), /** * You may specify the component to use as the root wrapper. * Defaults to 'div'. */ - component: PropTypes.any, + component: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + ]), // props coming from graphql must be passed through this property. queryData: PropTypes.object, diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 447d297ab..3a543a738 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -138,6 +138,9 @@ export const withSetCommentStatus = withMutation( const fragment = gql` fragment Talk_SetCommentStatus on Comment { status + status_history { + type + } }`; const fragmentId = `Comment_${commentId}`; @@ -145,6 +148,8 @@ export const withSetCommentStatus = withMutation( const data = proxy.readFragment({fragment, id: fragmentId}); data.status = status; + data.status_history = data.status_history ? data.status_history : []; + data.status_history.push({__typename: 'CommentStatusHistory', type: status}); proxy.writeFragment({fragment, id: fragmentId, data}); } diff --git a/client/talk-plugin-history/Comment.css b/client/talk-plugin-history/Comment.css index 23289c17d..40a003de1 100644 --- a/client/talk-plugin-history/Comment.css +++ b/client/talk-plugin-history/Comment.css @@ -13,6 +13,18 @@ border-bottom: solid 1px #EBEBEB; } +.main { + min-width: 70%; +} + +.sidebar { + min-width: 30%; +} + +.commentBody { + word-wrap: break-word; +} + .assetURL { text-decoration: none; font-weight: bold; @@ -44,7 +56,8 @@ margin-top: 0; margin-bottom: 0; list-style-type: none; - min-width: 136px; + min-width: 140px; + padding: 0px 10px; } li { diff --git a/client/talk-plugin-history/Comment.js b/client/talk-plugin-history/Comment.js index 9d1208708..463122160 100644 --- a/client/talk-plugin-history/Comment.js +++ b/client/talk-plugin-history/Comment.js @@ -19,7 +19,7 @@ class Comment extends React.Component { return (
-
+
{ const comment = await Comment.setStatus({id, status}); if (status === 'ACCEPTED') { - - // Publish the comment status change via the subscription. pubsub.publish('commentAccepted', comment); } else if (status === 'REJECTED') { - - // Publish the comment status change via the subscription. pubsub.publish('commentRejected', comment); + } else if (status === 'NONE') { + pubsub.publish('commentReset', comment); } }, addTag: async (_, {tag}, {mutators: {Tag}}) => { diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js index f6da9fb54..8be3236c1 100644 --- a/graph/resolvers/subscription.js +++ b/graph/resolvers/subscription.js @@ -11,6 +11,9 @@ const Subscription = { commentRejected(comment) { return comment; }, + commentReset(comment) { + return comment; + }, commentFlagged(comment) { return comment; }, diff --git a/graph/setupFunctions.js b/graph/setupFunctions.js index 86ae106b9..512452e64 100644 --- a/graph/setupFunctions.js +++ b/graph/setupFunctions.js @@ -2,6 +2,7 @@ const { SUBSCRIBE_COMMENT_ACCEPTED, SUBSCRIBE_COMMENT_REJECTED, SUBSCRIBE_COMMENT_FLAGGED, + SUBSCRIBE_COMMENT_RESET, SUBSCRIBE_ALL_COMMENT_EDITED, SUBSCRIBE_ALL_COMMENT_ADDED, SUBSCRIBE_ALL_USER_SUSPENDED, @@ -59,12 +60,18 @@ const setupFunctions = { } return !args.asset_id || comment.asset_id === args.asset_id; }, - commentRejected: (options, args) => (comment, context) => { + commentRejected: (options, args, comment, context) => { if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_REJECTED)) { return false; } return !args.asset_id || comment.asset_id === args.asset_id; }, + commentReset: (options, args, comment, context) => { + if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_RESET)) { + return false; + } + return !args.asset_id || comment.asset_id === args.asset_id; + }, userSuspended: (options, args, user, context) => { if ( !context.user diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 165c31fda..38d67053d 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1499,6 +1499,10 @@ type Subscription { # Requires the `ADMIN` or `MODERATOR` role. commentRejected(asset_id: ID): Comment + # Get an update whenever the status of a comment has been reset. + # Requires the `ADMIN` or `MODERATOR` role. + commentReset(asset_id: ID): Comment + # Get an update whenever a user has been suspended. # `user_id` must match id of current user except for # users with the `ADMIN` or `MODERATOR` role. diff --git a/locales/en.yml b/locales/en.yml index 83d7f266f..ffe106480 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -17,6 +17,7 @@ en: characters_remaining: "characters remaining" comment: anon: "Anonymous" + undo_reject: "Undo" ban_user: "Ban User" comment: "Post a comment" edited: Edited @@ -281,6 +282,7 @@ en: notify_accepted: '{0} accepted comment "{1}"' notify_rejected: '{0} rejected comment "{1}"' notify_flagged: '{0} flagged comment "{1}"' + notify_reset: '{0} reset status of comment "{1}"' approve: "Approve" approved: "Approved" ban_user: "Ban" diff --git a/package.json b/package.json index 8dc9ac46b..e2cc43a17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "3.7.1", + "version": "3.8.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "private": true, diff --git a/perms/constants/subscription.js b/perms/constants/subscription.js index 6c56529ea..94b417e3a 100644 --- a/perms/constants/subscription.js +++ b/perms/constants/subscription.js @@ -2,6 +2,7 @@ module.exports = { SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED', SUBSCRIBE_COMMENT_REJECTED: 'SUBSCRIBE_COMMENT_REJECTED', SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED', + SUBSCRIBE_COMMENT_RESET: 'SUBSCRIBE_COMMENT_RESET', SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED', SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED', SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED', diff --git a/perms/reducers/subscription.js b/perms/reducers/subscription.js index 3a6268553..799b3e4c4 100644 --- a/perms/reducers/subscription.js +++ b/perms/reducers/subscription.js @@ -6,6 +6,8 @@ module.exports = (user, perm) => { case types.SUBSCRIBE_COMMENT_FLAGGED: case types.SUBSCRIBE_COMMENT_ACCEPTED: case types.SUBSCRIBE_COMMENT_REJECTED: + case types.SUBSCRIBE_COMMENT_RESET: + return check(user, ['ADMIN', 'MODERATOR']); case types.SUBSCRIBE_ALL_COMMENT_EDITED: case types.SUBSCRIBE_ALL_COMMENT_ADDED: case types.SUBSCRIBE_ALL_USER_SUSPENDED: diff --git a/plugins/talk-plugin-featured-comments/client/components/Comment.css b/plugins/talk-plugin-featured-comments/client/components/Comment.css index 9481b4396..bd4428091 100644 --- a/plugins/talk-plugin-featured-comments/client/components/Comment.css +++ b/plugins/talk-plugin-featured-comments/client/components/Comment.css @@ -44,6 +44,7 @@ margin: 0; quotes: '\201c' '\201d'; margin-bottom: 10px; + word-wrap: break-word; } .quote:before { diff --git a/plugins/talk-plugin-moderation-actions/client/components/BanUserDialog.js b/plugins/talk-plugin-moderation-actions/client/components/BanUserDialog.js index c98e3e7c2..9db509869 100644 --- a/plugins/talk-plugin-moderation-actions/client/components/BanUserDialog.js +++ b/plugins/talk-plugin-moderation-actions/client/components/BanUserDialog.js @@ -33,9 +33,9 @@ const BanUserDialog = ({showBanDialog, closeBanDialog, banUser}) => ( ); BanUserDialog.propTypes = { - showBanDialog: PropTypes.func.isRequired, + showBanDialog: PropTypes.bool.isRequired, closeBanDialog: PropTypes.func.isRequired, banUser: PropTypes.func.isRequired, }; -export default BanUserDialog; \ No newline at end of file +export default BanUserDialog; diff --git a/services/passport.js b/services/passport.js index 0920c1c39..1d5d0ddcf 100644 --- a/services/passport.js +++ b/services/passport.js @@ -515,5 +515,6 @@ module.exports = { HandleAuthPopupCallback, HandleGenerateCredentials, HandleLogout, - CheckBlacklisted + CheckBlacklisted, + CheckRecaptcha, };