mirror of
https://github.com/wassname/talk.git
synced 2026-08-12 12:30:39 +08:00
Merge branch 'master' into subscriber-badge
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user