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/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js index 1867b9ac2..1b4af6eb1 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/components/BanUserDialog.js @@ -14,7 +14,7 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => { return ( handleClose()} onCancel={() => handleClose()} title={lang.t('bandialog.ban_user')}> - handleClose()}>× + handleClose()}>×

diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 3a6a796e4..5819076c6 100644 --- a/client/coral-admin/src/containers/Configure/CommentSettings.js +++ b/client/coral-admin/src/containers/Configure/CommentSettings.js @@ -1,4 +1,5 @@ import React from 'react'; +import {SelectField, Option} from 'react-mdl-selectfield'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; import styles from './Configure.css'; @@ -12,6 +13,12 @@ import { Icon } from 'react-mdl'; +const TIMESTAMPS = { + weeks: 60 * 60 * 24 * 7, + days: 60 * 60 * 24, + hours: 60 * 60 +}; + const updateCharCountEnable = (updateSettings, charCountChecked) => () => { const charCountEnable = !charCountChecked; updateSettings({charCountEnable}); @@ -47,6 +54,21 @@ const updateClosedMessage = (updateSettings) => (event) => { updateSettings({closedMessage}); }; +// If we are changing the measure we need to recalculate using the old amount +// Same thing if we are just changing the amount +const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => { + if (isMeasure) { + const amount = getTimeoutAmount(ts); + const closedTimeout = amount * TIMESTAMPS[event]; + updateSettings({closedTimeout}); + } else { + const val = event.target.value; + const measure = getTimeoutMeasure(ts); + const closedTimeout = val * TIMESTAMPS[measure]; + updateSettings({closedTimeout}); + } +}; + const CommentSettings = ({updateSettings, settingsError, settings, errors}) => @@ -110,6 +132,27 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => < rows={3}/> + + + {lang.t('configure.close-after')} +
+ +
+ + + + + +
+
+
{lang.t('configure.closed-comments-desc')} @@ -124,4 +167,20 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => < export default CommentSettings; +// To see if we are talking about weeks, days or hours +// We talk the remainder of the division and see if it's 0 +const getTimeoutMeasure = ts => { + if (ts % TIMESTAMPS['weeks'] === 0) { + return 'weeks'; + } else if (ts % TIMESTAMPS['days'] === 0) { + return 'days'; + } else if (ts % TIMESTAMPS['hours'] === 0) { + return 'hours'; + } +}; + +// Dividing the amount by it's measure (hours, days, weeks) we +// obtain the amount of time +const getTimeoutAmount = ts => ts / TIMESTAMPS[getTimeoutMeasure(ts)]; + const lang = new I18n(translations); diff --git a/client/coral-admin/src/containers/Configure/Configure.css b/client/coral-admin/src/containers/Configure/Configure.css index 667bb4744..c0646c9e2 100644 --- a/client/coral-admin/src/containers/Configure/Configure.css +++ b/client/coral-admin/src/containers/Configure/Configure.css @@ -63,6 +63,11 @@ display: block; } +.configTimeoutSelect { + display: inline-block; + margin-left: 20px; +} + .charCountTexfield { width: 4em; padding: 0px; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css index 6d40b5935..802fb88be 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.css @@ -14,6 +14,18 @@ color: white; } +.showShortcuts { + position: absolute; + right: 130px; + display: flex; + align-items: center; + font-size: 13px; + + span { + margin-left: 7px; + } +} + @media (--big-viewport) { .tab { flex: none; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index e0e3c636f..339a1fa55 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -1,5 +1,6 @@ import React from 'react'; import {connect} from 'react-redux'; +import {Icon} from 'react-mdl'; import key from 'keymaster'; import ModerationKeysModal from 'components/ModerationKeysModal'; @@ -70,6 +71,10 @@ class ModerationQueue extends React.Component { this.props.dispatch(banUser('banned', userId, commentId)); } + showShortcuts = () => { + this.setState({modalOpen: true}); + } + onTabClick (activeTab) { this.setState({activeTab}); } @@ -93,6 +98,11 @@ class ModerationQueue extends React.Component { className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')} this.onTabClick('flagged')} className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')} + + + {lang.t('modqueue.showshortcuts')} +


{status === 'open' ? 'Close' : 'Open'} Comment Stream

+ {status === 'open' ?

The comment stream will close in {this.getClosedIn()}.

: ''} ({ - config: state.config.toJS() + config: state.config.toJS(), + asset: state.items + .get('assets') + .first() + .toJS() }); const mapDispatchToProps = dispatch => ({ diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index b53832ce5..e6cc8fbbc 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -61,7 +61,11 @@ class CommentStream extends Component { // Set up messaging between embedded Iframe an parent component this.pym = new Pym.Child({polling: 100}); - const path = this.pym.parentUrl.split('#')[0]; + let path = this.pym.parentUrl.split('#')[0]; + + if (!path) { + path = window.location.href.split('#')[0]; + } this.props.getStream(path || window.location); this.path = path; @@ -90,7 +94,7 @@ class CommentStream extends Component { const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0]; const rootItem = this.props.items.assets && this.props.items.assets[rootItemId]; const {actions, users, comments} = this.props.items; - const {status, moderation, closedMessage} = this.props.config; + const {status, moderation, closedMessage, charCount, charCountEnable} = this.props.config; const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth; const {activeTab} = this.state; const banned = (this.props.userData.status === 'banned'); @@ -128,7 +132,7 @@ class CommentStream extends Component { currentUser={this.props.auth.user} banned={banned} author={user} - /> + charCount={charCountEnable && charCount}/>
:

{closedMessage}

@@ -198,6 +202,7 @@ class CommentStream extends Component { parent_id={commentId} premod={moderation} currentUser={user} + charCount={charCountEnable && charCount} showReply={comment.showReply}/> { comment.children && @@ -257,6 +262,7 @@ class CommentStream extends Component { premod={moderation} banned={banned} currentUser={user} + charCount={charCountEnable && charCount} showReply={reply.showReply}/>
; }) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index c27baf406..463c8dc68 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -95,7 +95,7 @@ hr { margin-top: 10px; } -#coralStream .coral-plugin-commentbox-button { +.coral-plugin-commentbox-button { float: right; margin-top: 10px; padding: 5px 10px; @@ -111,6 +111,16 @@ hr { margin-bottom: 5px; } +.coral-plugin-commentbox-char-count { + color: #ccc; + text-align: right; + font-size: 12px; +} + +.coral-plugin-commentbox-char-max { + color: #d50000; +} + /* Comment styles */ .comment { margin-bottom: 10px; diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index 8f0e789af..cd798c6b9 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -2,6 +2,10 @@ import coralApi from '../helpers/response'; import {fromJS} from 'immutable'; import {UPDATE_CONFIG} from '../constants/config'; +/** +* Action name constants +*/ + export const ADD_ITEM = 'ADD_ITEM'; export const UPDATE_ITEM = 'UPDATE_ITEM'; export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY'; diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index cda2d765d..0ee8659d8 100644 --- a/client/coral-framework/actions/user.js +++ b/client/coral-framework/actions/user.js @@ -1,5 +1,7 @@ import * as actions from '../constants/user'; +import * as assetActions from '../constants/assets'; import {addNotification} from '../actions/notification'; +import {addItem} from '../actions/items'; import coralApi from '../helpers/response'; import I18n from 'coral-framework/modules/i18n/i18n'; @@ -19,3 +21,30 @@ export const saveBio = (user_id, formData) => dispatch => { }) .catch(error => dispatch(saveBioFailure(error))); }; + +/** + * + * Get a list of comments by a single user + * + * @param {string} user_id + * @returns Promise + */ +export const fetchCommentsByUserId = userId => { + return (dispatch) => { + dispatch({type: actions.COMMENTS_BY_USER_REQUEST}); + return coralApi(`/comments?user_id=${userId}`) + .then(({comments, assets}) => { + comments.forEach(comment => dispatch(addItem(comment, 'comments'))); + + assets.forEach(asset => dispatch(addItem(asset, 'assets'))); + + dispatch({type: actions.COMMENTS_BY_USER_SUCCESS, comments: comments.map(comment => comment.id)}); + dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)}); + }) + .catch(error => { + console.error(error.stack); + console.error('FAILURE_COMMENTS_BY_USER', error); + dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error}); + }); + }; +}; diff --git a/client/coral-framework/constants/assets.js b/client/coral-framework/constants/assets.js new file mode 100644 index 000000000..3883ee835 --- /dev/null +++ b/client/coral-framework/constants/assets.js @@ -0,0 +1,3 @@ +export const MULTIPLE_ASSETS_REQUEST = 'MULTIPLE_ASSETS_REQUEST'; +export const MULTIPLE_ASSETS_SUCCESS = 'MULTIPLE_ASSETS_SUCCESS'; +export const MULTIPLE_ASSSETS_FAILURE = 'MULTIPLE_ASSSETS_FAILURE'; diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js index 0c316d48a..6e09726d3 100644 --- a/client/coral-framework/constants/user.js +++ b/client/coral-framework/constants/user.js @@ -1,3 +1,6 @@ export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST'; export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS'; export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE'; +export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST'; +export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS'; +export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE'; diff --git a/client/coral-framework/reducers/items.js b/client/coral-framework/reducers/items.js index 93388c1ab..268557809 100644 --- a/client/coral-framework/reducers/items.js +++ b/client/coral-framework/reducers/items.js @@ -6,6 +6,7 @@ import * as actions from '../actions/items'; const initialState = fromJS({ comments: {}, users: {}, + assets: {}, actions: {} }); diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js index 11b57fc15..bd5f78e87 100644 --- a/client/coral-framework/reducers/user.js +++ b/client/coral-framework/reducers/user.js @@ -1,11 +1,14 @@ import {Map} from 'immutable'; import * as authActions from '../constants/auth'; import * as actions from '../constants/user'; +import * as assetActions from '../constants/assets'; const initialState = Map({ displayName: '', profiles: [], - settings: {} + settings: {}, + myComments: [], + myAssets: [] // the assets from which myComments (above) originated }); const purge = user => { @@ -30,6 +33,10 @@ export default function user (state = initialState, action) { case actions.SAVE_BIO_SUCCESS: return state .set('settings', action.settings); + case actions.COMMENTS_BY_USER_SUCCESS: + return state.set('myComments', action.comments); + case assetActions.MULTIPLE_ASSETS_SUCCESS: + return state.set('myAssets', action.assets); default : return state; } diff --git a/client/coral-plugin-comment-count/CommentCount.js b/client/coral-plugin-comment-count/CommentCount.js index 7a27c1982..87f656c22 100644 --- a/client/coral-plugin-comment-count/CommentCount.js +++ b/client/coral-plugin-comment-count/CommentCount.js @@ -1,20 +1,23 @@ import React from 'react'; import {I18n} from '../coral-framework'; import translations from './translations.json'; +import has from 'lodash/has'; +import reduce from 'lodash/reduce'; const name = 'coral-plugin-comment-count'; const CommentCount = ({items, id}) => { let count = 0; - if (items.assets[id] && items.assets[id].comments) { + if (has(items, `assets.${id}.comments`)) { count += items.assets[id].comments.length; } - const itemKeys = Object.keys(items.comments); - for (let i = 0; i < itemKeys.length; i++) { - const item = items.comments[itemKeys[i]]; - if (item.children) { - count += item.children.length; + + // lodash reduce works on {} + count += reduce(items.comments, (accum, comment) => { + if (comment.children) { + accum += comment.children.length; } - } + return accum; + }, 0); return
{`${count} ${count === 1 ? lang.t('comment') : lang.t('comment-plural')}`} diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 501d2ecc9..fe67b22e0 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -23,7 +23,18 @@ class CommentBox extends Component { } postComment = () => { - const {postItem, updateItem, id, parent_id, child_id, addNotification, appendItemArray, premod, author} = this.props; + const { + postItem, + updateItem, + id, + parent_id, + child_id, + addNotification, + appendItemArray, + premod, + author + } = this.props; + let comment = { body: this.state.body, asset_id: id, @@ -42,6 +53,10 @@ class CommentBox extends Component { if (child_id || parent_id) { updateItem(child_id || parent_id, 'showReply', false, 'comments'); } + + if (this.props.charCount && this.state.body.length > this.props.charCount) { + return; + } postItem(comment, 'comments') .then((postedComment) => { const commentId = postedComment.id; @@ -59,8 +74,8 @@ class CommentBox extends Component { } render () { - const {styles, reply, author} = this.props; - // How to handle language in plugins? Should we have a dependency on our central translation file? + const {styles, reply, author, charCount} = this.props; + const length = this.state.body.length; return
@@ -79,10 +94,16 @@ class CommentBox extends Component { onChange={(e) => this.setState({body: e.target.value})} rows={3}/>
+
charCount ? `${name}-char-max` : ''}`}> + { + charCount && + `${charCount - length} ${lang.t('characters-remaining')}` + } +
{ author && (