mirror of
https://github.com/wassname/talk.git
synced 2026-07-10 17:48:01 +08:00
merge
This commit is contained in:
+1
-1
@@ -23,7 +23,7 @@ program
|
||||
.description('initilizes the talk settings')
|
||||
.action(() => {
|
||||
const defaults = {
|
||||
moderation: 'PRE',
|
||||
moderation: 'POST',
|
||||
wordlist: {
|
||||
banned: [],
|
||||
suspect: []
|
||||
|
||||
@@ -14,7 +14,10 @@ export const checkLogin = () => dispatch => {
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(checkLoginFailure(`${error.message}`));
|
||||
});
|
||||
};
|
||||
|
||||
// LogOut Actions
|
||||
|
||||
@@ -15,17 +15,18 @@ export const fetchModerationQueueComments = () => {
|
||||
|
||||
return Promise.all([
|
||||
coralApi('/queue/comments/pending'),
|
||||
coralApi('/queue/users/pending'),
|
||||
coralApi('/queue/comments/rejected'),
|
||||
coralApi('/queue/comments/flagged')
|
||||
])
|
||||
.then(([pending, rejected, flagged]) => {
|
||||
.then(([pendingComments, pendingUsers, rejected, flagged]) => {
|
||||
|
||||
/* Combine seperate calls into a single object */
|
||||
flagged.comments.forEach(comment => comment.flagged = true);
|
||||
return {
|
||||
comments: [...pending.comments, ...rejected.comments, ...flagged.comments],
|
||||
users: [...pending.users, ...rejected.users, ...flagged.users],
|
||||
actions: [...pending.actions, ...rejected.actions, ...flagged.actions]
|
||||
comments: [...pendingComments.comments, ...rejected.comments, ...flagged.comments],
|
||||
users: [...pendingComments.users, ...pendingUsers.users, ...rejected.users, ...flagged.users],
|
||||
actions: [...pendingComments.actions, ...pendingUsers.actions, ...rejected.actions, ...flagged.actions]
|
||||
};
|
||||
})
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
@@ -41,6 +42,15 @@ export const fetchPendingQueue = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchPendingUsersQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/users/pending')
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchRejectedQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import * as actions from '../constants/user';
|
||||
import * as userTypes from '../constants/users';
|
||||
|
||||
/**
|
||||
* Action disptacher related to users
|
||||
@@ -7,9 +7,17 @@ import * as actions from '../constants/user';
|
||||
// change status of a user
|
||||
export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return (dispatch) => {
|
||||
dispatch({type: actions.UPDATE_STATUS_REQUEST});
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error}));
|
||||
.then(res => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch(error => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
|
||||
};
|
||||
};
|
||||
|
||||
// change status of a user
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch(error => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import {FabButton, Button, Icon} from 'coral-ui';
|
||||
|
||||
const ActionButton = ({option, type, comment = {}, user, menuOptionsMap, onClickAction, onClickShowBanDialog}) =>
|
||||
{
|
||||
const banned = user.status === 'BANNED';
|
||||
|
||||
if (option === 'flag' && (type === 'USERS' || comment.status || comment.flagged === true)) {
|
||||
return null;
|
||||
}
|
||||
if (option === 'ban') {
|
||||
return (
|
||||
<div className={styles.ban}>
|
||||
<Button
|
||||
className={`ban ${styles.banButton}`}
|
||||
cStyle='darkGrey'
|
||||
disabled={banned ? 'disabled' : ''}
|
||||
onClick={() => onClickShowBanDialog(user.id, user.displayName, comment.id)
|
||||
}
|
||||
raised
|
||||
>
|
||||
<Icon name='not_interested' className={styles.banIcon} />
|
||||
{lang.t('comment.ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const menuOption = menuOptionsMap[option];
|
||||
const action = {
|
||||
item_type: type,
|
||||
item_id: type === 'COMMENTS' ? comment.id : user.id
|
||||
};
|
||||
return (
|
||||
<FabButton
|
||||
className={`${option} ${styles.actionButton}`}
|
||||
cStyle={option}
|
||||
icon={menuOption.icon}
|
||||
onClick={() => onClickAction(menuOption.status, type === 'COMMENTS' ? comment : user, action)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -35,7 +35,7 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => (
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} raised>
|
||||
{lang.t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={() => onClickBanUser(user.userId, user.commentId)} raised>
|
||||
<Button cStyle="black" className={styles.ban} onClick={() => onClickBanUser('BANNED', user.userId, user.commentId)} raised>
|
||||
{lang.t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,18 +2,19 @@ import React from 'react';
|
||||
import timeago from 'timeago.js';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
import styles from './CommentList.css';
|
||||
import styles from './ModerationList.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import {FabButton, Button, Icon} from 'coral-ui';
|
||||
import {Icon} from 'coral-ui';
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
// Render a single comment for the list
|
||||
export default props => {
|
||||
const Comment = props => {
|
||||
const {comment, author} = props;
|
||||
let authorStatus = author.status;
|
||||
const links = linkify.getMatches(comment.body);
|
||||
@@ -30,7 +31,18 @@ export default props => {
|
||||
{links ?
|
||||
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{props.modActions.map((action, i) => getActionButton(action, i, props))}
|
||||
{props.modActions.map(
|
||||
(action, i) =>
|
||||
<ActionButton
|
||||
option={action}
|
||||
key={i}
|
||||
type='COMMENTS'
|
||||
comment={comment}
|
||||
user={author}
|
||||
menuOptionsMap={props.menuOptionsMap}
|
||||
onClickAction={props.onClickAction}
|
||||
onClickShowBanDialog={props.onClickShowBanDialog}/>
|
||||
)}
|
||||
</div>
|
||||
{authorStatus === 'banned' ?
|
||||
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
|
||||
@@ -49,43 +61,7 @@ export default props => {
|
||||
);
|
||||
};
|
||||
|
||||
// Get the button of the action performed over a comment if any
|
||||
const getActionButton = (action, i, props) => {
|
||||
const {comment, author} = props;
|
||||
const status = comment.status;
|
||||
const flagged = comment.flagged;
|
||||
const banned = (author.status === 'banned');
|
||||
|
||||
if (action === 'flag' && (status || flagged === true)) {
|
||||
return null;
|
||||
}
|
||||
if (action === 'ban') {
|
||||
return (
|
||||
<div className={styles.ban}>
|
||||
<Button
|
||||
className={`ban ${styles.banButton}`}
|
||||
cStyle='darkGrey'
|
||||
disabled={banned ? 'disabled' : ''}
|
||||
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
|
||||
key={i}
|
||||
raised
|
||||
>
|
||||
<Icon name='not_interested' className={styles.banIcon} />
|
||||
{lang.t('comment.ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FabButton
|
||||
className={`${action} ${styles.actionButton}`}
|
||||
cStyle={action}
|
||||
icon={props.actionsMap[action].icon}
|
||||
key={i}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, comment)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default Comment;
|
||||
|
||||
const linkStyles = {
|
||||
backgroundColor: 'rgb(255, 219, 135)',
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentList.css';
|
||||
import key from 'keymaster';
|
||||
import Hammer from 'hammerjs';
|
||||
import Comment from 'components/Comment';
|
||||
|
||||
// Each action has different meaning and configuration
|
||||
const modActions = {
|
||||
'reject': {status: 'REJECTED', icon: 'close', key: 'r'},
|
||||
'approve': {status: 'ACCEPTED', icon: 'done', key: 't'},
|
||||
'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||
'ban': {status: 'BANNED', icon: 'not interested'}
|
||||
};
|
||||
|
||||
// Renders a comment list and allow performing actions
|
||||
export default class CommentList extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
singleView: PropTypes.bool,
|
||||
commentIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
comments: PropTypes.object.isRequired,
|
||||
users: PropTypes.object.isRequired,
|
||||
onClickAction: PropTypes.func,
|
||||
|
||||
// list of actions (flags, etc) associated with the comments
|
||||
modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {active: null};
|
||||
this.onClickAction = this.onClickAction.bind(this);
|
||||
this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this);
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
this.props.modActions.filter(action => modActions[action].key).forEach(action => {
|
||||
key(modActions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(modActions[action].status));
|
||||
});
|
||||
key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down'));
|
||||
key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up'));
|
||||
key.setScope('commentList');
|
||||
}
|
||||
|
||||
// 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('commentList');
|
||||
}
|
||||
|
||||
// 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 (action, id, author_id) {
|
||||
|
||||
// activate the next comment
|
||||
if (id === this.state.active) {
|
||||
const {commentIds} = this.props;
|
||||
if (commentIds[commentIds.length - 1] === this.state.active) {
|
||||
this.setState({active: commentIds[commentIds.length - 2]});
|
||||
} else {
|
||||
this.setState({active: commentIds[Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.length - 1)]});
|
||||
}
|
||||
}
|
||||
this.props.onClickAction(action, id, author_id);
|
||||
}
|
||||
|
||||
onClickShowBanDialog(userId, userName, commentId) {
|
||||
this.props.onClickShowBanDialog(userId, userName, commentId);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {singleView, commentIds, comments, users, hideActive, key, suspectWords} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
|
||||
id='commentList'
|
||||
>
|
||||
{commentIds.map((commentId, index) => {
|
||||
const comment = comments[commentId];
|
||||
const author = users[comment.author_id];
|
||||
return <Comment
|
||||
suspectWords={suspectWords}
|
||||
comment={comment}
|
||||
author={author}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={this.props.modActions}
|
||||
actionsMap={modActions}
|
||||
isActive={commentId === active}
|
||||
hideActive={hideActive} />;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
.inner {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
box-shadow: 2px 2px 5px 0px rgba(153,153,153,1);
|
||||
padding: 20px;
|
||||
@@ -37,7 +36,6 @@
|
||||
@media (--big-viewport) {
|
||||
.inner {
|
||||
width: 600px;
|
||||
height: 50vh;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -69,6 +69,7 @@
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -112,6 +113,12 @@
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
@@ -0,0 +1,227 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import key from 'keymaster';
|
||||
import Hammer from 'hammerjs';
|
||||
import Comment from './Comment';
|
||||
import UserAction from './UserAction';
|
||||
import SuspendUserModal from './SuspendUserModal';
|
||||
|
||||
// Each action has different meaning and configuration
|
||||
const menuOptionsMap = {
|
||||
'reject': {status: 'rejected', icon: 'close', key: 'r'},
|
||||
'approve': {status: 'accepted', icon: 'done', key: 't'},
|
||||
'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('ACTIVE', 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 = <Comment
|
||||
suspectWords={suspectWords}
|
||||
comment={item}
|
||||
author={author}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
} else {
|
||||
|
||||
// If the item is an action...
|
||||
const user = users[item.item_id];
|
||||
modItem = user && <UserAction
|
||||
suspectWords={suspectWords}
|
||||
action={item}
|
||||
user={user}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
}
|
||||
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 (
|
||||
<ul
|
||||
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
|
||||
id='moderationList'>
|
||||
{moderationIds.map(this.mapModItems)}
|
||||
<SuspendUserModal
|
||||
action = {this.state.suspendUserModal}
|
||||
onClose={() => this.setState({suspendUserModal:null})}
|
||||
suspendUser={suspendUser} />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
.modalButtons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.emailInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.writeContainer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.emailContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.emailMessage {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './SuspendUserModal.css';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
const stages = [
|
||||
{
|
||||
title: 'suspenduser.title_0',
|
||||
description: 'suspenduser.description_0',
|
||||
options: {
|
||||
'j': 'suspenduser.no_cancel',
|
||||
'k': 'suspenduser.yes_suspend'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'suspenduser.title_1',
|
||||
description: 'suspenduser.description_1',
|
||||
options: {
|
||||
'j': 'bandialog.cancel',
|
||||
'k': 'suspenduser.send'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class SuspendUserModal extends Component {
|
||||
|
||||
state = {email: '', stage: 0}
|
||||
|
||||
static propTypes = {
|
||||
stage: PropTypes.number,
|
||||
actionType: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
suspendUser: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const about = this.props.actionType === 'flag_bio' ? lang.t('suspenduser.bio') : lang.t('suspenduser.username');
|
||||
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)
|
||||
.then(this.props.onClose);
|
||||
const suspendModalActions = [
|
||||
[ cancel, next ],
|
||||
[ cancel, suspend ]
|
||||
];
|
||||
return suspendModalActions[stage][menuOption]();
|
||||
}
|
||||
|
||||
onEmailChange = (e) => this.setState({email: e.target.value})
|
||||
|
||||
render () {
|
||||
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 <Modal open={true} onClose={onClose}>
|
||||
<div className={styles.title}>{lang.t(stages[stage].title, about)}</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{lang.t(stages[stage].description, about)}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{lang.t('suspenduser.write_message')}</div>
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.emailInput}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{lang.t(stages[stage].options[key], about)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>;
|
||||
}
|
||||
}
|
||||
|
||||
export default SuspendUserModal;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
import styles from './ModerationList.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
import {Icon} from 'react-mdl';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
// Render a single comment for the list
|
||||
const UserAction = props => {
|
||||
const {action, user} = props;
|
||||
let userStatus = user.status;
|
||||
const links = user.settings.bio ? linkify.getMatches(user.settings.bio) : [];
|
||||
|
||||
// Do not display unless the user status is 'pending' or 'banned'.
|
||||
// This means that they have already been reviewed and approved.
|
||||
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
|
||||
<li tabIndex={props.index} className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>{user.displayName}</span>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
{links ?
|
||||
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{props.modActions.map(
|
||||
(action, i) =>
|
||||
<ActionButton
|
||||
option={action}
|
||||
key={i}
|
||||
type='USERS'
|
||||
user={user}
|
||||
menuOptionsMap={props.menuOptionsMap}
|
||||
onClickAction={props.onClickAction}
|
||||
onClickShowBanDialog={props.onClickShowBanDialog}/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{userStatus === 'banned' ?
|
||||
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
user.settings.bio &&
|
||||
<div>
|
||||
<div className={styles.itemBody}>
|
||||
<div>{lang.t('user.user_bio')}:</div>
|
||||
<span className={styles.body}>
|
||||
<Linkify component='span' properties={{style: linkStyles}}>
|
||||
<Highlighter
|
||||
searchWords={props.suspectWords}
|
||||
textToHighlight={user.settings.bio} />
|
||||
</Linkify>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.flagCount}>
|
||||
{`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default UserAction;
|
||||
|
||||
const linkStyles = {
|
||||
backgroundColor: 'rgb(255, 219, 135)',
|
||||
padding: '1px 2px'
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,3 +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';
|
||||
@@ -3,11 +3,11 @@ import styles from './CommentStream.css';
|
||||
import {Snackbar} from 'react-mdl';
|
||||
import {connect} from 'react-redux';
|
||||
import {createComment, flagComment} from 'actions/comments';
|
||||
import CommentList from 'components/CommentList';
|
||||
import ModerationList from 'components/ModerationList';
|
||||
import CommentBox from 'components/CommentBox';
|
||||
|
||||
/**
|
||||
* Renders a comment stream using a CommentList component
|
||||
* Renders a comment stream using a ModerationList component
|
||||
* and adds a box for adding a new comment
|
||||
*/
|
||||
|
||||
@@ -39,12 +39,12 @@ class CommentStream extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Render the comment box along with the CommentList
|
||||
// Render the comment box along with the ModerationList
|
||||
render ({comments, users}, {snackbar, snackbarMsg}) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CommentBox onSubmit={this.onSubmit}/>
|
||||
<CommentList isActive hideActive
|
||||
<CommentBox onSubmit={this.onSubmit} />
|
||||
<ModerationList isActive hideActive
|
||||
singleView={false}
|
||||
commentIds={comments.ids}
|
||||
comments={comments.byId}
|
||||
|
||||
@@ -32,4 +32,3 @@ export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LayoutContainer);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
fetchFlaggedQueue,
|
||||
fetchModerationQueueComments,
|
||||
} from 'actions/comments';
|
||||
import {userStatusUpdate} from 'actions/users';
|
||||
import {userStatusUpdate, sendNotificationEmail} from 'actions/users';
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
|
||||
import ModerationQueue from './ModerationQueue';
|
||||
@@ -22,7 +22,7 @@ class ModerationContainer extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTab: 'pending',
|
||||
activeTab: 'all',
|
||||
singleView: false,
|
||||
modalOpen: false
|
||||
};
|
||||
@@ -74,7 +74,7 @@ class ModerationContainer extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {comments} = this.props;
|
||||
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 =>
|
||||
@@ -82,12 +82,14 @@ class ModerationContainer extends React.Component {
|
||||
comments.byId[id].status !== 'REJECTED' &&
|
||||
comments.byId[id].status !== 'ACCEPTED'
|
||||
);
|
||||
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
|
||||
|
||||
return (
|
||||
<ModerationQueue
|
||||
onTabClick={this.onTabClick}
|
||||
onClose={this.onClose}
|
||||
premodIds={premodIds}
|
||||
userActionIds={userActionIds}
|
||||
rejectedIds={rejectedIds}
|
||||
flaggedIds={flaggedIds}
|
||||
{...this.props}
|
||||
@@ -100,7 +102,8 @@ class ModerationContainer extends React.Component {
|
||||
const mapStateToProps = state => ({
|
||||
comments: state.comments.toJS(),
|
||||
settings: state.settings.toJS(),
|
||||
users: state.users.toJS()
|
||||
users: state.users.toJS(),
|
||||
actions: state.actions.toJS(),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
@@ -112,9 +115,13 @@ const mapDispatchToProps = dispatch => {
|
||||
fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()),
|
||||
showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||
banUser: (userId, commentId) => dispatch(userStatusUpdate('BANNED', userId, commentId)).then(() => {
|
||||
userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => {
|
||||
dispatch(fetchModerationQueueComments());
|
||||
}),
|
||||
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('suspended', userId))
|
||||
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
|
||||
.then(() => dispatch(fetchModerationQueueComments()))
|
||||
,
|
||||
updateStatus: (action, comment) => dispatch(updateStatus(action, comment))
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import styles from './ModerationQueue.css';
|
||||
|
||||
import ModerationKeysModal from 'components/ModerationKeysModal';
|
||||
import CommentList from 'components/CommentList';
|
||||
import ModerationList from 'components/ModerationList';
|
||||
import BanUserDialog from 'components/BanUserDialog';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
@@ -10,23 +10,40 @@ import translations from '../../translations.json';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default ({onTabClick, ...props}) => (
|
||||
export default (props) => (
|
||||
<div>
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<a href='#all'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onTabClick('all');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`}
|
||||
>
|
||||
{lang.t('modqueue.all')}
|
||||
</a>
|
||||
<a href='#pending'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onTabClick('pending');
|
||||
props.onTabClick('pending');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'pending' ? styles.active : ''}`}
|
||||
>
|
||||
{lang.t('modqueue.pending')}
|
||||
</a>
|
||||
<a href='#account'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onTabClick('account');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
|
||||
{lang.t('modqueue.account')}
|
||||
</a>
|
||||
<a href='#rejected'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onTabClick('rejected');
|
||||
props.onTabClick('rejected');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
|
||||
>
|
||||
@@ -35,69 +52,146 @@ export default ({onTabClick, ...props}) => (
|
||||
<a href='#flagged'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onTabClick('flagged');
|
||||
props.onTabClick('flagged');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
|
||||
>
|
||||
{lang.t('modqueue.flagged')}
|
||||
</a>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='all'>
|
||||
{
|
||||
props.activeTab === 'pending'
|
||||
? <div>
|
||||
<CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'pending'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.premodIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading} />
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.banUser}
|
||||
user={props.comments.banUser} />
|
||||
</div>
|
||||
: null
|
||||
props.activeTab === 'all' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'all'}
|
||||
singleView={props.singleView}
|
||||
commentIds={[...props.premodIds, ...props.flaggedIds]}
|
||||
comments={props.comments.byId}
|
||||
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']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
{
|
||||
props.activeTab === 'rejected'
|
||||
? <CommentList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'rejected'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.rejectedIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
modActions={['approve']}
|
||||
loading={props.comments.loading} />
|
||||
: null
|
||||
}
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
|
||||
{
|
||||
props.activeTab === 'pending' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'pending'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.premodIds}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
actions={props.actions.byId}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
onClickShowBanDialog={props.showBanUserDialog}
|
||||
modActions={['reject', 'approve', 'ban']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='account'>
|
||||
{
|
||||
props.activeTab === 'account' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'account'}
|
||||
singleView={props.singleView}
|
||||
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']}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
|
||||
{
|
||||
props.activeTab === 'flagged'
|
||||
? <CommentList
|
||||
props.activeTab === 'flagged' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
isActive={props.activeTab === 'flagged'}
|
||||
singleView={props.singleView}
|
||||
commentIds={props.flaggedIds}
|
||||
userStatusUpdate={props.userStatusUpdate}
|
||||
suspendUser={props.suspendUser}
|
||||
comments={props.comments.byId}
|
||||
users={props.users.byId}
|
||||
onClickAction={props.updateStatus}
|
||||
updateCommentStatus={props.updateStatus}
|
||||
modActions={['reject', 'approve']}
|
||||
loading={props.comments.loading} />
|
||||
: null
|
||||
}
|
||||
loading={props.comments.loading}/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
{
|
||||
props.activeTab === 'rejected' &&
|
||||
<div>
|
||||
<ModerationList
|
||||
suspectWords={props.settings.settings.wordlist.suspect}
|
||||
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}
|
||||
modActions={['approve']}
|
||||
loading={props.comments.loading}
|
||||
/>
|
||||
<BanUserDialog
|
||||
open={props.comments.showBanUserDialog}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
onClickBanUser={props.userStatusUpdate}
|
||||
user={props.comments.banUser}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {Map, Set} from 'immutable';
|
||||
import {Map, Set, fromJS} from 'immutable';
|
||||
import * as types from '../constants/actions';
|
||||
|
||||
const initialState = Map({
|
||||
ids: Set()
|
||||
ids: Set(),
|
||||
byId: Map()
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
@@ -14,11 +15,13 @@ export default (state = initialState, action) => {
|
||||
};
|
||||
|
||||
const addActions = (state, action) => {
|
||||
const ids = action.actions.map(action => action.item_id);
|
||||
|
||||
// Make ids that are unique by item_id and by action type
|
||||
const typeId = (action) => `${action.action_type}_${action.item_id}`;
|
||||
const ids = action.actions.map(action => typeId(action));
|
||||
const map = action.actions.reduce((memo, action) => {
|
||||
memo[action.item_id] = action;
|
||||
memo[typeId(action)] = action;
|
||||
return memo;
|
||||
}, {});
|
||||
const newIds = state.get('ids').concat(ids);
|
||||
return state.merge(map).set('ids', newIds);
|
||||
return state.set('byId', fromJS(map)).set('ids', new Set(ids));
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as actions from '../constants/comments';
|
||||
import * as userActions from '../constants/user';
|
||||
import * as userActions from '../constants/users';
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
"loading": "Loading results"
|
||||
},
|
||||
"modqueue": {
|
||||
"all": "all",
|
||||
"pending": "pending",
|
||||
"rejected": "rejected",
|
||||
"flagged": "flagged",
|
||||
"account": "account flags",
|
||||
"shortcuts": "Shortcuts",
|
||||
"close": "Close",
|
||||
"actions": "Actions",
|
||||
@@ -37,6 +39,11 @@
|
||||
"ban_user": "Ban User",
|
||||
"banned_user": "Banned User"
|
||||
},
|
||||
"user": {
|
||||
"user_bio": "User Bio",
|
||||
"bio_flags": "flags for this bio",
|
||||
"username_flags": "flags for this username"
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copy to Clipboard"
|
||||
},
|
||||
@@ -79,6 +86,20 @@
|
||||
"cancel": "Cancel",
|
||||
"yes_ban_user": "Yes, Ban User"
|
||||
},
|
||||
"suspenduser": {
|
||||
"title_0": "We noticed you rejected a {0}",
|
||||
"description_0": "Would you like to temporarily ban this user becuase of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}.",
|
||||
"title_1": "Notify the user of their temporary suspension",
|
||||
"description_1": "Suspending this user will temporarily disable their account and hide all of their comments on the site.",
|
||||
"no_cancel": "No, cancel",
|
||||
"yes_suspend": "Yes, suspend",
|
||||
"send": "Send",
|
||||
"bio": "bio",
|
||||
"username": "username",
|
||||
"email_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"
|
||||
},
|
||||
"streams": {
|
||||
"search": "Search",
|
||||
"filter-streams": "Filter Streams",
|
||||
@@ -126,6 +147,11 @@
|
||||
"ban_user": "Suspender Usuario",
|
||||
"banned_user": "Usuario Suspendido"
|
||||
},
|
||||
"user": {
|
||||
"user_bio": "",
|
||||
"bio_flags": "",
|
||||
"username_flags": ""
|
||||
},
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Habilitar pre-moderación",
|
||||
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
|
||||
|
||||
@@ -32,6 +32,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
title: lang.t('configureCommentStream.enablePremod'),
|
||||
description: lang.t('configureCommentStream.enablePremodDescription')
|
||||
}} />
|
||||
{/* To be implimented
|
||||
<ul>
|
||||
<li>
|
||||
<Checkbox
|
||||
@@ -46,8 +47,10 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
}} />
|
||||
</li>
|
||||
</ul>
|
||||
*/}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
);
|
||||
|
||||
@@ -26,6 +26,11 @@ class Comment extends React.Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
reactKey: PropTypes.string.isRequired,
|
||||
|
||||
// id of currently opened ReplyBox. tracked in Stream.js
|
||||
activeReplyBox: PropTypes.string.isRequired,
|
||||
setActiveReplyBox: PropTypes.func.isRequired,
|
||||
refetch: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
postAction: PropTypes.func.isRequired,
|
||||
@@ -60,15 +65,6 @@ class Comment extends React.Component {
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
onReplyBoxClick = () => {
|
||||
if (this.props.currentUser) {
|
||||
this.setState({replyBoxVisible: !this.state.replyBoxVisible});
|
||||
} else {
|
||||
const offset = document.getElementById(`c_${this.props.comment.id}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comment,
|
||||
@@ -81,6 +77,8 @@ class Comment extends React.Component {
|
||||
addNotification,
|
||||
showSignInDialog,
|
||||
postAction,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
deleteAction
|
||||
} = this.props;
|
||||
|
||||
@@ -130,13 +128,13 @@ class Comment extends React.Component {
|
||||
currentUser={currentUser} />
|
||||
</div>
|
||||
{
|
||||
this.state.replyBoxVisible
|
||||
activeReplyBox === comment.id
|
||||
? <ReplyBox
|
||||
commentPostedHandler={() => {
|
||||
console.log('replyPostedHandler');
|
||||
this.setState({replyBoxVisible: false});
|
||||
setActiveReplyBox('');
|
||||
refetch();
|
||||
}}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
parentId={parentId || comment.id}
|
||||
addNotification={addNotification}
|
||||
authorId={currentUser.id}
|
||||
@@ -149,6 +147,8 @@ class Comment extends React.Component {
|
||||
comment.replies.map(reply => {
|
||||
return <Comment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
parentId={comment.id}
|
||||
postItem={postItem}
|
||||
@@ -158,6 +158,7 @@ class Comment extends React.Component {
|
||||
postAction={postAction}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply} />;
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {Component} from 'react';
|
||||
import {compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {isEqual} from 'lodash';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
|
||||
|
||||
|
||||
@@ -1,49 +1,72 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Comment from './Comment';
|
||||
|
||||
const Stream = ({
|
||||
comments,
|
||||
currentUser,
|
||||
asset,
|
||||
postItem,
|
||||
addNotification,
|
||||
postAction,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
comments.map(comment => {
|
||||
return <Comment
|
||||
refetch={refetch}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postItem={postItem}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
comment={comment} />;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
class Stream extends React.Component {
|
||||
|
||||
Stream.propTypes = {
|
||||
refetch: PropTypes.func.isRequired,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
comments: PropTypes.array.isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
displayName: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
};
|
||||
static propTypes = {
|
||||
refetch: PropTypes.func.isRequired,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
comments: PropTypes.array.isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
displayName: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {activeReplyBox: ''};
|
||||
this.setActiveReplyBox = this.setActiveReplyBox.bind(this);
|
||||
}
|
||||
|
||||
setActiveReplyBox (reactKey) {
|
||||
if (!this.props.currentUser) {
|
||||
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
} else {
|
||||
this.setState({activeReplyBox: reactKey});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comments,
|
||||
currentUser,
|
||||
asset,
|
||||
postItem,
|
||||
addNotification,
|
||||
postAction,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
comments.map(comment => {
|
||||
return <Comment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postItem={postItem}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postAction={postAction}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
reactKey={comment.id}
|
||||
comment={comment} />;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Stream;
|
||||
|
||||
@@ -132,5 +132,8 @@ export const checkLogin = () => dispatch => {
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(checkLoginFailure(`${error.message}`));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ class CommentBox extends Component {
|
||||
// comments: PropTypes.array,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
cancelButtonClicked: PropTypes.func,
|
||||
assetId: PropTypes.string.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
authorId: PropTypes.string.isRequired,
|
||||
@@ -89,7 +90,14 @@ class CommentBox extends Component {
|
||||
|
||||
render () {
|
||||
const {styles, isReply, authorId, charCount} = this.props;
|
||||
let {cancelButtonClicked} = this.props;
|
||||
const length = this.state.body.length;
|
||||
|
||||
if (isReply && typeof cancelButtonClicked !== 'function') {
|
||||
console.warn('the CommentBox component should have a cancelButtonClicked callback defined if it lives in a Reply');
|
||||
cancelButtonClicked = () => {};
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div
|
||||
className={`${name}-container`}>
|
||||
@@ -115,6 +123,19 @@ class CommentBox extends Component {
|
||||
}
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
{
|
||||
isReply && (
|
||||
<Button
|
||||
cStyle='darkGrey'
|
||||
className={`${name}-cancel-button`}
|
||||
onClick={() => {
|
||||
console.log('cancel button in comment box');
|
||||
cancelButtonClicked('');
|
||||
}}>
|
||||
{lang.t('cancel')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{ authorId && (
|
||||
<Button
|
||||
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"en": {
|
||||
"post": "Post",
|
||||
"cancel": "Cancel",
|
||||
"reply": "Reply",
|
||||
"comment": "Comment",
|
||||
"name": "Name",
|
||||
@@ -11,6 +12,7 @@
|
||||
},
|
||||
"es": {
|
||||
"post": "Publicar",
|
||||
"cancel": "Cancelar",
|
||||
"reply": "Respuesta",
|
||||
"comment": "Comentario",
|
||||
"name": "Nombre",
|
||||
|
||||
@@ -14,6 +14,7 @@ class FlagButton extends Component {
|
||||
reason: '',
|
||||
note: '',
|
||||
step: 0,
|
||||
posted: false,
|
||||
localPost: null,
|
||||
localDelete: false
|
||||
}
|
||||
@@ -38,7 +39,7 @@ class FlagButton extends Component {
|
||||
|
||||
onPopupContinue = () => {
|
||||
const {postAction, id, author_id} = this.props;
|
||||
const {itemType, reason, step, localPost} = this.state;
|
||||
const {itemType, reason, step, posted} = this.state;
|
||||
|
||||
// Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.props.getPopupMenu.length) {
|
||||
@@ -48,7 +49,8 @@ class FlagButton extends Component {
|
||||
}
|
||||
|
||||
// If itemType and reason are both set, post the action
|
||||
if (itemType && reason && !localPost) {
|
||||
if (itemType && reason && !posted) {
|
||||
this.setState({posted: true});
|
||||
|
||||
// Set the text from the "other" field if it exists.
|
||||
let item_id;
|
||||
|
||||
@@ -3,11 +3,12 @@ import CommentBox from '../coral-plugin-commentbox/CommentBox';
|
||||
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler}) => (
|
||||
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler, setActiveReplyBox}) => (
|
||||
<div className={`${name}-textarea`} style={styles && styles.container}>
|
||||
<CommentBox
|
||||
commentPostedHandler={commentPostedHandler}
|
||||
parentId={parentId}
|
||||
cancelButtonClicked={setActiveReplyBox}
|
||||
addNotification={addNotification}
|
||||
authorId={authorId}
|
||||
assetId={assetId}
|
||||
@@ -17,6 +18,7 @@ const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentI
|
||||
);
|
||||
|
||||
ReplyBox.propTypes = {
|
||||
setActiveReplyBox: PropTypes.func.isRequired,
|
||||
commentPostedHandler: PropTypes.func,
|
||||
parentId: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
|
||||
@@ -7,7 +7,7 @@ export default ({userData}) => (
|
||||
|
||||
{
|
||||
|
||||
// Hiding display of users ID unless there's a use case for it.
|
||||
// Hiding display of users ID unless there's a use case for it.
|
||||
// <h2>{userData.profiles.map(profile => profile.id)}</h2>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => (
|
||||
id="signInDialog"
|
||||
open={open}
|
||||
style={{
|
||||
position: 'relative',
|
||||
position: 'fixed',
|
||||
top: offset !== 0 && offset
|
||||
}}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
|
||||
@@ -821,6 +821,14 @@ definitions:
|
||||
current_user:
|
||||
type: object
|
||||
description: Will include the action performed by the currently logged in user if that user has taken an action on this item. Otherwise will return null.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Oldest created_at of actions in this set.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Newest updated_at of actions in this set.
|
||||
Action:
|
||||
type: object
|
||||
description: A single action taken by a user.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
|
||||
/**
|
||||
* Creates an action on a item.
|
||||
* Creates an action on a item. If the item is a user flag, sets the user's status to
|
||||
* pending.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} item_id id of the item to add the action to
|
||||
* @param {String} item_type type of the item
|
||||
@@ -16,7 +18,10 @@ const createAction = ({user = {}}, {item_id, item_type, action_type, metadata =
|
||||
user_id: user.id,
|
||||
action_type,
|
||||
metadata
|
||||
});
|
||||
}).then((result) =>
|
||||
item_type === 'USERS' && action_type === 'FLAG' ?
|
||||
UsersService.setStatus(item_id, 'PENDING').then(() => result)
|
||||
: result);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = () => Promise.all([
|
||||
|
||||
// Upsert the settings object.
|
||||
SettingsService.init({
|
||||
moderation: 'PRE',
|
||||
moderation: 'POST',
|
||||
wordlist: {
|
||||
banned: [],
|
||||
suspect: []
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ const SettingSchema = new Schema({
|
||||
'PRE',
|
||||
'POST'
|
||||
],
|
||||
default: 'PRE'
|
||||
default: 'POST'
|
||||
},
|
||||
infoBoxEnable: {
|
||||
type: Boolean,
|
||||
|
||||
+3
-1
@@ -10,7 +10,9 @@ const USER_ROLES = [
|
||||
// USER_STATUS is the list of statuses that are permitted for the user status.
|
||||
const USER_STATUS = [
|
||||
'ACTIVE',
|
||||
'BANNED'
|
||||
'BANNED',
|
||||
'PENDING',
|
||||
'SUSPENDED',
|
||||
];
|
||||
|
||||
// ProfileSchema is the mongoose schema defined as the representation of a
|
||||
|
||||
@@ -78,4 +78,24 @@ router.get('/comments/flagged', authorization.needed('ADMIN'), (req, res, next)
|
||||
});
|
||||
});
|
||||
|
||||
// Returns back all the users that are in the moderation queue.
|
||||
router.get('/users/pending', (req, res, next) => {
|
||||
UsersService.moderationQueue()
|
||||
.then((users) => {
|
||||
return Promise.all([
|
||||
users,
|
||||
ActionsService.getActionSummaries(users.map((user) => user.id))
|
||||
]);
|
||||
})
|
||||
.then(([users, actions]) => {
|
||||
res.json({
|
||||
users,
|
||||
actions
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -57,6 +57,34 @@ router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next)
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/:user_id/email', authorization.needed('admin'), (req, res, next) => {
|
||||
UsersService.findById(req.params.user_id)
|
||||
.then(user => {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
app: req.app, // needed to render the templates.
|
||||
template: 'email/notification', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: req.body.body
|
||||
},
|
||||
subject: req.body.subject,
|
||||
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
|
||||
};
|
||||
return mailer.sendSimple(options);
|
||||
} else {
|
||||
res.json({error: 'User does not have an e-mail address.'});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
// /**
|
||||
// * SendEmailConfirmation sends a confirmation email to the user.
|
||||
// * @param {Request} req express request object
|
||||
@@ -122,7 +150,6 @@ router.post('/', (req, res, next) => {
|
||||
});
|
||||
|
||||
router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
|
||||
|
||||
const {
|
||||
action_type,
|
||||
metadata
|
||||
@@ -130,10 +157,21 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
|
||||
|
||||
UsersService
|
||||
.addAction(req.params.user_id, req.user.id, action_type, metadata)
|
||||
.then((action) => {
|
||||
|
||||
// Set the user status to "pending" for review by moderators
|
||||
if (action_type.slice(0, 4) === 'FLAG') {
|
||||
return UsersService.setStatus(req.user.id, 'PENDING')
|
||||
.then(() => action);
|
||||
} else {
|
||||
return action;
|
||||
}
|
||||
})
|
||||
.then((action) => {
|
||||
res.status(201).json(action);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Error', err);
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -598,4 +598,12 @@ module.exports = class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all users with pending 'ADMIN'ation actions.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static moderationQueue() {
|
||||
return UserModel.find({status: 'PENDING'});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ const embedStreamCommands = {
|
||||
},
|
||||
approveComment() {
|
||||
return this
|
||||
.waitForElementVisible('@commentList')
|
||||
.waitForElementVisible('@moderationList')
|
||||
.waitForElementVisible('@approveButton')
|
||||
.click('@approveButton');
|
||||
}
|
||||
@@ -17,17 +17,17 @@ const embedStreamCommands = {
|
||||
module.exports = {
|
||||
commands: [embedStreamCommands],
|
||||
elements: {
|
||||
commentList: {
|
||||
selector: '#commentList'
|
||||
moderationList: {
|
||||
selector: '#moderationList'
|
||||
},
|
||||
banButton: {
|
||||
selector: '#commentList .actions:first-child .ban'
|
||||
selector: '#moderationList .actions:first-child .ban'
|
||||
},
|
||||
rejectButton: {
|
||||
selector: '#commentList .actions:first-child .reject'
|
||||
selector: '#moderationList .actions:first-child .reject'
|
||||
},
|
||||
approveButton: {
|
||||
selector: '#commentList .actions:first-child .approve'
|
||||
selector: '#moderationList .actions:first-child .approve'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,6 +62,10 @@ describe('/api/v1/queue', () => {
|
||||
action_type: 'LIKE',
|
||||
item_id: 'hij',
|
||||
item_type: 'COMMENTS'
|
||||
}, {
|
||||
action_type: 'FLAG',
|
||||
item_id: '123',
|
||||
item_type: 'USERS'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -72,11 +76,16 @@ describe('/api/v1/queue', () => {
|
||||
comments[1].author_id = u[1].id;
|
||||
comments[2].author_id = u[1].id;
|
||||
|
||||
return Comment.create(comments);
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
u,
|
||||
...u.map((user) => UsersService.setStatus(user.id, 'PENDING'))
|
||||
]);
|
||||
})
|
||||
.then((c) => {
|
||||
.then(([c, u]) => {
|
||||
actions[0].item_id = c[0].id;
|
||||
actions[1].item_id = c[1].id;
|
||||
actions[2].item_id = u[0].id;
|
||||
|
||||
return Promise.all([
|
||||
Action.create(actions),
|
||||
@@ -98,4 +107,17 @@ describe('/api/v1/queue', () => {
|
||||
expect(res.body.actions[0]).to.have.property('action_type');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all pending users and actions', function(done){
|
||||
chai.request(app)
|
||||
.get('/api/v1/queue/users/pending')
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.end(function(err, res){
|
||||
expect(err).to.be.null;
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.users[0]).to.have.property('displayName');
|
||||
expect(res.body.actions[0]).to.have.property('action_type');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,13 +73,11 @@ describe('/api/v1/users/:user_id/actions', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/users/abc/actions')
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}))
|
||||
.send({'action_type': 'flag', metadata: {reason: 'Bio is too awesome.'}})
|
||||
.send({'action_type': 'FLAG', metadata: {reason: 'Bio is too awesome.'}})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res).to.have.body;
|
||||
expect(res.body).to.have.property('action_type', 'flag');
|
||||
expect(res.body).to.have.property('metadata')
|
||||
.and.to.deep.equal({'reason': 'Bio is too awesome.'});
|
||||
expect(res.body).to.have.property('action_type', 'FLAG');
|
||||
expect(res.body).to.have.property('item_id', 'abc');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= body %>
|
||||
@@ -0,0 +1 @@
|
||||
<%= body %>
|
||||
Reference in New Issue
Block a user