Merge branch 'es6-modules-tree-shaking' of ssh://github.com/coralproject/talk into es6-modules-tree-shaking

This commit is contained in:
Chi Vinh Le
2017-10-03 20:57:09 +07:00
80 changed files with 1069 additions and 525 deletions
+2
View File
@@ -26,5 +26,7 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
node_modules
+2
View File
@@ -43,5 +43,7 @@ plugins/*
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
**/node_modules/*
@@ -1,24 +0,0 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
.minimal {
width: 45px;
min-width: 0;
}
.approve__active {
box-shadow: none;
color: white;
background-color: #519954;
cursor: not-allowed;
}
.reject__active, .rejected__active {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
@@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
const ActionButton = ({type = '', active, ...props}) => {
const typeName = type.toLowerCase();
let text = menuActionsMap[type].text;
if (text === 'approve' && active) {
text = 'approved';
} else if (text === 'reject' && active) {
text = 'rejected';
}
return (
<Button
className={`${typeName} ${styles.actionButton} ${props.minimal ? styles.minimal : ''} ${active ? styles[`${typeName}__active`] : ''}`}
cStyle={typeName}
icon={menuActionsMap[type].icon}
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
);
};
ActionButton.propTypes = {
active: PropTypes.bool,
type: PropTypes.oneOf(['APPROVE', 'REJECT']),
minimal: PropTypes.bool,
acceptComment: PropTypes.func,
rejectComment: PropTypes.func,
};
export default ActionButton;
@@ -0,0 +1,44 @@
.root {
display: block;
color: #519954;
border: solid 2px rgba(81, 153, 84, 0.75);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
&:hover {
box-shadow: none;
color: white;
background-color: #519954;
cursor: pointer;
}
}
.active {
box-shadow: none;
color: white;
background-color: #519954;
&:hover {
cursor: not-allowed;
}
}
.minimal {
width: 45px;
min-width: 0;
}
.icon {
margin-right: 5px;
}
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick}) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
{!minimal && text}
</button>
);
};
ApproveButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
};
export default ApproveButton;
@@ -0,0 +1,18 @@
.root {
min-height: 25px;
padding: 0 16px;
}
.moreDetail {
position: absolute;
font-size: 12px;
font-weight: 500;
color: black;
right: 16px;
&:hover {
opacity: 0.9;
cursor: pointer;
}
}
@@ -0,0 +1,66 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
import Slot from 'coral-framework/components/Slot';
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
class CommentDetails extends Component {
state = {
showDetail: false
};
constructor () {
super();
this.state = {
showDetail: false
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
}));
}
render() {
const {data, root, comment} = this.props;
const {showDetail} = this.state;
const queryData = {
root,
comment,
};
return (
<div className={styles.root}>
<IfSlotIsNotEmpty
queryData={queryData}
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
>
<a onClick={this.toggleDetail} className={styles.moreDetail}>
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
</a>
</IfSlotIsNotEmpty>
<Slot
fill="adminCommentDetailArea"
data={data}
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
queryData={queryData}
/>}
</div>
);
}
}
CommentDetails.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
};
export default CommentDetails;
@@ -1,83 +0,0 @@
.flagBox {
border-top: 1px solid rgba(66, 66, 66, 0.12);
margin-top: 10px;
.container {
padding: 0 14px;
}
.detail {
padding: 0 20px 16px;
ul {
padding: 0;
list-style: none;
font-size: 12px;
font-weight: 500;
}
}
.header {
position: relative;
.moreDetail {
float: right;
font-size: 12px;
font-weight: 500;
margin-right: 10px;
margin-top: 8px;
color: black;
&: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;
}
}
h3 {
vertical-align: middle;
margin: 0;
font-weight: 500;
display: inline-block;
margin-left: 7px;
font-size: 12px;
}
}
.lessDetail {
display: inline-block;
margin-right: 10px;
}
.subDetail {
font-weight: normal;
color: #888;
span {
color: black;
}
}
.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;
}
}
@@ -1,97 +0,0 @@
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';
const shortReasons = {
'This comment is offensive': t('modqueue.offensive'),
'This looks like an ad/marketing': t('modqueue.spam_ads'),
'This user is impersonating': t('modqueue.impersonating'),
'I don\'t like this username': t('modqueue.dont_like_username'),
'Other': t('modqueue.other')
};
class FlagBox extends Component {
constructor () {
super();
this.state = {
showDetail: false
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
}));
}
reasonMap = (reason) => {
const shortReason = shortReasons[reason];
// if the short reason isn't found, just return the long one.
return shortReason ? shortReason : reason;
}
render() {
const {actionSummaries, actions, viewUserDetail} = this.props;
const {showDetail} = this.state;
return (
<div className={styles.flagBox}>
<div className={styles.container}>
<div className={styles.header}>
<Icon name='flag'/><h3>{t('community.flags')} ({actionSummaries.length}):</h3>
<ul>
{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>
)}
</div>
</div>
);
}
}
FlagBox.propTypes = {
actionSummaries: PropTypes.arrayOf(PropTypes.shape({
reason: PropTypes.string,
count: PropTypes.number
})).isRequired,
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired
};
export default FlagBox;
@@ -0,0 +1,44 @@
.root {
display: block;
color: #D03235;
border: solid 1px #D03235;
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: pointer;
}
}
.active {
color: white;
background-color: #D03235;
box-shadow: none;
&:hover {
cursor: not-allowed;
}
}
.minimal {
width: 45px;
min-width: 0;
}
.icon {
margin-right: 5px;
}
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick}) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
{!minimal && text}
</button>
);
};
RejectButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
};
export default RejectButton;
@@ -5,7 +5,6 @@ import styles from './UserDetail.css';
import {Icon, Button, Drawer, Spinner} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import {actionsMap} from '../utils/moderationQueueActionsMap';
import ClickOutside from 'coral-framework/components/ClickOutside';
import LoadMore from '../components/LoadMore';
import cn from 'classnames';
@@ -70,6 +69,7 @@ export default class UserDetail extends React.Component {
renderLoaded() {
const {
data,
root,
root: {
user,
@@ -177,15 +177,15 @@ export default class UserDetail extends React.Component {
<div>
{
nodes.map((comment) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
suspectWords={suspectWords}
bannedWords={bannedWords}
actions={actionsMap[status]}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
@@ -1,4 +1,5 @@
.root {
position: relative;
display: block;
margin: 0;
border-bottom: 1px solid #e0e0e0;
@@ -3,37 +3,41 @@ import PropTypes from 'prop-types';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
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 CommentAnimatedEdit from './CommentAnimatedEdit';
import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
render() {
const {
actions = [],
comment,
viewUserDetail,
suspectWords,
bannedWords,
selected,
toggleSelect,
className,
user,
...props
data,
} = this.props;
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
return (
<li
tabIndex={0}
@@ -66,7 +70,7 @@ class UserDetailComment extends React.Component {
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
<p className={styles.body}>
<div className={styles.body}>
<CommentBodyHighlighter
suspectWords={suspectWords}
bannedWords={bannedWords}
@@ -80,7 +84,7 @@ class UserDetailComment extends React.Component {
>
<Icon name="open_in_new" /> {t('comment.view_context')}
</a>
</p>
</div>
<div className={styles.sideActions}>
<IfHasLink text={comment.body}>
<span className={styles.hasLinks}>
@@ -88,41 +92,26 @@ class UserDetailComment extends React.Component {
</span>
</IfHasLink>
<div className={styles.actions}>
{actions.map((action, i) => {
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
minimal={true}
key={i}
type={action}
user={user}
status={comment.status}
active={active}
acceptComment={() =>
(comment.status === 'ACCEPTED'
? null
: props.acceptComment({commentId: comment.id}))}
rejectComment={() =>
(comment.status === 'REJECTED'
? null
: props.rejectComment({commentId: comment.id}))}
/>
);
})}
<ApproveButton
active={comment.status === 'ACCEPTED'}
onClick={this.approve}
minimal
/>
<RejectButton
active={comment.status === 'REJECTED'}
onClick={this.reject}
minimal
/>
</div>
</div>
</div>
</CommentAnimatedEdit>
</div>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
<CommentDetails
data={data}
root={root}
comment={comment}
/>
</li>
);
}
@@ -138,8 +127,9 @@ UserDetailComment.propTypes = {
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
toggleSelect: PropTypes.func,
comment: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
actions: PropTypes.array,
created_at: PropTypes.string.isRequired,
asset: PropTypes.shape({
@@ -0,0 +1,24 @@
import {gql} from 'react-apollo';
import CommentDetails from '../components/CommentDetails';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import withFragments from 'coral-framework/hocs/withFragments';
const slots = [
'adminCommentDetailArea',
'adminCommentMoreDetails',
];
export default withFragments({
root: gql`
fragment CoralAdmin_CommentDetails_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
comment: gql`
fragment CoralAdmin_CommentDetails_comment on Comment {
__typename
${getSlotFragmentSpreads(slots, 'comment')}
}
`
})(CommentDetails);
@@ -8,6 +8,12 @@ const slots = [
];
export default withFragments({
root: gql`
fragment CoralAdmin_CommentLabels_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
comment: gql`
fragment CoralAdmin_CommentLabels_comment on Comment {
hasParent
@@ -160,8 +160,10 @@ export const withUserDetailQuery = withQuery(gql`
}) {
...CoralAdmin_Moderation_CommentConnection
}
...${getDefinitionName(UserDetailComment.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
}
${UserDetailComment.fragments.root}
${commentConnectionFragment}
`, {
options: ({userId, statuses}) => {
@@ -3,10 +3,20 @@ import UserDetailComment from '../components/UserDetailComment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getDefinitionName} from 'coral-framework/utils';
import CommentLabels from './CommentLabels';
import CommentDetails from './CommentDetails';
export default withFragments({
root: gql`
fragment CoralAdmin_UserDetailComment_root on RootQuery {
__typename
...${getDefinitionName(CommentLabels.fragments.root)}
...${getDefinitionName(CommentDetails.fragments.root)}
}
${CommentLabels.fragments.root}
${CommentDetails.fragments.root}
`,
comment: gql`
fragment CoralAdmin_UserDetail_comment on Comment {
fragment CoralAdmin_UserDetailComment_comment on Comment {
id
body
created_at
@@ -17,28 +27,13 @@ export default withFragments({
title
url
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
}
}
editing {
edited
}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
})(UserDetailComment);
@@ -1,4 +0,0 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
@@ -1,22 +0,0 @@
import React from 'react';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
// TODO: Needs refactoring.
const ActionButton = ({type = '', user, ...props}) => {
return (
<Button
className={`${type.toLowerCase()} ${styles.actionButton}`}
cStyle={type.toLowerCase()}
icon={menuActionsMap[type].icon}
onClick={() => {
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showRejectUsernameDialog({user: user});
}}
>{t(`modqueue.${menuActionsMap[type].text}`)}</Button>
);
};
export default ActionButton;
@@ -47,7 +47,6 @@ class FlaggedAccounts extends React.Component {
<FlaggedUser
user={user}
key={user.id}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
showRejectUsernameDialog={showRejectUsernameDialog}
@@ -1,13 +1,13 @@
import React from 'react';
import styles from './FlaggedUser.css';
import ActionButton from './ActionButton';
// TODO: Should not rely on plugin.
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
@@ -35,15 +35,16 @@ class User extends React.Component {
});
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
approveUser = () => this.props.approveUser({
userId: this.props.user.id,
});
render() {
const {
user,
modActionButtons,
viewUserDetail,
selected,
approveUser,
showRejectUsernameDialog,
me,
className,
} = this.props;
@@ -125,14 +126,12 @@ class User extends React.Component {
</div>
<div className={styles.sideActions}>
<div className={styles.actions}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={approveUser}
showRejectUsernameDialog={showRejectUsernameDialog}
/>
)}
<ApproveButton
onClick={this.approveUser}
/>
<RejectButton
onClick={this.showRejectUsernameDialog}
/>
</div>
</div>
</div>
@@ -51,7 +51,7 @@ class RejectUsernameDialog extends Component {
const next = () => this.setState({stage: stage + 1});
const suspend = async () => {
try {
await rejectUsername({id: user.user.id, message: this.state.email});
await rejectUsername({id: user.id, message: this.state.email});
this.props.handleClose();
} catch (err) {
@@ -78,7 +78,11 @@ const withData = withQuery(gql`
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`);
`, {
options: {
fetchPolicy: 'network-only',
},
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -58,6 +58,7 @@
font-size: 14px;
line-height: 1.5;
font-weight: 300;
margin-bottom: 8px;
}
.body {
@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import FlagBox from 'coral-admin/src/components/FlagBox';
import CommentDetails from 'coral-admin/src/components/CommentDetails';
import styles from './Comment.css';
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';
import ActionButton from 'coral-admin/src/components/ActionButton';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
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 ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
@@ -45,11 +45,19 @@ class Comment extends React.Component {
return viewUserDetail(comment.user.id);
};
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
render() {
const {
actions = [],
comment,
viewUserDetail,
suspectWords,
bannedWords,
selected,
@@ -58,15 +66,9 @@ class Comment extends React.Component {
root,
currentUserId,
currentAsset,
acceptComment,
rejectComment,
} = this.props;
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const queryData = {root, comment, asset: comment.asset};
return (
@@ -153,28 +155,14 @@ class Comment extends React.Component {
</span>
</IfHasLink>
<div className={`actions ${styles.actions}`}>
{actions.map((action, i) => {
const active =
(action === 'REJECT' && comment.status === 'REJECTED') ||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
<ActionButton
key={i}
type={action}
user={comment.user}
status={comment.status}
active={active}
acceptComment={() =>
(comment.status === 'ACCEPTED'
? null
: acceptComment({commentId: comment.id}))}
rejectComment={() =>
(comment.status === 'REJECTED'
? null
: rejectComment({commentId: comment.id}))}
/>
);
})}
<ApproveButton
active={comment.status === 'ACCEPTED'}
onClick={this.approve}
/>
<RejectButton
active={comment.status === 'REJECTED'}
onClick={this.reject}
/>
</div>
<Slot
fill="adminSideActions"
@@ -185,18 +173,11 @@ class Comment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<Slot
fill="adminCommentDetailArea"
<CommentDetails
data={data}
queryData={queryData}
root={root}
comment={comment}
/>
{flagActions && flagActions.length
? <FlagBox
actions={flagActions}
actionSummaries={flagActionSummaries}
viewUserDetail={viewUserDetail}
/>
: null}
</li>
);
}
@@ -214,6 +195,8 @@ Comment.propTypes = {
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
comment: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
actions: PropTypes.array,
@@ -230,7 +213,6 @@ Comment.propTypes = {
}),
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
actions: PropTypes.array.isRequired,
selected: PropTypes.bool,
};
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Comment from '../containers/Comment';
import styles from './ModerationQueue.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
import LoadMore from '../../../components/LoadMore';
import ViewMore from './ViewMore';
import t from 'coral-framework/services/i18n';
@@ -140,7 +139,6 @@ class ModerationQueue extends React.Component {
if (singleView) {
const index = comments.findIndex((comment) => comment.id === selectedCommentId);
const comment = comments[index];
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return (
<div className={styles.root}>
<Comment
@@ -152,7 +150,6 @@ class ModerationQueue extends React.Component {
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
@@ -190,7 +187,6 @@ class ModerationQueue extends React.Component {
{
view
.map((comment) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
data={this.props.data}
root={this.props.root}
@@ -200,7 +196,6 @@ class ModerationQueue extends React.Component {
suspectWords={props.suspectWords}
bannedWords={props.bannedWords}
viewUserDetail={viewUserDetail}
actions={actionsMap[status]}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
@@ -1,6 +1,7 @@
import {gql} from 'react-apollo';
import Comment from '../components/Comment';
import CommentLabels from '../../../containers/CommentLabels';
import CommentDetails from '../../../containers/CommentDetails';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads, getDefinitionName} from 'coral-framework/utils';
@@ -16,8 +17,12 @@ export default withFragments({
fragment CoralAdmin_ModerationComment_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
...${getDefinitionName(CommentLabels.fragments.root)}
...${getDefinitionName(CommentDetails.fragments.root)}
}
`,
${CommentLabels.fragments.root}
${CommentDetails.fragments.root}
`,
comment: gql`
fragment CoralAdmin_ModerationComment_comment on Comment {
id
@@ -34,33 +39,15 @@ export default withFragments({
title
url
}
action_summaries {
count
... on FlagActionSummary {
reason
__typename
}
}
actions {
... on FlagAction {
id
reason
message
user {
id
username
}
__typename
}
__typename
}
editing {
edited
}
hasParent
${getSlotFragmentSpreads(slots, 'comment')}
...${getDefinitionName(CommentLabels.fragments.comment)}
...${getDefinitionName(CommentDetails.fragments.comment)}
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
})(Comment);
@@ -363,7 +363,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
organizationName
moderation
}
...${getDefinitionName(Comment.fragments.root)}
}
${Comment.fragments.root}
${commentConnectionFragment}
`, {
options: (props) => {
@@ -1,14 +0,0 @@
export const actionsMap = {
PREMOD: ['APPROVE', 'REJECT'],
FLAGGED: ['APPROVE', 'REJECT'],
REJECTED: ['APPROVE', 'REJECTED']
};
export const menuActionsMap = {
'REJECT': {status: 'REJECTED', text: 'reject', icon: 'close', key: 'f'},
'REJECTED': {status: 'REJECTED', text: 'rejected', icon: 'close'},
'APPROVE': {status: 'ACCEPTED', text: 'approve', icon: 'done', key: 'd'},
'FLAGGED': {status: 'FLAGGED', text: 'flag', icon: 'flag', filter: 'Untouched'},
'BAN': {status: 'BANNED', text: 'ban_user', icon: 'not interested'},
'': {icon: 'done'}
};
+18 -5
View File
@@ -96,6 +96,11 @@ export default {
user {
id
username
tags {
tag {
name
}
}
}
action_summaries {
count
@@ -129,7 +134,19 @@ export default {
user: {
__typename: 'User',
id: auth.user.id,
username: auth.user.username
username: auth.user.username,
tags: tags.map((tag) => ({
tag: {
name: tag,
created_at: new Date().toISOString(),
__typename: 'Tag'
},
assigned_by: {
id: auth.user.id,
__typename: 'User'
},
__typename: 'TagLink'
})),
},
created_at: new Date().toISOString(),
body,
@@ -140,10 +157,6 @@ export default {
created_at: new Date().toISOString(),
__typename: 'Tag'
},
assigned_by: {
id: auth.user.id,
__typename: 'User'
},
__typename: 'TagLink'
})),
status: 'NONE',
@@ -0,0 +1,37 @@
.root {
}
.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;
&:empty {
display: none;
}
}
.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;
@@ -23,8 +23,9 @@ class IfSlotIsEmpty extends React.Component {
}
isSlotEmpty(props = this.props) {
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
const slots = Array.isArray(slot) ? slot : [slot];
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
}
render() {
@@ -34,7 +35,7 @@ class IfSlotIsEmpty extends React.Component {
}
IfSlotIsEmpty.propTypes = {
slot: PropTypes.string,
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};
const mapStateToProps = (state) => ({
@@ -23,8 +23,9 @@ class IfSlotIsNotEmpty extends React.Component {
}
isSlotEmpty(props = this.props) {
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
const slots = Array.isArray(slot) ? slot : [slot];
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
}
render() {
@@ -34,7 +35,7 @@ class IfSlotIsNotEmpty extends React.Component {
}
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.string,
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};
const mapStateToProps = (state) => ({
+19 -4
View File
@@ -1,4 +1,19 @@
export default (condition) => (BaseComponent) => {
BaseComponent.isExcluded = condition;
return BaseComponent;
};
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
/**
* ExcludeIf provides a property `emit: (eventName, value)`
* to the wrapped component.
*/
export default (condition) => hoistStatics((WrappedComponent) => {
class ExcludeIf extends React.Component {
render() {
return <WrappedComponent
{...this.props}
/>;
}
}
WrappedComponent.isExcluded = condition;
return ExcludeIf;
});
-53
View File
@@ -130,59 +130,6 @@
background: #00a291;
}
.type--approve {
display: block;
color: #519954;
border: solid 2px rgba(81, 153, 84, 0.75);
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
&:hover {
box-shadow: none;
color: white;
background-color: #519954;
}
}
.type--reject, .type--rejected {
display: block;
color: #D03235;
border: solid 1px #D03235;
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
&:hover {
color: white;
background-color: #D03235;
box-shadow: none;
}
}
.type--rejected {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: not-allowed;
}
.type--ban, .type--actions {
display: block;
color: #616161;
-5
View File
@@ -262,11 +262,9 @@ en:
ban_user: "Ban"
billion: B
close: Close
dont_like_username: "Don't like username"
empty_queue: "No more comments to moderate! You're all caught up. Go have some ☕️"
flagged: flagged
reported: reported
impersonating: Impersonating
less_detail: "Less detail"
likes: likes
million: M
@@ -277,9 +275,7 @@ en:
newest_first: "Newest First"
navigation: Navigation
next_comment: "Go to the next comment"
offensive: Offensive
oldest_first: "Oldest First"
other: Other
premod: pre-mod
prev_comment: "Go to the previous comment"
reject: "Reject"
@@ -290,7 +286,6 @@ en:
shortcuts: "Shortcuts"
show_shortcuts: "Show Shortcuts"
singleview: "Toggle single comment edit view"
spam_ads: Spam/Ads
thismenu: "Open this menu"
thousand: k
try_these: "Try these"
-5
View File
@@ -255,10 +255,8 @@ es:
approved: "Aprobado"
billion: B
close: Cerrar
dont_like_username: "No me gusta el nombre de usuario"
empty_queue: "¡No hay más comentarios para moderar! Tiempo para un ☕️"
flagged: Reportado
impersonating: Impersonando
less_detail: "Menos detalles"
likes: likes
million: M
@@ -269,9 +267,7 @@ es:
newest_first: "Primero el más nuevo"
navigation: Navegación
next_comment: "Ir al siguiente comentario"
offensive: Ofensivo
oldest_first: "Primero el más antiguo"
other: Otro
premod: pre-mod
prev_comment: "Ir al comentario anterior"
reject: "Rechazar"
@@ -282,7 +278,6 @@ es:
shortcuts: Atajos
show_shortcuts: "Mostrar Atajos"
singleview: "Colocar vista de edición de comentario único"
spam_ads: Spam/Publicidad
thismenu: "Abrir este menu"
thousand: k
try_these: "Intentar estos"
-5
View File
@@ -41,22 +41,17 @@ fr:
banned: Banni
banned_user: "Utilisateur banni"
cancel: Signalé
dont_like_username: "Je n'aime pas ce nom d'utilisateur"
flaggedaccounts: "Noms d'utilisateurs signalés"
flags: Signalements
impersonating: "Cet utilisateur se fait passer pour quelqu'un d'autre"
loading: "Chargement des résultats"
moderator: Modérateur
newsroom_role: "Rôle de la salle de presse"
no_flagged_accounts: "La liste des comptes signalés est vide."
no_results: "Aucun utilisateur n'a été trouvé avec ce nom d'utilisateur ou cette adresse de messagerie. Ils se cachent !"
note: "Remarque: bannir cet utilisateur ne lui permettra pas de modifier les commentaires ou de supprimer quoi que ce soit."
offensive: "Ce commentaire est offensant"
other: Autre
people: Gens
role: "Sélectionnez le rôle ..."
select_status: "Sélectionnez l'état ..."
spam_ads: "Spam / Annonces"
staff: "Équipe"
status: Statut
username_and_email: "Nom d'utilisateur et e-mail"
-4
View File
@@ -51,7 +51,6 @@ pt_BR:
banned: Proibida
banned_user: "Usuário proibido"
cancel: Cancelar
dont_like_username: "Não gostei do nome de usuário"
flaggedaccounts: "Nomes de usuários marcados"
flags: Marcadas
impersonating: "Representação"
@@ -60,12 +59,9 @@ pt_BR:
newsroom_role: "Papel de empresa"
no_flagged_accounts: "A fila de nomes de usuários marcados está atualmente vazia."
no_results: "Nenhum usuário encontrado com esse nome de usuário ou e-mail. Eles estão se escondendo!"
offensive: "Ofensiva"
other: Outra
people: Pessoas
role: "Selecione um papel..."
select_status: "Selecione um status..."
spam_ads: "Spam/anúncios"
staff: "Funcionários"
status: Status
username_and_email: "Nome de usuário e email"
+1
View File
@@ -0,0 +1 @@
export {viewUserDetail} from 'coral-admin/src/actions/userDetail';
@@ -4,4 +4,5 @@ export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty
export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty';
export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName';
export {default as CommentTimestamp} from 'coral-framework/components/CommentTimestamp';
export {default as CommentDetail} from 'coral-framework/components/CommentDetail';
export {default as CommentContent} from 'coral-framework/components/CommentContent';
+2 -1
View File
@@ -21,6 +21,7 @@
"talk-plugin-author-menu",
"talk-plugin-member-since",
"talk-plugin-ignore-user",
"talk-plugin-moderation-actions"
"talk-plugin-moderation-actions",
"talk-plugin-flag-details"
]
}
@@ -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,4 @@
{
"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,12 @@
.badge {
background-color: #616161;
color: white;
display: inline-block;
margin: 0px 5px;
padding: 5px 5px;
border-radius: 2px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: bold;
}
@@ -0,0 +1,9 @@
import React from 'react';
import styles from './SubscriberBadge.css';
import {t} from 'plugin-api/beta/client/services';
export default () =>(
<span className={styles.badge}>
{t('talk-plugin-subscriber.subscriber')}
</span>
);
@@ -0,0 +1,23 @@
import SubscriberBadge from '../components/SubscriberBadge';
import {compose, gql} from 'react-apollo';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
const isSubscriber = (tags = []) => tags.some((t) => t.tag.name === 'SUBSCRIBER');
const enhance = compose(
withFragments({
comment: gql`
fragment TalkSubscriberBadge_SubscriberBadge_comment on Comment {
user {
tags {
tag {
name
}
}
}
}`
}),
excludeIf(({comment}) => !isSubscriber(comment.user.tags))
);
export default enhance(SubscriberBadge);
@@ -0,0 +1,9 @@
import SubscriberBadge from './containers/SubscriberBadge';
import translations from './translations.yml';
export default {
translations,
slots: {
commentAuthorTags: [SubscriberBadge]
}
};
@@ -0,0 +1,6 @@
en:
talk-plugin-subscriber:
subscriber: "Subscriber"
es:
talk-plugin-subscriber:
subscriber: "Subscriptor"
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
tags: [
{
name: 'SUBSCRIBER',
permissions: {
public: true,
self: false,
roles: []
},
models: ['USERS'],
created_at: new Date()
}
]
};
@@ -0,0 +1,11 @@
{
"name": "@coralproject/talk-plugin-subscriber",
"version": "0.1.0",
"description": "A Recipe to display a user badge",
"main": "index.js",
"author": "The Coral Project Team <coral@mozillafoundation.org>",
"license": "Apache-2.0",
"dependencies": {
"moment": "^2.18.1"
}
}
+11
View File
@@ -0,0 +1,11 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
moment@^2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
momentjs@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/momentjs/-/momentjs-2.0.0.tgz#73df904b4fa418f6e3c605e831cef6ed5518ebd4"
@@ -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
}