diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js index 4f06806aa..b8b403761 100644 --- a/client/coral-admin/src/actions/users.js +++ b/client/coral-admin/src/actions/users.js @@ -14,3 +14,12 @@ export const userStatusUpdate = (status, userId, commentId) => { .catch(error => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error})); }; }; + +// change status of a user +export const sendNotificationEmail = (userId, subject, text) => { + return (dispatch, getState) => { + const _csrf = getState().auth.get('_csrf'); + return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, text}, _csrf}) + .catch(error => dispatch({type: userTypes.USER_EMAIL_FAILURE, error})); + }; +}; diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 977407849..10d4aa1cc 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -24,7 +24,8 @@ export default class ModerationList extends React.Component { comments: PropTypes.object, users: PropTypes.object.isRequired, actions: PropTypes.object, - updateCommentStatus: PropTypes.func.isRequired, + userStatusUpdate: PropTypes.func.isRequired, + suspendUser: PropTypes.func.isRequired, // list of actions (flags, etc) associated with the comments modActions: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -135,32 +136,13 @@ export default class ModerationList extends React.Component { // If a user bio or name is rejected, bring up a dialog before suspending them. if (menuOption === 'rejected') { - this.setState({suspendUserModal: {stage: 0, actionType: action.action_type}}); + this.setState({suspendUserModal: action}); } else { - this.props.updateUserStatus(menuOption, action.item_id); + this.props.userStatusUpdate(menuOption, action.item_id); } } } - /* - * When an admin clicks to suspend a user a dialog is shown, this function - * handles the possible actions for that dialog. - */ - onClickSuspendModalButton = (stage, menuOption) => () => { - const {updateUserStatus, action} = this.props; - const cancel = () => this.setState({suspendUserModal: null}); - const next = () => this.setState((prev) => { - prev.suspendUserModal.stage++; - return prev; - }); - const suspend = () => updateUserStatus('suspend', action.item_id); - const suspendModalActions = [ - [ cancel, next ], - [ cancel, suspend ] - ]; - return suspendModalActions[stage][menuOption](); - } - onClickShowBanDialog = (userId, userName, commentId) => { this.props.onClickShowBanDialog(userId, userName, commentId); } @@ -221,7 +203,7 @@ export default class ModerationList extends React.Component { } render () { - const {singleView, key} = this.props; + const {singleView, key, suspendUser} = this.props; // Combine moderations and actions into a single stream and sort by most recently updated. const moderationIds = this.getModerationIds(); @@ -232,9 +214,9 @@ export default class ModerationList extends React.Component { id='moderationList'> {moderationIds.map(this.mapModItems)} this.setState({suspendUserModal:null})} - onButtonClick={this.onClickSuspendModalButton} /> + suspendUser={suspendUser} /> ); } diff --git a/client/coral-admin/src/components/SuspendUserModal.js b/client/coral-admin/src/components/SuspendUserModal.js index f8491d615..5302a33a9 100644 --- a/client/coral-admin/src/components/SuspendUserModal.js +++ b/client/coral-admin/src/components/SuspendUserModal.js @@ -26,13 +26,13 @@ const stages = [ class SuspendUserModal extends Component { - state = {email: ''} + state = {email: '', stage: 0} static propTypes = { stage: PropTypes.number, actionType: PropTypes.string, onClose: PropTypes.func.isRequired, - onButtonClick: PropTypes.func.isRequired + suspendUser: PropTypes.func.isRequired } componentDidMount() { @@ -40,12 +40,36 @@ class SuspendUserModal extends Component { this.setState({email: lang.t('suspenduser.email', about)}); } + /* + * When an admin clicks to suspend a user a dialog is shown, this function + * handles the possible actions for that dialog. + */ + onActionClick = (stage, menuOption) => () => { + const {suspendUser, action} = this.props; + const {stage, email} = this.state; + const cancel = this.props.onClose; + const next = () => this.setState({stage: stage + 1}); + const suspend = () => suspendUser(action.item_id, lang.t('suspenduser.email_subject'), email); + const suspendModalActions = [ + [ cancel, next ], + [ cancel, suspend ] + ]; + return suspendModalActions[stage][menuOption](); + } + onEmailChange = (e) => this.setState({email: e.target.value}) render () { - const {stage, actionType, onClose, onButtonClick} = this.props; + const {action, onClose} = this.props; + + if (!action) { + return null; + } + + const {stage} = this.state; + const actionType = action.actionType; const about = actionType === 'flag_bio' ? lang.t('suspenduser.bio') : lang.t('suspenduser.username'); - return actionType ? + return
{lang.t(stages[stage].title, about)}
@@ -66,14 +90,13 @@ class SuspendUserModal extends Component { }
{Object.keys(stages[stage].options).map((key, i) => ( - ))}
- - : null; + ; } } diff --git a/client/coral-admin/src/constants/users.js b/client/coral-admin/src/constants/users.js index 813b5794d..3cd5e5b4b 100644 --- a/client/coral-admin/src/constants/users.js +++ b/client/coral-admin/src/constants/users.js @@ -1,4 +1,5 @@ export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST'; export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS'; export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE'; +export const USER_EMAIL_FAILURE = 'USER_EMAIL_FAILURE'; export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index 7e0c68a03..b3b134460 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -8,7 +8,7 @@ import { hideBanUserDialog, fetchModerationQueueComments } from 'actions/comments'; -import {userStatusUpdate} from 'actions/users'; +import {userStatusUpdate, sendNotificationEmail} from 'actions/users'; import {fetchSettings} from 'actions/settings'; import ModerationQueue from './ModerationQueue'; @@ -62,7 +62,7 @@ class ModerationContainer extends React.Component { render () { const {comments, actions} = this.props; - + const premodIds = comments.ids.filter(id => comments.byId[id].status === 'premod'); const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'rejected'); const flaggedIds = comments.ids.filter(id => comments.byId[id].flagged === true); @@ -99,6 +99,11 @@ const mapDispatchToProps = dispatch => { banUser: (userId, commentId) => dispatch(userStatusUpdate('banned', userId, commentId)).then(() => { dispatch(fetchModerationQueueComments()); }), + userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)), + suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('suspended', userId)) + .then(() => dispatch(sendNotificationEmail(userId, subject, text))) + .then(() => dispatch(fetchModerationQueueComments())) + , updateStatus: (action, comment) => dispatch(updateStatus(action, comment)) }; }; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 0aeb6f79b..16f7223c3 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -31,6 +31,8 @@ export default (props) => ( users={props.users.byId} actionIds={props.userActionIds} actions={props.actions.byId} + userStatusUpdate={props.userStatusUpdate} + suspendUser={props.suspendUser} updateCommentStatus={props.updateStatus} onClickShowBanDialog={props.showBanUserDialog} modActions={['reject', 'approve', 'ban']} @@ -48,6 +50,8 @@ export default (props) => ( isActive={props.activeTab === 'rejected'} singleView={props.singleView} commentIds={props.rejectedIds} + userStatusUpdate={props.userStatusUpdate} + suspendUser={props.suspendUser} comments={props.comments.byId} users={props.users.byId} updateCommentStatus={props.updateStatus} @@ -61,6 +65,8 @@ export default (props) => ( isActive={props.activeTab === 'rejected'} singleView={props.singleView} commentIds={props.flaggedIds} + userStatusUpdate={props.userStatusUpdate} + suspendUser={props.suspendUser} comments={props.comments.byId} users={props.users.byId} updateCommentStatus={props.updateStatus} diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 506c9bc42..870b84582 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -94,6 +94,7 @@ "send": "Send", "bio": "bio", "username": "username", + "subject": "Your account has been suspended", "email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.", "write_message": "Write a message" },