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: