diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index c11342c01..c5e6fc620 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -7,6 +7,12 @@ export const singleView = () => ({type: actions.SINGLE_VIEW}); export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote}); export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog}); +// Suspend User Dialog +export const showSuspendUserDialog = (userId, username, commentId, commentStatus) => + ({type: actions.SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus}); + +export const hideSuspendUserDialog = () => ({type: actions.HIDE_SUSPEND_USER_DIALOG}); + // hide shortcuts note export const hideShortcutsNote = () => { try { diff --git a/client/coral-admin/src/components/ActionsMenu.css b/client/coral-admin/src/components/ActionsMenu.css new file mode 100644 index 000000000..1d2f91f74 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenu.css @@ -0,0 +1,53 @@ +.button { + -webkit-transform: scale(.8); + transform: scale(.8); + margin: 0; +} + +.root { + color: black; + > :global(.mdl-menu__container) { + margin-left: 10px; + > :global(.mdl-menu__outline) { + box-shadow: none; + } + } +} + +.buttonOpen { + box-shadow: none; + color: white; + background-color: #616161; +} + +.arrowIcon { + margin-left: 6px; + margin-right: 0; + vertical-align: middle; + margin-right: 0; + font-size: 14px; +} + +.menu { + padding: 0; +} + +.menuItem { + background-color: #2a2a2a; + color: white; + &:first-child { + margin-bottom: 1px; + border-radius: 2px 2px 0px 0px; + } + &:last-child { + border-radius: 0px 0px 2px 2px; + } + &:hover, &:active, &:focus { + background-color: #767676; + } + &[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active { + background-color: #262626; + color: rgba(255, 255, 255, 0.5); + } +} + diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js new file mode 100644 index 000000000..eb173eb81 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenu.js @@ -0,0 +1,64 @@ +import React, {PropTypes} from 'react'; +import {Button, Icon} from 'coral-ui'; +import {Menu} from 'react-mdl'; +import cn from 'classnames'; +import {findDOMNode} from 'react-dom'; +import styles from './ActionsMenu.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; +const lang = new I18n(translations); + +let count = 0; + +class ActionsMenu extends React.Component { + id = `actions-dropdown-${count++}`; + menu = null; + state = {open: false}; + timeout = null; + + componentWillUnmount() { + clearTimeout(this.timeout); + } + + handleRef = (ref) => { + this.menu = ref ? findDOMNode(ref).parentNode : null; + } + + syncOpenState = () => { + clearTimeout(this.timeout); + this.timeout = setTimeout(() => { + this.setState({open: this.menu.className.indexOf('is-visible') >= 0}); + }, 150); + }; + + render() { + return ( +
+ + + {this.props.children} + +
+ ); + } +} + +ActionsMenu.propTypes = { + icon: PropTypes.string, +}; + +export default ActionsMenu; diff --git a/client/coral-admin/src/components/ActionsMenuItem.js b/client/coral-admin/src/components/ActionsMenuItem.js new file mode 100644 index 000000000..82dab96f0 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenuItem.js @@ -0,0 +1,9 @@ +import React from 'react'; +import cn from 'classnames'; +import {MenuItem} from 'react-mdl'; +import styles from './ActionsMenu.css'; + +const ActionsMenuItem = (props) => + ; + +export default ActionsMenuItem; diff --git a/client/coral-admin/src/components/App.js b/client/coral-admin/src/components/App.js index 3c6e88a14..1a15c72c3 100644 --- a/client/coral-admin/src/components/App.js +++ b/client/coral-admin/src/components/App.js @@ -1,16 +1,16 @@ import React from 'react'; -import {Provider} from 'react-redux'; +import ToastContainer from './ToastContainer'; import 'material-design-lite'; -import store from 'services/store'; import AppRouter from '../AppRouter'; export default class App extends React.Component { render () { return ( - - - +
+ + +
); } } diff --git a/client/coral-admin/src/components/ToastContainer.css b/client/coral-admin/src/components/ToastContainer.css new file mode 100644 index 000000000..319872ceb --- /dev/null +++ b/client/coral-admin/src/components/ToastContainer.css @@ -0,0 +1,226 @@ +@keyframes :global(bounceInRight) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + from { + opacity: 0; + transform: translate3d(3000px, 0, 0); } + 60% { + opacity: 1; + transform: translate3d(-25px, 0, 0); } + 75% { + transform: translate3d(10px, 0, 0); } + 90% { + transform: translate3d(-5px, 0, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutRight) { + 20% { + opacity: 1; + transform: translate3d(-20px, 0, 0); } + to { + opacity: 0; + transform: translate3d(2000px, 0, 0); } } + +@keyframes :global(bounceInLeft) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + 0% { + opacity: 0; + transform: translate3d(-3000px, 0, 0); } + 60% { + opacity: 1; + transform: translate3d(25px, 0, 0); } + 75% { + transform: translate3d(-10px, 0, 0); } + 90% { + transform: translate3d(5px, 0, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutLeft) { + 20% { + opacity: 1; + transform: translate3d(20px, 0, 0); } + to { + opacity: 0; + transform: translate3d(-2000px, 0, 0); } } + +@keyframes :global(bounceInUp) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + from { + opacity: 0; + transform: translate3d(0, 3000px, 0); } + 60% { + opacity: 1; + transform: translate3d(0, -20px, 0); } + 75% { + transform: translate3d(0, 10px, 0); } + 90% { + transform: translate3d(0, -5px, 0); } + to { + transform: translate3d(0, 0, 0); } } + +@keyframes :global(bounceOutUp) { + 20% { + transform: translate3d(0, -10px, 0); } + 40%, 45% { + opacity: 1; + transform: translate3d(0, 20px, 0); } + to { + opacity: 0; + transform: translate3d(0, -2000px, 0); } } + +@keyframes :global(bounceInDown) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + 0% { + opacity: 0; + transform: translate3d(0, -3000px, 0); } + 60% { + opacity: 1; + transform: translate3d(0, 25px, 0); } + 75% { + transform: translate3d(0, -10px, 0); } + 90% { + transform: translate3d(0, 5px, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutDown) { + 20% { + transform: translate3d(0, 10px, 0); } + 40%, 45% { + opacity: 1; + transform: translate3d(0, -20px, 0); } + to { + opacity: 0; + transform: translate3d(0, 2000px, 0); } } + +@keyframes :global(track-progress) { + 0% { + width: 100%; } + 100% { + width: 0; } } + +:global { + .bounceOutRight, .toast-exit--top-right, .toast-exit--bottom-right { + animation-name: bounceOutRight; } + + .bounceInRight, .toast-enter--top-right, .toast-enter--bottom-right { + animation-name: bounceInRight; } + + .bounceInLeft, .toast-enter--top-left, .toast-enter--bottom-left { + animation-name: bounceInLeft; } + + .bounceOutLeft, .toast-exit--top-left, .toast-exit--bottom-left { + animation-name: bounceOutLeft; } + + .bounceInUp, .toast-enter--bottom-center { + animation-name: bounceInUp; } + .bounceOutUp, .toast-exit--top-center { + animation-name: bounceOutUp; } + + .bounceInDown, .toast-enter--top-center { + animation-name: bounceInDown; } + + .bounceOutDown, .toast-exit--bottom-center { + animation-name: bounceOutDown; } + + .animated { + animation-duration: 0.75s; + animation-fill-mode: both; } + + .toastify { + z-index: 999; + position: fixed; + padding: 4px; + width: 350px; + max-width: 98%; + color: #999; + box-sizing: border-box; } + .toastify--top-left { + top: 1em; + left: 1em; } + .toastify--top-center { + top: 1em; + left: 50%; + margin-left: -175px; } + .toastify--top-right { + top: 1em; + right: 2em; } + .toastify--bottom-left { + bottom: 1em; + left: 1em; } + .toastify--bottom-center { + bottom: 1em; + left: 50%; + margin-left: -175px; } + .toastify--bottom-right { + bottom: 1em; + right: 2em; } + .toastify__img { + float: left; + margin-right: 8px; + vertical-align: middle; } + + .toastify__close { + position: absolute; + top: 18px; + left: 12px; + width: 20px; + height: 16px; + padding: 0; + text-align: center; + text-decoration: none; + color: white; + font-weight: bold; + font-size: 14px; + background: transparent; + outline: none; + border: none; + cursor: pointer; + opacity: 0.8; + transition: .3s ease; } + .toastify__close:hover, .toastify__close:focus { + opacity: 1; + } + + .toastify-content { + position: relative; + width: 100%; + margin-bottom: 12px; + padding: 18px 24px 20px 48px; + box-sizing: border-box; + background: #404040; + border-radius: 2px; + box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1), 0 3px 20px 0 rgba(0, 0, 0, 0.05); } + .toastify-content--info { + background: #2488cb; } + .toastify-content--success { + background: #008577; } + .toastify-content--warning { + background: #ef6c2b; } + .toastify-content--error { + background: #ef342b; } + + .toastify__body { + color: white; + font-size: 15px; + font-weight: 400; + } + + .toastify__progress { + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 4px; + z-index: 999; + opacity: 0.8; + border-radius: 2px; + animation: track-progress linear 1; + background-color: white; + } +} diff --git a/client/coral-admin/src/components/ToastContainer.js b/client/coral-admin/src/components/ToastContainer.js new file mode 100644 index 000000000..a751d2714 --- /dev/null +++ b/client/coral-admin/src/components/ToastContainer.js @@ -0,0 +1,7 @@ +import './ToastContainer.css'; +import {defaultProps} from 'recompose'; +import {ToastContainer} from 'react-toastify'; + +export default defaultProps({ + autoClose: 5000, +})(ToastContainer); diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index 1d09b0e1a..b960d95a2 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -3,5 +3,7 @@ export const SINGLE_VIEW = 'SINGLE_VIEW'; export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE'; +export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG'; +export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG'; export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL'; export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL'; diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js index 899958e17..7c0051477 100644 --- a/client/coral-admin/src/containers/Community/CommunityContainer.js +++ b/client/coral-admin/src/containers/Community/CommunityContainer.js @@ -3,7 +3,7 @@ import {connect} from 'react-redux'; import {compose} from 'react-apollo'; import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries'; -import {banUser, setUserStatus, suspendUser} from 'coral-admin/src/graphql/mutations'; +import {banUser, setUserStatus, rejectUsername} from 'coral-admin/src/graphql/mutations'; import { fetchAccounts, @@ -113,7 +113,7 @@ class CommunityContainer extends Component { error={data.error} showBanUserDialog={props.showBanUserDialog} approveUser={props.approveUser} - suspendUser={props.suspendUser} + rejectUsername={props.rejectUsername} showSuspendUserDialog={props.showSuspendUserDialog} /> ); @@ -165,5 +165,5 @@ export default compose( modUserFlaggedQuery, banUser, setUserStatus, - suspendUser + rejectUsername )(CommunityContainer); diff --git a/client/coral-admin/src/containers/Community/components/ActionButton.js b/client/coral-admin/src/containers/Community/components/ActionButton.js index 6352480fd..ad5346ac5 100644 --- a/client/coral-admin/src/containers/Community/components/ActionButton.js +++ b/client/coral-admin/src/containers/Community/components/ActionButton.js @@ -1,6 +1,6 @@ import React from 'react'; import styles from '../Community.css'; -import BanUserButton from '../../../components/BanUserButton'; +import BanUserButton from './BanUserButton'; import {Button} from 'coral-ui'; import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap'; diff --git a/client/coral-admin/src/components/BanUserButton.css b/client/coral-admin/src/containers/Community/components/BanUserButton.css similarity index 100% rename from client/coral-admin/src/components/BanUserButton.css rename to client/coral-admin/src/containers/Community/components/BanUserButton.css diff --git a/client/coral-admin/src/components/BanUserButton.js b/client/coral-admin/src/containers/Community/components/BanUserButton.js similarity index 100% rename from client/coral-admin/src/components/BanUserButton.js rename to client/coral-admin/src/containers/Community/components/BanUserButton.js diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js index 20e221c48..b717d1719 100644 --- a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js @@ -10,16 +10,16 @@ const lang = new I18n(translations); const stages = [ { - title: 'suspenduser.title_0', - description: 'suspenduser.description_0', + title: 'suspenduser.title_reject', + description: 'suspenduser.description_reject', options: { 'j': 'suspenduser.no_cancel', 'k': 'suspenduser.yes_suspend' } }, { - title: 'suspenduser.title_1', - description: 'suspenduser.description_1', + title: 'suspenduser.title_notify', + description: 'suspenduser.description_notify', options: { 'j': 'bandialog.cancel', 'k': 'suspenduser.send' @@ -34,11 +34,11 @@ class SuspendUserDialog extends Component { static propTypes = { stage: PropTypes.number, handleClose: PropTypes.func.isRequired, - suspendUser: PropTypes.func.isRequired + rejectUsername: PropTypes.func.isRequired } componentDidMount() { - this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')}); + this.setState({email: lang.t('suspenduser.email_message_reject'), about: lang.t('suspenduser.username')}); } /* @@ -46,13 +46,13 @@ class SuspendUserDialog extends Component { * handles the possible actions for that dialog. */ onActionClick = (stage, menuOption) => () => { - const {suspendUser, user} = this.props; + const {rejectUsername, user} = this.props; const {stage} = this.state; const cancel = this.props.handleClose; const next = () => this.setState({stage: stage + 1}); const suspend = () => { - suspendUser({userId: user.user.id, message: this.state.email}) + rejectUsername({id: user.user.id, message: this.state.email}) .then(() => { this.props.handleClose(); }); @@ -79,7 +79,7 @@ class SuspendUserDialog extends Component { open={open} onClose={handleClose} onCancel={handleClose} - title={lang.t('suspenduser.title')}> + title={lang.t('suspenduser.suspend_user')}>
{lang.t(stages[stage].title, lang.t('suspenduser.username'))}
diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index e6062674d..237059103 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -1,12 +1,16 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import {compose} from 'react-apollo'; +import * as notification from 'coral-admin/src/services/notification'; import key from 'keymaster'; import isEqual from 'lodash/isEqual'; import styles from './components/styles.css'; +import translations from 'coral-admin/src/translations'; +import I18n from 'coral-framework/modules/i18n/i18n'; import {modQueueQuery, getQueueCounts} from '../../graphql/queries'; -import {banUser, setCommentStatus} from '../../graphql/mutations'; +import {banUser, setCommentStatus, suspendUser} from '../../graphql/mutations'; import {fetchSettings} from 'actions/settings'; import {updateAssets} from 'actions/assets'; @@ -15,13 +19,16 @@ import { singleView, showBanUserDialog, hideBanUserDialog, + showSuspendUserDialog, + hideSuspendUserDialog, hideShortcutsNote, viewUserDetail, hideUserDetail } from 'actions/moderation'; import {Spinner} from 'coral-ui'; -import BanUserDialog from '../../components/BanUserDialog'; +import BanUserDialog from './components/BanUserDialog'; +import SuspendUserDialog from './components/SuspendUserDialog'; import ModerationQueue from './ModerationQueue'; import ModerationMenu from './components/ModerationMenu'; import ModerationHeader from './components/ModerationHeader'; @@ -29,6 +36,8 @@ import NotFoundAsset from './components/NotFoundAsset'; import ModerationKeysModal from '../../components/ModerationKeysModal'; import UserDetail from './UserDetail'; +const lang = new I18n(translations); + class ModerationContainer extends Component { state = { selectedIndex: 0, @@ -91,6 +100,33 @@ class ModerationContainer extends Component { this.props.modQueueResort(sort); } + suspendUser = async (args) => { + this.props.hideSuspendUserDialog(); + try { + const result = await this.props.suspendUser(args); + 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(err) { + notification.showMutationErrors(err); + } + }; + componentWillUnmount() { key.unbind('s'); key.unbind('shift+/'); @@ -184,12 +220,14 @@ class ModerationContainer extends Component { bannedWords={settings.wordlist.banned} suspectWords={settings.wordlist.suspect} showBanUserDialog={props.showBanUserDialog} + showSuspendUserDialog={props.showSuspendUserDialog} acceptComment={props.acceptComment} rejectComment={props.rejectComment} loadMore={props.loadMore} assetId={providedAssetId} sort={this.state.sort} commentCount={activeTabCount} + currentUserId={this.props.auth.user.id} viewUserDetail={viewUserDetail} hideUserDetail={hideUserDetail} /> @@ -203,6 +241,14 @@ class ModerationContainer extends Component { showRejectedNote={moderation.showRejectedNote} rejectComment={props.rejectComment} /> + ({ moderation: state.moderation.toJS(), settings: state.settings.toJS(), + auth: state.auth.toJS(), assets: state.assets.get('assets') }); const mapDispatchToProps = (dispatch) => ({ - toggleModal: (toggle) => dispatch(toggleModal(toggle)), onClose: () => dispatch(toggleModal(false)), - singleView: () => dispatch(singleView()), - updateAssets: (assets) => dispatch(updateAssets(assets)), - fetchSettings: () => dispatch(fetchSettings()), - viewUserDetail: (id) => dispatch(viewUserDetail(id)), - hideUserDetail: () => dispatch(hideUserDetail()), - showBanUserDialog: (user, commentId, commentStatus, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, commentStatus, showRejectedNote)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), - hideShortcutsNote: () => dispatch(hideShortcutsNote()), + ...bindActionCreators({ + toggleModal, + singleView, + updateAssets, + fetchSettings, + showBanUserDialog, + hideShortcutsNote, + showSuspendUserDialog, + hideSuspendUserDialog, + viewUserDetail, + hideUserDetail, + }, dispatch), }); export default compose( connect(mapStateToProps, mapDispatchToProps), setCommentStatus, getQueueCounts, + banUser, + suspendUser, modQueueQuery, - banUser )(ModerationContainer); diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index f6291c071..043173549 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -17,6 +17,7 @@ class ModerationQueue extends React.Component { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, currentAsset: PropTypes.object, showBanUserDialog: PropTypes.func.isRequired, + showSuspendUserDialog: PropTypes.func.isRequired, rejectComment: PropTypes.func.isRequired, acceptComment: PropTypes.func.isRequired, comments: PropTypes.array.isRequired @@ -63,9 +64,11 @@ class ModerationQueue extends React.Component { viewUserDetail={viewUserDetail} actions={actionsMap[status]} showBanUserDialog={props.showBanUserDialog} + showSuspendUserDialog={props.showSuspendUserDialog} acceptComment={props.acceptComment} rejectComment={props.rejectComment} currentAsset={props.currentAsset} + currentUserId={this.props.currentUserId} />; }) : {lang.t('modqueue.emptyqueue')} diff --git a/client/coral-admin/src/components/BanUserDialog.css b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css similarity index 97% rename from client/coral-admin/src/components/BanUserDialog.css rename to client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css index a46b9da32..f13f0e6aa 100644 --- a/client/coral-admin/src/components/BanUserDialog.css +++ b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css @@ -152,13 +152,14 @@ input.error{ .cancel { margin-right: 10px; - width: 47%; + width: 48%; } .ban { - width: 47%; + width: 48%; } .buttons { - margin: 20px 0; + margin: 20px; + text-align: center; } diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js similarity index 97% rename from client/coral-admin/src/components/BanUserDialog.js rename to client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js index b259012f3..87a5fe55a 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js @@ -5,7 +5,7 @@ import styles from './BanUserDialog.css'; import Button from 'coral-ui/components/Button'; import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations'; +import translations from '../../../translations'; const lang = new I18n(translations); const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => { diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index 2909f7644..6246dd5b9 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -11,7 +11,8 @@ import Highlighter from 'react-highlight-words'; import Slot from 'coral-framework/components/Slot'; import {getActionSummary} from 'coral-framework/utils'; import ActionButton from 'coral-admin/src/components/ActionButton'; -import BanUserButton from 'coral-admin/src/components/BanUserButton'; +import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; +import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; const linkify = new Linkify(); @@ -66,16 +67,19 @@ const Comment = ({ lang.getLocale().replace('-', '_') )} - - props.showBanUserDialog( - comment.user, - comment.id, - comment.status, - comment.status !== 'REJECTED' - )} - /> + {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' @@ -161,6 +165,9 @@ Comment.propTypes = { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired, 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.css b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css new file mode 100644 index 000000000..1c96f509a --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css @@ -0,0 +1,90 @@ +.dialog { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 400px; + top: 50%; + transform: translateY(-50%); + padding: 20px; + border-radius: 4px; +} + +.header { + color: black; + font-size: 1.5em; + font-weight: 500; + margin: 0 0 8px 0; +} + +.close { + display: block; + position: absolute; + top: 24px; + right: 20px; +} + +.closeButton { + userSelect: none; + outline: none; + border: none; + touchAction: manipulation; + &::-moz-focus-inner: { + border: 0; + } + background: 0; + padding: 0; + font-size: 24px; + line-height: 14px; + cursor: pointer; + color: #363636; + &:hover { + color: #6b6b6b; + } +} + +.legend { + padding: 0; + font-weight: bold; +} + +div.radioGroup { + margin-top: 6px; +} + +label.radioGroup { + + &:global(.is-checked) > :global(.mdl-radio__outer-circle), + > :global(.mdl-radio__outer-circle) { + border-color: #212121; + } + + > :global(.mdl-radio__inner-circle) { + background: #212121; + } + + > :global(.mdl-radio__label) { + font-size: 14px; + line-height: 20px; + } +} + +.messageInput { + border-radius: 3px; + width: 100%; + padding: 10px; + font-size: 14px; + box-sizing: border-box; +} + +.cancel { + margin-right: 5px; +} + +.perform { + min-width: 90px; +} + +.buttons { + margin-top: 8px; + margin-bottom: 6px; + text-align: right; +} diff --git a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js new file mode 100644 index 000000000..f257bcfb9 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js @@ -0,0 +1,165 @@ +import React, {PropTypes} from 'react'; +import {Dialog} from 'coral-ui'; +import {RadioGroup, Radio} from 'react-mdl'; +import styles from './SuspendUserDialog.css'; + +import Button from 'coral-ui/components/Button'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import {dateAdd} from 'coral-framework/utils'; +import translations from '../../../translations'; +const lang = new I18n(translations); + +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 { + + state = initialState; + + componentWillReceiveProps(next) { + if (this.props.open && !next.open) { + this.setState(initialState); + } + } + + handleDurationChange = (event) => { + this.setState({duration: event.target.value}); + } + + handleMessageChange = (event) => { + this.setState({message: event.target.value}); + } + + 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 = () => { + + this.props.onPerform({ + id: this.props.userId, + message: this.state.message, + + // Add 1 minute more to help `timeago.js` to display the correct duration. + until: durationsToDate(this.state.duration), + }); + }; + + renderStep0() { + const {onCancel, username} = this.props; + const {duration} = this.state; + return ( +
+

+ {lang.t('suspenduser.title_suspend')} +

+

+ {lang.t('suspenduser.description_suspend', username)} +

+
+ {lang.t('suspenduser.select_duration')} + + {lang.t('suspenduser.one_hour')} + {lang.t('suspenduser.hours', 3)} + {lang.t('suspenduser.hours', 24)} + {lang.t('suspenduser.days', 7)} + +
+
+ + +
+
+ ); + } + + renderStep1() { + const {onCancel, username} = this.props; + const {message} = this.state; + return ( +
+

+ {lang.t('suspenduser.title_notify')} +

+

+ {lang.t('suspenduser.description_notify', username)} +

+
+ {lang.t('suspenduser.write_message')} +