diff --git a/.eslintrc.json b/.eslintrc.json index d99b2254b..8b737cbd2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,7 +28,6 @@ "yoda": [1], "no-path-concat": [2], "eol-last": [1], - "no-continue": [1], "no-nested-ternary": [1], "no-tabs": [2], "no-unneeded-ternary": [1], diff --git a/app.json b/app.json index ba15f1ce4..e498c5168 100644 --- a/app.json +++ b/app.json @@ -15,7 +15,8 @@ }, "NODE_ENV": "production", "TALK_SMTP_PORT": "2525", - "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN" + "REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN", + "NPM_CONFIG_PRODUCTION": "false" }, "addons": [{ "plan": "mongolab:sandbox", diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js index e755f1ed1..5d2104560 100644 --- a/client/coral-admin/src/actions/comments.js +++ b/client/coral-admin/src/actions/comments.js @@ -1,10 +1,11 @@ import coralApi from '../../../coral-framework/helpers/response'; -import * as commentActions from '../constants/comments'; +import * as commentTypes from '../constants/comments'; +import * as actionTypes from '../constants/actions'; // Get comments to fill each of the three lists on the mod queue export const fetchModerationQueueComments = () => { return dispatch => { - dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH}); + dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST}); return Promise.all([ coralApi('/queue/comments/pending'), coralApi('/comments?status=rejected'), @@ -20,11 +21,12 @@ export const fetchModerationQueueComments = () => { actions: [...pending.actions, ...rejected.actions, ...flagged.actions] }; }) - .then(({comments, users}) => { + .then(({comments, users, actions}) => { /* Post comments and users to redux store. Actions will be posted when they are needed. */ - dispatch({type: commentActions.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); - dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments}); + dispatch({type: commentTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users}); + dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments}); + dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions}); }); }; @@ -35,8 +37,8 @@ export const createComment = (name, body) => { return dispatch => { const comment = {body, name}; return coralApi('/comments', {method: 'POST', comment}) - .then(res => dispatch({type: commentActions.COMMENT_CREATE_SUCCESS, comment: res})) - .catch(error => dispatch({type: commentActions.COMMENT_CREATE_FAILED, error})); + .then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res})) + .catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error})); }; }; @@ -47,23 +49,23 @@ export const createComment = (name, body) => { // Update a comment. Now to update a comment we need to send back the whole object export const updateStatus = (status, comment) => { return dispatch => { - dispatch({type: commentActions.COMMENT_STATUS_UPDATE, id: comment.id, status}); + dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_REQUEST, id: comment.id, status}); return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}}) - .then(res => dispatch({type: commentActions.COMMENT_UPDATE_SUCCESS, res})) - .catch(error => dispatch({type: commentActions.COMMENT_UPDATE_FAILED, error})); + .then(res => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_SUCCESS, res})) + .catch(error => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_FAILURE, error})); }; }; export const flagComment = id => (dispatch, getState) => { - dispatch({type: commentActions.COMMENT_FLAG, id}); + dispatch({type: commentTypes.COMMENT_FLAG, id}); dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)}); }; // Dialog Actions export const showBanUserDialog = (userId, userName, commentId) => { - return {type: commentActions.SHOW_BANUSER_DIALOG, userId, userName, commentId}; + return {type: commentTypes.SHOW_BANUSER_DIALOG, userId, userName, commentId}; }; export const hideBanUserDialog = (showDialog) => { - return {type: commentActions.HIDE_BANUSER_DIALOG, showDialog}; + return {type: commentTypes.HIDE_BANUSER_DIALOG, showDialog}; }; diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index 1dfda51b9..85ad5149e 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -10,6 +10,8 @@ export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING'; export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS'; export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; +export const WORDLIST_UPDATED = 'WORDLIST_UPDATED'; + export const fetchSettings = () => dispatch => { dispatch({type: SETTINGS_LOADING}); coralApi('/settings') @@ -21,10 +23,16 @@ export const fetchSettings = () => dispatch => { }); }; +// for updating top-level settings export const updateSettings = settings => { return {type: SETTINGS_UPDATED, settings}; }; +// this is a nested property, so it needs a special action. +export const updateWordlist = (listName, list) => { + return {type: WORDLIST_UPDATED, listName, list}; +}; + export const saveSettingsToServer = () => (dispatch, getState) => { let settings = getState().settings.toJS().settings; if (settings.charCount) { diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 291825c71..17bd56714 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -8,6 +8,7 @@ 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 {FabButton, Button} from 'coral-ui'; const linkify = new Linkify(); @@ -31,7 +32,7 @@ export default props => { {links ? Contains Link : null}
- {props.actions.map((action, i) => getActionButton(action, i, props))} + {props.modActions.map((action, i) => getActionButton(action, i, props))}
@@ -42,7 +43,9 @@ export default props => {
- {comment.body} +
diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 39de5121b..326d63cf1 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -1,12 +1,11 @@ - -import React from 'react'; +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 actions = { +const modActions = { 'reject': {status: 'rejected', icon: 'close', key: 'r'}, 'approve': {status: 'accepted', icon: 'done', key: 't'}, 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, @@ -15,6 +14,23 @@ const actions = { // 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, + modActions: PropTypes.arrayOf(PropTypes.string), + loading: PropTypes.bool, + + // list of actions (flags, etc) associated with the comments + actions: PropTypes.shape({ + ids: PropTypes.arrayOf(PropTypes.string) + }), + suspectWords: PropTypes.arrayOf(PropTypes.string) + } + constructor (props) { super(props); @@ -44,22 +60,22 @@ export default class CommentList extends React.Component { // Add swipe to approve or reject bindGestures () { - const {actions} = this.props; + const {modActions} = this.props; this._hammer = new Hammer(this.base); this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL}); - if (actions.indexOf('reject') !== -1) { + if (modActions.indexOf('reject') !== -1) { this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected')); } - if (actions.indexOf('approve') !== -1) { + 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.actions.filter(action => actions[action].key).forEach(action => { - key(actions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(actions[action].status)); + 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')); @@ -122,7 +138,7 @@ export default class CommentList extends React.Component { } render () { - const {singleView, commentIds, comments, users, hideActive, key} = this.props; + const {singleView, commentIds, comments, users, hideActive, key, suspectWords} = this.props; const {active} = this.state; return ( @@ -133,14 +149,16 @@ export default class CommentList extends React.Component { {commentIds.map((commentId, index) => { const comment = comments[commentId]; const author = users[comment.author_id]; - return ; })} diff --git a/client/coral-admin/src/constants/actions.js b/client/coral-admin/src/constants/actions.js new file mode 100644 index 000000000..d64c62d26 --- /dev/null +++ b/client/coral-admin/src/constants/actions.js @@ -0,0 +1 @@ +export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS'; diff --git a/client/coral-admin/src/constants/comments.js b/client/coral-admin/src/constants/comments.js index c17aa1fd1..35a915a1b 100644 --- a/client/coral-admin/src/constants/comments.js +++ b/client/coral-admin/src/constants/comments.js @@ -1,12 +1,12 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS'; -export const COMMENTS_MODERATION_QUEUE_FETCH = 'COMMENTS_MODERATION_QUEUE_FETCH'; +export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST'; export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS'; export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS'; export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED'; export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS'; -export const COMMENT_UPDATE_SUCCESS = 'COMMENT_UPDATE_SUCCESS'; -export const COMMENT_UPDATE_FAILED = 'COMMENT_UPDATE_FAILED'; -export const COMMENT_STATUS_UPDATE = 'COMMENT_STATUS_UPDATE'; +export const COMMENT_STATUS_UPDATE_SUCCESS = 'COMMENT_STATUS_UPDATE_SUCCESS'; +export const COMMENT_STATUS_UPDATE_FAILURE = 'COMMENT_STATUS_UPDATE_FAILURE'; +export const COMMENT_STATUS_UPDATE_REQUEST = 'COMMENT_STATUS_UPDATE_REQUEST'; export const COMMENT_FLAG = 'COMMENT_FLAG'; diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 6bcf1194b..6000c1d43 100644 --- a/client/coral-admin/src/containers/Configure/CommentSettings.js +++ b/client/coral-admin/src/containers/Configure/CommentSettings.js @@ -69,110 +69,115 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => { } }; -const CommentSettings = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => { +const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => { if (fetchingSettings) { /* maybe a spinner here at some point */ return

Loading settings...

; } - return - - - - - -
{lang.t('configure.enable-pre-moderation')}
-

- {lang.t('configure.enable-pre-moderation-text')} -

-
-
- - - - - -
{lang.t('configure.comment-count-header')}
-

- {lang.t('configure.comment-count-text-pre')} - - {lang.t('configure.comment-count-text-post')} - { - errors.charCount && - -
- - {lang.t('configure.comment-count-error')} -
- } -

-
-
- - - - - - {lang.t('configure.include-comment-stream')} -

- {lang.t('configure.include-comment-stream-desc')} -

-
-
- - - - - - - - {lang.t('configure.close-after')} -
- -
- - - - - -
-
-
- - - {lang.t('configure.closed-comments-desc')} - - - -
; + return ( +
+

{title}

+ + + + + + +
{lang.t('configure.enable-pre-moderation')}
+

+ {lang.t('configure.enable-pre-moderation-text')} +

+
+
+ + + + + +
{lang.t('configure.comment-count-header')}
+

+ {lang.t('configure.comment-count-text-pre')} + + {lang.t('configure.comment-count-text-post')} + { + errors.charCount && + +
+ + {lang.t('configure.comment-count-error')} +
+ } +

+
+
+ + + + + + {lang.t('configure.include-comment-stream')} +

+ {lang.t('configure.include-comment-stream-desc')} +

+
+
+ + + + + + + + {lang.t('configure.close-after')} +
+ +
+ + + + + +
+
+
+ + + {lang.t('configure.closed-comments-desc')} + + + +
+
+ ); }; export default CommentSettings; diff --git a/client/coral-admin/src/containers/Configure/Configure.css b/client/coral-admin/src/containers/Configure/Configure.css index c0646c9e2..2b5c8d578 100644 --- a/client/coral-admin/src/containers/Configure/Configure.css +++ b/client/coral-admin/src/containers/Configure/Configure.css @@ -112,12 +112,12 @@ letter-spacing: 0.03em; } -#bannedWordlist { +#bannedWordlist, #suspectWordlist { width: 100%; padding: 10px; } -.bannedWordHeader { +.wordlistHeader { font-weight: bold; font-size:18px; margin-bottom:3px; diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index ed8ba9b26..9fc24ad53 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -1,6 +1,11 @@ import React from 'react'; import {connect} from 'react-redux'; -import {fetchSettings, updateSettings, saveSettingsToServer} from '../../actions/settings'; +import { + fetchSettings, + updateSettings, + saveSettingsToServer, + updateWordlist, +} from '../../actions/settings'; import { List, ListItem, @@ -14,6 +19,7 @@ import translations from '../../translations.json'; import EmbedLink from './EmbedLink'; import CommentSettings from './CommentSettings'; import Wordlist from './Wordlist'; +import has from 'lodash/has'; class Configure extends React.Component { constructor (props) { @@ -21,7 +27,6 @@ class Configure extends React.Component { this.state = { activeSection: 'comments', - wordlist: [], changed: false, errors: {} }; @@ -31,15 +36,6 @@ class Configure extends React.Component { this.props.dispatch(fetchSettings()); } - componentWillUpdate = (newProps) => { - if ((!this.props.settings - || !this.props.settings.wordlist) - && newProps.settings.wordlist - && newProps.settings.wordlist.length !== 0 ) { - this.setState({wordlist: newProps.settings.wordlist.join(', ')}); - } - } - saveSettings = () => { this.props.dispatch(saveSettingsToServer()); this.setState({changed: false}); @@ -49,15 +45,9 @@ class Configure extends React.Component { this.setState({activeSection}); } - onChangeWordlist = (event) => { - event.preventDefault(); - const newlist = event.target.value; - this.setState({wordlist: newlist.toLowerCase(), changed: true}); - this.props.dispatch(updateSettings({ - wordlist: newlist.toLowerCase() - .split(',') - .map((word) => word.trim()) - })); + onChangeWordlist = (listName, list) => { + this.setState({changed: true}); + this.props.dispatch(updateWordlist(listName, list)); } onSettingUpdate = (setting) => { @@ -74,46 +64,46 @@ class Configure extends React.Component { }); } - getSection = (section) => { + getSection (section) { + const pageTitle = this.getPageTitle(section); switch(section){ case 'comments': return ; case 'embed': - return ; + return ; case 'wordlist': - return ; + return has(this, 'props.settings.wordlist') + ? + :

loading wordlists

; } } - getPageTitle = (section) => { + getPageTitle (section) { switch(section) { case 'comments': return lang.t('configure.comment-settings'); case 'embed': return lang.t('configure.embed-comment-stream'); - case 'wordlist': - return lang.t('configure.wordlist'); + default: + return ''; } } render () { - let pageTitle = this.getPageTitle(this.state.activeSection); const section = this.getSection(this.state.activeSection); const showSave = Object.keys(this.state.errors).reduce( (bool, error) => this.state.errors[error] ? false : bool, this.state.changed); - if (this.props.fetchingSettings) { - pageTitle += ' - Loading...'; - } - return (
@@ -151,7 +141,6 @@ class Configure extends React.Component {
-

{pageTitle}

{ this.props.saveFetchingError } { this.props.fetchSettingsError } { section } diff --git a/client/coral-admin/src/containers/Configure/EmbedLink.js b/client/coral-admin/src/containers/Configure/EmbedLink.js index 6c5c3da99..374d78ea3 100644 --- a/client/coral-admin/src/containers/Configure/EmbedLink.js +++ b/client/coral-admin/src/containers/Configure/EmbedLink.js @@ -31,16 +31,21 @@ class EmbedLink extends Component { render () { const embedText = `
`; - return - -

{lang.t('configure.copy-and-paste')}

-