Moderation Actions

This commit is contained in:
Belen Curcio
2017-09-07 08:05:39 -03:00
parent 12be487783
commit 7b59323b26
29 changed files with 635 additions and 4 deletions
+1
View File
@@ -22,5 +22,6 @@ plugins/*
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
!plugins/talk-plugin-moderation-actions
node_modules
+1
View File
@@ -38,5 +38,6 @@ plugins/*
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
!plugins/talk-plugin-moderation-actions
**/node_modules/*
@@ -31,15 +31,14 @@ export default class Tag extends React.Component {
render() {
const {tooltip} = this.state;
return(
<div className={styles.noSelect} onMouseEnter={this.showTooltip}
<span className={styles.noSelect} onMouseEnter={this.showTooltip}
onMouseLeave={this.hideTooltip} onTouchStart={this.showTooltip}
onTouchEnd={this.hideTooltip}>
<span
className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
<span className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
{t('talk-plugin-featured-comments.featured')}
</span>
{tooltip && <Tooltip className={styles.tooltip} />}
</div>
</span>
);
}
}
@@ -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,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -0,0 +1,8 @@
.moderationActions {
display: inline;
padding: 0 4px;
}
.moderationActions:hover {
cursor: pointer;
}
@@ -0,0 +1,54 @@
import React from 'react';
import cn from 'classnames';
import styles from './ModerationActions.css';
import Tooltip from './Tooltip';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import RejectCommentAction from '../containers/RejectCommentAction.js';
import ClickOutside from 'coral-framework/components/ClickOutside';
export default class Tag extends React.Component {
constructor() {
super();
this.state = {
tooltip: false
};
}
toggleTooltip = (e) => {
e.preventDefault();
const {tooltip} = this.state;
this.setState({
tooltip: !tooltip
});
}
hideTooltip = () => {
this.setState({
tooltip: false
});
}
render() {
const {tooltip} = this.state;
return(
<ClickOutside onClickOutside={this.hideTooltip}>
<div className={styles.moderationActions}>
<span className={cn(styles.arrow, 'talk-plugin-moderation-action-arrow')} onClick={this.toggleTooltip}>
{tooltip ? <Icon name="keyboard_arrow_up" /> : <Icon name="keyboard_arrow_down" />}
</span>
{tooltip && (
<Tooltip className={styles.tooltip}>
<RejectCommentAction />
</Tooltip>
)}
</div>
</ClickOutside>
);
}
}
@@ -0,0 +1,17 @@
.root {
white-space: nowrap;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
border: 1px solid rgba(1, 1, 1, 0.8);
border-radius: 1px;
padding: 3px 6px;
font-size: 12px;
transition: color 100ms, background 100ms;
&:hover {
background-color: rgba(1, 1, 1, 0.8);
color: white;
}
}
@@ -0,0 +1,14 @@
import React from 'react';
import styles from './RejectCommentAction.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
export default ({rejectComment}) => (
<button
className={cn(styles.button, 'talk-plugin-reject-comment-action')}
onClick={rejectComment}>
<Icon name="clear" />
{t('talk-plugin-reject-comment.reject_comment')}
</button>
);
@@ -0,0 +1,64 @@
.tooltip {
background-color: white;
border: solid 1px #999;
border-radius: 3px;
padding: 10px;
position: absolute;
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
z-index: 10;
top: 26px;
right: 0px;
width: 200px;
text-align: left;
color: #616161;
}
.tooltip::before{
content: '';
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 0px;
top: -20px;
transform: rotate(180deg);
}
.tooltip::after{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 0px;
top: -19px;
transform: rotate(180deg);
}
.headline {
color: #484747;
display: inline-block;
margin: 0;
padding: 0;
font-size: 1.02em;
margin-left: 6px;
letter-spacing: 0.2px;
vertical-align: middle;
margin-bottom: 2px;
line-height: 22px;
}
.icon {
font-size: 1.4em;
vertical-align: middle;
}
.description {
width: 100%;
box-sizing: border-box;
white-space: pre-wrap;
padding: 0;
margin: 0;
padding-left: 25px;
font-weight: 400;
font-size: 1em;
}
@@ -0,0 +1,9 @@
import React from 'react';
import cn from 'classnames';
import styles from './Tooltip.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
export default ({className = '', children}) => (
<div className={cn(styles.tooltip, className)}>{children}</div>
);
@@ -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,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
@@ -0,0 +1,17 @@
.root {
white-space: nowrap;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
border: 1px solid rgba(1, 1, 1, 0.8);
border-radius: 1px;
padding: 3px 6px;
font-size: 12px;
transition: color 100ms, background 100ms;
&:hover {
background-color: rgba(1, 1, 1, 0.8);
color: white;
}
}
@@ -0,0 +1,12 @@
import React from 'react';
import styles from './IgnoreUserAction.css';
import {t} from 'plugin-api/beta/client/services';
import cn from 'classnames';
export default ({ignoreUser}) => (
<button
className={cn(styles.button, 'talk-plugin-ignore-user-action')}
onClick={ignoreUser}>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
);
@@ -0,0 +1,45 @@
.root {
width: 200px;
}
.actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.message {
font-size: 13px;
}
.title {
padding: 0;
margin: 0 0 8px 0;
font-size: 15px;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
border: 1px solid rgba(1, 1, 1, 0.8);
border-radius: 1px;
padding: 3px 6px;
font-size: 12px;
transition: color 100ms, background 100ms;
&:hover {
background-color: rgba(1, 1, 1, 0.8);
color: white;
}
}
.cancel {
composes: buttonReset from "coral-framework/styles/reset.css";
padding: 3px 6px;
font-size: 12px;
transition: color 100ms;
margin-right: 4px;
&:hover {
color: rgba(1, 1, 1, 0.75);
}
}
@@ -0,0 +1,23 @@
import React from 'react';
import styles from './IgnoreUserConfirmation.css';
import {t} from 'plugin-api/beta/client/services';
import cn from 'classnames';
export default ({ignoreUser, cancel, username}) => (
<aside className={cn(styles.root, 'talk-plugin-ignore-user-confirmation')}>
<div className={styles.message}>
<h1 className={styles.title}>
{t('talk-plugin-ignore-user.confirmation_title', username)}
</h1>
{t('talk-plugin-ignore-user.confirmation')}
</div>
<div className={cn(styles.actions, 'talk-plugin-ignore-user-confirmation-actions')}>
<button className={cn(styles.cancel, 'talk-plugin-ignore-user-confirmation-cancel')} onClick={cancel}>
{t('talk-plugin-ignore-user.cancel')}
</button>
<button className={cn(styles.button, 'talk-plugin-ignore-user-confirmation-button')} onClick={ignoreUser}>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
</div>
</aside>
);
@@ -0,0 +1,22 @@
.list {
display: table;
list-style-type: none;
margin-left: 0;
padding-left: 0;
}
.listItem {
display: table-row;
}
.username {
display: table-cell;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
margin-left: 16px;
color: #D0011B;
text-decoration: underline;
}
@@ -0,0 +1,23 @@
import React from 'react';
import styles from './IgnoredUserSection.css';
import {t} from 'plugin-api/beta/client/services';
export default ({ignoredUsers, stopIgnoringUser}) => (
<section className={'talk-plugin-ignore-user-section'}>
<h3>{t('talk-plugin-ignore-user.section_title')}</h3>
<p>{t('talk-plugin-ignore-user.section_info')}</p>
<ul className={styles.list}>
{ignoredUsers.map(({username, id}) => (
<li className={styles.listItem} key={id}>
<span className={styles.username}>{username}</span>
<button
onClick={() => stopIgnoringUser({id})}
className={styles.button}
>
{t('talk-plugin-ignore-user.stop_ignoring')}
</button>
</li>
))}
</ul>
</section>
);
@@ -0,0 +1,53 @@
import React from 'react';
import IgnoreUserAction from '../components/IgnoreUserAction';
import {compose, gql} from 'react-apollo';
import {connect, withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions';
import IgnoreUserConfirmation from './IgnoreUserConfirmation';
import {getDefinitionName} from 'plugin-api/beta/client/utils';
class IgnoreUserActionContainer extends React.Component {
ignoreUser = () => {
this.props.setContentSlot('ignoreUserConfirmation');
};
render() {
return <IgnoreUserAction
ignoreUser={this.ignoreUser}
/>;
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setContentSlot,
}, dispatch);
const withIgnoreUserActionFragments = withFragments({
root: gql`
fragment TalkIgnoreUser_IgnoreUserAction_root on RootQuery {
me {
id
}
}
`,
comment: gql`
fragment TalkIgnoreUser_IgnoreUserAction_comment on Comment {
user {
id
}
...${getDefinitionName(IgnoreUserConfirmation.fragments.comment)}
}
${IgnoreUserConfirmation.fragments.comment}
`,
});
const enhance = compose(
connect(null, mapDispatchToProps),
withIgnoreUserActionFragments,
excludeIf(({root: {me}, comment}) => !me || me.id === comment.user.id),
);
export default enhance(IgnoreUserActionContainer);
@@ -0,0 +1,60 @@
import React from 'react';
import IgnoreUserConfirmation from '../components/IgnoreUserConfirmation';
import {compose, gql} from 'react-apollo';
import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {closeMenu} from 'plugins/talk-plugin-author-menu/client/actions';
import {notify} from 'plugin-api/beta/client/actions/notification';
import {t} from 'plugin-api/beta/client/services';
import {getErrorMessages} from 'plugin-api/beta/client/utils';
class IgnoreUserConfirmationContainer extends React.Component {
ignoreUser = () => {
const {ignoreUser, notify, comment, closeMenu} = this.props;
ignoreUser(comment.user.id)
.then(() => {
notify('success', t('talk-plugin-ignore-user.notify_success', comment.user.username));
})
.catch((err) => {
notify('error', getErrorMessages(err));
});
closeMenu();
};
cancel = () => {
this.props.closeMenu();
}
render() {
return <IgnoreUserConfirmation
username={this.props.comment.user.username}
ignoreUser={this.ignoreUser}
cancel={this.cancel}
/>;
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
closeMenu,
notify,
}, dispatch);
const withIgnoreUserConfirmationFragments = withFragments({
comment: gql`
fragment TalkIgnoreUser_IgnoreUserConfirmation_comment on Comment {
user {
id
username
}
}`,
});
const enhance = compose(
connect(null, mapDispatchToProps),
withIgnoreUserConfirmationFragments,
withIgnoreUser,
);
export default enhance(IgnoreUserConfirmationContainer);
@@ -0,0 +1,36 @@
import React from 'react';
import IgnoredUserSection from '../components/IgnoredUserSection';
import {compose, gql} from 'react-apollo';
import {withFragments, excludeIf, withStopIgnoringUser} from 'plugin-api/beta/client/hocs';
class IgnoredUserSectionContainer extends React.Component {
render() {
return <IgnoredUserSection
stopIgnoringUser={this.props.stopIgnoringUser}
ignoredUsers={this.props.root.me.ignoredUsers}
/>;
}
}
const withIgnoredUserSectionFragments = withFragments({
root: gql`
fragment TalkIgnoreUser_IgnoredUserSection_root on RootQuery {
me {
id
ignoredUsers {
id,
username,
}
}
}
`,
});
const enhance = compose(
withIgnoredUserSectionFragments,
withStopIgnoringUser,
excludeIf(({root: {me}}) => me.ignoredUsers.length === 0),
);
export default enhance(IgnoredUserSectionContainer);
@@ -0,0 +1,43 @@
import IgnoreUserAction from './containers/IgnoreUserAction';
import IgnoreUserConfirmation from './containers/IgnoreUserConfirmation';
import IgnoredUserSection from './containers/IgnoredUserSection';
import translations from './translations.yml';
import update from 'immutability-helper';
export default {
slots: {
authorMenuActions: [IgnoreUserAction],
ignoreUserConfirmation: [IgnoreUserConfirmation],
profileSections: [IgnoredUserSection],
},
translations,
mutations: {
IgnoreUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Embed: (previousData) => {
const ignoredUserId = variables.id;
const updated = update(previousData, {me: {ignoredUsers: {$push: [{
id: ignoredUserId,
__typename: 'User',
}]}}});
return updated;
}
}
}),
StopIgnoringUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Profile: (previousData) => {
const noLongerIgnoredUserId = variables.id;
// remove noLongerIgnoredUserId from ignoredUsers
const updated = update(previousData, {me: {ignoredUsers: {
$apply: (ignoredUsers) => {
return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId);
}
}}});
return updated;
}
}
}),
},
};
@@ -0,0 +1,25 @@
en:
talk-plugin-ignore-user:
section_title: Ignored users
section_info: Because you ignored the following commenters, their comments are hidden.
stop_ignoring: Stop ignoring
ignore_user: Ignore User
cancel: Cancel
confirmation: |
When you ignore a user, all comments they wrote on the site will be hidden from you. You can
undo this later from My Profile.
notify_success: |
You are now ignoring {0}. You can undo this action from My Profile.
confirmation_title: Ignore {0}?
es:
talk-plugin-ignore-user:
section_title: "Usuarios ignorados"
stop_ignoring: "No ignorar más"
fr:
talk-plugin-ignore-user:
section_title: "Utilisateurs ignorés"
pt_Br:
talk-plugin-ignore-user:
section_title: "Usuários ignorados"
section_info: "Porque você ignorou os seguintes comentadores, seus comentários estão ocultos."
stop_ignoring: "Pare de ignorar"
@@ -0,0 +1 @@
module.exports = {};
@@ -0,0 +1,12 @@
import React from 'react';
import {compose} from 'react-apollo';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import RejectCommentAction from '../components/RejectCommentAction';
// change this
const enhance = compose(
withSetCommentStatus
);
export default enhance(RejectCommentAction);
@@ -0,0 +1,11 @@
// import RejectCommentAction from './containers/RejectCommentAction';
import ModerationActions from './components/ModerationActions';
import translations from './translations.yml';
import update from 'immutability-helper';
export default {
slots: {
commentInfoBar: [ModerationActions],
},
translations
};
@@ -0,0 +1,6 @@
en:
talk-plugin-reject-comment:
reject_comment: "Reject comment"
es:
talk-plugin-reject-user:
reject_comment: "Rechazar commentario"
@@ -0,0 +1 @@
module.exports = {};