diff --git a/client/coral-admin/src/components/ActionButton.css b/client/coral-admin/src/components/ActionButton.css new file mode 100644 index 000000000..226006c6b --- /dev/null +++ b/client/coral-admin/src/components/ActionButton.css @@ -0,0 +1,24 @@ +.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; +} + diff --git a/client/coral-admin/src/components/ActionButton.js b/client/coral-admin/src/components/ActionButton.js index 79fa2f182..77456b939 100644 --- a/client/coral-admin/src/components/ActionButton.js +++ b/client/coral-admin/src/components/ActionButton.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styles from './ModerationList.css'; +import styles from './ActionButton.css'; import {Button} from 'coral-ui'; import {menuActionsMap} from '../utils/moderationQueueActionsMap'; diff --git a/client/coral-admin/src/components/ModerationList.css b/client/coral-admin/src/components/ModerationList.css deleted file mode 100644 index 4ac894290..000000000 --- a/client/coral-admin/src/components/ModerationList.css +++ /dev/null @@ -1,208 +0,0 @@ - -@custom-media --big-viewport (min-width: 780px); - -.list { - padding: 8px 0; - list-style: none; - display: block; - - &.singleView .listItem { - display: none; - } - - &.singleView .listItem.activeItem { - display: block; - height: 100%; - font-size: 1.5em; - line-height: 1.5em; - border: none; - - .actions { - position: fixed; - bottom: 60px; - left: 25%; - margin: 0 auto; - display: flex; - justify-content: space-around; - width: 50%; - margin: 0; - } - - .actionButton { - transform: scale(1.4); - } - } -} - -.listItem { - border-bottom: 1px solid #e0e0e0; - font-size: 16px; - width: 100%; - max-width: 660px; - min-width: 400px; - margin: 0 auto; - padding: 16px 14px; - position: relative; - transition: box-shadow 200ms; - - - &:hover { - box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); - } - - &:last-child { - border-bottom: none; - } - - .sideActions { - position: absolute; - right: 0; - height: 100%; - top: 0; - padding: 40px 18px; - box-sizing: border-box; - } - - .itemHeader { - display: flex; - align-items: center; - justify-content: space-between; - - .author { - min-width: 230px; - display: flex; - align-items: center; - } - } - - .itemBody { - display: flex; - justify-content: space-between; - } - - .avatar { - margin-right: 16px; - height: 40px; - width: 40px; - border-radius: 50%; - background-color: #757575; - font-size: 40px; - color: #fff; - } - - .created { - color: #666; - font-size: 13px; - margin-left: 40px; - } - - .body { - margin-top: 20px; - flex: 1; - font-size: 0.88em; - color: black; - } - - .flagged { - color: rgba(255, 0, 0, .5); - padding-top: 15px; - padding-left: 10px; - } - - .flagCount{ - font-size: 12px; - color: #d32f2f; - } - -} - -.empty { - color: #444; - margin-top: 50px; - text-align: center; -} - - -@media (--big-viewport) { - .listItem { - border: 1px solid #e0e0e0; - margin-bottom: 30px; - - &:last-child { - border-bottom: 1px solid #e0e0e0; - } - - &.activeItem { - border: 2px solid #333; - } - } - -} - -.hasLinks { - color: #f00; - text-align: right; - display: flex; - align-items: center; - - i { - margin-right: 5px; - } -} - -.banned { - color: #f00; - text-align: left; - display: flex; - align-items: center; - - i { - margin-right: 5px; - } -} - -.ban { - display: block; - text-align: center; - margin-top: 5px; -} - -.banButton { - width: 114px; - letter-spacing: 1px; - - i { - vertical-align: middle; - margin-right: 10px; - font-size: 14px; - } -} - -.selected { - border-radius: 10px; -} - - -.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; -} diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js deleted file mode 100644 index adfe63051..000000000 --- a/client/coral-admin/src/components/ModerationList.js +++ /dev/null @@ -1,228 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './ModerationList.css'; -import key from 'keymaster'; -import Hammer from 'hammerjs'; -import Comment from './Comment'; -import User from './User'; -import SuspendUserModal from './SuspendUserModal'; - -// Each action has different meaning and configuration -const menuOptionsMap = { - 'reject': {status: 'REJECTED', icon: 'close', key: 'f'}, - 'approve': {status: 'ACCEPTED', icon: 'done', key: 'd'}, - 'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'}, - 'ban': {status: 'BANNED', icon: 'not interested'} -}; - -// Renders a comment list and allow performing actions -export default class ModerationList extends React.Component { - static propTypes = { - isActive: PropTypes.bool, - singleView: PropTypes.bool, - commentIds: PropTypes.arrayOf(PropTypes.string), - actionIds: PropTypes.arrayOf(PropTypes.string), - comments: PropTypes.object, - users: PropTypes.object.isRequired, - actions: PropTypes.object, - userStatusUpdate: PropTypes.func.isRequired, - suspendUser: PropTypes.func.isRequired, - - // list of actions (flags, etc) associated with the comments - modActions: PropTypes.arrayOf(PropTypes.string).isRequired, - loading: PropTypes.bool, - - suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired - } - - state = {active: null, suspendUserModal: null, email: null}; - - // remove key handlers before leaving - componentWillUnmount () { - this.unbindKeyHandlers(); - } - - // add key handlers and gestures - componentDidMount () { - this.bindKeyHandlers(); - - // this.bindGestures() // need to check whether we're on a mobile device or this throws an Error - } - - // If entering to singleview and no active, active is the first eleement - componentWillReceiveProps (nextProps) { - if (nextProps.singleView && !this.state.active) { - this.setState({active: nextProps.commentIds[0]}); - } - } - - // Add swipe to approve or reject - bindGestures () { - const {modActions} = this.props; - this._hammer = new Hammer(this.base); - this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL}); - - if (modActions.indexOf('reject') !== -1) { - this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected')); - } - if (modActions.indexOf('approve') !== -1) { - this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved')); - } - } - - // Add key handlers. Each action has one and added j/k for moving around - bindKeyHandlers () { - const {modActions, isActive} = this.props; - modActions.filter((action) => menuOptionsMap[action].key).forEach((action) => { - key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status)); - }); - key('j', 'moderationList', () => isActive && this.moveKeyHandler('down')); - key('k', 'moderationList', () => isActive && this.moveKeyHandler('up')); - key.setScope('moderationList'); - } - - // Perform an action using the keys only if the comment is active - actionKeyHandler (action) { - if (this.props.isActive && this.state.active) { - this.onClickAction(action, this.state.active); - } - } - - // move around with j/k - moveKeyHandler (direction) { - if (!this.props.isActive) { - return; - } - - const {commentIds} = this.props; - const {active} = this.state; - - // check boundaries - if (active === null || !commentIds.length) { - this.setState({active: commentIds[0]}); - } else if (direction === 'up' && active !== commentIds[0]) { - this.setState({active: commentIds[commentIds.indexOf(active) - 1]}); - } else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) { - this.setState({active: commentIds[commentIds.indexOf(active) + 1]}); - } - - // scroll to the position - const index = Math.max(commentIds.indexOf(this.state.active), 0); - this.base.childNodes[index] && this.base.childNodes[index].focus(); - } - - unbindKeyHandlers () { - key.deleteScope('moderationList'); - } - - // If we are performing an action over a comment (aka removing from the list) we need to select a new active. - // TODO: In the future this can be improved and look at the actual state to - // resolve since the content of the list could change externally. For now it works as expected - onClickAction = (menuOption, id, action) => { - - // activate the next comment - if (id === this.state.active) { - const moderationIds = this.getModerationIds(); - if (moderationIds[moderationIds.length - 1] === this.state.active) { - this.setState({active: moderationIds[moderationIds.length - 2]}); - } else { - this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]}); - } - } - - // Update the status right away if this is a comment - if (action.item_type === 'COMMENTS') { - this.props.updateCommentStatus(menuOption, id); - } else if (action.item_type === 'USERS') { - - // If a user bio or name is rejected, bring up a dialog before suspending them. - if (menuOption === 'REJECTED') { - this.setState({suspendUserModal: action}); - } else if (menuOption === 'ACCEPTED') { - this.props.userStatusUpdate('APPROVED', action.item_id); - } - } - } - - onClickShowBanDialog = (userId, userName, commentId) => { - this.props.onClickShowBanDialog(userId, userName, commentId); - } - - mapModItems = (itemId, index) => { - - const {comments = {}, users, actions = {}, modActions, suspectWords, hideActive} = this.props; - const {active} = this.state; - - // Because ids are unique, the id will either appear as an action or as a comment. - - const item = comments[itemId] || actions[itemId]; - let modItem; - - if (item.body) { - - // If the item is a comment... - const author = users[item.author_id]; - modItem = ; - } else { - - // If the item is an action... - const user = users[item.item_id]; - modItem = user && ; - } - return modItem; - } - - getModerationIds = () => { - const {commentIds = [], actionIds = [], comments, actions} = this.props; - if (comments && actions) { - return [ ...commentIds, ...actionIds ].sort((a, b) => { - const itemA = comments[a] || actions[a]; - const itemB = comments[b] || actions[b]; - return itemB.updated_at - itemA.updated_at; - }); - } else { - return comments ? commentIds : actionIds; - } - } - - render () { - const {singleView, key, suspendUser} = this.props; - - // Combine moderations and actions into a single stream and sort by most recently updated. - const moderationIds = this.getModerationIds(); - - return ( - - ); - } -} diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css index f3866f55a..6f1d4ae95 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.css @@ -1,11 +1,13 @@ -.list { +.root { padding: 8px 0; list-style: none; display: block; + margin-top: 16px; } -.emptyCardContainer { - margin-top: 16px; +.list { + padding: 0; + margin: 0; } .commentLeave { diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js index d4838e5e0..efa281ecd 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js @@ -143,10 +143,8 @@ class ModerationQueue extends React.Component { if (comments.length === 0) { return ( -
-
- {t('modqueue.empty_queue')} -
+
+ {t('modqueue.empty_queue')}
); } @@ -156,7 +154,7 @@ class ModerationQueue extends React.Component { const comment = comments[index]; const status = comment.action_summaries ? 'FLAGGED' : comment.status; return ( -
+
+