From 5ff27be16067edc86f88e0c59c0df5674edcc46f Mon Sep 17 00:00:00 2001 From: David Erwin Date: Tue, 22 Nov 2016 11:59:54 -0500 Subject: [PATCH 01/21] Add CONTRIBUTING.md first draft --- CONTRIBUTING.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..91ed3cf34 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# Contribution Guide + +We're very excited that you're interested in contributing to Talk! There is much to do. Before you begin, please review this document to get a sense of the practices and philosophies that hold this project together. + + +## Doing the Work + +We are here to make it as seamless as possible to contribute to Talk. The following lists and meant to make it straight forward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content. + + +### Code Reviews + +Code Reviews one of the most valuable aspects of working in software. It is something that should challenge the reviewer and and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants. + +Code reviews are a collaboration to make _the work_ as good as it can be. Code reviews are not a good venue for providing direct instruction to _the author._ Focus on positive, incremental improvements that can be made on the work at hand. + +Please take your time when writing and reviewing code. Here are some fundamental questions to open up a reviewing headspace. + +**Is the code clear, efficient and a pleasure to read?** + +Somewhere at the intersection of good variable names, well laid out file structures, consistent formatting and appropriate comments lies beautiful code. Code is language spoken to at least two very distinct audiences, the computer that interprets it and the developer who encounters it. Both should be at the front of your mind when reviewing code. + +Thinking like a computer, you could ask: + +* Is the code using memory efficiently? +* Is data being moved around unnecessarily? +* Are multiple network requests being made where fewer would do? +* Is there excess processing happening in a synchronous flow that may disrupt user experience? +* Are there large libraries included for small gains? + +Then, returning to your human roots... Is the code readable? + +* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward? +* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices? +* Are the files separated logically such that each one contains a clear concept of code? + + +**Is the API documentation up to date? Are all client calls written against the docs?** + +We use swagger to track our API documentation: [https://github.com/coralproject/talk/blob/master/swagger.yaml](https://github.com/coralproject/talk/blob/master/swagger.yaml) + +* If APIs are created or updated, is the swagger.yml file up to date? There's nothing more frustrating than trying to develop against docs that are out of date or wrong. We need to be meticulous here as it's the little differences that can cause the most frustration and tricky bugs. +* If client code calls apis, are they written against the swagger.yml file? Are all return codes handled? + +**Is there sufficient test coverage?** + +Our tests folder is set up to mirror the code folders: [https://github.com/coralproject/talk/tree/master/tests](https://github.com/coralproject/talk/tree/master/tests) + +* Can you a sense of the logic behind the code by reading the tests? +* Can you see both what should happen and what should _never, never_ be allowed to happen? +* Are there future cases that are guarded against via the creation of unit tests (aka, making sure things are typed, specifically checking for all values that will be used, etc...)? + + +### Forking, Branching and Merging + +Talk follows the _master as tip_ repo structure. Master is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added. + +Releases are _tagged_ off the master branch. + +Contributions to Talk follow this process. There are a lot of steps, but mechanically following these steps will standardize communication, help stop errors and let you focus on your contribution. + +* At the outset of a piece of work, a branch or fork is made from master. +* The work is done in that fork. +* As soon as the work has taken shape, a PR is created for discussion. (If the PR is created for review before it's ready to merge, please make that clear in the description/title.) +* At least one other contributor to the project must review all code (see Code Reviews below.) +* If there are merge conflicts with master, merge master into the branch. +* Ensure that circleci passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.) +* If merge conflicts exist with master, merge master into your branch and re-run CI before merging into master. +* Merge to master, but _you're not quite done yet!_ +* Deploy master to staging (or have a core member do so.) +* Ensure that all your changes are working on staging. +* Have your reviewer verify the same. +* ... aaaand the work is delivered! + + +## Continuous Integration + +We use circleci to run our ci: [https://circleci.com/gh/coralproject/talk](https://circleci.com/gh/coralproject/talk) + +Our pipeline will _test_, _lint_, and _build_ all pushes to the repo. + +Any branch not passing CI will not be merged into master. + +If you're working in a fork, please run each of the steps locally before submitting a PR. + + +## Coding Style + +### API Design + +When building APIs, we follow these principles: + +* Follow [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) principles for basic operations. +* Avoid routing yourself into a corner, for example, by putting a variable other than an object's id directly after an object. +* Put non-required, flexible variables into query params, required/identity based values in request params. + + + + + + + + From 66de272c56fc37295935a49f6913d14507040d5f Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 23 Nov 2016 13:38:24 -0700 Subject: [PATCH 02/21] typos and such --- CONTRIBUTING.md | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91ed3cf34..9fa7985c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,12 +5,12 @@ We're very excited that you're interested in contributing to Talk! There is much ## Doing the Work -We are here to make it as seamless as possible to contribute to Talk. The following lists and meant to make it straight forward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content. +We are here to make it as seamless as possible to contribute to Talk. The following lists are meant to make it straightforward to perform the mechanics of working on the project so you can focus your energy toward writing and reviewing content. ### Code Reviews -Code Reviews one of the most valuable aspects of working in software. It is something that should challenge the reviewer and and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants. +One of the most valuable aspects of working in software. It is something that should challenge the reviewer and author alike. It is a way of focusing knowledge, experience and opinions for the benefit of the project and the participants. Code reviews are a collaboration to make _the work_ as good as it can be. Code reviews are not a good venue for providing direct instruction to _the author._ Focus on positive, incremental improvements that can be made on the work at hand. @@ -22,53 +22,53 @@ Somewhere at the intersection of good variable names, well laid out file structu Thinking like a computer, you could ask: -* Is the code using memory efficiently? -* Is data being moved around unnecessarily? -* Are multiple network requests being made where fewer would do? -* Is there excess processing happening in a synchronous flow that may disrupt user experience? +* Is the code using memory efficiently? +* Is data being moved around unnecessarily? +* Are multiple network requests being made where fewer would do? +* Is there excess processing happening in a synchronous flow that may disrupt user experience? * Are there large libraries included for small gains? -Then, returning to your human roots... Is the code readable? +Then, returning to your human roots... Is the code readable? -* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward? -* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices? +* Can I understand what is happening here (and maybe even why) by simply opening up the file, starting at the top and reading downward? +* Do comments convey clear, full thoughts in a narrative language that provides background for the code choices? * Are the files separated logically such that each one contains a clear concept of code? **Is the API documentation up to date? Are all client calls written against the docs?** -We use swagger to track our API documentation: [https://github.com/coralproject/talk/blob/master/swagger.yaml](https://github.com/coralproject/talk/blob/master/swagger.yaml) +We use [swagger](https://github.com/coralproject/talk/blob/master/swagger.yaml) to track our API documentation. * If APIs are created or updated, is the swagger.yml file up to date? There's nothing more frustrating than trying to develop against docs that are out of date or wrong. We need to be meticulous here as it's the little differences that can cause the most frustration and tricky bugs. -* If client code calls apis, are they written against the swagger.yml file? Are all return codes handled? +* If client code calls APIs, are they written against the swagger.yml file? Are all return codes handled? **Is there sufficient test coverage?** Our tests folder is set up to mirror the code folders: [https://github.com/coralproject/talk/tree/master/tests](https://github.com/coralproject/talk/tree/master/tests) -* Can you a sense of the logic behind the code by reading the tests? -* Can you see both what should happen and what should _never, never_ be allowed to happen? +* Can you a sense of the logic behind the code by reading the tests? +* Can you see both what should happen and what should _never, ever_ be allowed to happen? * Are there future cases that are guarded against via the creation of unit tests (aka, making sure things are typed, specifically checking for all values that will be used, etc...)? ### Forking, Branching and Merging -Talk follows the _master as tip_ repo structure. Master is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added. +Talk follows the _master as tip_ repo structure. `master` is the bleeding edge. It should be _as stable as possible_ but may suffer instabilities, generally during times that fundamental architectural elements are added. Releases are _tagged_ off the master branch. Contributions to Talk follow this process. There are a lot of steps, but mechanically following these steps will standardize communication, help stop errors and let you focus on your contribution. -* At the outset of a piece of work, a branch or fork is made from master. -* The work is done in that fork. +* At the outset of a piece of work, a branch or fork is made from master. +* The work is done in that fork. * As soon as the work has taken shape, a PR is created for discussion. (If the PR is created for review before it's ready to merge, please make that clear in the description/title.) * At least one other contributor to the project must review all code (see Code Reviews below.) * If there are merge conflicts with master, merge master into the branch. -* Ensure that circleci passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.) -* If merge conflicts exist with master, merge master into your branch and re-run CI before merging into master. +* Ensure that [circleci](https://circleci.com/) passes all tests for your branch. (If you have forked and do not have circleci set up, you and the reviewer should independently ensure that all the of Continuous Integration steps pass before merging.) +* If merge conflicts exist with `master`, merge `master` into your branch and re-run CI before merging into master. * Merge to master, but _you're not quite done yet!_ * Deploy master to staging (or have a core member do so.) -* Ensure that all your changes are working on staging. +* Ensure that all your changes are working on staging. * Have your reviewer verify the same. * ... aaaand the work is delivered! @@ -93,11 +93,3 @@ When building APIs, we follow these principles: * Follow [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) principles for basic operations. * Avoid routing yourself into a corner, for example, by putting a variable other than an object's id directly after an object. * Put non-required, flexible variables into query params, required/identity based values in request params. - - - - - - - - From 8af300a9ec4aaf824ac35a81541ba49c4c0f3822 Mon Sep 17 00:00:00 2001 From: David Jay Date: Mon, 28 Nov 2016 15:51:15 -0500 Subject: [PATCH 03/21] Adding users, comments, and actions to endpoint and moving to store. --- client/coral-admin/src/components/Comment.js | 15 ++++---- .../coral-admin/src/components/CommentList.js | 12 +++--- .../ModerationQueue/ModerationQueue.js | 7 +++- client/coral-admin/src/reducers/index.js | 5 ++- client/coral-admin/src/reducers/users.js | 20 ++++++++++ .../coral-admin/src/services/talk-adapter.js | 8 +++- routes/api/queue/index.js | 24 ++++++++++-- tests/routes/api/queue/index.js | 37 ++++++++++++------- 8 files changed, 92 insertions(+), 36 deletions(-) create mode 100644 client/coral-admin/src/reducers/users.js diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 7cba85788..3481ad671 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -12,26 +12,27 @@ const linkify = new Linkify(); // Render a single comment for the list export default props => { - const links = linkify.getMatches(props.comment.get('body')); + const {comment, author} = props; + const links = linkify.getMatches(comment.get('body')); return (
  • person - {props.comment.get('name') || lang.t('comment.anon')} - {timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - {props.comment.get('flagged') ?

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

    : null} + {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}
    {links ? Contains Link : null}
    - {props.actions.map((action, i) => canShowAction(action, props.comment) ? ( + {props.actions.map((action, i) => canShowAction(action, comment) ? ( props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))} + onClick={() => props.onClickAction(props.actionsMap[action].status, comment.get('id'))} /> ) : null)}
    @@ -40,7 +41,7 @@ export default props => {
    - {props.comment.get('body')} + {comment.get('body')}
    diff --git a/client/coral-admin/src/components/CommentList.js b/client/coral-admin/src/components/CommentList.js index e4682252d..40b99b892 100644 --- a/client/coral-admin/src/components/CommentList.js +++ b/client/coral-admin/src/components/CommentList.js @@ -112,13 +112,15 @@ export default class CommentList extends React.Component { } render () { - const {singleView, commentIds, comments, hideActive, key} = this.props; + const {singleView, commentIds, comments, users, hideActive, key} = this.props; const {active} = this.state; return (
      - {commentIds.map((commentId, index) => ( - { + const comment = comments.get(commentId); + return { if (el && commentId === active) { this._active = el; } }} key={index} index={index} @@ -126,8 +128,8 @@ export default class CommentList extends React.Component { actions={this.props.actions} actionsMap={actions} isActive={commentId === active} - hideActive={hideActive} /> - )).toArray()} + hideActive={hideActive} />; + }).toArray()}
    ); } diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index a4d82fddc..956d3df15 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -61,7 +61,7 @@ class ModerationQueue extends React.Component { // Render the tabbed lists moderation queues render () { - const {comments} = this.props; + const {comments, users} = this.props; const {activeTab, singleView, modalOpen} = this.state; return ( @@ -86,6 +86,7 @@ class ModerationQueue extends React.Component { .get('status')) } comments={comments.get('byId')} + users={users.get('byId')} onClickAction={(action, id) => this.onCommentAction(action, id)} actions={['reject', 'approve']} loading={comments.loading} /> @@ -104,6 +105,7 @@ class ModerationQueue extends React.Component { .get('status') === 'rejected') } comments={comments.get('byId')} + users={users.get('byId')} onClickAction={(action, id) => this.onCommentAction(action, id)} actions={['approve']} loading={comments.loading} /> @@ -117,6 +119,7 @@ class ModerationQueue extends React.Component { return !data.get('status') && data.get('flagged') === true; })} comments={comments.get('byId')} + users={users.get('byId')} onClickAction={(action, id) => this.onCommentAction(action, id)} actions={['reject', 'approve']} loading={comments.loading} /> @@ -129,6 +132,6 @@ class ModerationQueue extends React.Component { } } -export default connect(({comments}) => ({comments}))(ModerationQueue); +export default connect(({comments, users}) => ({comments, users}))(ModerationQueue); const lang = new I18n(translations); diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 61029539a..1f1b444fc 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -2,6 +2,7 @@ import {combineReducers} from 'redux'; import comments from 'reducers/comments'; import settings from 'reducers/settings'; import community from 'reducers/community'; +import users from 'reducers/users'; import auth from 'reducers/auth'; // Combine all reducers into a main one @@ -9,6 +10,6 @@ export default combineReducers({ settings, comments, community, - auth + auth, + users }); - diff --git a/client/coral-admin/src/reducers/users.js b/client/coral-admin/src/reducers/users.js new file mode 100644 index 000000000..872ae904a --- /dev/null +++ b/client/coral-admin/src/reducers/users.js @@ -0,0 +1,20 @@ +import {Map, List, fromJS} from 'immutable'; + +const initialState = Map({ + byId: Map(), + ids: List() +}); + +export default (state = initialState, action) => { + switch (action.type) { + case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state); + default: return state; + } +}; + +// Replace the comment list with a new one +const replaceUsers = (action, state) => { + const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {})); + return state.set('byId', users) + .set('ids', List(users.keys())); +}; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 361a6479e..dfdcec342 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -44,8 +44,12 @@ Promise.all([ res[2] = res[2].map(comment => { comment.flagged = true; return comment; }); return res.reduce((prev, curr) => prev.concat(curr), []); }) -.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', - comments: res})) +.then(res => { + store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', + comments: res.comments}); + store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS', + users: res.users}); +}) .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/routes/api/queue/index.js b/routes/api/queue/index.js index f661992f1..448149387 100644 --- a/routes/api/queue/index.js +++ b/routes/api/queue/index.js @@ -1,6 +1,9 @@ const express = require('express'); const Comment = require('../../../models/comment'); +const User = require('../../../models/user'); +const Action = require('../../../models/action'); const Setting = require('../../../models/setting'); +const _ = require('lodash'); const router = express.Router(); @@ -13,11 +16,24 @@ const router = express.Router(); // Pre-moderation: New comments are shown in the moderator queues immediately. // Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users. router.get('/comments/pending', (req, res, next) => { - Setting.getModerationSetting().then(function({moderation}){ - Comment.moderationQueue(moderation).then((comments) => { - res.status(200).json(comments); - }); + Setting.getModerationSetting().then(({moderation}) => + Comment.moderationQueue(moderation)) + .then((comments) => { + return Promise.all([ + comments, + User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))), + Action.getActionSummaries(_.uniq([ + ...comments.map((comment) => comment.id), + ...comments.map((comment) => comment.author_id) + ])) + ]); }) + .then(([comments, users, actions])=> + res.status(200).json({ + comments, + users, + actions + })) .catch(error => { next(error); }); diff --git a/tests/routes/api/queue/index.js b/tests/routes/api/queue/index.js index 74137a49a..1daaacbb3 100644 --- a/tests/routes/api/queue/index.js +++ b/tests/routes/api/queue/index.js @@ -20,17 +20,15 @@ beforeEach(() => { }); describe('Get moderation queues rejected, pending, flags', () => { - const comments = [{ + let comments = [{ id: 'abc', body: 'comment 10', asset_id: 'asset', - author_id: '123', status: 'rejected' }, { id: 'def', body: 'comment 20', - asset_id: 'asset', - author_id: '456' + asset_id: 'asset' }, { id: 'hij', body: 'comment 30', @@ -39,41 +37,52 @@ describe('Get moderation queues rejected, pending, flags', () => { }]; const users = [{ + id: '456', displayName: 'Ana', email: 'ana@gmail.com', password: '123' }, { + id: '123', displayName: 'Maria', email: 'maria@gmail.com', password: '123' }]; - const actions = [{ + let actions = [{ action_type: 'flag', - item_id: 'abc', item_type: 'comment' }, { action_type: 'like', - item_id: 'hij', item_type: 'comment' }]; beforeEach(() => { - return Promise.all([ - Comment.create(comments), - User.createLocalUsers(users), - Action.create(actions) - ]); + return User.createLocalUsers(users) + .then((u) => { + comments[0].author_id = u[0].id; + comments[1].author_id = u[1].id; + comments[2].author_id = u[1].id; + + return Comment.create(comments); + }) + .then((c) => { + actions[0].item_id = c[0].id; + actions[1].item_id = c[1].id; + + return Action.create(actions); + }); }); - it('should return all the pending comments', function(done){ + it('should return all the pending comments, users and actions', function(done){ chai.request(app) .get('/api/v1/queue/comments/pending') .set(passport.inject({roles: ['admin']})) .end(function(err, res){ expect(err).to.be.null; expect(res).to.have.status(200); - expect(res.body[0]).to.have.property('id', 'def'); + expect(res.body.comments[0]).to.have.property('body'); + expect(res.body.users[0]).to.have.property('displayName'); + expect(res.body.actions[0]).to.have.property('action_type'); done(); }); }); From 5f2f70fd517d9dd983274e5485532533aa129c4e Mon Sep 17 00:00:00 2001 From: David Jay Date: Mon, 28 Nov 2016 18:59:27 -0500 Subject: [PATCH 04/21] Fixing bug when adding users to redux. --- client/coral-admin/src/services/talk-adapter.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 31978453a..a2bd5140c 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -40,13 +40,15 @@ Promise.all([ .then(res => Promise.all(res.map(handleResp))) .then(res => { res[2] = res[2].map(comment => { comment.flagged = true; return comment; }); - return res.reduce((prev, curr) => prev.concat(curr), []); + res[0].comments = res[0].comments.concat(res[1]).concat(res[2]); + return res[0]; }) .then(res => { - store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', - comments: res.comments}); + console.log(res); store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS', users: res.users}); + store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', + comments: res.comments}); }) .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error})); From 9fcd7b8d3a392a2e93e35ebdef4a50512cff3677 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 29 Nov 2016 12:16:51 -0700 Subject: [PATCH 05/21] retreiving comments based on settings was backwards --- routes/api/stream/index.js | 2 +- tests/routes/api/stream/index.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/routes/api/stream/index.js b/routes/api/stream/index.js index ddab5b87b..5a110e2d6 100644 --- a/routes/api/stream/index.js +++ b/routes/api/stream/index.js @@ -40,7 +40,7 @@ router.get('/', (req, res, next) => { // Fetch the appropriate comments stream. let comments; - if (settings.moderation === 'post') { + if (settings.moderation === 'pre') { comments = Comment.findAcceptedByAssetId(asset.id); } else { comments = Comment.findAcceptedAndNewByAssetId(asset.id); diff --git a/tests/routes/api/stream/index.js b/tests/routes/api/stream/index.js index 2b86f00ff..ea08ceebb 100644 --- a/tests/routes/api/stream/index.js +++ b/tests/routes/api/stream/index.js @@ -17,7 +17,7 @@ describe('/api/v1/stream', () => { const settings = { id: '1', - moderation: 'pre' + moderation: 'post' }; const comments = [{ @@ -73,7 +73,7 @@ describe('/api/v1/stream', () => { .findOrCreateByUrl('http://coralproject.net/asset2') .then((asset) => { return Asset - .overrideSettings(asset.id, {moderation: 'post'}) + .overrideSettings(asset.id, {moderation: 'pre'}) .then(() => asset); }) ]) @@ -108,7 +108,7 @@ describe('/api/v1/stream', () => { expect(res.body.comments.length).to.equal(2); expect(res.body.users.length).to.equal(2); expect(res.body.actions.length).to.equal(1); - expect(res.body.settings).to.have.property('moderation', 'pre'); + expect(res.body.settings).to.have.property('moderation', 'post'); }); }); @@ -121,7 +121,7 @@ describe('/api/v1/stream', () => { expect(res.body.assets.length).to.equal(1); expect(res.body.comments.length).to.equal(1); expect(res.body.users.length).to.equal(1); - expect(res.body.settings).to.have.property('moderation', 'post'); + expect(res.body.settings).to.have.property('moderation', 'pre'); }); }); }); From b7ffd31ea47acbee417cf9b256b0b5ed2d947dee Mon Sep 17 00:00:00 2001 From: David Jay Date: Tue, 29 Nov 2016 16:37:39 -0500 Subject: [PATCH 06/21] Cleaning up tests after merge. --- models/setting.js | 1 + routes/api/stream/index.js | 2 +- tests/routes/api/stream/index.js | 152 +++++++++++++++---------------- 3 files changed, 78 insertions(+), 77 deletions(-) diff --git a/models/setting.js b/models/setting.js index f0c6a1abc..e23f654a7 100644 --- a/models/setting.js +++ b/models/setting.js @@ -42,6 +42,7 @@ SettingSchema.statics.getSettings = function () { * @return {Promise} moderation the settings for how to moderate comments */ SettingSchema.statics.getModerationSetting = function () { + console.log('Getting moderation setting'); return this.findOne({id: '1'}).select('moderation'); }; diff --git a/routes/api/stream/index.js b/routes/api/stream/index.js index 5a110e2d6..ce8871ab8 100644 --- a/routes/api/stream/index.js +++ b/routes/api/stream/index.js @@ -34,7 +34,7 @@ router.get('/', (req, res, next) => { // Merge the asset specific settings with the returned settings object in // the event that the asset that was returned also had settings. if (asset.settings) { - settings = Object.assign(settings, asset.settings); + settings = Object.assign({}, settings, asset.settings); } // Fetch the appropriate comments stream. diff --git a/tests/routes/api/stream/index.js b/tests/routes/api/stream/index.js index ea08ceebb..6484fdc12 100644 --- a/tests/routes/api/stream/index.js +++ b/tests/routes/api/stream/index.js @@ -14,90 +14,90 @@ const Asset = require('../../../../models/asset'); const Setting = require('../../../../models/setting'); describe('/api/v1/stream', () => { + describe('#get', () => { + const settings = { + id: '1', + moderation: 'post' + }; - const settings = { - id: '1', - moderation: 'post' - }; + let comments; - const comments = [{ - id: 'abc', - body: 'comment 10', - author_id: '', - parent_id: '', - status: 'accepted' - }, { - id: 'def', - body: 'comment 20', - author_id: '', - parent_id: '', - status: '' - }, { - id: 'uio', - body: 'comment 30', - asset_id: 'asset', - author_id: '456', - parent_id: '', - status: 'accepted' - }, { - id: 'hij', - body: 'comment 40', - asset_id: '456', - status: 'rejected' - }]; + const users = [{ + displayName: 'Ana', + email: 'ana@gmail.com', + password: '123' + }, { + displayName: 'Maria', + email: 'maria@gmail.com', + password: '123' + }]; - const users = [{ - displayName: 'Ana', - email: 'ana@gmail.com', - password: '123' - }, { - displayName: 'Maria', - email: 'maria@gmail.com', - password: '123' - }]; + const actions = [{ + action_type: 'flag', + item_id: 'abc' + }, { + action_type: 'like', + item_id: 'hij' + }]; - const actions = [{ - action_type: 'flag', - item_id: 'abc' - }, { - action_type: 'like', - item_id: 'hij' - }]; + beforeEach(() => { - beforeEach(() => { - - return Promise.all([ - User.createLocalUsers(users), - Asset.findOrCreateByUrl('http://test.com'), - Asset - .findOrCreateByUrl('http://coralproject.net/asset2') - .then((asset) => { - return Asset - .overrideSettings(asset.id, {moderation: 'pre'}) - .then(() => asset); - }) - ]) - .then(([users, asset1, asset2]) => { - - comments[0].author_id = users[0].id; - comments[1].author_id = users[1].id; - comments[2].author_id = users[0].id; - comments[3].author_id = users[1].id; - - comments[0].asset_id = asset1.id; - comments[1].asset_id = asset1.id; - comments[2].asset_id = asset2.id; - comments[3].asset_id = asset2.id; + comments = [{ + id: 'abc', + body: 'comment 10', + author_id: '', + parent_id: '', + status: 'accepted' + }, { + id: 'def', + body: 'comment 20', + author_id: '', + parent_id: '', + status: '' + }, { + id: 'uio', + body: 'comment 30', + asset_id: 'asset', + author_id: '456', + parent_id: '', + status: 'accepted' + }, { + id: 'hij', + body: 'comment 40', + asset_id: '456', + status: 'rejected' + }]; return Promise.all([ - Comment.create(comments), - Action.create(actions), - Setting.create(settings) - ]); - }); - }); + User.createLocalUsers(users), + Asset.findOrCreateByUrl('http://test.com'), + Asset + .findOrCreateByUrl('http://coralproject.net/asset2') + .then((asset) => { + return Asset + .overrideSettings(asset.id, {moderation: 'pre'}) + .then(() => asset); + }) + ]) + .then(([users, asset1, asset2]) => { - describe('#get', () => { + comments[0].author_id = users[0].id; + comments[1].author_id = users[1].id; + comments[2].author_id = users[0].id; + comments[3].author_id = users[1].id; + + comments[0].asset_id = asset1.id; + comments[1].asset_id = asset1.id; + comments[2].asset_id = asset2.id; + comments[3].asset_id = asset2.id; + + return Promise.all([ + Comment.create(comments), + Action.create(actions), + Setting.init().then(() => Setting.updateSettings(settings)) + ]); + }); + }); it('should return a stream with comments, users and actions for an existing asset', () => { return chai.request(app) .get('/api/v1/stream') From a7f94fce83a2c6e6d74670f4a62ced6a5e2d0270 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 29 Nov 2016 14:44:37 -0700 Subject: [PATCH 07/21] implement coralApi fetch --- client/coral-admin/src/actions/auth.js | 8 ++--- client/coral-admin/src/actions/community.js | 7 ++--- client/coral-admin/src/actions/settings.js | 8 ++--- client/coral-admin/src/helpers/response.js | 30 ------------------- .../coral-admin/src/services/talk-adapter.js | 21 ++++++------- client/coral-framework/actions/auth.js | 23 ++++---------- client/coral-framework/actions/items.js | 17 ++++------- client/coral-framework/helpers/response.js | 20 ++++++++----- 8 files changed, 43 insertions(+), 91 deletions(-) delete mode 100644 client/coral-admin/src/helpers/response.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 2f8f1041e..54763259d 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,5 +1,5 @@ import * as actions from '../constants/auth'; -import {base, handleResp, getInit} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; // Check Login @@ -9,8 +9,7 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); - fetch(`${base}/auth`, getInit('GET')) - .then(handleResp) + coralApi('/auth') .then(user => { const isAdmin = !!user.roles.filter(i => i === 'admin').length; dispatch(checkLoginSuccess(user, isAdmin)); @@ -26,8 +25,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE}); export const logout = () => dispatch => { dispatch(logOutRequest()); - fetch(`${base}/auth`, getInit('DELETE')) - .then(handleResp) + coralApi('/auth', {method: 'DELETE'}) .then(() => dispatch(logOutSuccess())) .catch(error => dispatch(logOutFailure(error))); }; diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 5921573d1..8b8e883d8 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -9,12 +9,11 @@ import { SET_ROLE } from '../constants/community'; -import {base, getInit, handleResp} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; export const fetchCommenters = (query = {}) => dispatch => { dispatch(requestFetchCommenters()); - fetch(`${base}/user?${qs.stringify(query)}`, getInit('GET')) - .then(handleResp) + coralApi(`/user?${qs.stringify(query)}`) .then(({result, page, count, limit, totalPages}) => dispatch({ type: FETCH_COMMENTERS_SUCCESS, @@ -42,7 +41,7 @@ export const newPage = () => ({ }); export const setRole = (id, role) => dispatch => { - return fetch(`${base}/user/${id}/role`, getInit('POST', {role})) + return coralApi(`/user/${id}/role`, {method: 'POST', body: {role}}) .then(() => { return dispatch({type: SET_ROLE, id, role}); }); diff --git a/client/coral-admin/src/actions/settings.js b/client/coral-admin/src/actions/settings.js index 6a133ddb5..71106e1f7 100644 --- a/client/coral-admin/src/actions/settings.js +++ b/client/coral-admin/src/actions/settings.js @@ -1,4 +1,4 @@ -import {base, handleResp, getInit} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; export const SETTINGS_LOADING = 'SETTINGS_LOADING'; export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED'; @@ -12,8 +12,7 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED'; export const fetchSettings = () => dispatch => { dispatch({type: SETTINGS_LOADING}); - fetch(`${base}/settings`, getInit('GET')) - .then(handleResp) + coralApi('/settings') .then(settings => { dispatch({type: SETTINGS_RECEIVED, settings}); }) @@ -29,8 +28,7 @@ export const updateSettings = settings => { export const saveSettingsToServer = () => (dispatch, getState) => { const settings = getState().settings.toJS().settings; dispatch({type: SAVE_SETTINGS_LOADING}); - fetch(`${base}/settings`, getInit('PUT', settings)) - .then(handleResp) + coralApi('/settings', {method: 'PUT', body: settings}) .then(() => { dispatch({type: SAVE_SETTINGS_SUCCESS, settings}); }) diff --git a/client/coral-admin/src/helpers/response.js b/client/coral-admin/src/helpers/response.js deleted file mode 100644 index bccfc5a04..000000000 --- a/client/coral-admin/src/helpers/response.js +++ /dev/null @@ -1,30 +0,0 @@ -export const base = '/api/v1'; - -export const getInit = (method, body) => { - let init = { - method, - headers: new Headers({ - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }), - credentials: 'same-origin' - }; - - if (method.toLowerCase() !== 'get') { - init.body = JSON.stringify(body); - } - - return init; -}; - -export const handleResp = res => { - if (res.status === 401) { - throw new Error('Not Authorized to make this request'); - } else if (res.status > 399) { - throw new Error('Error! Status ', res.status); - } else if (res.status === 204) { - return res.text(); - } else { - return res.json(); - } -}; diff --git a/client/coral-admin/src/services/talk-adapter.js b/client/coral-admin/src/services/talk-adapter.js index 6b872d12d..15c9cf76a 100644 --- a/client/coral-admin/src/services/talk-adapter.js +++ b/client/coral-admin/src/services/talk-adapter.js @@ -1,4 +1,4 @@ -import {base, handleResp, getInit} from '../../../coral-framework/helpers/response'; +import coralApi from '../../../coral-framework/helpers/response'; /** * The adapter is a redux middleware that interecepts the actions that need @@ -33,14 +33,13 @@ export default store => next => action => { const fetchModerationQueueComments = store => Promise.all([ - fetch(`${base}/queue/comments/pending`, getInit('GET')), - fetch(`${base}/comments?status=rejected`, getInit('GET')), - fetch(`${base}/comments?action_type=flag`, getInit('GET')) + coralApi('/queue/comments/pending'), + coralApi('/comments?status=rejected'), + coralApi('/comments?action_type=flag') ]) -.then(res => Promise.all(res.map(handleResp))) -.then(res => { - res[2] = res[2].map(comment => { comment.flagged = true; return comment; }); - return res.reduce((prev, curr) => prev.concat(curr), []); +.then(([pending, rejected, flagged]) => { + flagged.forEach(comment => comment.flagged = true); + return [...pending, ...rejected, ...flagged]; }) .then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS', comments: res})) @@ -49,8 +48,7 @@ Promise.all([ // Update a comment. Now to update a comment we need to send back the whole object const updateComment = (store, comment) => { - fetch(`${base}/comments/${comment.get('id')}/status`, getInit('PUT', {status: comment.get('status')})) - .then(handleResp) + coralApi(`/comments/${comment.get('id')}/status`, {method: 'PUT', body: {status: comment.get('status')}}) .then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res})) .catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error})); }; @@ -63,8 +61,7 @@ const createComment = (store, name, comment) => { name: name, createdAt: Date.now() }; - return fetch(`${base}/comments`, getInit('POST', body)) - .then(handleResp) + return coralApi('/comments', {method: 'POST', body}) .then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res})) .catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error})); }; diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 1058edbbb..4139df842 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -2,7 +2,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from './../translations'; const lang = new I18n(translations); import * as actions from '../constants/auth'; -import {base, handleResp, getInit} from '../helpers/response'; +import coralApi, {base} from '../helpers/response'; import {addItem} from './items'; // Dialog Actions @@ -25,8 +25,7 @@ const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); export const fetchSignIn = (formData) => dispatch => { dispatch(signInRequest()); - fetch(`${base}/auth/local`, getInit('POST', formData)) - .then(handleResp) + coralApi('/auth/local', {method: 'POST', body: formData}) .then(({user}) => { dispatch(hideSignInDialog()); dispatch(signInSuccess(user)); @@ -74,8 +73,7 @@ const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error}); export const fetchSignUp = formData => dispatch => { dispatch(signUpRequest()); - fetch(`${base}/user`, getInit('POST', formData)) - .then(handleResp) + coralApi('/user', {method: 'POST', body: formData}) .then(({user}) => { dispatch(signUpSuccess(user)); setTimeout(() =>{ @@ -93,8 +91,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU export const fetchForgotPassword = email => dispatch => { dispatch(forgotPassowordRequest(email)); - fetch(`${base}/user/request-password-reset`, getInit('POST', {email})) - .then(handleResp) + coralApi('/user/request-password-reset', {method: 'POST', body: {email}}) .then(() => dispatch(forgotPassowordSuccess())) .catch(error => dispatch(forgotPassowordFailure(error))); }; @@ -107,8 +104,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE}); export const logout = () => dispatch => { dispatch(logOutRequest()); - fetch(`${base}/auth`, getInit('DELETE')) - .then(handleResp) + coralApi('/auth', {method: 'DELETE'}) .then(() => dispatch(logOutSuccess())) .catch(error => dispatch(logOutFailure(error))); }; @@ -126,14 +122,7 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); - fetch(`${base}/auth`, getInit('GET')) - .then((res) => { - if (res.status !== 200) { - throw new Error('not logged in'); - } - - return res.json(); - }) + coralApi('/auth') .then(user => dispatch(checkLoginSuccess(user))) .catch(error => dispatch(checkLoginFailure(error))); }; diff --git a/client/coral-framework/actions/items.js b/client/coral-framework/actions/items.js index 00deba5ef..2029714c7 100644 --- a/client/coral-framework/actions/items.js +++ b/client/coral-framework/actions/items.js @@ -1,4 +1,4 @@ -import {getInit, base, handleResp} from '../helpers/response'; +import coralApi from '../helpers/response'; import {fromJS} from 'immutable'; /* Item Actions */ @@ -95,8 +95,7 @@ export const appendItemArray = (id, property, value, add_to_front, item_type) => */ export function getStream (assetUrl) { return (dispatch) => { - return fetch(`${base}/stream?asset_url=${encodeURIComponent(assetUrl)}`, getInit('GET')) - .then(handleResp) + return coralApi(`/stream?asset_url=${encodeURIComponent(assetUrl)}`) .then((json) => { /* Add items to the store */ @@ -166,8 +165,7 @@ export function getStream (assetUrl) { export function getItemsArray (ids) { return (dispatch) => { - return fetch(`${base}/item/${ids}`, getInit('GET')) - .then(handleResp) + return coralApi(`/item/${ids}`) .then((json) => { for (let i = 0; i < json.items.length; i++) { dispatch(addItem(json.items[i])); @@ -196,8 +194,7 @@ export function postItem (item, type, id) { if (id) { item.id = id; } - return fetch(`${base}/${type}`, getInit('POST', item)) - .then(handleResp) + return coralApi(`/${type}`, {method: 'POST', body: item}) .then((json) => { dispatch(addItem({...item, id:json.id}, type)); return json.id; @@ -227,8 +224,7 @@ export function postAction (item_id, action_type, user_id, item_type) { user_id }; - return fetch(`${base}/${item_type}/${item_id}/actions`, getInit('POST', action)) - .then(handleResp); + return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action}); }; } @@ -249,7 +245,6 @@ export function postAction (item_id, action_type, user_id, item_type) { export function deleteAction (action_id) { return () => { - return fetch(`${base}/actions/${action_id}`, {method: 'DELETE'}) - .then(handleResp); + return coralApi(`/actions/${action_id}`, {method: 'DELETE'}); }; } diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index 83f51e3ec..1b4340dc4 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -1,23 +1,25 @@ export const base = '/api/v1'; -export const getInit = (method, body) => { - let init = { - method, +const buildOptions = (inputOptions = {}) => { + + const defaultOptions = { + method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'same-origin' }; + const options = Object.assign({}, defaultOptions, inputOptions); - if (method.toLowerCase() !== 'get') { - init.body = JSON.stringify(body); + if (options.method.toLowerCase() !== 'get') { + options.body = JSON.stringify(options.body); } - return init; + return options; }; -export const handleResp = res => { +const handleResp = res => { if (res.status === 401) { throw new Error('Not Authorized to make this request'); } else if (res.status > 399) { @@ -28,3 +30,7 @@ export const handleResp = res => { return res.json(); } }; + +export default (url, options) => { + return fetch(`${base}${url}`, buildOptions(options)).then(handleResp); +}; From ef172cabfca72378f82b1625b3684fe5ca4598da Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 29 Nov 2016 15:19:56 -0700 Subject: [PATCH 08/21] Scroll to comment (#92) * host pym locally * host pym. fix some permalink styles * get url from parent document * reset copy dialog timeout * use pym.parentUrl instead of depending on the parent document * use a simpler method to find the url * add some horrible setInterval code * add some awesome polling * merge master * bail out of comment look up after 10 seconds * re-update permalink attrs * re-add my listeners to article.ejs * allow for overriding the talk db name in an environment var --- .eslintignore | 3 +- .../public/samplearticle.html | 2 +- .../coral-embed-stream/src/CommentStream.js | 47 +++++++++++++------ client/coral-embed-stream/style/default.css | 41 ++++++++++++++-- .../PermalinkButton.js | 35 ++++---------- .../coral-sign-in/components/ForgotContent.js | 2 +- client/coral-sign-in/components/styles.css | 3 +- client/lib/pym.v1.min.js | 2 + mongoose.js | 6 +-- routes/index.js | 10 +++- views/article.ejs | 28 +++++++---- views/embed-stream.ejs | 26 +++++++--- webpack.config.dev.js | 14 ++++-- 13 files changed, 145 insertions(+), 74 deletions(-) create mode 100644 client/lib/pym.v1.min.js diff --git a/.eslintignore b/.eslintignore index 53c37a166..a4865e1f6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -dist \ No newline at end of file +dist +client/lib diff --git a/client/coral-embed-stream/public/samplearticle.html b/client/coral-embed-stream/public/samplearticle.html index 64c3fd0f8..454cb249c 100644 --- a/client/coral-embed-stream/public/samplearticle.html +++ b/client/coral-embed-stream/public/samplearticle.html @@ -7,7 +7,7 @@

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut lobortis sollicitudin eros a ornare. Curabitur dignissim vestibulum massa non rhoncus. Cras laoreet ante vel nunc hendrerit, ac imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Pellentesque interdum nec elit sed tincidunt. Donec volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non enim nec neque congue faucibus porttitor sit amet dui.

    Nunc pharetra orci id diam feugiat, vitae rutrum magna efficitur. Morbi porttitor blandit lorem, et facilisis tellus luctus at. Morbi tincidunt eget nisl id placerat. Nullam consectetur quam vel mauris lacinia, non consectetur est faucibus. Duis cursus auctor nulla nec sagittis. Aenean sem erat, ultrices a hendrerit consectetur, accumsan non lorem. Integer ac neque sed magna sodales vulputate at quis neque. Praesent eget ornare lacus. Donec ultricies, dolor eget commodo faucibus, arcu velit ullamcorper tellus, in cursus tellus elit sed urna. Suspendisse in consequat magna. Duis vel ullamcorper tortor, vel cursus libero. Proin et nisi luctus ligula faucibus luctus. Morbi pulvinar, justo ac feugiat elementum, libero tellus congue justo, pharetra ultrices felis felis id leo. Integer mattis quam tempus libero porta, ac pretium ligula elementum.

    - + diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index be40ecff6..95a6f63f3 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -78,13 +78,32 @@ class CommentStream extends Component { componentDidMount () { // 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}); + this.pym = new Pym.Child({polling: 100}); - if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) { - this.props.getStream(pym.parentUrl); - } else { - this.props.getStream(window.location); - } + const path = this.pym.parentUrl.split('#')[0]; + + this.props.getStream(path || window.location); + this.path = path; + + this.pym.sendMessage('childReady'); + + this.pym.onMessage('DOMContentLoaded', hash => { + // the comment ids can start with numbers, which is invalid for DOM id attributes + const commentId = hash.replace('#', 'c_'); + let count = 0; + const interval = setInterval(() => { + if (document.getElementById(commentId)) { + window.clearInterval(interval); + this.pym.scrollParentToChildEl(commentId); + } + + if (++count > 100) { // ~10 seconds + // give up waiting for the comments to load. + // it would be weird for the page to jump after that long. + window.clearInterval(interval); + } + }, 100); + }); } render () { @@ -109,11 +128,11 @@ class CommentStream extends Component { const {actions, users, comments} = this.props.items; const {loggedIn, user, showSignInDialog} = this.props.auth; const {activeTab} = this.state; + return
    { rootItem - ?
    - + ?
    Settings @@ -141,7 +160,7 @@ class CommentStream extends Component { { rootItem.comments && rootItem.comments.map((commentId) => { const comment = comments[commentId]; - return
    + return

    @@ -172,8 +191,8 @@ class CommentStream extends Component { updateItem={this.props.updateItem} currentUser={this.props.auth.user}/> + commentId={commentId} + articleURL={this.path}/>
    { let reply = this.props.items.comments[replyId]; - return
    + return

    @@ -220,8 +239,8 @@ class CommentStream extends Component { updateItem={this.props.updateItem} currentUser={this.props.auth.user}/>
    +
    + className={`${name}-popover ${this.state.popoverOpen ? 'active' : ''}`}> this.permalinkInput = input} - value={`${publisherUrl}${this.props.asset_id}#${this.props.comment_id}`} + value={`${this.props.articleURL}#${this.props.commentId}`} onChange={() => {}} /> { - this.state.copySuccessful ?

    copied to clipboard

    : null + this.state.copySuccessful ?

    copied to clipboard

    : null } { this.state.copyFailure - ?

    copying to clipboard not supported in this browser. Use Cmd + C.

    + ?

    copying to clipboard not supported in this browser. Use Cmd + C.

    : null }
    @@ -75,20 +73,3 @@ class PermalinkButton extends React.Component { } export default onClickOutside(PermalinkButton); - -const styles = { - position: 'relative', - - popover: active => { - return { - display: active ? 'block' : 'none', - backgroundColor: 'white', - border: '1px solid black', - minWidth: 400, - position: 'absolute', - top: 30, - right: 0, - padding: 5 - }; - } -}; diff --git a/client/coral-sign-in/components/ForgotContent.js b/client/coral-sign-in/components/ForgotContent.js index bc06ae5f1..f76ebe45d 100644 --- a/client/coral-sign-in/components/ForgotContent.js +++ b/client/coral-sign-in/components/ForgotContent.js @@ -44,7 +44,7 @@ class ForgotContent extends React.Component { } { passwordRequestFailure - ?

    {passwordRequestFailure}

    + ?

    {passwordRequestFailure}

    : null } diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index 1561f557b..e645885ce 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -138,5 +138,6 @@ input.error{ .passwordRequestFailure { border: 1px solid orange; - background-color: 1px solid coral + background-color: 1px solid coral; + padding: 10px; } diff --git a/client/lib/pym.v1.min.js b/client/lib/pym.v1.min.js new file mode 100644 index 000000000..7aa54058d --- /dev/null +++ b/client/lib/pym.v1.min.js @@ -0,0 +1,2 @@ +/*! pym.js - v1.1.2 - 2016-10-25 */ +!function(a){"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&module.exports?module.exports=a():window.pym=a.call(this)}(function(){var a="xPYMx",b={},c=function(a){var b=new RegExp("[\\?&]"+a.replace(/[\[]/,"\\[").replace(/[\]]/,"\\]")+"=([^&#]*)"),c=b.exec(location.search);return null===c?"":decodeURIComponent(c[1].replace(/\+/g," "))},d=function(a,b){if("*"===b.xdomain||a.origin.match(new RegExp(b.xdomain+"$")))return!0},e=function(b,c,d){var e=["pym",b,c,d];return e.join(a)},f=function(b){var c=["pym",b,"(\\S+)","(.*)"];return new RegExp("^"+c.join(a)+"$")},g=function(){for(var a=b.autoInitInstances.length,c=a-1;c>=0;c--){var d=b.autoInitInstances[c];d.el.getElementsByTagName("iframe").length&&d.el.getElementsByTagName("iframe")[0].contentWindow||b.autoInitInstances.splice(c,1)}};return b.autoInitInstances=[],b.autoInit=function(){var a=document.querySelectorAll("[data-pym-src]:not([data-pym-auto-initialized])"),c=a.length;g();for(var d=0;d-1&&(b=this.url.substring(c,this.url.length),this.url=this.url.substring(0,c)),this.url.indexOf("?")<0?this.url+="?":this.url+="&",this.iframe.src=this.url+"initialWidth="+a+"&childId="+this.id+"&parentTitle="+encodeURIComponent(document.title)+"&parentUrl="+encodeURIComponent(window.location.href)+b,this.iframe.setAttribute("width","100%"),this.iframe.setAttribute("scrolling","no"),this.iframe.setAttribute("marginheight","0"),this.iframe.setAttribute("frameborder","0"),this.settings.title&&this.iframe.setAttribute("title",this.settings.title),void 0!==this.settings.allowfullscreen&&this.settings.allowfullscreen!==!1&&this.iframe.setAttribute("allowfullscreen",""),void 0!==this.settings.sandbox&&"string"==typeof this.settings.sandbox&&this.iframe.setAttribute("sandbox",this.settings.sandbox),this.settings.id&&(document.getElementById(this.settings.id)||this.iframe.setAttribute("id",this.settings.id)),this.settings.name&&this.iframe.setAttribute("name",this.settings.name);this.el.firstChild;)this.el.removeChild(this.el.firstChild);this.el.appendChild(this.iframe),window.addEventListener("resize",this._onResize)},this._onResize=function(){this.sendWidth()}.bind(this),this._fire=function(a,b){if(a in this.messageHandlers)for(var c=0;c { - res.render('article', {title: 'Coral Talk'}); + return res.render('article', { + title: 'Coral Talk', + basePath: '/client/embed/stream' + }); }); router.get('/assets/:asset_title', (req, res) => { - res.render('article', {title: req.params.asset_title.split('-').join(' ')}); + return res.render('article', { + title: req.params.asset_title.split('-').join(' '), + basePath: '/client/embed/stream' + }); }); module.exports = router; diff --git a/views/article.ejs b/views/article.ejs index 71db59593..6fd420f82 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -32,13 +32,25 @@
    - - - + + diff --git a/views/embed-stream.ejs b/views/embed-stream.ejs index e1ddb17e7..a05b699f1 100644 --- a/views/embed-stream.ejs +++ b/views/embed-stream.ejs @@ -19,14 +19,26 @@
    - - - + diff --git a/webpack.config.dev.js b/webpack.config.dev.js index 571c31625..913ee35da 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -74,10 +74,16 @@ module.exports = { ] }, plugins: [ - new Copy(buildEmbeds.map(embed => ({ - from: path.join(__dirname, 'client', `coral-embed-${embed}`, 'style'), - to: path.join(__dirname, 'dist', 'embed', embed) - }))), + new Copy([ + ...buildEmbeds.map(embed => ({ + from: path.join(__dirname, 'client', `coral-embed-${embed}`, 'style'), + to: path.join(__dirname, 'dist', 'embed', embed) + })), + { + from: path.join(__dirname, 'client', 'lib'), + to: path.join(__dirname, 'dist', 'embed', 'stream') + } + ]), autoprefixer, precss, new webpack.ProvidePlugin({ From 3e86bfdceb3f4193b44c57426252a392c3e029f5 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 29 Nov 2016 15:58:05 -0700 Subject: [PATCH 09/21] false positive on user signed in --- client/coral-framework/actions/auth.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 4139df842..13a9c72bc 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -123,6 +123,12 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); coralApi('/auth') - .then(user => dispatch(checkLoginSuccess(user))) + .then(user => { + if (!user) { + throw new Error('not logged in'); + } + + dispatch(checkLoginSuccess(user)); + }) .catch(error => dispatch(checkLoginFailure(error))); }; From bd704bf2cee46a8e5418b54315543fe423a46959 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 30 Nov 2016 08:42:25 -0300 Subject: [PATCH 10/21] User Settings, Restricted User, Banned User, Bio and User actions and Reducers --- .../coral-embed-stream/src/CommentStream.js | 310 +++++++++--------- client/coral-embed-stream/style/default.css | 1 + client/coral-framework/actions/user.js | 16 + .../components/RestrictedContent.css | 4 + .../components/RestrictedContent.js | 20 ++ client/coral-framework/constants/user.js | 3 + client/coral-framework/reducers/user.js | 20 ++ client/coral-framework/translations.json | 6 +- client/coral-plugin-commentbox/CommentBox.js | 8 +- client/coral-settings/components/Bio.js | 6 +- .../coral-settings/components/NotLoggedIn.css | 15 + .../coral-settings/components/NotLoggedIn.js | 17 + .../components/SettingsHeader.js | 6 +- .../containers/SettingsContainer.js | 24 +- client/coral-sign-in/components/UserBox.js | 7 +- client/coral-sign-in/components/styles.css | 10 +- client/coral-sign-in/translations.js | 4 + client/coral-ui/components/Button.css | 10 + client/coral-ui/components/TabBar.css | 1 + client/coral-ui/index.js | 1 + models/user.js | 4 +- 21 files changed, 313 insertions(+), 180 deletions(-) create mode 100644 client/coral-framework/actions/user.js create mode 100644 client/coral-framework/components/RestrictedContent.css create mode 100644 client/coral-framework/components/RestrictedContent.js create mode 100644 client/coral-framework/constants/user.js create mode 100644 client/coral-framework/reducers/user.js create mode 100644 client/coral-settings/components/NotLoggedIn.css create mode 100644 client/coral-settings/components/NotLoggedIn.js diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index be40ecff6..870ea93ad 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -1,11 +1,18 @@ import React, {Component, PropTypes} from 'react'; +import Pym from 'pym.js'; +import {connect} from 'react-redux'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-framework/translations.json'; +const lang = new I18n(translations); + import { itemActions, Notification, notificationActions, authActions, } from '../../coral-framework'; -import {connect} from 'react-redux'; + import CommentBox from '../../coral-plugin-commentbox/CommentBox'; import InfoBox from '../../coral-plugin-infobox/InfoBox'; import Content from '../../coral-plugin-commentcontent/CommentContent'; @@ -13,27 +20,25 @@ import PubDate from '../../coral-plugin-pubdate/PubDate'; import Count from '../../coral-plugin-comment-count/CommentCount'; import AuthorName from '../../coral-plugin-author-name/AuthorName'; import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'; -import Pym from 'pym.js'; import FlagButton from '../../coral-plugin-flags/FlagButton'; import LikeButton from '../../coral-plugin-likes/LikeButton'; import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton'; import SignInContainer from '../../coral-sign-in/containers/SignInContainer'; import UserBox from '../../coral-sign-in/components/UserBox'; -import {TabBar, Tab, TabContent} from '../../coral-ui'; +import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui'; import SettingsContainer from '../../coral-settings/containers/SettingsContainer'; +import RestrictedContent from '../../coral-framework/components/RestrictedContent'; const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions; const {addNotification, clearNotification} = notificationActions; const {logout} = authActions; -const mapStateToProps = (state) => { - return { - config: state.config.toJS(), - items: state.items.toJS(), - notification: state.notification.toJS(), - auth: state.auth.toJS() - }; -}; +const mapStateToProps = state => ({ + config: state.config.toJS(), + items: state.items.toJS(), + notification: state.notification.toJS(), + auth: state.auth.toJS() +}); const mapDispatchToProps = (dispatch) => ({ addItem: (item, itemType) => dispatch(addItem(item, itemType)), @@ -43,12 +48,9 @@ const mapDispatchToProps = (dispatch) => ({ addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)), - deleteAction: (item, action, user, itemType) => { - return dispatch(deleteAction(item, action, user, itemType)); - }, - appendItemArray: (item, property, value, addToFront, itemType) => - dispatch(appendItemArray(item, property, value, addToFront, itemType)), - logout: () => dispatch(logout()), + deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)), + appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)), + logout: () => dispatch(logout()) }); class CommentStream extends Component { @@ -94,12 +96,6 @@ class CommentStream extends Component { comments: [], url: 'http://coralproject.net' }, 'asset', 'assetTest'); - - // Loading mock user - //this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989') - // .then((id) => { - // this.props.setLoggedInUser(id); - // }); } // TODO: Replace teststream id with id from params @@ -113,145 +109,149 @@ class CommentStream extends Component { { rootItem ?
    - Settings - - -
    - - {loggedIn && } - - {!loggedIn && } -
    - { - rootItem.comments && rootItem.comments.map((commentId) => { - const comment = comments[commentId]; - return
    -
    - - - -
    - - -
    -
    - - -
    - - { - comment.children && - comment.children.map((replyId) => { - let reply = this.props.items.comments[replyId]; - return
    -
    - - - -
    - } + {/* Add to the restricted param a boolean if the user is suspended*/} + + +
    + + + {!loggedIn && } +
    + { + rootItem.comments && rootItem.comments.map(commentId => { + const comment = comments[commentId]; + // This should be a return Comment Component + return
    +
    + + + +
    + + +
    +
    + + +
    + + { + comment.children && + comment.children.map((replyId) => { + let reply = this.props.items.comments[replyId]; + // This should be a Reply Component + return
    +
    + + + +
    + + +
    +
    + + +
    + - -
    -
    - - -
    - -
    ; - }) - } -
    ; - }) - } - - - - - +
    ; + }) + } +
    ; + }) + } + +
    + + + +
    - : 'Loading' + : + }
    ; } diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 2da0c09c4..d6fc43dff 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -79,6 +79,7 @@ hr { .coral-plugin-commentbox-textarea { flex: 1; padding: 5px; + min-height: 100px; } .coral-plugin-commentbox-button-container { diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js new file mode 100644 index 000000000..5afbee14d --- /dev/null +++ b/client/coral-framework/actions/user.js @@ -0,0 +1,16 @@ +import * as actions from '../constants/user'; +import {base, handleResp, getInit} from '../helpers/response'; + +const saveBioRequest = () => ({type: actions.SAVE_BIO_REQUEST}); +const saveBioSuccess = bio => ({type: actions.SAVE_BIO_SUCCESS, bio}); +const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error}); + +export const saveBio = (formData) => dispatch => { + dispatch(saveBioRequest()); + fetch(`${base}/auth/local`, getInit('POST', formData)) + .then(handleResp) + .then(({user}) => { + dispatch(saveBioSuccess(user)); + }) + .catch(error => dispatch(saveBioFailure(error))); +}; diff --git a/client/coral-framework/components/RestrictedContent.css b/client/coral-framework/components/RestrictedContent.css new file mode 100644 index 000000000..47ced7b0b --- /dev/null +++ b/client/coral-framework/components/RestrictedContent.css @@ -0,0 +1,4 @@ +.message { + background: #D8D8D8; + padding: 25px; +} diff --git a/client/coral-framework/components/RestrictedContent.js b/client/coral-framework/components/RestrictedContent.js new file mode 100644 index 000000000..8d019c996 --- /dev/null +++ b/client/coral-framework/components/RestrictedContent.js @@ -0,0 +1,20 @@ +import React from 'react'; +import styles from './RestrictedContent.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-framework/translations.json'; +const lang = new I18n(translations); + +export default ({children, restricted, message = lang.t('contentNotAvailable'), restrictedComp}) => { + if (restricted) { + return restrictedComp ? restrictedComp() : messageBox(message); + } else { + return ( +
    + {children} +
    + ); + } +}; + +const messageBox = (message) =>
    {message}
    ; diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js new file mode 100644 index 000000000..0c316d48a --- /dev/null +++ b/client/coral-framework/constants/user.js @@ -0,0 +1,3 @@ +export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST'; +export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS'; +export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE'; diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js new file mode 100644 index 000000000..42a71df3b --- /dev/null +++ b/client/coral-framework/reducers/user.js @@ -0,0 +1,20 @@ +import {Map} from 'immutable'; +import {FETCH_SIGNIN_SUCCESS} from '../constants/auth'; +import * as actions from '../constants/user'; + +const initialState = Map({ + bio: '' +}); + +export default function user (state = initialState, action) { + switch (action.type) { + case FETCH_SIGNIN_SUCCESS : + return state + .set('bio', action.user.bio); + case actions.SAVE_BIO_SUCCESS : + return state + .set('bio', action.bio); + default : + return state; + } +} diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index 4abd1ed03..e2193f726 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -1,5 +1,7 @@ { "en": { + "contentNotAvailable": "This content is not available", + "suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information", "error": { "email": "Not a valid E-Mail", "password": "Password must be at least 8 characters", @@ -10,6 +12,8 @@ } }, "es": { + "contentNotAvailable": "El contenido no se encuentra disponible", + "suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information", "error": { "email": "No es un email válido", "password": "La contraseña debe tener por lo menos 8 caracteres", @@ -19,4 +23,4 @@ "emailInUse": "Email address already in use" } } -} \ No newline at end of file +} diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 79641d856..9ae524652 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -1,6 +1,7 @@ import React, {Component, PropTypes} from 'react'; import {I18n} from '../coral-framework'; import translations from './translations.json'; +import {Button} from 'coral-ui'; const name = 'coral-plugin-commentbox'; @@ -75,12 +76,11 @@ class CommentBox extends Component {
    { author && ( - + ) }
    diff --git a/client/coral-settings/components/Bio.js b/client/coral-settings/components/Bio.js index b82587322..7310db114 100644 --- a/client/coral-settings/components/Bio.js +++ b/client/coral-settings/components/Bio.js @@ -2,14 +2,14 @@ import React from 'react'; import styles from './Bio.css'; import {Button} from '../../coral-ui'; -export default () => ( +export default ({user, handleSaveBio}) => (

    Bio

    Tell the community about yourself

    -