mirror of
https://github.com/wassname/talk.git
synced 2026-08-10 12:41:02 +08:00
Refactor ComemntDetail and implement ToxicDetail
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
.root {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 0 20px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentDetail.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const CommentDetail = ({icon, header, info, children, className}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.headerContainer}>
|
||||
{icon && <Icon className={styles.icon} name={icon}/>}
|
||||
<h3 className={styles.header}>{header}:</h3>
|
||||
<div className={styles.info}>{info}</div>
|
||||
</div>
|
||||
{children &&
|
||||
<div className={styles.details}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentDetail.propTypes = {
|
||||
className: PropTypes.string,
|
||||
header: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
info: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default CommentDetail;
|
||||
@@ -1,56 +1,42 @@
|
||||
.flagBox {
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
margin-top: 10px;
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 20px 16px;
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.info {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
.moreDetail {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
margin-top: 8px;
|
||||
color: black;
|
||||
.detail {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
i {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
}
|
||||
ul {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.subDetail {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
h3 {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
.moreDetail {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
right: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +45,6 @@
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.subDetail {
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
|
||||
span {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
text-decoration: none;
|
||||
@@ -77,7 +54,7 @@
|
||||
border-radius: 2px;
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import CommentDetail from './CommentDetail';
|
||||
|
||||
const shortReasons = {
|
||||
'This comment is offensive': t('modqueue.offensive'),
|
||||
@@ -39,45 +39,42 @@ class FlagBox extends Component {
|
||||
|
||||
return (
|
||||
<div className={styles.flagBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name='flag'/><h3>{t('community.flags')} ({actionSummaries.length}):</h3>
|
||||
<ul>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}</a>
|
||||
<CommentDetail
|
||||
icon={'flag'}
|
||||
header={`${t('community.flags')} (${actionSummaries.length})`}
|
||||
info={
|
||||
<ul className={styles.info}>
|
||||
{actionSummaries.map((action, i) =>
|
||||
<li key={i} className={styles.lessDetail}> {this.reasonMap(action.reason)} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}</a>
|
||||
</div>
|
||||
}>
|
||||
{showDetail && (
|
||||
<div className={styles.detail}>
|
||||
<ul>
|
||||
{actionSummaries.map((summary, i) => {
|
||||
|
||||
const actionList = actions.filter((a) => a.reason === summary.reason);
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
|
||||
<ul>
|
||||
{actionList.map((action, j) =>
|
||||
<li key={`${i}_${j}`} className={styles.subDetail}>
|
||||
{action.user &&
|
||||
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
}
|
||||
{action.message}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<ul className={styles.detail}>
|
||||
{actionSummaries.map((summary, i) => {
|
||||
const actionList = actions.filter((a) => a.reason === summary.reason);
|
||||
return (
|
||||
<li key={i}>
|
||||
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
|
||||
<ul>
|
||||
{actionList.map((action, j) =>
|
||||
<li key={`${i}_${j}`} className={styles.subDetail}>
|
||||
{action.user &&
|
||||
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
}
|
||||
{action.message}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</CommentDetail>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,11 +185,6 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
@@ -197,6 +192,11 @@ class Comment extends React.Component {
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,46 @@
|
||||
import React from 'react';
|
||||
import CommentDetail from 'coral-admin/src/components/CommentDetail';
|
||||
import {isToxic} from '../utils';
|
||||
import styles from './ToxicDetail.css';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const getInfo = (toxicity, actions) => {
|
||||
const toxic = isToxic(actions);
|
||||
let text = 'Unlikely';
|
||||
if (toxicity > 0.8) {
|
||||
text = 'Highly Likely';
|
||||
}
|
||||
else if (toxicity >= 0.5) {
|
||||
text = 'Possibly';
|
||||
}
|
||||
else if (toxicity >= 0.7) {
|
||||
text = '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={'Toxic Comment'}
|
||||
info={getInfo(toxicity, actions)}
|
||||
/>
|
||||
);
|
||||
|
||||
ToxicLabel.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
actions: PropTypes.array,
|
||||
toxicity: PropTypes.toxicity,
|
||||
}),
|
||||
};
|
||||
|
||||
export default ToxicLabel;
|
||||
@@ -0,0 +1,21 @@
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import {withFragments, excludeIf} 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
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}),
|
||||
excludeIf(({comment: {toxicity}}) => toxicity === null),
|
||||
);
|
||||
|
||||
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,13 @@
|
||||
import translations from './translations.yml';
|
||||
import CheckToxicityHook from './containers/CheckToxicityHook';
|
||||
import ToxicLabel from './containers/ToxicLabel';
|
||||
import ToxicDetail from './containers/ToxicDetail';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
commentInputDetailArea: [CheckToxicityHook],
|
||||
adminCommentLabels: [ToxicLabel],
|
||||
adminCommentDetailArea: [ToxicDetail],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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