diff --git a/.babelrc b/.babelrc index cf99c0639..ca2e2cead 100644 --- a/.babelrc +++ b/.babelrc @@ -2,7 +2,7 @@ "sourceMaps": true, "presets": [ "stage-0", - "es2015-minimal" + "es2015" ], "plugins": [ ["transform-decorators-legacy"], diff --git a/.eslintignore b/.eslintignore index a4865e1f6..83564d3ff 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ dist client/lib +**/*.html diff --git a/app.js b/app.js index fe392932b..15a7a5442 100644 --- a/app.js +++ b/app.js @@ -3,10 +3,10 @@ const bodyParser = require('body-parser'); const morgan = require('morgan'); const path = require('path'); const helmet = require('helmet'); -const passport = require('./passport'); +const passport = require('./services/passport'); const session = require('express-session'); const RedisStore = require('connect-redis')(session); -const redis = require('./redis'); +const redis = require('./services/redis'); const app = express(); diff --git a/bin/cli-assets b/bin/cli-assets index 9ab36685a..4b0d65c2a 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -15,7 +15,7 @@ const pkg = require('../package.json'); const parseDuration = require('parse-duration'); const Table = require('cli-table'); const Asset = require('../models/asset'); -const mongoose = require('../mongoose'); +const mongoose = require('../services/mongoose'); const scraper = require('../services/scraper'); const util = require('../util'); diff --git a/bin/cli-jobs b/bin/cli-jobs index bc320d14b..91a0a6e59 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -13,8 +13,8 @@ process.env.DEBUG = process.env.TALK_DEBUG; const program = require('commander'); const scraper = require('../services/scraper'); const util = require('../util'); -const mongoose = require('../mongoose'); -const kue = require('../kue'); +const mongoose = require('../services/mongoose'); +const kue = require('../services/kue'); util.onshutdown([ () => mongoose.disconnect() diff --git a/bin/cli-serve b/bin/cli-serve index dd682f5ad..c22920eaa 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -11,7 +11,7 @@ const debug = require('debug')('talk:server'); const http = require('http'); const init = require('../init'); const scraper = require('../services/scraper'); -const mongoose = require('../mongoose'); +const mongoose = require('../services/mongoose'); const util = require('../util'); /** diff --git a/bin/cli-settings b/bin/cli-settings index e7ba30151..e38249199 100755 --- a/bin/cli-settings +++ b/bin/cli-settings @@ -11,7 +11,7 @@ process.env.DEBUG = process.env.TALK_DEBUG; */ const program = require('commander'); -const mongoose = require('../mongoose'); +const mongoose = require('../services/mongoose'); const Setting = require('../models/setting'); const util = require('../util'); diff --git a/bin/cli-users b/bin/cli-users index adfe3bf23..e212a477f 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -14,7 +14,7 @@ const program = require('commander'); const pkg = require('../package.json'); const prompt = require('prompt'); const User = require('../models/user'); -const mongoose = require('../mongoose'); +const mongoose = require('../services/mongoose'); const util = require('../util'); const Table = require('cli-table'); diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index 71106e1f7..1dfda51b9 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -26,7 +26,10 @@ export const updateSettings = settings => { }; export const saveSettingsToServer = () => (dispatch, getState) => { - const settings = getState().settings.toJS().settings; + let settings = getState().settings.toJS().settings; + if (settings.charCount) { + settings.charCount = parseInt(settings.charCount); + } dispatch({type: SAVE_SETTINGS_LOADING}); coralApi('/settings', {method: 'PUT', body: settings}) .then(() => { diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 380a001c8..4a21b0846 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -14,18 +14,18 @@ const linkify = new Linkify(); // Render a single comment for the list export default props => { - const authorStatus = props.author.get('status'); const {comment, author} = props; - const links = linkify.getMatches(comment.get('body')); + let authorStatus = author.status; + const links = linkify.getMatches(comment.body); return (
  • person - {author.get('displayName') || lang.t('comment.anon')} - {timeago().format(comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - {comment.get('flagged') ?

    {lang.t('comment.flagged')}

    : null} + {author.displayName || lang.t('comment.anon')} + {timeago().format(comment.createdAt || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} + {comment.flagged ?

    {lang.t('comment.flagged')}

    : null}
    {links ? @@ -42,7 +42,7 @@ export default props => {
    - {comment.get('body')} + {comment.body}
    @@ -52,9 +52,10 @@ export default props => { // Get the button of the action performed over a comment if any const getActionButton = (action, i, props) => { - const status = props.comment.get('status'); - const flagged = props.comment.get('flagged'); - const banned = (props.author.get('status') === 'banned'); + 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; @@ -64,17 +65,19 @@ const getActionButton = (action, i, props) => { ); } return ( - props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))} + onClick={() => props.onClickAction(props.actionsMap[action].status, comment.id)} /> ); }; diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index b4547335e..8c6655bb5 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -37,7 +37,7 @@ export default class CommentList extends React.Component { // 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.get(0)}); + this.setState({active: nextProps.commentIds[0]}); } } @@ -81,12 +81,12 @@ export default class CommentList extends React.Component { const {commentIds} = this.props; const {active} = this.state; // check boundaries - if (active === null || !commentIds.size) { - this.setState({active: commentIds.get(0)}); - } else if (direction === 'up' && active !== commentIds.first()) { - this.setState({active: commentIds.get(commentIds.indexOf(active) - 1)}); - } else if (direction === 'down' && active !== commentIds.last()) { - this.setState({active: commentIds.get(commentIds.indexOf(active) + 1)}); + 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 @@ -105,10 +105,10 @@ export default class CommentList extends React.Component { // activate the next comment if (id === this.state.active) { const {commentIds} = this.props; - if (commentIds.last() === this.state.active) { - this.setState({active: commentIds.get(commentIds.size - 2)}); + if (commentIds[commentIds.length - 1] === this.state.active) { + this.setState({active: commentIds[commentIds.length - 2]}); } else { - this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))}); + this.setState({active: commentIds[Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.length - 1)]}); } } this.props.onClickAction(action, id, author_id); @@ -125,10 +125,10 @@ export default class CommentList extends React.Component { return (
      {commentIds.map((commentId, index) => { - const comment = comments.get(commentId); + const comment = comments[commentId]; + const author = users[comment.author_id]; return { if (el && commentId === active) { this._active = el; } }} + author={author} key={index} index={index} onClickAction={this.onClickAction} @@ -137,7 +137,7 @@ export default class CommentList extends React.Component { actionsMap={actions} isActive={commentId === active} hideActive={hideActive} />; - }).toArray()} + })}
    ); } diff --git a/client/coral-admin/src/containers/CommentStream/CommentStream.js b/client/coral-admin/src/containers/CommentStream/CommentStream.js index 113a7bc86..556e50463 100644 --- a/client/coral-admin/src/containers/CommentStream/CommentStream.js +++ b/client/coral-admin/src/containers/CommentStream/CommentStream.js @@ -46,9 +46,9 @@ class CommentStream extends React.Component { @@ -58,4 +58,9 @@ class CommentStream extends React.Component { } } -export default connect(({comments, users}) => ({comments, users}))(CommentStream); +const mapStateToProps = state => ({ + comments: state.comments.toJS(), + users: state.users.toJS() +}); + +export default connect(mapStateToProps)(CommentStream); diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 194af720c..3a6a796e4 100644 --- a/client/coral-admin/src/containers/Configure/CommentSettings.js +++ b/client/coral-admin/src/containers/Configure/CommentSettings.js @@ -8,9 +8,25 @@ import { ListItemContent, ListItemAction, Textfield, - Checkbox + Checkbox, + Icon } from 'react-mdl'; +const updateCharCountEnable = (updateSettings, charCountChecked) => () => { + const charCountEnable = !charCountChecked; + updateSettings({charCountEnable}); +}; + +const updateCharCount = (updateSettings, settingsError) => (event) => { + const charCount = event.target.value; + if (charCount.match(/[^0-9]/) || charCount.length === 0) { + settingsError('charCount', true); + } else { + settingsError('charCount', false); + } + updateSettings({charCount: charCount}); +}; + const updateModeration = (updateSettings, mod) => () => { const moderation = mod === 'pre' ? 'post' : 'pre'; updateSettings({moderation}); @@ -31,48 +47,80 @@ const updateClosedMessage = (updateSettings) => (event) => { updateSettings({closedMessage}); }; -const CommentSettings = (props) => - - - - - {lang.t('configure.enable-pre-moderation')} - - - - - - - {lang.t('configure.include-comment-stream')} -

    - {lang.t('configure.include-comment-stream-desc')} +const CommentSettings = ({updateSettings, settingsError, settings, errors}) => + + + + + +

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

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

    -
    - - - - - - - - {lang.t('configure.closed-comments-desc')} - - - -
    ; + + + + + + +
    {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.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 388813a64..667bb4744 100644 --- a/client/coral-admin/src/containers/Configure/Configure.css +++ b/client/coral-admin/src/containers/Configure/Configure.css @@ -18,9 +18,27 @@ .configSetting { border: 1px solid #ccc; border-radius: 4px; - height: 90px; + height: 95px; margin-bottom: 10px; - cursor: pointer; + align-items: flex-start; +} + +.settingsError { + color: #d50000; +} + +.settingsError i { + font-size: 14px; + margin-right: 3px; +} + +.settingsHeader { + margin-top: 3px; + margin-bottom: 10px; +} + +.disabledSettingText { + color: #ccc; } .configSettingInfoBox { @@ -45,6 +63,22 @@ display: block; } +.charCountTexfield { + width: 4em; + padding: 0px; + border-color: #ccc; + border-style: solid; + border-width: 0px 0px 1px 0px; +} + +.charCountTexfieldEnabled { + border-color: #4caf50; +} + +.charCountTexfield:focus { + outline: none; +} + .changedSave { background-color:#4caf50; } @@ -84,7 +118,15 @@ margin-bottom:3px; } +.enabledSetting { + border-left-color: #4caf50; + border-left-style: solid; + border-left-width: 7px; +} +.disabledSetting { + padding-left: 22px; +} .hidden { display: none; diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index db3bf6f20..8dd106cd0 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -22,7 +22,8 @@ class Configure extends React.Component { this.state = { activeSection: 'comments', wordlist: [], - changed: false + changed: false, + errors: {} }; } @@ -64,12 +65,23 @@ class Configure extends React.Component { this.props.dispatch(updateSettings(setting)); } + // Sets an arbitrary error string and a boolean state. + // This allows the system to track multiple errors. + onSettingError = (error, state) => { + this.setState((prevState) => { + prevState.errors[error] = state; + return prevState; + }); + } + getSection = (section) => { switch(section){ case 'comments': return ; + updateSettings={this.onSettingUpdate} + errors={this.state.errors} + settingsError={this.onSettingError}/>; case 'embed': return ; case 'wordlist': @@ -94,6 +106,9 @@ class Configure extends React.Component { 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...'; } @@ -119,7 +134,7 @@ class Configure extends React.Component { { - this.state.changed ? + showSave ?