diff --git a/app.js b/app.js index cff8f6294..8008cb01e 100644 --- a/app.js +++ b/app.js @@ -97,7 +97,7 @@ app.use('/api', (err, req, res, next) => { res.status(err.status || 500); res.json({ message: err.message, - error: app.get('env') === 'development' ? err : null + error: app.get('env') === 'development' ? err : {} }); }); @@ -109,7 +109,7 @@ app.use('/', (err, req, res, next) => { res.status(err.status || 500); res.render('error', { message: err.message, - error: app.get('env') === 'development' ? err : null + error: app.get('env') === 'development' ? err : {} }); }); diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index cb5fcd89a..9887b46ac 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -61,8 +61,8 @@ class CommentStream extends Component { // Set up messaging between embedded Iframe an parent component // Using recommended Pym init code which violates .eslint standards const pym = new Pym.Child({polling: 100}); - const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1]; - this.props.getStream(path); + const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl); + this.props.getStream(path && path[1] || window.location); } render () { @@ -84,8 +84,9 @@ 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 {loggedIn, user} = this.props.auth; - return
+ const {actions, users, comments} = this.props.items; + const {loggedIn, user, showSignInDialog} = this.props.auth; + return
{ rootItem ?
@@ -105,16 +106,16 @@ class CommentStream extends Component { id={rootItemId} premod={this.props.config.moderation} reply={false} - canPost={loggedIn} + author={user} /> {!loggedIn && }
{ rootItem.comments && rootItem.comments.map((commentId) => { - const comment = this.props.items.comments[commentId]; + const comment = comments[commentId]; return

- +
@@ -124,7 +125,7 @@ class CommentStream extends Component { @@ -160,13 +162,13 @@ class CommentStream extends Component { let reply = this.props.items.comments[replyId]; return

- +
+ parent_id={reply.parent_id}/>
; diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 285dd3cb9..174f3ef16 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -3,8 +3,12 @@ body { font-family: 'Open Sans', sans-serif; width: 100%; font-size: 12px; - margin: 0; - min-height: 700px; + margin: 0px; + padding: 0px 0px 50px 0px; +} + +.expandForSignin { + min-height: 550px; } button { diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 537ffadc2..7313bb30f 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -3,6 +3,7 @@ import translations from './../translations'; const lang = new I18n(translations); import * as actions from '../constants/auth'; import {base, handleResp, getInit} from '../helpers/response'; +import {addItem} from './items'; // Dialog Actions export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG}); @@ -29,6 +30,7 @@ export const fetchSignIn = (formData) => dispatch => { .then(({user}) => { dispatch(hideSignInDialog()); dispatch(signInSuccess(user)); + dispatch(addItem(user, 'users')); }) .catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError')))); }; @@ -54,8 +56,10 @@ export const facebookCallback = (err, data) => dispatch => { return; } try { - dispatch(signInFacebookSuccess(JSON.parse(data))); + const user = JSON.parse(data); + dispatch(signInFacebookSuccess(user)); dispatch(hideSignInDialog()); + dispatch(addItem(user, 'users')); } catch (err) { dispatch(signInFacebookFailure(err)); return; diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index 419934c16..f922280c1 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -106,8 +106,16 @@ export function getStream (assetUrl) { /* Add items to the store */ const itemTypes = Object.keys(json); for (let i = 0; i < itemTypes.length; i++ ) { - for (let j = 0; j < json[itemTypes[i]].length; j++ ) { - dispatch(addItem(json[itemTypes[i]][j], itemTypes[i])); + if (itemTypes[i] === 'actions') { + for (let j = 0; j < json[itemTypes[i]].length; j++ ) { + let action = json[itemTypes[i]][j]; + action.id = `${action.action_type}_${action.item_id}`; + dispatch(addItem(action, 'actions')); + } + } else { + for (let j = 0; j < json[itemTypes[i]].length; j++ ) { + dispatch(addItem(json[itemTypes[i]][j], itemTypes[i])); + } } } diff --git a/client/coral-plugin-author-name/AuthorName.js b/client/coral-plugin-author-name/AuthorName.js index 585c1c07d..aef819468 100644 --- a/client/coral-plugin-author-name/AuthorName.js +++ b/client/coral-plugin-author-name/AuthorName.js @@ -1,9 +1,9 @@ import React from 'react'; const packagename = 'coral-plugin-author-name'; -const AuthorName = ({name}) => +const AuthorName = ({author}) =>
- {name} + {author && author.displayName}
; export default AuthorName; diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index e712d8e24..76f7e43be 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -12,7 +12,8 @@ class CommentBox extends Component { id: PropTypes.string, comments: PropTypes.array, reply: PropTypes.bool, - canPost: PropTypes.bool + canPost: PropTypes.bool, + currentUser: PropTypes.object } state = { @@ -21,11 +22,11 @@ class CommentBox extends Component { } postComment = () => { - const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod} = this.props; + const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod, author} = this.props; let comment = { body: this.state.body, asset_id: id, - username: this.state.username + author_id: author.id }; let related; let parent_type; @@ -52,7 +53,7 @@ class CommentBox extends Component { } render () { - const {styles, reply, canPost} = this.props; + const {styles, reply, author} = this.props; // How to handle language in plugins? Should we have a dependency on our central translation file? return
- { canPost && ( + { author && (
; diff --git a/client/coral-plugin-likes/LikeButton.js b/client/coral-plugin-likes/LikeButton.js index 1f3395a40..07f27c426 100644 --- a/client/coral-plugin-likes/LikeButton.js +++ b/client/coral-plugin-likes/LikeButton.js @@ -4,11 +4,14 @@ import translations from './translations.json'; const name = 'coral-plugin-flags'; -const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem}) => { +const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => { const liked = like && like.current_user; const onLikeClick = () => { + if (!currentUser) { + return; + } if (!liked) { - postAction(id, 'like', '123', 'comments') + postAction(id, 'like', currentUser.id, 'comments') .then((action) => { let id = `${action.action_type}_${action.item_id}`; addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions'); diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index a821006fb..1561f557b 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -106,7 +106,7 @@ input.error{ .userBox a { color: #2c69b6; cursor: pointer; - margin: 0 5px; + margin: 0px; } .attention { @@ -140,4 +140,3 @@ input.error{ border: 1px solid orange; background-color: 1px solid coral } - diff --git a/models/action.js b/models/action.js index 2f018b3be..91378b0b0 100644 --- a/models/action.js +++ b/models/action.js @@ -42,31 +42,58 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) { * @param {String} ids array of user identifiers (uuid) */ ActionSchema.statics.getActionSummaries = function(item_ids) { - return ActionSchema.statics.findByItemIdArray(item_ids).then((rawActions) => { - // Create an object with a count of each action type for each item - const actionSummaries = rawActions.reduce((actionObj, action) => { - if (!actionObj[action.item_id]) { - actionObj[action.item_id] = { - id: action.id, - item_type: action.item_type, - action_type: action.action_type, - count: 1, - current_user: false //Update this later when we have authentication - }; - } else { - actionObj[action.item_id].count ++; - } - return actionObj; - }, {}); + return Action.aggregate([ + { - // Return an array extracted from the actionSummaries object - return Object.keys(actionSummaries).reduce((actions, key) => { - let actionSummary = actionSummaries[key]; - actionSummary.item_id = key; - actions.push(actionSummary); - return actions; - }, []); - }); + // only grab items that match the specified item id's + $match: { + item_id: { + $in: item_ids + } + } + }, + { + $group: { + + // group unique documents by these properties, we are leveraging the + // fact that each uuid is completly unique. + _id: { + item_id: '$item_id', + action_type: '$action_type' + }, + + // and sum up all actions matching the above grouping criteria + count: { + $sum: 1 + }, + + // we are leveraging the fact that each uuid is completly unique and + // just grabbing the last instance of the item type here. + item_type: { + $last: '$item_type' + } + } + }, + { + $project: { + + // suppress the _id field + _id: false, + + // map the fields from the _id grouping down a level + item_id: '$_id.item_id', + action_type: '$_id.action_type', + + // map the field directly + count: '$count', + item_type: '$item_type', + + // set the current user to false here + current_user: {$literal: false} + } + } + ]) + .exec(); }; /* diff --git a/models/user.js b/models/user.js index 83677e12c..bde2363c2 100644 --- a/models/user.js +++ b/models/user.js @@ -402,7 +402,7 @@ UserService.findById = (id) => { }; /** - * Finds users in an array of idd. + * Finds users in an array of ids. * @param {Array} ids array of user identifiers (uuid) */ UserService.findByIdArray = (ids) => { @@ -411,6 +411,16 @@ UserService.findByIdArray = (ids) => { }); }; +/** + * Finds public user information by an array of ids. + * @param {Array} ids array of user identifiers (uuid) +*/ +UserService.findPublicByIdArray = (ids) => { + return UserModel.find({ + 'id': {$in: ids} + }, 'id displayName'); +}; + /** * Creates a JWT from a user email. Only works for local accounts. * @param {String} email of the local user diff --git a/swagger.yaml b/swagger.yaml index 8994b4ef9..fd3b0012b 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -342,9 +342,9 @@ definitions: type: string format: date-time description: Display name of comment - user_id: + author_id: type: string - description: Display name of comment + description: User who posted the comment parent_id: type: string description: Display name of comment diff --git a/tests/client/coral-framework/store/itemActions.spec.js b/tests/client/coral-framework/store/itemActions.spec.js index 0eb4165d3..340ec9549 100644 --- a/tests/client/coral-framework/store/itemActions.spec.js +++ b/tests/client/coral-framework/store/itemActions.spec.js @@ -29,21 +29,23 @@ describe('itemActions', () => { ], actions: [ { - type: 'like', - id: '123', + action_type: 'like', + item_id: '123', count: 1, + id: 'like_123', current_user: false }, { - type: 'flag', - id: '456', + action_type: 'flag', + item_id: '456', count: 5, + id: 'flag_456', current_user: true } ] }; - it('should get an stream from an asset_id and send the appropriate dispatches', () => { + it('should get an stream from an asset_url and send the appropriate dispatches', () => { fetchMock.get('*', JSON.stringify(response)); return actions.getStream(assetUrl)(store.dispatch) .then((res) => { diff --git a/tests/models/action.js b/tests/models/action.js index 257c191d6..78f5fd1d0 100644 --- a/tests/models/action.js +++ b/tests/models/action.js @@ -10,10 +10,6 @@ describe('Action: models', () => { action_type: 'flag', item_id: '123', item_type: 'comments' - }, { - action_type: 'like', - item_id: '789', - item_type: 'comments' }, { action_type: 'flag', item_id: '456', @@ -22,6 +18,10 @@ describe('Action: models', () => { action_type: 'flag', item_id: '123', item_type: 'comments' + }, { + action_type: 'like', + item_id: '123', + item_type: 'comments' }]).then((actions) => { mockActions = actions; }); @@ -39,7 +39,7 @@ describe('Action: models', () => { describe('#findByItemIdArray()', () => { it('should find an array of actions from an array of item_ids', () => { return Action.findByItemIdArray(['123', '456']).then((result) => { - expect(result).to.have.length(3); + expect(result).to.have.length(4); }); }); }); @@ -48,16 +48,17 @@ describe('Action: models', () => { it('should return properly formatted summaries from an array of item_ids', () => { return Action.getActionSummaries(['123', '789']).then((result) => { expect(result).to.have.length(2); + const sorted = result.sort((a, b) => a.count - b.count); - delete sorted[0].id; - delete sorted[1].id; + expect(sorted[0]).to.deep.equal({ action_type: 'like', count: 1, - item_id: '789', + item_id: '123', item_type: 'comments', current_user: false }); + expect(sorted[1]).to.deep.equal({ action_type: 'flag', count: 2, diff --git a/tests/models/user.js b/tests/models/user.js index 056aafeed..f77543b4b 100644 --- a/tests/models/user.js +++ b/tests/models/user.js @@ -43,6 +43,22 @@ describe('User: models', () => { }); }); + describe('#findPublicByIdArray()', () => { + it('should find an array of users from an array of ids', () => { + const ids = mockUsers.map((user) => user.id); + return User.findPublicByIdArray(ids).then((result) => { + expect(result).to.have.length(3); + const sorted = result.sort((a, b) => { + if(a.displayName < b.displayName) {return -1;} + if(a.displayName > b.displayName) {return 1;} + return 0; + }); + expect(sorted[0]).to.have.property('displayName') + .and.to.equal('Marvel'); + }); + }); + }); + describe('#findLocalUser', () => { it('should find a user when we give the right credentials', () => {