Merge branch 'master' into subscriber-badge

This commit is contained in:
Kim Gardner
2017-10-02 18:31:15 +01:00
committed by GitHub
68 changed files with 933 additions and 520 deletions
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk"
}
@@ -0,0 +1,14 @@
{
"presets": [
"es2015"
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
"transform-object-rest-spread",
"transform-async-to-generator",
"transform-react-jsx"
]
}
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -0,0 +1,45 @@
.info {
vertical-align: middle;
list-style: none;
display: inline-block;
padding: 0;
font-size: 12px;
margin: 0;
}
.detail {
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-weight: 500;
}
.subDetail {
margin-left:10px;
padding: 0;
list-style: none;
font-size: 12px;
font-weight: normal;
color: #888;
}
.lessDetail {
display: inline-block;
margin-right: 10px;
}
.username {
color: #393B44;
text-decoration: none;
cursor: pointer;
font-weight: 600;
padding: 2px 5px;
border-radius: 2px;
margin-left: -5px;
transition: background-color 200ms ease;
&:hover {
background-color: #E0E0E0;
}
}
@@ -0,0 +1,72 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import styles from './FlagDetails.css';
import {t} from 'plugin-api/beta/client/services';
import {Slot, IfSlotIsNotEmpty, CommentDetail} from 'plugin-api/beta/client/components';
class FlagDetails extends Component {
render() {
const {comment: {actions}, more, data, root, comment} = this.props;
const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction');
const summaries = flagActions.reduce((sum, action) => {
if (!(action.reason in sum)) {
sum[action.reason] = {count: 0, actions: []};
}
sum[action.reason].count++;
if (action.user) {
sum[action.reason].userFlagged = true;
}
return sum;
}, {});
const reasons = Object.keys(summaries);
const queryData = {
root,
comment,
};
return (
<CommentDetail
icon={'flag'}
header={`${t('talk-plugin-flag-details.flags')} (${Object.keys(summaries).length})`}
info={
<ul className={styles.info}>
{reasons.map((reason) =>
<li key={reason} className={styles.lessDetail}>
{reason} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
</li>
)}
</ul>
}>
{more && (
<IfSlotIsNotEmpty
slot="adminCommentMoreFlagDetails"
queryData={queryData}
>
<Slot
fill="adminCommentMoreFlagDetails"
data={data}
queryData={queryData}
/>
</IfSlotIsNotEmpty>
)}
</CommentDetail>
);
}
}
FlagDetails.propTypes = {
more: PropTypes.bool,
data: PropTypes.object,
root: PropTypes.object,
comment: PropTypes.shape({
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired,
}).isRequired,
};
export default FlagDetails;
@@ -0,0 +1,45 @@
.info {
vertical-align: middle;
list-style: none;
display: inline-block;
padding: 0;
font-size: 12px;
margin: 0;
}
.detail {
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
font-weight: 500;
}
.subDetail {
margin-left:10px;
padding: 0;
list-style: none;
font-size: 12px;
font-weight: normal;
color: #888;
}
.lessDetail {
display: inline-block;
margin-right: 10px;
}
.username {
color: #393B44;
text-decoration: none;
cursor: pointer;
font-weight: 600;
padding: 2px 5px;
border-radius: 2px;
margin-left: -5px;
transition: background-color 200ms ease;
&:hover {
background-color: #E0E0E0;
}
}
@@ -0,0 +1,59 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import styles from './UserFlagDetails.css';
class UserFlagDetails extends Component {
render() {
const {comment: {actions}, viewUserDetail} = this.props;
const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction');
const summaries = flagActions.reduce((sum, action) => {
if (!action.user) {
return sum;
}
if (!(action.reason in sum)) {
sum[action.reason] = {count: 0, actions: []};
}
sum[action.reason].count++;
sum[action.reason].actions.push(action);
return sum;
}, {});
return (
<ul className={styles.detail}>
{Object.keys(summaries)
.map((reason) => (
<li key={reason}>
{reason} ({summaries[reason].count})
<ul className={styles.subDetail}>
{summaries[reason].actions.map((action) =>
<li key={action.user.id}>
{action.user &&
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
{action.user.username}
</a>
}
{action.message}
</li>
)}
</ul>
</li>
))
}
</ul>
);
}
}
UserFlagDetails.propTypes = {
comment: PropTypes.shape({
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired,
}).isRequired,
viewUserDetail: PropTypes.func.isRequired,
};
export default UserFlagDetails;
@@ -0,0 +1,39 @@
import {compose, gql} from 'react-apollo';
import FlagDetails from '../components/FlagDetails';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils';
const slots = [
'adminCommentMoreFlagDetails',
];
const enhance = compose(
withFragments({
root: gql`
fragment CoralAdmin_FlagDetails_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
comment: gql`
fragment CoralAdmin_FlagDetails_comment on Comment {
actions {
__typename
... on FlagAction {
id
reason
message
user {
id
username
}
}
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`
}),
excludeIf(({comment: {actions}}) => !actions.some((action) => action.__typename === 'FlagAction')),
);
export default enhance(FlagDetails);
@@ -0,0 +1,39 @@
import {compose, gql} from 'react-apollo';
import UserFlagDetails from '../components/UserFlagDetails';
import {bindActionCreators} from 'redux';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {viewUserDetail} from 'plugin-api/beta/client/actions/admin';
import {connect} from 'react-redux';
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
viewUserDetail,
}, dispatch)
});
const enhance = compose(
connect(null, mapDispatchToProps),
withFragments({
comment: gql`
fragment CoralAdmin_UserFlagDetails_comment on Comment {
actions {
__typename
... on FlagAction {
id
reason
message
user {
id
username
}
}
}
}
`
}),
excludeIf(({comment: {actions}}) =>
!actions.some((action) => action.__typename === 'FlagAction' && action.user
)),
);
export default enhance(UserFlagDetails);
@@ -0,0 +1,11 @@
import FlagDetails from './containers/FlagDetails';
import UserFlagDetails from './containers/UserFlagDetails';
import translations from './translations.yml';
export default {
translations,
slots: {
adminCommentDetailArea: [FlagDetails],
adminCommentMoreFlagDetails: [UserFlagDetails],
}
};
@@ -0,0 +1,6 @@
en:
talk-plugin-flag-details:
flags: Flags
es:
talk-plugin-flag-details:
flags: Reportes
@@ -0,0 +1 @@
module.exports = {};
@@ -0,0 +1,11 @@
.info {
background-color: rgba(44, 44, 44, 0.89);
color: white;
padding: 2px 4px;
font-size: 0.8em;
margin-left: 6px;
}
.toxic {
background-color: #d03235;
}
@@ -0,0 +1,47 @@
import React from 'react';
import {CommentDetail} from 'plugin-api/beta/client/components';
import {isToxic} from '../utils';
import styles from './ToxicDetail.css';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {t} from 'plugin-api/beta/client/services';
const getInfo = (toxicity, actions) => {
const toxic = isToxic(actions);
let text = t('talk-plugin-toxic-comments.unlikely');
if (toxicity > 0.8) {
text = t('talk-plugin-toxic-comments.highly_likely');
}
else if (toxicity >= 0.5) {
text = t('talk-plugin-toxic-comments.possibly');
}
else if (toxicity >= 0.7) {
text = t('talk-plugin-toxic-comments.likely');
}
return (
<div>
{text}
<span className={cn(styles.info, {[styles.toxic]: toxic})}>
{Math.round(toxicity * 100)}%
</span>
</div>
);
};
const ToxicLabel = ({comment: {actions, toxicity}}) => (
<CommentDetail
icon={'add_box'}
header={t('talk-plugin-toxic-comments.toxic_comment')}
info={getInfo(toxicity, actions)}
/>
);
ToxicLabel.propTypes = {
comment: PropTypes.shape({
actions: PropTypes.array,
toxicity: PropTypes.toxicity,
}),
};
export default ToxicLabel;
@@ -0,0 +1,10 @@
import {compose} from 'react-apollo';
import {excludeIf} from 'plugin-api/beta/client/hocs';
import ToxicDetail from './ToxicDetail';
import {isToxic} from '../utils';
const enhance = compose(
excludeIf(({comment: {toxicity, actions}}) => toxicity === null || isToxic(actions)),
);
export default enhance(ToxicDetail);
@@ -0,0 +1,10 @@
import {compose} from 'react-apollo';
import {excludeIf} from 'plugin-api/beta/client/hocs';
import ToxicDetail from './ToxicDetail';
import {isToxic} from '../utils';
const enhance = compose(
excludeIf(({comment: {toxicity, actions}}) => toxicity === null || !isToxic(actions)),
);
export default enhance(ToxicDetail);
@@ -0,0 +1,20 @@
import {compose, gql} from 'react-apollo';
import {withFragments} from 'plugin-api/beta/client/hocs';
import ToxicDetail from '../components/ToxicDetail';
const enhance = compose(
withFragments({
comment: gql`
fragment TalkToxicComments_ToxicDetail_Comment on Comment {
toxicity
actions {
__typename
... on FlagAction {
reason
}
}
}`,
}),
);
export default enhance(ToxicDetail);
@@ -1,15 +1,12 @@
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');
}
import {isToxic} from '../utils';
const enhance = compose(
withFragments({
comment: gql`
fragment TalkToxicComments_Comment on Comment {
fragment TalkToxicComments_ToxicLabel_Comment on Comment {
actions {
__typename
... on FlagAction {
@@ -1,11 +1,15 @@
import translations from './translations.yml';
import CheckToxicityHook from './containers/CheckToxicityHook';
import ToxicLabel from './containers/ToxicLabel';
import ToxicCommentDetail from './containers/ToxicCommentDetail';
import ToxicCommentFlagDetail from './containers/ToxicCommentFlagDetail';
export default {
translations,
slots: {
commentInputDetailArea: [CheckToxicityHook],
adminCommentLabels: [ToxicLabel],
adminCommentMoreDetails: [ToxicCommentDetail],
adminCommentMoreFlagDetails: [ToxicCommentFlagDetail],
},
};
@@ -4,6 +4,11 @@ en:
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:
unlikely: Unlikely
highly_likely: Highly Likely
possibly: Possibly
likely: Likely
toxic_comment: Toxic Comment
still_toxic: |
This edited comment might still violate our community guidelines.
Our moderation team will review your comment shortly.
@@ -0,0 +1,4 @@
export function isToxic(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TOXIC_COMMENT');
}
@@ -1,8 +1,10 @@
const {readFileSync} = require('fs');
const path = require('path');
const hooks = require('./server/hooks');
const resolvers = require('./server/resolvers');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
hooks,
resolvers,
};
@@ -0,0 +1,7 @@
const get = require('lodash/get');
module.exports = {
Comment: {
toxicity: (comment) => get(comment, 'metadata.perspective.SEVERE_TOXICITY.summaryScore'),
}
};
@@ -5,3 +5,6 @@ input CreateCommentInput {
checkToxicity: Boolean
}
type Comment {
toxicity: Float
}