mirror of
https://github.com/wassname/talk.git
synced 2026-07-23 13:10:20 +08:00
Merge branch 'master' into redis-cluster-support
This commit is contained in:
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
@@ -10,6 +11,7 @@ const {
|
||||
BASE_PATH,
|
||||
MOUNT_PATH,
|
||||
STATIC_URL,
|
||||
HELMET_CONFIGURATION,
|
||||
} = require('./url');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
@@ -31,9 +33,9 @@ app.set('trust proxy', 1);
|
||||
|
||||
// Enable a suite of security good practices through helmet. We disable
|
||||
// frameguard to allow crossdomain injection of the embed.
|
||||
app.use(helmet({
|
||||
app.use(helmet(merge(HELMET_CONFIGURATION, {
|
||||
frameguard: false,
|
||||
}));
|
||||
})));
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
@@ -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
-1
@@ -5,7 +5,7 @@
|
||||
vertical-align: middle;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: 2px;
|
||||
margin-left: 5px;
|
||||
line-height: 18px;
|
||||
box-sizing: border-box;
|
||||
height: 18px;
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CommentCount.css';
|
||||
import styles from './CountBadge.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CommentCount = ({count}) => {
|
||||
const CountBadge = ({count}) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
@@ -21,8 +21,8 @@ const CommentCount = ({count}) => {
|
||||
);
|
||||
};
|
||||
|
||||
CommentCount.propTypes = {
|
||||
CountBadge.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default CommentCount;
|
||||
export default CountBadge;
|
||||
@@ -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);
|
||||
|
||||
@@ -4,6 +4,7 @@ import CommunityMenu from './CommunityMenu';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class Community extends Component {
|
||||
|
||||
@@ -76,7 +77,10 @@ export default class Community extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts />
|
||||
<FlaggedAccounts
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>
|
||||
<RejectUsernameDialog
|
||||
open={community.rejectUsernameDialog}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
@@ -90,15 +94,25 @@ export default class Community extends Component {
|
||||
render() {
|
||||
const {searchValue} = this.state;
|
||||
const tab = this.getTabContent(searchValue, this.props);
|
||||
const {root: {flaggedUsernamesCount}} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CommunityMenu />
|
||||
<div>
|
||||
{ tab }
|
||||
</div>
|
||||
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
|
||||
<div>{tab}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Community.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './CommunityMenu.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Link} from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
|
||||
const CommunityMenu = () => {
|
||||
const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
|
||||
const flaggedPath = '/admin/community/flagged';
|
||||
const peoplePath = '/admin/community/people';
|
||||
|
||||
return (
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{t('community.flaggedaccounts')}
|
||||
<CountBadge count={flaggedUsernamesCount} />
|
||||
</Link>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{t('community.people')}
|
||||
@@ -23,4 +26,8 @@ const CommunityMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
CommunityMenu.propTypes = {
|
||||
flaggedUsernamesCount: PropTypes.number,
|
||||
};
|
||||
|
||||
export default CommunityMenu;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
@@ -20,15 +27,57 @@ class CommunityContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Community {...this.props} />
|
||||
);
|
||||
return <Community
|
||||
fetchAccounts={this.props.fetchAccounts}
|
||||
community={this.props.community}
|
||||
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
|
||||
updateSorting={this.props.updateSorting}
|
||||
newPage={this.props.newPage}
|
||||
route={this.props.route}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community,
|
||||
});
|
||||
|
||||
CommunityContainer.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_FlaggedUsernamesCount {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
@@ -41,4 +90,5 @@ export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData,
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -2,8 +2,9 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {withFragments} from 'plugin-api/beta/client/hocs';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {withSetUserStatus} from 'coral-framework/graphql/mutations';
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
@@ -24,7 +25,10 @@ class FlaggedAccountsContainer extends Component {
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'APPROVED'});
|
||||
return this.props.setUserStatus({
|
||||
userId,
|
||||
status: 'APPROVED'
|
||||
});
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
@@ -74,6 +78,16 @@ class FlaggedAccountsContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
FlaggedAccountsContainer.propTypes = {
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
setUserStatus: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
|
||||
@@ -88,31 +102,6 @@ const LOAD_MORE_QUERY = gql`
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
export const withFlaggedAccountsyQuery = withQuery(gql`
|
||||
query TalkAdmin_FlaggedAccounts {
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
}
|
||||
}
|
||||
me {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.user}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
showBanUserDialog,
|
||||
@@ -123,6 +112,23 @@ const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withFlaggedAccountsyQuery,
|
||||
withSetUserStatus,
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery {
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`,
|
||||
}),
|
||||
)(FlaggedAccountsContainer);
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CommentCount from './CommentCount';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
import styles from './styles.css';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import {Icon} from 'coral-ui';
|
||||
@@ -28,7 +28,7 @@ const ModerationMenu = ({
|
||||
to={getModPath(queue.key, asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === queue.key})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name} <CommentCount count={queue.count} />
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name} <CountBadge count={queue.count} />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ class Stream extends React.Component {
|
||||
loading={loading}
|
||||
appendTabs={
|
||||
<Tab tabId={'all'} key='all'>
|
||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
{t('stream.all_comments')} <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
</Tab>
|
||||
}
|
||||
appendTabPanes={
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -79,6 +79,14 @@ const CONFIG = {
|
||||
|
||||
INSTALL_LOCK: process.env.TALK_INSTALL_LOCK === 'TRUE',
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Middleware Configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// HELMET_CONFIGURATION provides the entrypoint to override options for the
|
||||
// helmet middleware used.
|
||||
HELMET_CONFIGURATION: JSON.parse(process.env.TALK_HELMET_CONFIGURATION || '{}'),
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// External database url's
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -3,6 +3,21 @@ title: Frequently Asked Questions
|
||||
permalink: /docs/faq/
|
||||
---
|
||||
|
||||
{% include toc %}
|
||||
|
||||
### My site doesn't use HSTS headers, how do I stop Talk from sending them too?
|
||||
|
||||
You can specify the configuration option `TALK_HELMET_CONFIGURATION` and set it
|
||||
to:
|
||||
|
||||
```
|
||||
TALK_HELMET_CONFIGURATION={"hsts": false}
|
||||
```
|
||||
|
||||
Which will disable the HSTS module. See the
|
||||
[helmet](https://github.com/helmetjs/helmet) repository for more information on
|
||||
how to configure other security middleware used by default.
|
||||
|
||||
### How are new stories/assets added to Talk? Is there an API?
|
||||
|
||||
There are three ways that new assets can make their way into Talk:
|
||||
|
||||
@@ -99,6 +99,12 @@ These are only used during the webpack build.
|
||||
and you would then specify the CDN/Storage url. (Default `process.env.TALK_ROOT_URL`)
|
||||
- `TALK_DISABLE_STATIC_SERVER` (_optional_) - When `TRUE`, it will not mount the
|
||||
static asset serving routes on the router. (Default `FALSE`)
|
||||
- `TALK_HELMET_CONFIGURATION` (_optional_) - A JSON string representing the
|
||||
configuration passed to the
|
||||
[helmet](https://github.com/helmetjs/helmet) middleware. It can be used to
|
||||
disable features like [HSTS](https://helmetjs.github.io/docs/hsts/) and others
|
||||
by simply providing the configuration as detailed on the
|
||||
[helmet README](https://github.com/helmetjs/helmet). (Default `{}`)
|
||||
|
||||
### Word Filter
|
||||
|
||||
|
||||
+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) {
|
||||
|
||||
+36
-1
@@ -107,6 +107,40 @@ const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the count of users based on the passed in query.
|
||||
* @param {Object} context graph context
|
||||
* @param {Object} query query to execute against the users collection
|
||||
* to compute the counts
|
||||
* @return {Promise} resolves to the counts of the users from the
|
||||
* query
|
||||
*/
|
||||
const getCountByQuery = async ({loaders: {Actions}}, {action_type, statuses}) => {
|
||||
let query = UserModel.find();
|
||||
|
||||
if (action_type) {
|
||||
const userIds = await Actions.getByTypes({action_type, item_type: 'USERS'});
|
||||
|
||||
query = query.find({
|
||||
id: {
|
||||
$in: userIds
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (statuses) {
|
||||
query = query.where({
|
||||
status: {
|
||||
$in: statuses
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return UserModel
|
||||
.find(query)
|
||||
.count();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -115,6 +149,7 @@ const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor,
|
||||
module.exports = (context) => ({
|
||||
Users: {
|
||||
getByQuery: (query) => getUsersByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genUserByIDs(context, ids))
|
||||
getByID: new DataLoader((ids) => genUserByIDs(context, ids)),
|
||||
getCountByQuery: (query) => getCountByQuery(context, query)
|
||||
}
|
||||
});
|
||||
|
||||
+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}}),
|
||||
}),
|
||||
|
||||
@@ -50,6 +50,14 @@ const RootQuery = {
|
||||
return Comments.getCountByQuery(query);
|
||||
},
|
||||
|
||||
async userCount(_, {query}, {user, loaders: {Users}}) {
|
||||
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Users.getCountByQuery(query);
|
||||
},
|
||||
|
||||
assetMetrics(_, query, {user, loaders: {Metrics: {Assets}}}) {
|
||||
if (user == null || !user.can(SEARCH_ASSETS)) {
|
||||
return null;
|
||||
|
||||
+27
-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
|
||||
@@ -341,6 +343,18 @@ input CommentCountQuery {
|
||||
tags: [String!]
|
||||
}
|
||||
|
||||
# UserCountQuery allows the ability to query user counts by specific
|
||||
# methods.
|
||||
input UserCountQuery {
|
||||
|
||||
# comments returned will only be ones which have at least one action of this
|
||||
# type.
|
||||
action_type: ACTION_TYPE
|
||||
|
||||
# Current status of a user.
|
||||
statuses: [USER_STATUS]
|
||||
}
|
||||
|
||||
type EditInfo {
|
||||
edited: Boolean!
|
||||
editableUntil: Date
|
||||
@@ -688,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
|
||||
@@ -838,6 +852,10 @@ type RootQuery {
|
||||
# expensive as it is not batched. Requires the `ADMIN` role.
|
||||
commentCount(query: CommentCountQuery!): Int
|
||||
|
||||
# Return the count of users satisfied by the query. Note that this edge is
|
||||
# expensive as it is not batched. This field is restricted.
|
||||
userCount(query: UserCountQuery!): Int
|
||||
|
||||
# The currently logged in user based on the request. Requires any logged in
|
||||
# role.
|
||||
me: User
|
||||
@@ -875,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
-1
@@ -313,7 +313,6 @@ en:
|
||||
report_notif_remove: "Your report has been removed."
|
||||
reported: Reported
|
||||
settings:
|
||||
all_comments: "All Comments"
|
||||
from_settings_page: "From the Profile Page you can see your comment history."
|
||||
my_comment_history: "My comment History"
|
||||
profile: Profile
|
||||
@@ -322,6 +321,7 @@ en:
|
||||
to_access: "to access Profile"
|
||||
user_no_comment: "You've never left a comment. Join the conversation!"
|
||||
stream:
|
||||
all_comments: "All Comments"
|
||||
temporarily_suspended: "In accordance with {0}'s community guidelines, your account has been temporarily suspended. Please rejoin the conversation {1}."
|
||||
comment_not_found: "Comment was not found"
|
||||
step_1_header: "Report an issue"
|
||||
|
||||
+1
-1
@@ -305,7 +305,6 @@ es:
|
||||
report_notif_remove: "Tu reporte ha sido eliminado."
|
||||
reported: "Reportado"
|
||||
settings:
|
||||
all_comments: "Todos los comentarios"
|
||||
from_settings_page: "Desde la página de configuración puedes ver tu historial de comentarios."
|
||||
my_comment_history: "Mi historial de comentarios"
|
||||
profile: Perfil
|
||||
@@ -348,6 +347,7 @@ es:
|
||||
step_2_header: "Ayúdanos a comprender"
|
||||
step_3_header: "Gracias por tu participación"
|
||||
stream:
|
||||
all_comments: "Todos los comentarios"
|
||||
temporarily_suspended: "De acuerdo con la guía de la comunidad de {0}, su cuenta ha sido temporalmente suspendida. Por favor unirse a la conversación {1}."
|
||||
comment_not_found: "Comentario no encontrado"
|
||||
streams:
|
||||
|
||||
+1
-1
@@ -263,7 +263,6 @@ fr:
|
||||
reported: Signalé
|
||||
set_best: "Sélectionner comme le meilleur"
|
||||
settings:
|
||||
all_comments: "Tous les commentaires"
|
||||
from_settings_page: "Dans la page Profil, vous pouvez voir l'historique de vos commentaires."
|
||||
my_comment_history: "Mon historique de commentaires"
|
||||
profile: Profil
|
||||
@@ -272,6 +271,7 @@ fr:
|
||||
to_access: "Accéder au profil"
|
||||
user_no_comment: "Vous n'avez jamais laissé de commentaire. Rejoignez la conversation!"
|
||||
stream:
|
||||
all_comments: "Tous les commentaires"
|
||||
temporarily_suspended: "Conformément à la charte d'utilisation des commentaires de {0}, votre compte a été temporairement suspendu. Merci de revenir dans la conversation {1}."
|
||||
step_1_header: "Signaler un problème"
|
||||
step_2_header: "Aidez-nous à comprendre"
|
||||
|
||||
+2
-3
@@ -303,7 +303,6 @@ pt_BR:
|
||||
report_notif_remove: "Seu marcado foi removido."
|
||||
reported: Reportado
|
||||
settings:
|
||||
all_comments: "Todos os comentários"
|
||||
from_settings_page: "Na página de Perfil, você pode ver seu histórico de comentários."
|
||||
my_comment_history: "Histórico de comentários"
|
||||
profile: Perfil
|
||||
@@ -312,8 +311,8 @@ pt_BR:
|
||||
to_access: "para acessar o perfil"
|
||||
user_no_comment: "Você nunca deixou um comentário. Participe da conversa!"
|
||||
stream:
|
||||
temporarily_suspended: "
|
||||
De acordo com as diretrizes da comunidade de {0}, sua conta foi temporariamente suspensa. Por favor, volte para a conversa {1}."
|
||||
all_comments: "Todos os comentários"
|
||||
temporarily_suspended: "De acordo com as diretrizes da comunidade de {0}, sua conta foi temporariamente suspensa. Por favor, volte para a conversa {1}."
|
||||
step_1_header: "Relatar um problema"
|
||||
step_2_header: "Ajude-nos a entender"
|
||||
step_3_header: "Obrigdo por sua contribuição"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"talk-plugin-sort-most-replied",
|
||||
"talk-plugin-author-menu",
|
||||
"talk-plugin-member-since",
|
||||
"talk-plugin-ignore-user"
|
||||
"talk-plugin-ignore-user",
|
||||
"talk-plugin-moderation-actions"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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