diff --git a/client/coral-admin/src/components/CommentLabels.css b/client/coral-admin/src/components/CommentLabels.css new file mode 100644 index 000000000..b7a5699fe --- /dev/null +++ b/client/coral-admin/src/components/CommentLabels.css @@ -0,0 +1,27 @@ +.root { + display: flex; + justify-content: flex-end; +} + +.coreLabels { + > *:not(:last-child) { + margin-right: 3px; + } +} + +.slot { + &:not(:empty) { + padding-left: 3px; + } + > *:not(:last-child) { + margin-right: 3px; + } +} + +.replyLabel { + background-color: #3D73D5; +} + +.premodLabel { + background-color: #063B9A; +} diff --git a/client/coral-admin/src/components/CommentLabels.js b/client/coral-admin/src/components/CommentLabels.js new file mode 100644 index 000000000..7af7e9a7e --- /dev/null +++ b/client/coral-admin/src/components/CommentLabels.js @@ -0,0 +1,45 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Label from 'coral-ui/components/Label'; +import Slot from 'coral-framework/components/Slot'; +import FlagLabel from 'coral-ui/components/FlagLabel'; +import cn from 'classnames'; +import styles from './CommentLabels.css'; + +function isUserFlagged(actions) { + return actions.some((action) => action.__typename === 'FlagAction' && action.user); +} + +function hasSuspectedWords(actions) { + return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter'); +} + +function hasHistoryFlag(actions) { + return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST'); +} + +const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => { + return ( +
+
+ {hasParent && } + {status === 'PREMOD' && } + {isUserFlagged(actions) && User} + {hasSuspectedWords(actions) && Suspect} + {hasHistoryFlag(actions) && History} +
+ +
+ ); +}; + +CommentLabels.propTypes = { + comment: PropTypes.shape({ + className: PropTypes.string, + status: PropTypes.string, + actions: PropTypes.array, + hasParent: PropTypes.bool, + }), +}; + +export default CommentLabels; diff --git a/client/coral-admin/src/components/CommentType.css b/client/coral-admin/src/components/CommentType.css deleted file mode 100644 index 969d9511d..000000000 --- a/client/coral-admin/src/components/CommentType.css +++ /dev/null @@ -1,30 +0,0 @@ -.commentType { - display: inline-block; - color: white; - background: grey; - box-sizing: border-box; - padding: 2px 5px; - font-size: 12px; - height: 24px; - letter-spacing: 0.4px; - line-height: 22px; - - > i { - font-size: 14px; - vertical-align: text-top; - margin: 0; - margin-right: 4px; - } - - &.premod { - background: #063B9A; - } - - &.flagged { - background: #d03235; - } - - &.no-type { - display: none; - } -} diff --git a/client/coral-admin/src/components/CommentType.js b/client/coral-admin/src/components/CommentType.js deleted file mode 100644 index 4d1ff7e67..000000000 --- a/client/coral-admin/src/components/CommentType.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './CommentType.css'; -import {Icon} from 'coral-ui'; -import cn from 'classnames'; - -const CommentType = (props) => { - const typeData = getTypeData(props.type); - - return ( - - {typeData.text} - - ); -}; - -const getTypeData = (type) => { - switch (type) { - case 'premod': - return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'}; - case 'flagged': - return {icon: 'flag', text: 'Flagged', className: 'flagged'}; - default: - return {icon: 'flag', className: 'no-type'}; - } -}; - -CommentType.propTypes = { - type: PropTypes.string.isRequired -}; - -export default CommentType; diff --git a/client/coral-admin/src/components/ReplyBadge.js b/client/coral-admin/src/components/ReplyBadge.js deleted file mode 100644 index 5c8bb88ba..000000000 --- a/client/coral-admin/src/components/ReplyBadge.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import {Badge} from 'coral-ui'; -import t from 'coral-framework/services/i18n'; - -const ReplyBadge = () => ( - - {t('modqueue.reply')} - -); - -export default ReplyBadge; diff --git a/client/coral-admin/src/components/UserDetailComment.css b/client/coral-admin/src/components/UserDetailComment.css index 2dfd61cf3..d1e2b007a 100644 --- a/client/coral-admin/src/components/UserDetailComment.css +++ b/client/coral-admin/src/components/UserDetailComment.css @@ -55,7 +55,7 @@ position: relative; } -.badgeBar { +.labels { position: absolute; right: 0px; } diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index fa196dc41..2aec28ab9 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -4,16 +4,14 @@ import {Link} from 'react-router'; import {Icon} from 'coral-ui'; import FlagBox from './FlagBox'; -import ReplyBadge from './ReplyBadge'; import styles from './UserDetailComment.css'; -import CommentType from './CommentType'; import {getActionSummary} from 'coral-framework/utils'; import ActionButton from 'coral-admin/src/components/ActionButton'; import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter'; import IfHasLink from 'coral-admin/src/components/IfHasLink'; import cn from 'classnames'; -import {getCommentType} from 'coral-admin/src/utils/comment'; import CommentAnimatedEdit from './CommentAnimatedEdit'; +import CommentLabels from '../containers/CommentLabels'; import t, {timeago} from 'coral-framework/services/i18n'; @@ -35,7 +33,6 @@ class UserDetailComment extends React.Component { const flagActionSummaries = getActionSummary('FlagActionSummary', comment); const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction'); - const commentType = getCommentType(comment); return (
  • - {comment.hasParent && } - +
    +
    diff --git a/client/coral-admin/src/containers/CommentLabels.js b/client/coral-admin/src/containers/CommentLabels.js new file mode 100644 index 000000000..af6a0d963 --- /dev/null +++ b/client/coral-admin/src/containers/CommentLabels.js @@ -0,0 +1,27 @@ +import {gql} from 'react-apollo'; +import CommentLabels from '../components/CommentLabels'; +import withFragments from 'coral-framework/hocs/withFragments'; +import {getSlotFragmentSpreads} from 'coral-framework/utils'; + +const slots = [ + 'adminCommentLabels', +]; + +export default withFragments({ + comment: gql` + fragment CoralAdmin_CommentLabels_comment on Comment { + hasParent + status + actions { + __typename + ... on FlagAction { + reason + } + user { + id + } + } + ${getSlotFragmentSpreads(slots, 'comment')} + } + ` +})(CommentLabels); diff --git a/client/coral-admin/src/containers/UserDetailComment.js b/client/coral-admin/src/containers/UserDetailComment.js index 9f0e3a793..9f0199bce 100644 --- a/client/coral-admin/src/containers/UserDetailComment.js +++ b/client/coral-admin/src/containers/UserDetailComment.js @@ -1,6 +1,8 @@ import {gql} from 'react-apollo'; import UserDetailComment from '../components/UserDetailComment'; import withFragments from 'coral-framework/hocs/withFragments'; +import {getDefinitionName} from 'coral-framework/utils'; +import CommentLabels from './CommentLabels'; export default withFragments({ comment: gql` @@ -35,6 +37,8 @@ export default withFragments({ editing { edited } + ...${getDefinitionName(CommentLabels.fragments.comment)} } + ${CommentLabels.fragments.comment} ` })(UserDetailComment); diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js index a3edb4196..b210d8f23 100644 --- a/client/coral-admin/src/routes/Moderation/components/Comment.js +++ b/client/coral-admin/src/routes/Moderation/components/Comment.js @@ -3,10 +3,9 @@ import PropTypes from 'prop-types'; import {Link} from 'react-router'; import {Icon} from 'coral-ui'; -import ReplyBadge from 'coral-admin/src/components/ReplyBadge'; import FlagBox from 'coral-admin/src/components/FlagBox'; import styles from './styles.css'; -import CommentType from 'coral-admin/src/components/CommentType'; +import CommentLabels from 'coral-admin/src/components/CommentLabels'; import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit'; import Slot from 'coral-framework/components/Slot'; import {getActionSummary} from 'coral-framework/utils'; @@ -16,7 +15,6 @@ import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter'; import IfHasLink from 'coral-admin/src/components/IfHasLink'; import cn from 'classnames'; -import {getCommentType} from 'coral-admin/src/utils/comment'; import t, {timeago} from 'coral-framework/services/i18n'; @@ -66,7 +64,6 @@ class Comment extends React.Component { const flagActionSummaries = getActionSummary('FlagActionSummary', comment); const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction'); - const commentType = getCommentType(comment); const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'; @@ -109,8 +106,9 @@ class Comment extends React.Component { }
    - {comment.hasParent && } - + ; } - const premodEnabled = assetId ? isPremod(asset.settings.moderation) : + const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation); const currentQueueConfig = Object.assign({}, this.props.queueConfig); @@ -293,8 +294,8 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql` `; const LOAD_MORE_QUERY = gql` - query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $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}) { + 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}) { nodes { ...${getDefinitionName(Comment.fragments.comment)} } @@ -319,10 +320,10 @@ const commentConnectionFragment = gql` `; const withModQueueQuery = withQuery(({queueConfig}) => gql` - query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!) { + query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!, $nullStatuses: [COMMENT_STATUS!]) { ${Object.keys(queueConfig).map((queue) => ` ${queue}: comments(query: { - ${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''} + statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'} ${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''} ${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''} asset_id: $asset_id, @@ -333,7 +334,7 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql` `)} ${Object.keys(queueConfig).map((queue) => ` ${queue}Count: commentCount(query: { - ${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''} + statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'} ${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''} ${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''} asset_id: $asset_id, @@ -361,6 +362,7 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql` asset_id: id, sortOrder: props.moderation.sortOrder, allAssets: id === null, + nullStatuses: null, }, fetchPolicy: 'network-only' }; diff --git a/client/coral-admin/src/routes/Moderation/queueConfig.js b/client/coral-admin/src/routes/Moderation/queueConfig.js index b90979efe..f17c38a09 100644 --- a/client/coral-admin/src/routes/Moderation/queueConfig.js +++ b/client/coral-admin/src/routes/Moderation/queueConfig.js @@ -28,7 +28,6 @@ export default { name: t('modqueue.rejected'), }, all: { - statuses: ['NONE', 'PREMOD', 'ACCEPTED', 'REJECTED'], icon: 'question_answer', name: t('modqueue.all'), }, diff --git a/client/coral-admin/src/utils/comment.js b/client/coral-admin/src/utils/comment.js deleted file mode 100644 index 26530f6c9..000000000 --- a/client/coral-admin/src/utils/comment.js +++ /dev/null @@ -1,9 +0,0 @@ -export function getCommentType(comment) { - let commentType = ''; - if (comment.status === 'PREMOD') { - commentType = 'premod'; - } else if (comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction')) { - commentType = 'flagged'; - } - return commentType; -} diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index dc81b56a4..20ed8a1d6 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -64,6 +64,13 @@ export default { hasNextPage } } + actions { + __typename + ... on FlagAction { + reason + message + } + } } fragment CoralEmbedStream_CreateCommentResponse_Comment on Comment { @@ -77,14 +84,6 @@ export default { title url } - actions { - __typename - id - ... on FlagAction { - reason - message - } - } tags { tag { name @@ -123,6 +122,8 @@ export default { optimisticResponse: { createComment: { __typename: 'CreateCommentResponse', + errors: null, + actions: [], comment: { __typename: 'Comment', user: { diff --git a/client/coral-ui/components/Badge.js b/client/coral-ui/components/Badge.js deleted file mode 100644 index c1ba8100b..000000000 --- a/client/coral-ui/components/Badge.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import styles from './Badge.css'; -import Icon from './Icon'; -import cn from 'classnames'; - -const Badge = ({className, children, icon, props}) => ( - - {icon && } - {children} - -); - -export default Badge; diff --git a/client/coral-ui/components/FlagLabel.css b/client/coral-ui/components/FlagLabel.css new file mode 100644 index 000000000..070e117d1 --- /dev/null +++ b/client/coral-ui/components/FlagLabel.css @@ -0,0 +1,4 @@ +.flag { + background: #d03235; +} + diff --git a/client/coral-ui/components/FlagLabel.js b/client/coral-ui/components/FlagLabel.js new file mode 100644 index 000000000..8a7864c8b --- /dev/null +++ b/client/coral-ui/components/FlagLabel.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './FlagLabel.css'; +import Label from './Label'; +import cn from 'classnames'; + +const FlagLabel = ({iconName, children, className}) => { + return ( + + ); +}; + +FlagLabel.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + iconName: PropTypes.string, +}; + +export default FlagLabel; diff --git a/client/coral-ui/components/Badge.css b/client/coral-ui/components/Label.css similarity index 82% rename from client/coral-ui/components/Badge.css rename to client/coral-ui/components/Label.css index db68ebd58..5e7f656ee 100644 --- a/client/coral-ui/components/Badge.css +++ b/client/coral-ui/components/Label.css @@ -1,4 +1,4 @@ -.badge { +.root { display: inline-block; color: white; background: grey; @@ -8,8 +8,8 @@ height: 24px; letter-spacing: 0.4px; line-height: 22px; - background-color: #3D73D5; - margin-right: 4px; + min-width: 80px; + text-align: center; } .icon { @@ -17,4 +17,6 @@ vertical-align: text-top; margin: 0; margin-right: 4px; -} \ No newline at end of file +} + + diff --git a/client/coral-ui/components/Label.js b/client/coral-ui/components/Label.js new file mode 100644 index 000000000..a73b6183d --- /dev/null +++ b/client/coral-ui/components/Label.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './Label.css'; +import {Icon} from 'coral-ui'; +import cn from 'classnames'; + +const Label = ({iconName, children, className}) => { + return ( + + {children} + + ); +}; + +Label.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + iconName: PropTypes.string, +}; + +export default Label; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index 8f261bcca..93fe5af64 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -26,4 +26,5 @@ export {default as Option} from './components/Option'; export {default as SnackBar} from './components/SnackBar'; export {default as TextArea} from './components/TextArea'; export {default as Drawer} from './components/Drawer'; -export {default as Badge} from './components/Badge'; +export {default as Label} from './components/Label'; +export {default as FlagLabel} from './components/FlagLabel'; diff --git a/client/talk-plugin-commentbox/CommentBox.js b/client/talk-plugin-commentbox/CommentBox.js index e5717e08a..b210f4e79 100644 --- a/client/talk-plugin-commentbox/CommentBox.js +++ b/client/talk-plugin-commentbox/CommentBox.js @@ -11,12 +11,20 @@ import {CommentForm} from './CommentForm'; export const name = 'talk-plugin-commentbox'; +const notifyReasons = ['LINKS', 'TRUST']; + +function shouldNotify(actions = []) { + return actions.some(({__typename, reason}) => __typename === 'FlagAction' && notifyReasons.includes(reason)); +} + // Given a newly posted comment's status, show a notification to the user // if needed -export const notifyForNewCommentStatus = (notify, comment) => { +export const notifyForNewCommentStatus = (notify, comment, actions) => { if (comment.status === 'REJECTED') { notify('error', t('comment_box.comment_post_banned_word')); - } else if (comment.status === 'PREMOD' || comment.status === 'SYSTEM_WITHHELD') { + } else if ( + comment.status === 'PREMOD' || + comment.status === 'SYSTEM_WITHHELD' && shouldNotify(actions)) { notify('success', t('comment_box.comment_post_notif_premod')); } }; @@ -70,11 +78,12 @@ class CommentBox extends React.Component { .then(({data}) => { this.setState({loadingState: 'success', body: ''}); const postedComment = data.createComment.comment; + const actions = data.createComment.actions; // Execute postSubmit Hooks this.state.hooks.postSubmit.forEach((hook) => hook(data)); - notifyForNewCommentStatus(notify, postedComment); + notifyForNewCommentStatus(notify, postedComment, actions); if (commentPostedHandler) { commentPostedHandler(); @@ -187,6 +196,7 @@ CommentBox.propTypes = { isReply: PropTypes.bool.isRequired, canPost: PropTypes.bool, notify: PropTypes.func.isRequired, + commentBox: PropTypes.object, }; const mapStateToProps = ({commentBox}) => ({commentBox}); diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 386d33fd1..b6a347b8e 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -91,14 +91,23 @@ const getParentCountsByAssetID = (context, asset_ids) => { const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id, tags, action_type}) => { let query = CommentModel.find(); - if (ids) { - query = query.where({id: {$in: ids}}); + // If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs + // special priviledges. + if ( + (!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) && + (context.user == null || !context.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS)) + ) { + return null; } if (statuses) { query = query.where({status: {$in: statuses}}); } + if (ids) { + query = query.where({id: {$in: ids}}); + } + if (asset_id != null) { query = query.where({asset_id}); } @@ -279,20 +288,17 @@ const executeWithSort = async (ctx, query, {cursor, sortOrder, sortBy, limit}) = const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sortOrder, sortBy, excludeIgnored, tags, action_type}) => { let comments = CommentModel.find(); - // Only administrators can search for comments with statuses that are not - // `null`, or `'ACCEPTED'`. - if (ctx.user != null && ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS) && statuses && statuses.length > 0) { - comments = comments.where({ - status: { - $in: statuses - } - }); - } else { - comments = comments.where({ - status: { - $in: ['NONE', 'ACCEPTED'] - } - }); + // If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs + // special priviledges. + if ( + (!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) && + (ctx.user == null || !ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS)) + ) { + return null; + } + + if (statuses) { + comments = comments.where({status: {$in: statuses}}); } if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) { diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 75cad99b7..a1315d847 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -393,13 +393,12 @@ const moderationPhases = [ ]; /** - * This resolves a given comment's status to take into account moderator actions - * are applied. + * This resolves a given comment's status and actions. * @param {Object} context graphql context * @param {String} body body of the comment * @param {String} [asset_id] asset for the comment * @param {Object} [wordlist={}] the results of the wordlist scan - * @return {Promise} resolves to the comment's status + * @return {Promise} resolves to the comment's status and actions */ const resolveCommentModeration = async (context, comment) => { @@ -418,6 +417,8 @@ const resolveCommentModeration = async (context, comment) => { // Combine the asset and the settings to get the asset settings. const assetSettings = await AssetsService.rectifySettings(asset, settings); + let actions = comment.actions || []; + // Loop over all the moderation phases and see if we've resolved the status. for (const phase of moderationPhases) { const result = await phase(context, comment, { @@ -429,13 +430,14 @@ const resolveCommentModeration = async (context, comment) => { if (result) { - // Merge the comment and the result together. - comment = merge(comment, result); + if (result.actions) { + actions.push(...result.actions); + } // If this result contained a status, then we've finished resolving // phases! if (result.status) { - return comment.actions; + return {status: result.status, actions}; } } } @@ -454,17 +456,20 @@ const createPublicComment = async (context, comment) => { // We then take the wordlist and the comment into consideration when // considering what status to assign the new comment, and resolve the new // status to set the comment to. - let actions = await resolveCommentModeration(context, comment); + let {actions, status} = await resolveCommentModeration(context, comment); + + // Assign status to comment. + comment.status = status; // Then we actually create the comment with the new status. - comment = await createComment(context, comment); + const result = await createComment(context, comment); // Create all the actions that were determined during the moderation check // phase. - await createActions(comment.id, actions); + await createActions(result.id, actions); // Finally, we return the comment. - return comment; + return result; }; // createActions will for each of the provided actions, create the given action @@ -515,10 +520,10 @@ const edit = async (context, {id, asset_id, edit: {body}}) => { let comment = {id, asset_id, body}; // Determine the new status of the comment. - const actions = await resolveCommentModeration(context, comment); + const {actions, status} = await resolveCommentModeration(context, comment); // Execute the edit. - comment = await CommentsService.edit({id, author_id: context.user.id, body, status: comment.status}); + comment = await CommentsService.edit({id, author_id: context.user.id, body, status}); // Create all the actions that were determined during the moderation check // phase. diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index cd1e93141..8e063ad3c 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -26,6 +26,7 @@ const Comment = { query.asset_id = asset_id; query.parent_id = id; + query.statuses = ['NONE', 'ACCEPTED']; return Comments.getByQuery(query); }, diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index df67b8e8a..17235a59e 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -1,7 +1,12 @@ const RootMutation = { - createComment: async (_, {input}, {mutators: {Comment}}) => ({ - comment: await Comment.create(input), - }), + createComment: async (_, {input}, {mutators: {Comment}, loaders: {Actions}}) => { + const comment = await Comment.create(input); + + // Retrieve actions that was assigned to comment. + const actions = await Actions.getByID.load(comment.id); + + return {comment, actions}; + }, editComment: async (_, {id, asset_id, edit: {body}}, {mutators: {Comment}}) => ({ comment: await Comment.edit({id, asset_id, edit: {body}}), }), diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index db296e45d..70167bb67 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -261,11 +261,12 @@ enum ACTION_TYPE { # CommentsQuery allows the ability to query comments by a specific methods. input CommentsQuery { - # Author of the comments + # Author of the commente author_id: ID - # Current status of a comment. Requires the `ADMIN` role. - statuses: [COMMENT_STATUS!] + # Current status of a comment. + # This field is restricted. + statuses: [COMMENT_STATUS!] = [NONE, ACCEPTED] # Asset that a comment is on. asset_id: ID @@ -317,8 +318,9 @@ input RepliesQuery { # methods. input CommentCountQuery { - # Current status of a comment. Requires the `ADMIN` role. - statuses: [COMMENT_STATUS!] + # Current status of a comment. + # This field is restricted. + statuses: [COMMENT_STATUS!] = [NONE, ACCEPTED] # Asset that a comment is on. asset_id: ID @@ -700,7 +702,7 @@ type Asset { # The comments that are attached to the asset. When `deep` is true, the # comments returned will be at all depths. - comments(query: CommentsQuery = {}, deep: Boolean = false): CommentConnection! + comments(query: CommentsQuery = {}, deep: Boolean = false): CommentConnection # A Comment from the Asset by comment's ID comment(id: ID!): Comment @@ -891,6 +893,9 @@ type CreateCommentResponse implements Response { # The comment that was created. comment: Comment + # Actions that was assigned during creation of the comment. + actions: [Action] + # An array of errors relating to the mutation that occurred. errors: [UserError!] } diff --git a/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js b/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js index 22c378435..e4bd68097 100644 --- a/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js +++ b/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js @@ -1,4 +1,6 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import {t} from 'plugin-api/beta/client/services'; /** * CheckToxicityHook adds hooks to the `commentBox` @@ -20,7 +22,11 @@ export default class CheckToxicityHook extends React.Component { } }); - this.toxicityPostHook = this.props.registerHook('postSubmit', () => { + this.toxicityPostHook = this.props.registerHook('postSubmit', (result) => { + const actions = result.createComment.actions; + if (actions && actions.some(({__typename, reason}) => __typename === 'FlagAction' && reason === 'TOXIC_COMMENT')) { + this.props.notify('error', t('talk-plugin-toxic-comments.still_toxic')); + } // Reset `checked` after comment was successfully posted. this.checked = false; @@ -36,3 +42,9 @@ export default class CheckToxicityHook extends React.Component { return null; } } + +CheckToxicityHook.propTypes = { + notify: PropTypes.func.isRequired, + registerHook: PropTypes.func.isRequired, + unregisterHook: PropTypes.func.isRequired, +}; diff --git a/plugins/talk-plugin-toxic-comments/client/components/ToxicLabel.js b/plugins/talk-plugin-toxic-comments/client/components/ToxicLabel.js new file mode 100644 index 000000000..3c17703dd --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/components/ToxicLabel.js @@ -0,0 +1,8 @@ +import React from 'react'; +import {FlagLabel} from 'plugin-api/beta/client/components/ui'; + +const ToxicLabel = () => ( + Toxic +); + +export default ToxicLabel; diff --git a/plugins/talk-plugin-toxic-comments/client/containers/CheckToxicityHook.js b/plugins/talk-plugin-toxic-comments/client/containers/CheckToxicityHook.js new file mode 100644 index 000000000..f2b010b0d --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/containers/CheckToxicityHook.js @@ -0,0 +1,9 @@ +import {bindActionCreators} from 'redux'; +import {connect} from 'plugin-api/beta/client/hocs'; +import {notify} from 'plugin-api/beta/client/actions/notification'; +import CheckToxicityHook from '../components/CheckToxicityHook'; + +const mapDispatchToProps = (dispatch) => + bindActionCreators({notify}, dispatch); + +export default connect(null, mapDispatchToProps)(CheckToxicityHook); diff --git a/plugins/talk-plugin-toxic-comments/client/containers/ToxicLabel.js b/plugins/talk-plugin-toxic-comments/client/containers/ToxicLabel.js new file mode 100644 index 000000000..0d2e9e14b --- /dev/null +++ b/plugins/talk-plugin-toxic-comments/client/containers/ToxicLabel.js @@ -0,0 +1,24 @@ +import {compose, gql} from 'react-apollo'; +import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs'; +import ToxicLabel from '../components/ToxicLabel'; + +function isToxic(actions) { + return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TOXIC_COMMENT'); +} + +const enhance = compose( + withFragments({ + comment: gql` + fragment TalkToxicComments_Comment on Comment { + actions { + __typename + ... on FlagAction { + reason + } + } + }`, + }), + excludeIf(({comment: {actions}}) => !isToxic(actions)), +); + +export default enhance(ToxicLabel); diff --git a/plugins/talk-plugin-toxic-comments/client/index.js b/plugins/talk-plugin-toxic-comments/client/index.js index c364a2a00..8a16b8dea 100644 --- a/plugins/talk-plugin-toxic-comments/client/index.js +++ b/plugins/talk-plugin-toxic-comments/client/index.js @@ -1,9 +1,11 @@ import translations from './translations.yml'; -import CheckToxicityHook from './components/CheckToxicityHook'; +import CheckToxicityHook from './containers/CheckToxicityHook'; +import ToxicLabel from './containers/ToxicLabel'; export default { translations, slots: { commentInputDetailArea: [CheckToxicityHook], + adminCommentLabels: [ToxicLabel], }, }; diff --git a/plugins/talk-plugin-toxic-comments/client/translations.yml b/plugins/talk-plugin-toxic-comments/client/translations.yml index ebcc31903..787095046 100644 --- a/plugins/talk-plugin-toxic-comments/client/translations.yml +++ b/plugins/talk-plugin-toxic-comments/client/translations.yml @@ -3,4 +3,8 @@ en: COMMENT_IS_TOXIC: | Are you sure? The language in this comment might violate our community guidelines. You can edit the comment or submit it for moderator review. + talk-plugin-toxic-comments: + still_toxic: | + This edited comment might still violate our community guidelines. + Our moderation team will review your comment shortly. es: