mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
Merge pull request #971 from coralproject/new-flags
Flags: Implement Labels, bugfixes and improvements
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.coreLabels}>
|
||||
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
|
||||
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
|
||||
{isUserFlagged(actions) && <FlagLabel iconName="person">User</FlagLabel>}
|
||||
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
|
||||
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
|
||||
</div>
|
||||
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentLabels.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
actions: PropTypes.array,
|
||||
hasParent: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CommentLabels;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<span className={cn(styles.commentType, styles[typeData.className], props.className)}>
|
||||
<Icon name={typeData.icon}/>{typeData.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Badge} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ReplyBadge = () => (
|
||||
<Badge icon="reply">
|
||||
{t('modqueue.reply')}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export default ReplyBadge;
|
||||
@@ -55,7 +55,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badgeBar {
|
||||
.labels {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<li
|
||||
@@ -59,9 +56,8 @@ class UserDetailComment extends React.Component {
|
||||
: null
|
||||
}
|
||||
|
||||
<div className={styles.badgeBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType}/>
|
||||
<div className={styles.labels}>
|
||||
<CommentLabels comment={comment} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
</ActionsMenu>
|
||||
}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType} className={styles.commentType}/>
|
||||
<CommentLabels
|
||||
comment={comment}
|
||||
/>
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import Comment from '../components/Comment';
|
||||
import CommentLabels from '../../../containers/CommentLabels';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {getSlotFragmentSpreads, getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
const slots = [
|
||||
'adminCommentInfoBar',
|
||||
@@ -58,6 +59,8 @@ export default withFragments({
|
||||
}
|
||||
hasParent
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
}
|
||||
${CommentLabels.fragments.comment}
|
||||
`
|
||||
})(Comment);
|
||||
|
||||
@@ -170,7 +170,8 @@ class ModerationContainer extends Component {
|
||||
cursor: this.props.root[tab].endCursor,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
asset_id: this.props.data.variables.asset_id,
|
||||
statuses: this.props.queueConfig[tab].statuses,
|
||||
statuses: this.props.queueConfig[tab].statuses || null,
|
||||
tags: this.props.queueConfig[tab].tags || null,
|
||||
action_type: this.props.queueConfig[tab].action_type,
|
||||
};
|
||||
return this.props.data.fetchMore({
|
||||
@@ -214,7 +215,7 @@ class ModerationContainer extends Component {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
@@ -28,7 +28,6 @@ export default {
|
||||
name: t('modqueue.rejected'),
|
||||
},
|
||||
all: {
|
||||
statuses: ['NONE', 'PREMOD', 'ACCEPTED', 'REJECTED'],
|
||||
icon: 'question_answer',
|
||||
name: t('modqueue.all'),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}) => (
|
||||
<span className={cn(styles.badge, className)} {...props}>
|
||||
{icon && <Icon name={icon} className={styles.icon} />}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export default Badge;
|
||||
@@ -0,0 +1,4 @@
|
||||
.flag {
|
||||
background: #d03235;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Label iconName={iconName} className={cn(className, styles.flag)}>
|
||||
{children}
|
||||
</Label>
|
||||
);
|
||||
};
|
||||
|
||||
FlagLabel.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
iconName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FlagLabel;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<span className={cn(className, styles.root)}>
|
||||
<Icon name={iconName} className={styles.icon} /> {children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
iconName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Label;
|
||||
@@ -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';
|
||||
|
||||
@@ -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});
|
||||
|
||||
+22
-16
@@ -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) {
|
||||
|
||||
+17
-12
@@ -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.
|
||||
|
||||
@@ -26,6 +26,7 @@ const Comment = {
|
||||
|
||||
query.asset_id = asset_id;
|
||||
query.parent_id = id;
|
||||
query.statuses = ['NONE', 'ACCEPTED'];
|
||||
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
|
||||
@@ -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}}),
|
||||
}),
|
||||
|
||||
+11
-6
@@ -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!]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import {FlagLabel} from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
const ToxicLabel = () => (
|
||||
<FlagLabel iconName="add_box">Toxic</FlagLabel>
|
||||
);
|
||||
|
||||
export default ToxicLabel;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user