From f641a3ec40b21534d5db1f8d106af56d64c9ee24 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 19 May 2017 01:52:03 +0700 Subject: [PATCH] Complete suspend user implementation --- .../ModerationQueue/ModerationContainer.js | 49 ++++++++++++------- .../ModerationQueue/ModerationQueue.js | 1 + .../ModerationQueue/components/Comment.js | 25 +++++----- .../components/SuspendUserDialog.js | 33 +++++++++---- .../src/graphql/queries/modQueueQuery.graphql | 3 ++ .../coral-admin/src/services/notification.js | 28 +++++++++++ client/coral-admin/src/translations.json | 6 ++- .../src/components/Stream.js | 18 +++++-- .../src/containers/Stream.js | 3 ++ .../components/RestrictedMessageBox.css | 1 + client/coral-framework/translations.json | 2 + graph/typeDefs.graphql | 1 + services/email/suspension.ejs | 1 - services/email/suspension.html.ejs | 2 +- services/users.js | 7 +-- 15 files changed, 126 insertions(+), 54 deletions(-) create mode 100644 client/coral-admin/src/services/notification.js delete mode 100644 services/email/suspension.ejs diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index e65551487..5dc2930a2 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {compose} from 'react-apollo'; -import {toast} from 'react-toastify'; +import * as notification from 'coral-admin/src/services/notification'; import key from 'keymaster'; import isEqual from 'lodash/isEqual'; import styles from './components/styles.css'; @@ -97,6 +97,31 @@ class ModerationContainer extends Component { this.props.modQueueResort(sort); } + suspendUser = (args) => { + this.props.suspendUser(args) + .then((result) => { + if (result.data.suspendUser.errors) { + throw result.data.suspendUser.errors; + } + notification.success( + lang.t('suspenduser.notify_suspend_until', + this.props.moderation.suspendUserDialog.username, + lang.timeago(args.until)), + ); + const {commentStatus, commentId} = this.props.moderation.suspendUserDialog; + if (commentStatus !== 'REJECTED') { + return this.props.rejectComment({commentId}) + .then((result) => { + if (result.data.setCommentStatus.errors) { + throw result.data.setCommentStatus.errors; + } + }); + } + }) + .catch(notification.handleMutationErrors); + this.props.hideSuspendUserDialog(); + }; + componentWillUnmount() { key.unbind('s'); key.unbind('shift+/'); @@ -197,6 +222,7 @@ class ModerationContainer extends Component { assetId={providedAssetId} sort={this.state.sort} commentCount={activeTabCount} + currentUserId={this.props.auth.user.id} /> { - props.suspendUser(args) - .then(() => { - toast( - lang.t('suspenduser.notify_suspend_until', - moderation.suspendUserDialog.username, - lang.timeago(args.until)), - {type: 'success'} - ); - }) - .catch((err) => { - toast( - err, - {type: 'error'} - ); - }); - props.hideSuspendUserDialog(); - }} + onPerform={this.suspendUser} /> ({ moderation: state.moderation.toJS(), settings: state.settings.toJS(), + auth: state.auth.toJS(), assets: state.assets.get('assets') }); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index b93f185f0..f69e9c1b4 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -56,6 +56,7 @@ class ModerationQueue extends React.Component { acceptComment={props.acceptComment} rejectComment={props.rejectComment} currentAsset={props.currentAsset} + currentUserId={this.props.currentUserId} />; }) : {lang.t('modqueue.emptyqueue')} diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index 0b675702e..b76144d97 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -66,17 +66,19 @@ const Comment = ({ lang.getLocale().replace('-', '_') )} - - props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}> - Suspend User - props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}> - Ban User - - + {props.currentUserId !== comment.user.id && + + props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}> + Suspend User + props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}> + Ban User + + + } {comment.user.status === 'banned' @@ -163,6 +165,7 @@ Comment.propTypes = { currentAsset: PropTypes.object, showBanUserDialog: PropTypes.func.isRequired, showSuspendUserDialog: PropTypes.func.isRequired, + currentUserId: PropTypes.string.isRequired, comment: PropTypes.shape({ body: PropTypes.string.isRequired, action_summaries: PropTypes.array, diff --git a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js index 47c449d0e..f257bcfb9 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js @@ -10,7 +10,13 @@ import {dateAdd} from 'coral-framework/utils'; import translations from '../../../translations'; const lang = new I18n(translations); -const initialState = {step: 0, duration: '3', message: lang.t('suspenduser.email_message_suspend')}; +const initialState = {step: 0, duration: '3'}; + +function durationsToDate(hours) { + + // Add 1 minute more to help `timeago.js` to display the correct duration. + return dateAdd(new Date(), 'minute', hours * 60 + 1); +} class SuspendUserDialog extends React.Component { @@ -30,12 +36,16 @@ class SuspendUserDialog extends React.Component { this.setState({message: event.target.value}); } - increaseStep = () => { - this.setState({step: this.state.step + 1}); - } - - resetStep = () => { - this.setState({step: 0}); + goToStep1 = () => { + this.setState({ + step: 1, + message: lang.t( + 'suspenduser.email_message_suspend', + this.props.username, + this.props.organizationName, + lang.timeago(durationsToDate(this.state.duration)), + ), + }); } handlePerform = () => { @@ -45,7 +55,7 @@ class SuspendUserDialog extends React.Component { message: this.state.message, // Add 1 minute more to help `timeago.js` to display the correct duration. - until: dateAdd(new Date(), 'minute', this.state.duration * 60 + 1), + until: durationsToDate(this.state.duration), }); }; @@ -69,6 +79,7 @@ class SuspendUserDialog extends React.Component { onChange={this.handleDurationChange} className={styles.radioGroup} > + {lang.t('suspenduser.one_hour')} {lang.t('suspenduser.hours', 3)} {lang.t('suspenduser.hours', 24)} {lang.t('suspenduser.days', 7)} @@ -78,7 +89,7 @@ class SuspendUserDialog extends React.Component { - @@ -146,7 +157,9 @@ SuspendUserDialog.propTypes = { open: PropTypes.bool.isRequired, onCancel: PropTypes.func.isRequired, onPerform: PropTypes.func.isRequired, - username: PropTypes.string.isRequired, + username: PropTypes.string, + userId: PropTypes.string, + organizationName: PropTypes.string, }; export default SuspendUserDialog; diff --git a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql index 80dec5ff7..6bfcf81ec 100644 --- a/client/coral-admin/src/graphql/queries/modQueueQuery.graphql +++ b/client/coral-admin/src/graphql/queries/modQueueQuery.graphql @@ -62,4 +62,7 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) { asset_id: $asset_id, statuses: [NONE, PREMOD] }) + settings { + organizationName + } } diff --git a/client/coral-admin/src/services/notification.js b/client/coral-admin/src/services/notification.js new file mode 100644 index 000000000..6223d1b4d --- /dev/null +++ b/client/coral-admin/src/services/notification.js @@ -0,0 +1,28 @@ +import translations from 'coral-admin/src/translations'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import {toast} from 'react-toastify'; + +const lang = new I18n(translations); + +export function success(msg) { + return toast(msg, {type: 'success'}); +} + +export function error(msg) { + return toast(msg, {type: 'error'}); +} + +export function info(msg) { + return toast(msg, {type: 'info'}); +} + +export function handleMutationErrors(err) { + const errors = Array.isArray(err) ? err : [err]; + errors.forEach((err) => { + console.error(err); + toast( + err.translation_key ? lang.t(`errors.${err.translation_key}`) : err, + {type: 'error'} + ); + }); +} diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 4ff3bc0a6..023270f14 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -1,7 +1,7 @@ { "en": { "errors": { - "NOT_AUTHORIZED": "Your username or password is not recognized by our system.", + "NOT_AUTHORIZED": "You are not authorized to perform this action.", "LOGIN_MAXIMUM_EXCEEDED": "You have made too many unsuccessful password attempts. Please wait." }, "community": { @@ -155,8 +155,9 @@ "username": "username", "email_subject": "Your account has been suspended", "email_message_reject": "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please e-mail us if you have any questions or concerns.", - "email_message_suspend": "Your user account has been suspended. This means you can no longer comment, like, or flag content until you rewrite your username. Please e-mail us if you have any questions or concerns.", + "email_message_suspend": "Dear {0},\n\nIn accordance with {1}’s community guidelines, your account has been temporarily suspended. During the suspension, you will be unable to comment, flag or engage with fellow commenters. Please rejoin the conversation {2}.", "write_message": "Write a message", + "one_hour": "1 hour", "hours": "{0} hours", "days": "{0} days", "suspend_user": "Suspend User", @@ -240,6 +241,7 @@ "email_message_reject": "Otra persona de la comunidad recientemente marcó su nombre de usuario para ser revisado. Por su contenido, el nombre de usuario ha sido rechazado. Esto quiere decir que no puede comentar, gustar o marcar contenido hasta que modifique su nombre de usuario. Por favor, envienos un correo a moderator@newsorg.com si tiene alguna pregunta o preocupación", "write_message": "Escribir un mensaje", "loading": "Cargando resultados", + "hour": "una hora", "hours": "{0} horas", "days": "{0} días", "suspend_user": "Suspender", diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 6c84dd1ea..89723f0c0 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -10,9 +10,14 @@ import CommentBox from 'coral-plugin-commentbox/CommentBox'; import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import SuspendedAccount from './SuspendedAccount'; -import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox'; +import RestrictedMessageBox + from 'coral-framework/components/RestrictedMessageBox'; import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-framework/translations'; + +const lang = new I18n(translations); class Stream extends React.Component { setActiveReplyBox = (reactKey) => { @@ -75,9 +80,12 @@ class Stream extends React.Component { /> {!banned && temporarilySuspended && - In accordance with <organization name>'s Community Guidlines, - you have been temporarily suspended. You can rejoin the conversation - with our community in x hours. + { + lang.t('temporarilySuspended', + this.props.root.settings.organizationName, + lang.timeago(user.suspension.until), + ) + } } {banned && @@ -86,7 +94,7 @@ class Stream extends React.Component { editName={editName} /> } - {loggedIn && !banned && + {loggedIn && !banned && !temporarilySuspended && diff --git a/services/email/suspension.html.ejs b/services/email/suspension.html.ejs index b36560ec5..4fd8f191c 100644 --- a/services/email/suspension.html.ejs +++ b/services/email/suspension.html.ejs @@ -1 +1 @@ -<%= body %> +<%= body.replace(/\n/g, '
') %> diff --git a/services/users.js b/services/users.js index e828307f5..03b180150 100644 --- a/services/users.js +++ b/services/users.js @@ -456,7 +456,6 @@ module.exports = class UsersService { * @param {Date} until date until the suspension is valid. */ static suspendUser(id, message, until) { - console.log('SUSPEND'); return UserModel.findOneAndUpdate( {id}, { $set: { @@ -466,7 +465,6 @@ module.exports = class UsersService { } }) .then((user) => { - console.log(user); if (message) { let localProfile = user.profiles.find((profile) => profile.provider === 'local'); @@ -477,14 +475,13 @@ module.exports = class UsersService { locals: { // specifies the template locals. body: message }, - subject: 'Email Suspension', + subject: 'Your account has been suspended', to: localProfile.id // This only works if the user has registered via e-mail. // We may want a standard way to access a user's e-mail address in the future }; + console.log(options); return MailerService.sendSimple(options); - } else { - return Promise.reject(errors.ErrMissingEmail); } } });