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/.eslintrc.json b/.eslintrc.json index 035a86189..d99b2254b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,6 +21,8 @@ "no-eval": [2], "no-global-assign": [2], "no-implied-eval": [2], + "lines-around-comment": ["warn", {"beforeLineComment": true}], + "spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }], "no-script-url": [2], "no-throw-literal": [2], "yoda": [1], diff --git a/app.js b/app.js index 15a7a5442..79d09db65 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,7 @@ if (app.get('env') !== 'test') { //============================================================================== app.set('trust proxy', 1); + // We disable frameward on helmet to allow crossdomain injection of the embed app.use(helmet({ frameguard: false diff --git a/bin/cli-serve b/bin/cli-serve index c22920eaa..5478b7c1e 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -60,11 +60,13 @@ function normalizePort(val) { let port = parseInt(val, 10); if (isNaN(port)) { + // named pipe return val; } if (port >= 0) { + // port number return port; } 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/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index 8c6655bb5..597ea62a6 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -31,6 +31,7 @@ export default class CommentList extends React.Component { // 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 } @@ -80,6 +81,7 @@ export default class CommentList extends React.Component { const {commentIds} = this.props; const {active} = this.state; + // check boundaries if (active === null || !commentIds.length) { this.setState({active: commentIds[0]}); @@ -102,6 +104,7 @@ export default class CommentList extends React.Component { // 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; diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 3a6a796e4..050432743 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,11 +54,32 @@ const updateClosedMessage = (updateSettings) => (event) => { updateSettings({closedMessage}); }; -const CommentSettings = ({updateSettings, settingsError, settings, errors}) => +// 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 = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => { + if (fetchingSettings) { + /* maybe a spinner here at some point */ + return

Loading settings...

; + } + + return @@ -64,7 +92,7 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => < @@ -91,7 +119,7 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => < @@ -110,6 +138,29 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => < rows={3}/> + + + {lang.t('configure.close-after')} +
+ +
+ + + + + +
+
+
{lang.t('configure.closed-comments-desc')} @@ -121,7 +172,24 @@ 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/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index 8dd106cd0..ed8ba9b26 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -78,6 +78,7 @@ class Configure extends React.Component { switch(section){ case 'comments': return { + this.setState({modalOpen: true}); + } + onTabClick (activeTab) { this.setState({activeTab}); } @@ -93,6 +100,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')} +

this.hideBanUserDialog()} onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)} user={comments.banUser}/> -
+
- this.setState({modalOpen: false})} /> +
+ this.setState({modalOpen: false})} /> +
); + } } diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index ab542b4a0..7c9bc968f 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -38,6 +38,7 @@ Promise.all([ coralApi('/comments?action_type=flag') ]) .then(([pending, rejected, flagged]) => { + /* Combine seperate calls into a single object */ let all = {}; all.comments = pending.comments @@ -55,6 +56,7 @@ Promise.all([ return all; }) .then(all => { + /* Post comments and users to redux store. Actions will be posted when they are needed. */ store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS', users: all.users}); @@ -62,6 +64,7 @@ Promise.all([ comments: all.comments}); }); + // .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error})); // Update a comment. Now to update a comment we need to send back the whole object diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index e462f7567..6f8e76c4b 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -28,7 +28,8 @@ "nextcomment": "Go to the next comment", "prevcomment": "Go to the previous comment", "singleview": "Toggle single comment edit view", - "thismenu": "Open this menu" + "thismenu": "Open this menu", + "showshortcuts": "Show Shortcuts" }, "comment": { "flagged": "flagged", @@ -57,6 +58,10 @@ "community": "Community", "closed-comments-desc": "Write a message for closed threads", "closed-comments-label": "Write a message...", + "hours": "Hours", + "days": "Days", + "weeks": "Weeks", + "close-after": "Close comments after", "comment-count-header": "Limit Comment Length", "comment-count-text-pre": "Comments will be limited to ", "comment-count-text-post": " characters.", @@ -91,7 +96,8 @@ "rejected": "rechazado", "flagged": "marcado", "shortcuts": "Atajos de teclado", - "close": "Cerrar" + "close": "Cerrar", + "showshortcuts": "Mostrar atajos" }, "comment": { "flagged": "marcado", @@ -117,6 +123,11 @@ "community": "Comunidad", "closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados", "closed-comments-label": "Escribe un mensaje...", + "never": "Nunca", + "hours": "Horas", + "days": "Días", + "weeks": "Semanas", + "close-after": "Cerrar comentarios luego de", "comment-count-header": "Limitar el largo del comentario", "comment-count-text-pre": "El largo de comentarios será ", "comment-count-text-post": " caracteres", diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index d4b71f4e5..71da184ab 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -1,11 +1,14 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; +import {I18n} from '../../coral-framework'; import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config'; import CloseCommentsInfo from '../components/CloseCommentsInfo'; import ConfigureCommentStream from '../components/ConfigureCommentStream'; +const lang = new I18n(); + class ConfigureStreamContainer extends Component { constructor (props) { super(props); @@ -47,8 +50,15 @@ class ConfigureStreamContainer extends Component { this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open'); } + getClosedIn () { + const {closedTimeout} = this.props.config; + const {created_at} = this.props.asset; + return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout)); + } + render () { const {status} = this.props; + return (

{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 c052e5aa1..7d1e6c612 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -58,10 +58,15 @@ class CommentStream extends Component { } componentDidMount () { + // 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; diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index 8f0e789af..160dcb24c 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'; @@ -114,6 +118,7 @@ export function getStream (assetUrl) { /* Sort comments by date*/ json.comments.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); const rels = json.comments.reduce((h, item) => { + /* Check for root and child comments. */ if ( item.asset_id === assetId && 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/modules/i18n/i18n.js b/client/coral-framework/modules/i18n/i18n.js index 1d81e885c..d4ed36b09 100644 --- a/client/coral-framework/modules/i18n/i18n.js +++ b/client/coral-framework/modules/i18n/i18n.js @@ -9,6 +9,7 @@ import get from 'lodash/get'; class i18n { constructor (translations) { + /** * Register locales */ @@ -16,6 +17,7 @@ class i18n { this.locales = {'en': 'en', 'es': 'es'}; timeago.register('es_ES', esTA); this.timeagoInstance = new timeago(); + /** * Load translations */ @@ -55,6 +57,7 @@ class i18n { this.t = (key, ...replacements) => { if (has(this.translations, key)) { let translation = get(this.translations, key); + // replace any {n} with the arguments passed to this method replacements.forEach((str, i) => { translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str); 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 086a0cf36..fe67b22e0 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -103,7 +103,7 @@ class CommentBox extends Component {
{ author && (