From 2dc882a12ec58deac3881841abf0de47b08b919d Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Tue, 4 Apr 2017 14:56:23 -0700 Subject: [PATCH 01/44] Start UI for TopRightMenu and IgnoreUserWizard on Comments --- client/coral-embed-stream/src/Comment.css | 43 +++++++ client/coral-embed-stream/src/Comment.js | 133 ++++++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index 4383cbc5b..1e75887f2 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -12,3 +12,46 @@ filter: blur(2px); pointer-events: none; } + +.topRightMenu { + float: right; + text-align: right; +} + +.topRightMenu > * { + text-align: initial; +} + +.topRightMenu .toggler { + cursor: pointer; +} + +.topRightMenu .Menu { + background-color: white; + text-align: initial; +} + +.Toggleable:focus { + outline: none; +} + +.Menu { + border: 1px solid #ddd; + margin: 0; + +} + +.MenuItem { + cursor: pointer; + padding: 1em; +} + +.IgnoreUserWizard { + background-color: #2E343B; + color: white; + padding: 1em; +} + +.IgnoreUserWizard header { + font-weight: bold; +} \ No newline at end of file diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 45f2ef084..59d82c5ab 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -11,6 +11,7 @@ import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton'; import AuthorName from 'coral-plugin-author-name/AuthorName'; +import {Button} from 'coral-ui'; import TagLabel from 'coral-plugin-tag-label/TagLabel'; import Content from 'coral-plugin-commentcontent/CommentContent'; import PubDate from 'coral-plugin-pubdate/PubDate'; @@ -141,6 +142,91 @@ class Comment extends React.Component { tag: BEST_TAG, }), () => 'Failed to remove best comment tag'); + class IgnoreUserWizard extends React.Component { + static propTypes = { + + // comment on which this menu appears + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + cancel: PropTypes.func.isRequired, + } + constructor(props) { + super(props); + this.state = {}; + this.onClickCancel = this.onClickCancel.bind(this); + } + onClickCancel() { + this.props.cancel(); + } + render() { + return ( +
+
Ignore User
+

When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.

+
+ + +
+
+ ); + } + } + + // Menu of actions in top right of Comment. + // When you choose 'Ignore User', the menu choices are replaced with the Ignore User flow + class TopRightMenu extends React.Component { + static propTypes = { + + // comment on which this menu appears + comment: PropTypes.shape({ + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired + }).isRequired, + } + constructor(props) { + super(props); + this.state = { + chosenItem: null + }; + } + render() { + const {chosenItem} = this.state; + const {comment} = this.props; + let child; + const chooseIgnoreUser = () => { + this.setState({chosenItem: 'IGNORE_USER'}); + }; + const reset = () => this.setState({chosenItem: null}); + switch (chosenItem) { + case 'IGNORE_USER': + child = ( + + ); + break; + default: + child = ( + + Ignore User + + ); + } + return ( + +
+ { child } +
+
+ ); + } + } + return (
+ + + +
@@ -266,4 +356,47 @@ class Comment extends React.Component { } } +// TODO (bengo): use arrows that match designs, probably with css borders http://stackoverflow.com/questions/15938933/creating-a-chevron-in-css +const upArrow = String.fromCharCode(0x25B2); +const downArrow = String.fromCharCode(0x25BC); +class Toggleable extends React.Component { + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + this.close = this.close.bind(this); + this.state = { + isOpen: false + }; + } + toggle() { + this.setState({isOpen: ! this.state.isOpen}); + } + close() { + this.setState({isOpen: false}); + } + render() { + const {children} = this.props; + const {isOpen} = this.state; + return ( + + // /*onBlur={ this.close } */ + + {isOpen ? upArrow : downArrow} + {isOpen ? children : null} + + ); + } +} +const Menu = ({children}) => ( +
    + { children } +
+); +Menu.Item = ({children, onClick}) => ( +
  • + { children } +
  • +); + export default Comment; From d3d7045227c502b45924234f63347704742cf4b7 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Wed, 5 Apr 2017 16:39:48 -0700 Subject: [PATCH 02/44] IgnoreUserWizard steps and proper chevron --- client/coral-embed-stream/src/Comment.css | 27 ++++++++++++- client/coral-embed-stream/src/Comment.js | 48 +++++++++++++++++++---- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index 1e75887f2..a1f7b8654 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -54,4 +54,29 @@ .IgnoreUserWizard header { font-weight: bold; -} \ No newline at end of file +} + +.IgnoreUserWizard .textAlignRight { + text-align: right; +} + +/** + * Up/Down Chevrons for the top right menu + */ +.chevron { +} +.chevron:before { + content: '⌃'; + display: inline-block; + position: relative; + top: 0.25em; +} + +/* Down Arrow */ +.chevron.down:before { + display: inline-block; + position: relative; + transform: rotate(180deg); + top: 0; + /*top: -0.25em;*/ +} diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 59d82c5ab..26ff01f0f 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -23,6 +23,7 @@ import LoadMore from 'coral-embed-stream/src/LoadMore'; import {Slot} from 'coral-framework'; import styles from './Comment.css'; +import classnames from 'classnames'; const getActionSummary = (type, comment) => comment.action_summaries .filter((a) => a.__typename === type)[0]; @@ -154,23 +155,56 @@ class Comment extends React.Component { } constructor(props) { super(props); - this.state = {}; + this.state = { + + // what step of the wizard is the user on + step: 1 + }; this.onClickCancel = this.onClickCancel.bind(this); } onClickCancel() { this.props.cancel(); } render() { - return ( -
    + const {user} = this.props; + const goToStep = (stepNum) => this.setState({step: stepNum}); + const step1 = ( +
    Ignore User

    When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.

    -
    +
    - +
    ); + const step2Confirmation = ( +
    +
    Ignore User
    +

    Are you sure you want to ignore { user.name }?

    +
    + + +
    +
    + ); + const step3Confirmed = ( +
    +
    User Ignored
    +

    You will no longer see comments from { user.name }. You can undo this later from the Profile tab

    +
    + +
    +
    + ); + const elsForStep = [step1, step2Confirmation, step3Confirmed]; + const {step} = this.state; + const elForThisStep = elsForStep[step - 1]; + return ( +
    + { elForThisStep } +
    + ); } } @@ -357,8 +391,8 @@ class Comment extends React.Component { } // TODO (bengo): use arrows that match designs, probably with css borders http://stackoverflow.com/questions/15938933/creating-a-chevron-in-css -const upArrow = String.fromCharCode(0x25B2); -const downArrow = String.fromCharCode(0x25BC); +const upArrow = ; +const downArrow = ; class Toggleable extends React.Component { constructor(props) { super(props); From 545f176bb821cc5a9175af2f55759e8b33ed4219 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Wed, 5 Apr 2017 18:47:39 -0700 Subject: [PATCH 03/44] Add ignoreUser mutation and myIgnoredUsers root query --- graph/mutators/user.js | 7 +++- graph/resolvers/root_mutation.js | 3 ++ graph/resolvers/root_query.js | 10 ++++++ graph/typeDefs.graphql | 13 +++++++ models/user.js | 8 ++++- services/users.js | 20 +++++++++++ test/graph/mutations/ignoreUser.js | 55 ++++++++++++++++++++++++++++++ test/server/services/users.js | 18 ++++++++++ 8 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 test/graph/mutations/ignoreUser.js diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 48966c5e8..04235c217 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -13,11 +13,16 @@ const suspendUser = ({user}, {id, message}) => { }); }; +const ignoreUser = async ({user}, userToIgnore) => { + return await UsersService.ignoreUsers(user.id, [userToIgnore.id]); +}; + module.exports = (context) => { let mutators = { User: { setUserStatus: () => Promise.reject(errors.ErrNotAuthorized), - suspendUser: () => Promise.reject(errors.ErrNotAuthorized) + suspendUser: () => Promise.reject(errors.ErrNotAuthorized), + ignoreUser: (action) => ignoreUser(context, action), } }; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 3d5ea9ad9..0e763889f 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -23,6 +23,9 @@ const RootMutation = { suspendUser(_, {id, message}, {mutators: {User}}) { return wrapResponse(null)(User.suspendUser({id, message})); }, + ignoreUser(_, {id}, {mutators: {User}}) { + return wrapResponse(null)(User.ignoreUser({id})); + }, setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 32e2e4263..42f5a021d 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -83,6 +83,16 @@ const RootQuery = { return user; }, + myIgnoredUsers: async (_, args, {user, loaders: {Users}}) => { + + // get currentUser again since context.user was out of date when running test/graph/mutations/ignoreUser + const currentUser = (await Users.getByQuery({ids: [user.id], limit: 1}))[0]; + if ( ! (currentUser && Array.isArray(currentUser.ignoresUsers) && currentUser.ignoresUsers.length)) { + return []; + } + return await Users.getByQuery({ids: currentUser.ignoresUsers}); + }, + // This endpoint is used for loading the user moderation queues (users whose username has been flagged), // so hide it in the event that we aren't an admin. users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 8b07c2ab2..86aafbb0e 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -537,6 +537,9 @@ type RootQuery { # role. me: User + # Users that the currently logged in user ignores + myIgnoredUsers: [User] + # Users returned based on a query. users(query: UsersQuery): [User] @@ -703,6 +706,13 @@ type RemoveCommentTagResponse implements Response { errors: [UserError] } +# Response to ignoreUser mutation +type IgnoreUserResponse implements Response { + # An array of errors relating to the mutation that occured. + errors: [UserError] +} + + # All mutations for the application are defined on this object. type RootMutation { @@ -735,6 +745,9 @@ type RootMutation { # Remove tag from comment. removeCommentTag(id: ID!, tag: String!): RemoveCommentTagResponse + + # Ignore comments by another user + ignoreUser(id: ID!): IgnoreUserResponse } ################################################################################ diff --git a/models/user.js b/models/user.js index e1cbb466b..2663410a7 100644 --- a/models/user.js +++ b/models/user.js @@ -117,7 +117,13 @@ const UserSchema = new mongoose.Schema({ type: String, default: '' } - } + }, + + ignoresUsers: [{ + + // user id of another user + type: String, + }] }, { // This will ensure that we have proper timestamps available on this model. diff --git a/services/users.js b/services/users.js index a86a6686d..45b113f35 100644 --- a/services/users.js +++ b/services/users.js @@ -1,3 +1,4 @@ +const assert = require('assert'); const bcrypt = require('bcrypt'); const url = require('url'); const jwt = require('jsonwebtoken'); @@ -834,4 +835,23 @@ module.exports = class UsersService { throw err; }); } + + /** + * Ignore another user + * @param {String} userId the id of the user that is ignoring another users + * @param {String[]} usersToIgnore Array of user IDs to ignore + */ + static ignoreUsers(userId, usersToIgnore) { + assert(Array.isArray(usersToIgnore), 'usersToIgnore is an array'); + assert(usersToIgnore.every(u => typeof u === 'string'), 'usersToIgnore is an array of string user IDs'); + + // TODO: For each usersToIgnore, make sure they exist? + return UserModel.update({id: userId}, { + $addToSet: { + ignoresUsers: { + $each: usersToIgnore + } + } + }); + } }; diff --git a/test/graph/mutations/ignoreUser.js b/test/graph/mutations/ignoreUser.js new file mode 100644 index 000000000..7dd103e5b --- /dev/null +++ b/test/graph/mutations/ignoreUser.js @@ -0,0 +1,55 @@ +const expect = require('chai').expect; +const {graphql} = require('graphql'); + +const schema = require('../../../graph/schema'); +const Context = require('../../../graph/context'); +const UsersService = require('../../../services/users'); +const SettingsService = require('../../../services/settings'); + +describe('graph.mutations.ignoreUser', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + const ignoreUserMutation = ` + mutation ignoreUser ($id: ID!) { + ignoreUser(id:$id) { + errors { + translation_key + } + } + } + `; + + const getMyIgnoredUsersQuery = ` + query myIgnoredUsers { + myIgnoredUsers { + id, + username + } + } + `; + + it('users can ignoreUser', async () => { + const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); + const context = new Context({user}); + const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: userToIgnore.id}); + if (ignoreUserResponse.errors && ignoreUserResponse.errors.length) { + console.error(ignoreUserResponse.errors); + } + expect(ignoreUserResponse.errors).to.be.empty; + + // now check my ignored users + const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); + if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { + console.error(myIgnoredUsersResponse.errors); + } + expect(myIgnoredUsersResponse.errors).to.be.empty; + const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; + expect(myIgnoredUsers.length).to.equal(1); + expect(myIgnoredUsers[0].id).to.equal(userToIgnore.id); + expect(myIgnoredUsers[0].username).to.equal(userToIgnore.username); + }); + +}); diff --git a/test/server/services/users.js b/test/server/services/users.js index 22fe7ddd5..04c5c457e 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -169,6 +169,24 @@ describe('services.UsersService', () => { }); }); + describe('#ignoreUser', () => { + + // @TODO: assert cannot ignore yourself + + it('should add user id to ignoredUsers set', async () => { + const user = mockUsers[0]; + const usersToIgnore = [mockUsers[1], mockUsers[2]]; + await UsersService.ignoreUsers(user.id, usersToIgnore.map(u => u.id)); + const userAfterIgnoring = await UsersService.findById(user.id); + expect(userAfterIgnoring.ignoresUsers.length).to.equal(2); + + // ignore same user another time, make sure it's not added to the list. + await UsersService.ignoreUsers(user.id, usersToIgnore.slice(0, 1).map(u => u.id)); + const userAfterIgnoring2 = await UsersService.findById(user.id); + expect(userAfterIgnoring2.ignoresUsers.length).to.equal(2); + }); + }); + describe('#ban', () => { it('should set the status to banned', () => { return UsersService From a9412c17c9b065f2dfe068f081fd5e18edbf79d0 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Thu, 6 Apr 2017 16:49:06 -0700 Subject: [PATCH 04/44] Ignore User UI actually sends mutation --- client/coral-embed-stream/src/Comment.js | 56 ++++++++++++++----- client/coral-embed-stream/src/Embed.js | 7 ++- client/coral-embed-stream/src/Stream.js | 7 ++- .../graphql/mutations/ignoreUser.graphql | 7 +++ .../graphql/mutations/index.js | 12 ++++ 5 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 client/coral-framework/graphql/mutations/ignoreUser.graphql diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 26ff01f0f..b3dfc5f8e 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -91,6 +91,9 @@ class Comment extends React.Component { // dispatch action to remove a tag from a comment removeCommentTag: React.PropTypes.func, + + // dispatch action to ignore another user + ignoreUser: React.PropTypes.func, } render () { @@ -113,6 +116,7 @@ class Comment extends React.Component { deleteAction, addCommentTag, removeCommentTag, + ignoreUser, disableReply, } = this.props; @@ -152,6 +156,9 @@ class Comment extends React.Component { name: PropTypes.string.isRequired }).isRequired, cancel: PropTypes.func.isRequired, + + // actually submit the ignore. Provide {id: user id to ignore} + ignoreUser: PropTypes.func, } constructor(props) { super(props); @@ -166,7 +173,7 @@ class Comment extends React.Component { this.props.cancel(); } render() { - const {user} = this.props; + const {user, ignoreUser} = this.props; const goToStep = (stepNum) => this.setState({step: stepNum}); const step1 = (
    @@ -178,13 +185,17 @@ class Comment extends React.Component {
    ); + const ignoreUserAndGotoStep3 = async () => { + await ignoreUser({id: user.id}); + goToStep(3); + }; const step2Confirmation = (
    Ignore User

    Are you sure you want to ignore { user.name }?

    - +
    ); @@ -220,16 +231,31 @@ class Comment extends React.Component { name: PropTypes.string.isRequired }).isRequired }).isRequired, + ignoreUser: PropTypes.func, } constructor(props) { + + // console.log('TopRightMenu#constructor', props) super(props); this.state = { chosenItem: null }; } + componentWillUnmount() { + + // console.log('TopRightMenu#componentWillUnmount') + } + componentDidMount() { + + // console.log('TopRightMenu#componentDidMount') + } + componentWillReceiveProps(nextProps) { + + // console.log('TopRightMenu#componentWillReceiveProps', nextProps) + } render() { const {chosenItem} = this.state; - const {comment} = this.props; + const {comment, ignoreUser} = this.props; let child; const chooseIgnoreUser = () => { this.setState({chosenItem: 'IGNORE_USER'}); @@ -238,18 +264,19 @@ class Comment extends React.Component { switch (chosenItem) { case 'IGNORE_USER': child = ( - - ); + + ); break; default: child = ( - - Ignore User - - ); + + Ignore User + + ); } return ( @@ -281,7 +308,9 @@ class Comment extends React.Component { - + @@ -365,6 +394,7 @@ class Comment extends React.Component { deleteAction={deleteAction} addCommentTag={addCommentTag} removeCommentTag={removeCommentTag} + ignoreUser={ignoreUser} showSignInDialog={showSignInDialog} reactKey={reply.id} key={reply.id} diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index d9b5c8310..437152eac 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -14,7 +14,7 @@ const {fetchAssetSuccess} = assetActions; import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments'; import {queryStream} from 'coral-framework/graphql/queries'; -import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations'; +import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; import {editName} from 'coral-framework/actions/user'; import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset'; import {notificationActions, authActions, assetActions, pym} from 'coral-framework'; @@ -64,6 +64,9 @@ class Embed extends Component { // dispatch action to remove a tag from a comment removeCommentTag: React.PropTypes.func, + + // dispatch action to ignore another user + ignoreUser: React.PropTypes.func, } componentDidMount () { @@ -250,6 +253,7 @@ class Embed extends Component { postDontAgree={this.props.postDontAgree} addCommentTag={this.props.addCommentTag} removeCommentTag={this.props.removeCommentTag} + ignoreUser={this.props.ignoreUser} loadMore={this.props.loadMore} deleteAction={this.props.deleteAction} showSignInDialog={this.props.showSignInDialog} @@ -311,6 +315,7 @@ export default compose( postLike, postDontAgree, addCommentTag, + ignoreUser, removeCommentTag, deleteAction, queryStream, diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js index 66c19e0ab..8ac9d2e40 100644 --- a/client/coral-embed-stream/src/Stream.js +++ b/client/coral-embed-stream/src/Stream.js @@ -19,6 +19,9 @@ class Stream extends React.Component { // dispatch action to remove a tag from a comment removeCommentTag: PropTypes.func, + + // dispatch action to ignore another user + ignoreUser: React.PropTypes.func, } constructor(props) { @@ -42,7 +45,8 @@ class Stream extends React.Component { showSignInDialog, addCommentTag, removeCommentTag, - pluginProps + pluginProps, + ignoreUser, } = this.props; return ( @@ -63,6 +67,7 @@ class Stream extends React.Component { postDontAgree={postDontAgree} addCommentTag={addCommentTag} removeCommentTag={removeCommentTag} + ignoreUser ={ignoreUser} loadMore={loadMore} deleteAction={deleteAction} showSignInDialog={showSignInDialog} diff --git a/client/coral-framework/graphql/mutations/ignoreUser.graphql b/client/coral-framework/graphql/mutations/ignoreUser.graphql new file mode 100644 index 000000000..ad3c399f3 --- /dev/null +++ b/client/coral-framework/graphql/mutations/ignoreUser.graphql @@ -0,0 +1,7 @@ +mutation ignoreUser ($id: ID!) { + ignoreUser(id:$id) { + errors { + translation_key + } + } +} diff --git a/client/coral-framework/graphql/mutations/index.js b/client/coral-framework/graphql/mutations/index.js index 04dba8402..114303d2d 100644 --- a/client/coral-framework/graphql/mutations/index.js +++ b/client/coral-framework/graphql/mutations/index.js @@ -6,6 +6,7 @@ import POST_DONT_AGREE from './postDontAgree.graphql'; import DELETE_ACTION from './deleteAction.graphql'; import ADD_COMMENT_TAG from './addCommentTag.graphql'; import REMOVE_COMMENT_TAG from './removeCommentTag.graphql'; +import IGNORE_USER from './ignoreUser.graphql'; import commentView from '../fragments/commentView.graphql'; @@ -148,3 +149,14 @@ export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, { }); }}), }); + +export const ignoreUser = graphql(IGNORE_USER, { + props: ({mutate}) => ({ + ignoreUser: ({id}) => { + return mutate({ + variables: { + id, + } + }); + }}), +}); From 990b9794e94ce1f07ca3586a0ad2768a6dad7a9e Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Thu, 6 Apr 2017 18:27:10 -0700 Subject: [PATCH 05/44] When a user is ignored, replace their comments with a tombstone --- .../components/ConfigureCommentStream.css | 8 +-- client/coral-embed-stream/src/Comment.css | 6 +- client/coral-embed-stream/src/Embed.js | 22 ++++-- client/coral-embed-stream/src/Stream.js | 70 ++++++++++++------- client/coral-framework/actions/user.js | 4 ++ client/coral-framework/constants/user.js | 1 + client/coral-framework/reducers/user.js | 7 +- 7 files changed, 81 insertions(+), 37 deletions(-) diff --git a/client/coral-configure/components/ConfigureCommentStream.css b/client/coral-configure/components/ConfigureCommentStream.css index ea20e4c05..335c94377 100644 --- a/client/coral-configure/components/ConfigureCommentStream.css +++ b/client/coral-configure/components/ConfigureCommentStream.css @@ -9,12 +9,12 @@ right: 0; } -ul { +.wrapper ul { list-style: none; padding: 0; } -ul ul { +.wrapper ul ul { padding-left: 20px } @@ -23,12 +23,12 @@ ul ul { margin: 12px 12px 12px 0; } -h4 { +.wrapper h4 { font-size: 14px; margin-bottom: 5px; } -p { +.wrapper p { max-width: 380px; } diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index a1f7b8654..6f89208bc 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -38,7 +38,10 @@ .Menu { border: 1px solid #ddd; margin: 0; - +} +ul.Menu { + list-style-type: none; + padding: 0; } .MenuItem { @@ -50,6 +53,7 @@ background-color: #2E343B; color: white; padding: 1em; + max-width: 220px; } .IgnoreUserWizard header { diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 437152eac..3afd749cb 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -15,7 +15,7 @@ import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comment import {queryStream} from 'coral-framework/graphql/queries'; import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; -import {editName} from 'coral-framework/actions/user'; +import {editName, ignoreUserSuccess} from 'coral-framework/actions/user'; import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset'; import {notificationActions, authActions, assetActions, pym} from 'coral-framework'; @@ -117,6 +117,15 @@ class Embed extends Component { } } + ignoreUser = async ({id}) => { + const {ignoreUser, dispatch} = this.props; + await ignoreUser({id}); + + // dispatch ignoreUserSuccess so other reducers can know about the newly + // ignored user (e.g. to hide coments by that user) + dispatch(ignoreUserSuccess({id})); + } + render () { const {activeTab} = this.state; const {closedAt, countCache = {}} = this.props.asset; @@ -253,11 +262,12 @@ class Embed extends Component { postDontAgree={this.props.postDontAgree} addCommentTag={this.props.addCommentTag} removeCommentTag={this.props.removeCommentTag} - ignoreUser={this.props.ignoreUser} + ignoreUser={this.ignoreUser} loadMore={this.props.loadMore} deleteAction={this.props.deleteAction} showSignInDialog={this.props.showSignInDialog} - comments={asset.comments} /> + comments={asset.comments} + ignoredUsers={this.props.userData.ignoredUsers} />
    ({ auth: state.auth.toJS(), userData: state.user.toJS(), - asset: state.asset.toJS() + asset: state.asset.toJS(), }); const mapDispatchToProps = dispatch => ({ @@ -305,7 +315,7 @@ const mapDispatchToProps = dispatch => ({ updateCountCache: (id, count) => dispatch(updateCountCache(id, count)), viewAllComments: () => dispatch(viewAllComments()), logout: () => dispatch(logout()), - dispatch: d => dispatch(d) + dispatch: d => dispatch(d), }); export default compose( @@ -315,8 +325,8 @@ export default compose( postLike, postDontAgree, addCommentTag, - ignoreUser, removeCommentTag, + ignoreUser, deleteAction, queryStream, )(Embed); diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js index 8ac9d2e40..4758af1c0 100644 --- a/client/coral-embed-stream/src/Stream.js +++ b/client/coral-embed-stream/src/Stream.js @@ -22,6 +22,9 @@ class Stream extends React.Component { // dispatch action to ignore another user ignoreUser: React.PropTypes.func, + + // list of user ids that should be rendered as ignored + ignoredUsers: React.PropTypes.arrayOf(React.PropTypes.string) } constructor(props) { @@ -47,35 +50,40 @@ class Stream extends React.Component { removeCommentTag, pluginProps, ignoreUser, + ignoredUsers, } = this.props; - + const commentIsIgnored = (comment) => ignoredUsers && ignoredUsers.includes(comment.user.id); return (
    { comments.map(comment => - + commentIsIgnored(comment) + ? + : ) }
    @@ -83,4 +91,18 @@ class Stream extends React.Component { } } +const IgnoredCommentTombstone = () => ( +
    +
    +

    + This comment is hidden because you ignored this user. +

    +
    +); + export default Stream; diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index 3e80b718c..dd63cf215 100644 --- a/client/coral-framework/actions/user.js +++ b/client/coral-framework/actions/user.js @@ -1,6 +1,7 @@ import {addNotification} from '../actions/notification'; import coralApi from '../helpers/response'; import * as actions from '../constants/auth'; +import {IGNORE_USER_SUCCESS} from '../constants/user'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from './../translations'; @@ -19,3 +20,6 @@ export const editName = (username) => (dispatch) => { dispatch(editUsernameFailure(lang.t(`error.${error.translation_key}`))); }); }; + +// a user was successfully ignored +export const ignoreUserSuccess = ({id}) => ({type: IGNORE_USER_SUCCESS, id}); diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js index 5f040db5b..66e58e930 100644 --- a/client/coral-framework/constants/user.js +++ b/client/coral-framework/constants/user.js @@ -6,3 +6,4 @@ export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS'; export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; export const UPDATE_USERNAME = 'UPDATE_USERNAME'; +export const IGNORE_USER_SUCCESS = 'IGNORE_USER_SUCCESS'; diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js index b8ff54380..0c77b3f69 100644 --- a/client/coral-framework/reducers/user.js +++ b/client/coral-framework/reducers/user.js @@ -1,4 +1,4 @@ -import {Map} from 'immutable'; +import {Map, Set} from 'immutable'; import * as authActions from '../constants/auth'; import * as actions from '../constants/user'; import * as assetActions from '../constants/assets'; @@ -8,7 +8,8 @@ const initialState = Map({ profiles: [], settings: {}, myComments: [], - myAssets: [] // the assets from which myComments (above) originated + myAssets: [], // the assets from which myComments (above) originated + ignoredUsers: Set(), }); const purge = user => { @@ -38,6 +39,8 @@ export default function user (state = initialState, action) { return state.set('myAssets', action.assets); case actions.LOGOUT_SUCCESS: return initialState; + case actions.IGNORE_USER_SUCCESS: + return state.updateIn(['ignoredUsers'], i => i.add(action.id)); default : return state; } From 803db345accc668eee96d193ea7161d782b6c0de Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Fri, 7 Apr 2017 10:54:18 -0700 Subject: [PATCH 06/44] TopRightMenu no longer shows a list of choices, just IgnoreUserWizard --- client/coral-embed-stream/src/Comment.js | 60 ++++++------------------ 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index b3dfc5f8e..b0f83a684 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -185,9 +185,8 @@ class Comment extends React.Component {
    ); - const ignoreUserAndGotoStep3 = async () => { + const onClickIgnoreUser = async () => { await ignoreUser({id: user.id}); - goToStep(3); }; const step2Confirmation = (
    @@ -195,7 +194,7 @@ class Comment extends React.Component {

    Are you sure you want to ignore { user.name }?

    - +
    ); @@ -219,8 +218,9 @@ class Comment extends React.Component { } } - // Menu of actions in top right of Comment. - // When you choose 'Ignore User', the menu choices are replaced with the Ignore User flow + // TopRightMenu appears as a dropdown in the top right of the comment. + // when you click the down cehvron, it expands and shows IgnoreUserWizard + // when you click 'cancel' in the wizard, it closes the menu class TopRightMenu extends React.Component { static propTypes = { @@ -234,54 +234,24 @@ class Comment extends React.Component { ignoreUser: PropTypes.func, } constructor(props) { - - // console.log('TopRightMenu#constructor', props) super(props); this.state = { - chosenItem: null + timesReset: 0 }; } - componentWillUnmount() { - - // console.log('TopRightMenu#componentWillUnmount') - } - componentDidMount() { - - // console.log('TopRightMenu#componentDidMount') - } - componentWillReceiveProps(nextProps) { - - // console.log('TopRightMenu#componentWillReceiveProps', nextProps) - } render() { - const {chosenItem} = this.state; const {comment, ignoreUser} = this.props; - let child; - const chooseIgnoreUser = () => { - this.setState({chosenItem: 'IGNORE_USER'}); - }; - const reset = () => this.setState({chosenItem: null}); - switch (chosenItem) { - case 'IGNORE_USER': - child = ( - - ); - break; - default: - child = ( - - Ignore User - - ); - } + + // timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable) + const reset = () => this.setState({timesReset: this.state.timesReset + 1}); return ( - +
    - { child } +
    ); From bc3ec84180f74fbb621318a066f68705021ab133 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Fri, 7 Apr 2017 12:25:33 -0700 Subject: [PATCH 07/44] IgnoredCommentTombstone renders for replies too --- client/coral-embed-stream/src/Comment.js | 50 +++++++++++-------- .../src/IgnoredCommentTombstone.js | 18 +++++++ client/coral-embed-stream/src/Stream.js | 18 ++----- 3 files changed, 49 insertions(+), 37 deletions(-) create mode 100644 client/coral-embed-stream/src/IgnoredCommentTombstone.js diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index b0f83a684..7f9f948ed 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -21,6 +21,7 @@ import LikeButton from 'coral-plugin-likes/LikeButton'; import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton'; import LoadMore from 'coral-embed-stream/src/LoadMore'; import {Slot} from 'coral-framework'; +import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import styles from './Comment.css'; import classnames from 'classnames'; @@ -86,6 +87,9 @@ class Comment extends React.Component { }).isRequired }).isRequired, + // given a comment, return whether it should be rendered as ignored + commentIsIgnored: React.PropTypes.func, + // dispatch action to add a tag to a comment addCommentTag: React.PropTypes.func, @@ -118,6 +122,7 @@ class Comment extends React.Component { removeCommentTag, ignoreUser, disableReply, + commentIsIgnored, } = this.props; const like = getActionSummary('LikeActionSummary', comment); @@ -348,28 +353,29 @@ class Comment extends React.Component { { comment.replies && comment.replies.map(reply => { - return ; + return commentIsIgnored(reply) + ? + : ; }) } { diff --git a/client/coral-embed-stream/src/IgnoredCommentTombstone.js b/client/coral-embed-stream/src/IgnoredCommentTombstone.js new file mode 100644 index 000000000..aca235a02 --- /dev/null +++ b/client/coral-embed-stream/src/IgnoredCommentTombstone.js @@ -0,0 +1,18 @@ +import React from 'react'; + +// Render in place of a Comment when the author of the comment is ignored +const IgnoredCommentTombstone = () => ( +
    +
    +

    + This comment is hidden because you ignored this user. +

    +
    +); + +export default IgnoredCommentTombstone; diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js index 4758af1c0..f91e496e8 100644 --- a/client/coral-embed-stream/src/Stream.js +++ b/client/coral-embed-stream/src/Stream.js @@ -1,5 +1,6 @@ import React, {PropTypes} from 'react'; import Comment from './Comment'; +import IgnoredCommentTombstone from './IgnoredCommentTombstone'; class Stream extends React.Component { @@ -75,7 +76,8 @@ class Stream extends React.Component { postDontAgree={postDontAgree} addCommentTag={addCommentTag} removeCommentTag={removeCommentTag} - ignoreUser ={ignoreUser} + ignoreUser={ignoreUser} + commentIsIgnored={commentIsIgnored} loadMore={loadMore} deleteAction={deleteAction} showSignInDialog={showSignInDialog} @@ -91,18 +93,4 @@ class Stream extends React.Component { } } -const IgnoredCommentTombstone = () => ( -
    -
    -

    - This comment is hidden because you ignored this user. -

    -
    -); - export default Stream; From 70ec5d5f54ea6739061c1fe42ff660f67d877822 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Fri, 7 Apr 2017 12:26:13 -0700 Subject: [PATCH 08/44] Remove font-size: 100% for all elements in embed-stream --- client/coral-embed-stream/style/default.css | 1 - 1 file changed, 1 deletion(-) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 2ef617593..2b252b323 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -2,7 +2,6 @@ font-weight: inherit; font-family: inherit; font-style: inherit; - font-size: 100%; } html, body { From c59ddb72a6386b6a52a198ad672bae406cbf8cf4 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Fri, 7 Apr 2017 13:49:33 -0700 Subject: [PATCH 09/44] Manage ignored users UI --- client/coral-embed-stream/src/Embed.js | 37 +++++++------- client/coral-embed-stream/style/default.css | 1 - .../coral-framework/graphql/queries/index.js | 9 ++++ .../graphql/queries/myIgnoredUsers.graphql | 6 +++ client/coral-plugin-history/Comment.css | 4 ++ .../components/IgnoredUsers.css | 24 ++++++++++ .../coral-settings/components/IgnoredUsers.js | 32 +++++++++++++ .../components/ProfileHeader.css | 8 ---- .../components/ProfileHeader.js | 12 ----- .../containers/ProfileContainer.js | 48 ++++++++++++------- test/graph/mutations/ignoreUser.js | 1 + 11 files changed, 126 insertions(+), 56 deletions(-) create mode 100644 client/coral-framework/graphql/queries/myIgnoredUsers.graphql create mode 100644 client/coral-settings/components/IgnoredUsers.css create mode 100644 client/coral-settings/components/IgnoredUsers.js delete mode 100644 client/coral-settings/components/ProfileHeader.css delete mode 100644 client/coral-settings/components/ProfileHeader.js diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 3afd749cb..86bd29c13 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -152,6 +152,8 @@ class Embed extends Component { ? asset.comments[0].created_at : new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(); + const userBox = this.props.logout().then(refetch)} changeTab={this.changeTab}/>; + return (
    @@ -170,8 +172,8 @@ class Embed extends Component { this.props.data.refetch(); }}>{lang.t('showAllComments')} } - {loggedIn && this.props.logout().then(refetch)} changeTab={this.changeTab}/>} + { loggedIn ? userBox : null } { openStream ?
    @@ -277,22 +279,23 @@ class Embed extends Component { loadMore={this.props.loadMore} />
    } -
    - - - - - - - - + + + + + + + { loggedIn ? userBox : null } + + +
    ); diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 2b252b323..3c40908f5 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -1,5 +1,4 @@ * { - font-weight: inherit; font-family: inherit; font-style: inherit; } diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index 32bdfb6c9..5bbe58f7a 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -3,6 +3,7 @@ import STREAM_QUERY from './streamQuery.graphql'; import LOAD_MORE from './loadMore.graphql'; import GET_COUNTS from './getCounts.graphql'; import MY_COMMENT_HISTORY from './myCommentHistory.graphql'; +import MY_IGNORED_USERS from './myIgnoredUsers.graphql'; import uniqBy from 'lodash/uniqBy'; import sortBy from 'lodash/sortBy'; import isNil from 'lodash/isNil'; @@ -144,3 +145,11 @@ export const queryStream = graphql(STREAM_QUERY, { }); export const myCommentHistory = graphql(MY_COMMENT_HISTORY, {}); + +export const myIgnoredUsers = graphql(MY_IGNORED_USERS, { + props: ({data}) => { + return ({ + myIgnoredUsersData: data + }); + } +}); diff --git a/client/coral-framework/graphql/queries/myIgnoredUsers.graphql b/client/coral-framework/graphql/queries/myIgnoredUsers.graphql new file mode 100644 index 000000000..d81531e37 --- /dev/null +++ b/client/coral-framework/graphql/queries/myIgnoredUsers.graphql @@ -0,0 +1,6 @@ +query myIgnoredUsers { + myIgnoredUsers { + id, + username, + } +} diff --git a/client/coral-plugin-history/Comment.css b/client/coral-plugin-history/Comment.css index 2c0dee094..bc2134639 100644 --- a/client/coral-plugin-history/Comment.css +++ b/client/coral-plugin-history/Comment.css @@ -1,6 +1,7 @@ @custom-media --big-viewport (min-width: 780px); .myComment { + margin: 1em 0; border-bottom: 1px solid lightgrey; display: flex; align-items: baseline; @@ -24,6 +25,9 @@ .sidebar { ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; min-width: 136px; } diff --git a/client/coral-settings/components/IgnoredUsers.css b/client/coral-settings/components/IgnoredUsers.css new file mode 100644 index 000000000..716140256 --- /dev/null +++ b/client/coral-settings/components/IgnoredUsers.css @@ -0,0 +1,24 @@ +.ignoredUser { + display: table-row; +} + +.ignoredUserList { + display: table; +} + +.ignoredUser > * { + display: table-cell; +} + +.stopListening { + color: #D0011B; +} + +.link { + text-decoration: underline; + cursor: pointer; +} + +.stopListening:before { + content: '\00a0\00a0\00a0\00a0'; +} \ No newline at end of file diff --git a/client/coral-settings/components/IgnoredUsers.js b/client/coral-settings/components/IgnoredUsers.js new file mode 100644 index 000000000..5719337d4 --- /dev/null +++ b/client/coral-settings/components/IgnoredUsers.js @@ -0,0 +1,32 @@ +import React, {Component} from 'react'; + +import styles from './IgnoredUsers.css'; + +export class IgnoredUsers extends Component { + render() { + const {users} = this.props; + return ( +
    + { + users.length + ?

    Because you ignored these, you do not see their comments.

    + : null + } +
    + { + users.map(({username, id}) => ( + +
    { username }
    +
    + Stop ignoring +
    +
    + )) + } +
    +
    + ); + } +} + +export default IgnoredUsers; diff --git a/client/coral-settings/components/ProfileHeader.css b/client/coral-settings/components/ProfileHeader.css deleted file mode 100644 index f97c4731e..000000000 --- a/client/coral-settings/components/ProfileHeader.css +++ /dev/null @@ -1,8 +0,0 @@ -.header h1 { - margin: 4px 0; -} - -.header h2 { - font-size: 13px; -} - diff --git a/client/coral-settings/components/ProfileHeader.js b/client/coral-settings/components/ProfileHeader.js deleted file mode 100644 index 24b2222bd..000000000 --- a/client/coral-settings/components/ProfileHeader.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, {PropTypes} from 'react'; -import styles from './ProfileHeader.css'; - -const ProfileHeader = ({username}) => ( -
    -

    {username}

    -
    -); - -ProfileHeader.propTypes = {username: PropTypes.string.isRequired}; - -export default ProfileHeader; diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 9a8117e87..480c9e1fe 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -3,12 +3,12 @@ import {compose} from 'react-apollo'; import React, {Component} from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; -import {myCommentHistory} from 'coral-framework/graphql/queries'; +import {myCommentHistory, myIgnoredUsers} from 'coral-framework/graphql/queries'; import {link} from 'coral-framework/services/PymConnection'; import NotLoggedIn from '../components/NotLoggedIn'; +import IgnoredUsers from '../components/IgnoredUsers'; import {Spinner} from 'coral-ui'; -import ProfileHeader from '../components/ProfileHeader'; import CommentHistory from 'coral-plugin-history/CommentHistory'; import translations from '../translations'; @@ -31,7 +31,7 @@ class ProfileContainer extends Component { } render() { - const {loggedIn, asset, showSignInDialog, data} = this.props; + const {loggedIn, asset, showSignInDialog, data, myIgnoredUsersData} = this.props; const {me} = this.props.data; if (!loggedIn || !me) { @@ -42,17 +42,34 @@ class ProfileContainer extends Component { return ; } + const localProfile = this.props.user.profiles.find(p => p.provider === 'local'); + const emailAddress = localProfile && localProfile.id; + return (
    - - { +

    {this.props.userData.username}

    + { emailAddress + ?

    { emailAddress }

    + : null + } - // Hiding bio until moderation can get figured out - /* - {lang.t('allComments')} ({user.myComments.length}) - {lang.t('profileSettings')} - - */ + { + myIgnoredUsersData.myIgnoredUsers && myIgnoredUsersData.myIgnoredUsers.length + ? ( +
    +

    Ignored users

    + +
    + ) + : null + } + +
    + +

    My comments

    + { me.comments.length ? :

    {lang.t('userNoComment')}

    - - // Hiding user bio pending effective moderation system. - /*
    - - - */ }
    @@ -87,5 +98,6 @@ const mapDispatchToProps = () => ({ export default compose( connect(mapStateToProps, mapDispatchToProps), - myCommentHistory + myCommentHistory, + myIgnoredUsers, )(ProfileContainer); diff --git a/test/graph/mutations/ignoreUser.js b/test/graph/mutations/ignoreUser.js index 7dd103e5b..6ede39477 100644 --- a/test/graph/mutations/ignoreUser.js +++ b/test/graph/mutations/ignoreUser.js @@ -30,6 +30,7 @@ describe('graph.mutations.ignoreUser', () => { } `; + // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); const userToIgnore = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); From 2656141d5912ac5e58d598ed50a4b9f4e301b562 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Fri, 7 Apr 2017 16:05:30 -0700 Subject: [PATCH 10/44] Add stopIgnoringUser mutation and Embed uses it --- client/coral-embed-stream/src/Embed.js | 13 +-- client/coral-framework/actions/user.js | 4 - client/coral-framework/constants/user.js | 1 + .../graphql/mutations/index.js | 22 ++++- .../mutations/stopIgnoringUser.graphql | 7 ++ client/coral-framework/reducers/user.js | 13 ++- .../coral-settings/components/IgnoredUsers.js | 19 +++- .../containers/ProfileContainer.js | 5 +- graph/mutators/user.js | 6 ++ graph/resolvers/root_mutation.js | 3 + graph/typeDefs.graphql | 8 ++ services/users.js | 16 ++++ test/graph/mutations/ignoreUser.js | 95 +++++++++++++++---- 13 files changed, 168 insertions(+), 44 deletions(-) create mode 100644 client/coral-framework/graphql/mutations/stopIgnoringUser.graphql diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 86bd29c13..a7a0b5740 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -15,7 +15,7 @@ import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comment import {queryStream} from 'coral-framework/graphql/queries'; import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag, ignoreUser} from 'coral-framework/graphql/mutations'; -import {editName, ignoreUserSuccess} from 'coral-framework/actions/user'; +import {editName} from 'coral-framework/actions/user'; import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset'; import {notificationActions, authActions, assetActions, pym} from 'coral-framework'; @@ -117,15 +117,6 @@ class Embed extends Component { } } - ignoreUser = async ({id}) => { - const {ignoreUser, dispatch} = this.props; - await ignoreUser({id}); - - // dispatch ignoreUserSuccess so other reducers can know about the newly - // ignored user (e.g. to hide coments by that user) - dispatch(ignoreUserSuccess({id})); - } - render () { const {activeTab} = this.state; const {closedAt, countCache = {}} = this.props.asset; @@ -264,7 +255,7 @@ class Embed extends Component { postDontAgree={this.props.postDontAgree} addCommentTag={this.props.addCommentTag} removeCommentTag={this.props.removeCommentTag} - ignoreUser={this.ignoreUser} + ignoreUser={this.props.ignoreUser} loadMore={this.props.loadMore} deleteAction={this.props.deleteAction} showSignInDialog={this.props.showSignInDialog} diff --git a/client/coral-framework/actions/user.js b/client/coral-framework/actions/user.js index dd63cf215..3e80b718c 100644 --- a/client/coral-framework/actions/user.js +++ b/client/coral-framework/actions/user.js @@ -1,7 +1,6 @@ import {addNotification} from '../actions/notification'; import coralApi from '../helpers/response'; import * as actions from '../constants/auth'; -import {IGNORE_USER_SUCCESS} from '../constants/user'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from './../translations'; @@ -20,6 +19,3 @@ export const editName = (username) => (dispatch) => { dispatch(editUsernameFailure(lang.t(`error.${error.translation_key}`))); }); }; - -// a user was successfully ignored -export const ignoreUserSuccess = ({id}) => ({type: IGNORE_USER_SUCCESS, id}); diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js index 66e58e930..1557a42c9 100644 --- a/client/coral-framework/constants/user.js +++ b/client/coral-framework/constants/user.js @@ -7,3 +7,4 @@ export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; export const UPDATE_USERNAME = 'UPDATE_USERNAME'; export const IGNORE_USER_SUCCESS = 'IGNORE_USER_SUCCESS'; +export const STOP_IGNORING_USER_SUCCESS = 'STOP_IGNORING_USER_SUCCESS'; diff --git a/client/coral-framework/graphql/mutations/index.js b/client/coral-framework/graphql/mutations/index.js index 114303d2d..64a39d108 100644 --- a/client/coral-framework/graphql/mutations/index.js +++ b/client/coral-framework/graphql/mutations/index.js @@ -7,6 +7,9 @@ import DELETE_ACTION from './deleteAction.graphql'; import ADD_COMMENT_TAG from './addCommentTag.graphql'; import REMOVE_COMMENT_TAG from './removeCommentTag.graphql'; import IGNORE_USER from './ignoreUser.graphql'; +import STOP_IGNORING_USER from './stopIgnoringUser.graphql'; + +import MY_IGNORED_USERS from '../queries/myIgnoredUsers.graphql'; import commentView from '../fragments/commentView.graphql'; @@ -156,7 +159,24 @@ export const ignoreUser = graphql(IGNORE_USER, { return mutate({ variables: { id, - } + }, + refetchQueries: [{ + query: MY_IGNORED_USERS, + }] + }); + }}), +}); + +export const stopIgnoringUser = graphql(STOP_IGNORING_USER, { + props: ({mutate}) => ({ + stopIgnoringUser: ({id}) => { + return mutate({ + variables: { + id, + }, + refetchQueries: [{ + query: MY_IGNORED_USERS, + }] }); }}), }); diff --git a/client/coral-framework/graphql/mutations/stopIgnoringUser.graphql b/client/coral-framework/graphql/mutations/stopIgnoringUser.graphql new file mode 100644 index 000000000..042452ff5 --- /dev/null +++ b/client/coral-framework/graphql/mutations/stopIgnoringUser.graphql @@ -0,0 +1,7 @@ +mutation stopIgnoringUser ($id: ID!) { + stopIgnoringUser(id:$id) { + errors { + translation_key + } + } +} diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js index 0c77b3f69..efa967cf0 100644 --- a/client/coral-framework/reducers/user.js +++ b/client/coral-framework/reducers/user.js @@ -39,9 +39,14 @@ export default function user (state = initialState, action) { return state.set('myAssets', action.assets); case actions.LOGOUT_SUCCESS: return initialState; - case actions.IGNORE_USER_SUCCESS: - return state.updateIn(['ignoredUsers'], i => i.add(action.id)); - default : - return state; + case 'APOLLO_MUTATION_RESULT': + switch (action.operationName) { + case 'ignoreUser': + return state.updateIn(['ignoredUsers'], i => i.add(action.variables.id)); + case 'stopIgnoringUser': + return state.updateIn(['ignoredUsers'], i => i.delete(action.variables.id)); + } + break; } + return state; } diff --git a/client/coral-settings/components/IgnoredUsers.js b/client/coral-settings/components/IgnoredUsers.js index 5719337d4..4bbf6cc50 100644 --- a/client/coral-settings/components/IgnoredUsers.js +++ b/client/coral-settings/components/IgnoredUsers.js @@ -1,10 +1,19 @@ -import React, {Component} from 'react'; +import React, {Component, PropTypes} from 'react'; import styles from './IgnoredUsers.css'; export class IgnoredUsers extends Component { + static propTypes = { + users: PropTypes.arrayOf(PropTypes.shape({ + username: PropTypes.string, + id: PropTypes.string, + })).isRequired, + + // accepts { id } + stopIgnoring: PropTypes.func.isRequired, + } render() { - const {users} = this.props; + const {users, stopIgnoring} = this.props; return (
    { @@ -15,10 +24,12 @@ export class IgnoredUsers extends Component {
    { users.map(({username, id}) => ( - +
    { username }
    - Stop ignoring + stopIgnoring({id})} + className={styles.link}>Stop ignoring
    )) diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index 480c9e1fe..2d756077d 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -4,6 +4,7 @@ import React, {Component} from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import {myCommentHistory, myIgnoredUsers} from 'coral-framework/graphql/queries'; +import {stopIgnoringUser} from 'coral-framework/graphql/mutations'; import {link} from 'coral-framework/services/PymConnection'; import NotLoggedIn from '../components/NotLoggedIn'; @@ -31,7 +32,7 @@ class ProfileContainer extends Component { } render() { - const {loggedIn, asset, showSignInDialog, data, myIgnoredUsersData} = this.props; + const {loggedIn, asset, showSignInDialog, data, myIgnoredUsersData, stopIgnoringUser} = this.props; const {me} = this.props.data; if (!loggedIn || !me) { @@ -60,6 +61,7 @@ class ProfileContainer extends Component {

    Ignored users

    ) @@ -100,4 +102,5 @@ export default compose( connect(mapStateToProps, mapDispatchToProps), myCommentHistory, myIgnoredUsers, + stopIgnoringUser, )(ProfileContainer); diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 04235c217..fd888a402 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -17,12 +17,18 @@ const ignoreUser = async ({user}, userToIgnore) => { return await UsersService.ignoreUsers(user.id, [userToIgnore.id]); }; +const stopIgnoringUser = async ({user}, userToStopIgnoring) => { + console.log('stopIgnoringUser!!'); + return await UsersService.stopIgnoringUsers(user.id, [userToStopIgnoring.id]); +}; + module.exports = (context) => { let mutators = { User: { setUserStatus: () => Promise.reject(errors.ErrNotAuthorized), suspendUser: () => Promise.reject(errors.ErrNotAuthorized), ignoreUser: (action) => ignoreUser(context, action), + stopIgnoringUser: (action) => stopIgnoringUser(context, action), } }; diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index 0e763889f..38183bff6 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -26,6 +26,9 @@ const RootMutation = { ignoreUser(_, {id}, {mutators: {User}}) { return wrapResponse(null)(User.ignoreUser({id})); }, + stopIgnoringUser(_, {id}, {mutators: {User}}) { + return wrapResponse(null)(User.stopIgnoringUser({id})); + }, setCommentStatus(_, {id, status}, {mutators: {Comment}}) { return wrapResponse(null)(Comment.setCommentStatus({id, status})); }, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 86aafbb0e..87926a5da 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -712,6 +712,11 @@ type IgnoreUserResponse implements Response { errors: [UserError] } +# Response to stopIgnoringUser mutation +type StopIgnoringUserResponse implements Response { + # An array of errors relating to the mutation that occured. + errors: [UserError] +} # All mutations for the application are defined on this object. type RootMutation { @@ -748,6 +753,9 @@ type RootMutation { # Ignore comments by another user ignoreUser(id: ID!): IgnoreUserResponse + + # Stop Ignoring comments by another user + stopIgnoringUser(id: ID!): StopIgnoringUserResponse } ################################################################################ diff --git a/services/users.js b/services/users.js index 45b113f35..da3b23bc6 100644 --- a/services/users.js +++ b/services/users.js @@ -854,4 +854,20 @@ module.exports = class UsersService { } }); } + + /** + * Stop ignoring other users + * @param {String} userId the id of the user that is ignoring another users + * @param {String[]} usersToStopIgnoring Array of user IDs to stop ignoring + */ + static async stopIgnoringUsers(userId, usersToStopIgnoring) { + assert(Array.isArray(usersToStopIgnoring), 'usersToStopIgnoring is an array'); + assert(usersToStopIgnoring.every(u => typeof u === 'string'), 'usersToStopIgnoring is an array of string user IDs'); + await UserModel.update({id: userId}, { + $pullAll: { + ignoresUsers: usersToStopIgnoring + } + }); + console.log('Mongo wrote stopIgnoringUsers', usersToStopIgnoring); + } }; diff --git a/test/graph/mutations/ignoreUser.js b/test/graph/mutations/ignoreUser.js index 6ede39477..fc6035839 100644 --- a/test/graph/mutations/ignoreUser.js +++ b/test/graph/mutations/ignoreUser.js @@ -6,30 +6,30 @@ const Context = require('../../../graph/context'); const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); +const ignoreUserMutation = ` + mutation ignoreUser ($id: ID!) { + ignoreUser(id:$id) { + errors { + translation_key + } + } + } +`; + +const getMyIgnoredUsersQuery = ` + query myIgnoredUsers { + myIgnoredUsers { + id, + username + } + } +`; + describe('graph.mutations.ignoreUser', () => { beforeEach(async () => { await SettingsService.init(); }); - const ignoreUserMutation = ` - mutation ignoreUser ($id: ID!) { - ignoreUser(id:$id) { - errors { - translation_key - } - } - } - `; - - const getMyIgnoredUsersQuery = ` - query myIgnoredUsers { - myIgnoredUsers { - id, - username - } - } - `; - // @TODO (bengo) - test a user can't ignore themselves it('users can ignoreUser', async () => { const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); @@ -54,3 +54,60 @@ describe('graph.mutations.ignoreUser', () => { }); }); + +describe('graph.mutations.stopIgnoringUser', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + it('users can stop ignoring another user they ignore', async () => { + + // We're going to ignore 2 users, + // then stopIgnoring 1 of them + // then assert myIgnoredUsers only lists the one remaining + const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + const usersToIgnore = await Promise.all([ + UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'), + UsersService.createLocalUser('usernameC@example.com', 'password', 'usernameC'), + ]); + const context = new Context({user}); + + // ignore two users + const ignoreUserResponses = await Promise.all(usersToIgnore.map(u => graphql(schema, ignoreUserMutation, {}, context, {id: u.id}))); + ignoreUserResponses.forEach(response => { + if (response.errors && response.errors.length) { + console.error(response.errors); + } + expect(response.errors).to.be.empty; + }); + + const stopIgnoringUserMutation = ` + mutation stopIgnoringUser ($id: ID!) { + stopIgnoringUser(id:$id) { + errors { + translation_key + } + } + } + `; + + // stop ignoring one user + const stopIgnoringUserResponse = await graphql(schema, stopIgnoringUserMutation, {}, context, {id: usersToIgnore[0].id}); + if (stopIgnoringUserResponse.errors && stopIgnoringUserResponse.errors.length) { + console.error(stopIgnoringUserResponse.errors); + } + expect(stopIgnoringUserResponse.errors).to.be.empty; + + // now check my ignored users + const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); + if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { + console.error(myIgnoredUsersResponse.errors); + } + expect(myIgnoredUsersResponse.errors).to.be.empty; + const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; + expect(myIgnoredUsers.length).to.equal(1); + expect(myIgnoredUsers[0].id).to.equal(usersToIgnore[1].id); + expect(myIgnoredUsers[0].username).to.equal(usersToIgnore[1].username); + }); + +}); From 80658f51d1a7ad69ac9a55ea2b5cb4f813b104c4 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Fri, 7 Apr 2017 16:48:53 -0700 Subject: [PATCH 11/44] Handle errors when ignoring a user --- client/coral-embed-stream/src/Comment.js | 42 +++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 7f9f948ed..a087f133b 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -132,10 +132,10 @@ class Comment extends React.Component { commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : ''; // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar) - const notifyOnError = (fn, errorToMessage) => async () => { + const notifyOnError = (fn, errorToMessage) => async function (...args) { if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;} try { - return await fn(); + return await fn(...args); } catch (error) { addNotification('error', errorToMessage(error)); throw error; @@ -163,7 +163,7 @@ class Comment extends React.Component { cancel: PropTypes.func.isRequired, // actually submit the ignore. Provide {id: user id to ignore} - ignoreUser: PropTypes.func, + ignoreUser: PropTypes.func.isRequired, } constructor(props) { super(props); @@ -203,16 +203,7 @@ class Comment extends React.Component { ); - const step3Confirmed = ( -
    -
    User Ignored
    -

    You will no longer see comments from { user.name }. You can undo this later from the Profile tab

    -
    - -
    -
    - ); - const elsForStep = [step1, step2Confirmation, step3Confirmed]; + const elsForStep = [step1, step2Confirmation]; const {step} = this.state; const elForThisStep = elsForStep[step - 1]; return ( @@ -237,6 +228,9 @@ class Comment extends React.Component { }).isRequired }).isRequired, ignoreUser: PropTypes.func, + + // show notification to the user (e.g. for errors) + addNotification: PropTypes.func.isRequired, } constructor(props) { super(props); @@ -245,17 +239,32 @@ class Comment extends React.Component { }; } render() { - const {comment, ignoreUser} = this.props; + const {comment, ignoreUser, addNotification} = this.props; // timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable) const reset = () => this.setState({timesReset: this.state.timesReset + 1}); + const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => { + + // close menu + reset(); + + // ignore user + let errorToThrow; + try { + await ignoreUser({id}); + } catch (error) { + addNotification('error', 'Failed to ignore user'); + errorToThrow = error; + } + throw errorToThrow; + }; return (
    @@ -285,7 +294,8 @@ class Comment extends React.Component { + ignoreUser={ignoreUser} + addNotification={addNotification} /> From c7d8c407708fd5d55bc202f5481d9ee77ab19df7 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 10:47:02 -0700 Subject: [PATCH 12/44] Rename embed's My Comments tab to 'My profile' --- client/coral-embed-stream/src/Embed.js | 2 +- client/coral-framework/translations.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index a7a0b5740..7a72165ec 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -150,7 +150,7 @@ class Embed extends Component {
    - {lang.t('MY_COMMENTS')} + {lang.t('myProfile')} Configure Stream { diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index 31c3e98ea..f13db92a8 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -2,6 +2,7 @@ "en": { "MY_COMMENTS": "My Comments", "profile": "Profile", + "myProfile": "My profile", "successUpdateSettings": "The changes you have made have been applied to the comment stream on this article", "successNameUpdate": "Your username has been updated", "contentNotAvailable": "This content is not available", @@ -44,7 +45,7 @@ "es": { "profile": "Pérfil", "MY_COMMENTS": "Mis Comentarios", - "profile": "Pérfil", + "myProfile": "Mi pérfil", "successUpdateSettings": "La configuración de este articulo fue actualizada", "successBioUpdate": "Tu biografia fue actualizada", "contentNotAvailable": "El contenido no se encuentra disponible", From 873da1cea94f87efaa87d38730d5502a4ed3047b Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 14:40:25 -0700 Subject: [PATCH 13/44] Embed doesn't show comments from ignored users --- .../coral-framework/graphql/queries/index.js | 13 +-- .../graphql/queries/loadMore.graphql | 4 +- .../graphql/queries/streamQuery.graphql | 6 +- graph/loaders/comments.js | 16 +++- graph/mutators/user.js | 1 - graph/resolvers/asset.js | 5 +- graph/resolvers/comment.js | 6 +- graph/resolvers/root_query.js | 6 +- graph/typeDefs.graphql | 7 +- test/graph/queries/asset.js | 93 +++++++++++++++++++ 10 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 test/graph/queries/asset.js diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index 5bbe58f7a..a1f8aecf0 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -29,10 +29,10 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => { variables: { asset_id, limit, - sort + sort, + notIgnoredBy: data.variables.notIgnoredBy, }, updateQuery: (oldData, {fetchMoreResult:{asset}}) => { - return { ...oldData, asset: { @@ -53,7 +53,8 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s cursor, // the date of the first/last comment depending on the sort order parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment asset_id, // the id of the asset we're currently on - sort // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL + sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL + notIgnoredBy: data.variables.notIgnoredBy, }, updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => { let updatedAsset; @@ -122,18 +123,18 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s // load the comment stream. export const queryStream = graphql(STREAM_QUERY, { - options: () => { + options: ({auth} = {}) => { // where the query string is from the embeded iframe url let comment_id = getQueryVariable('comment_id'); let has_comment = comment_id != null; - return { variables: { asset_id: getQueryVariable('asset_id'), asset_url: getQueryVariable('asset_url'), comment_id: has_comment ? comment_id : 'no-comment', - has_comment + has_comment, + notIgnoredBy: (auth && auth.user) ? auth.user.id : undefined, } }; }, diff --git a/client/coral-framework/graphql/queries/loadMore.graphql b/client/coral-framework/graphql/queries/loadMore.graphql index 10b3c2e34..6b15117da 100644 --- a/client/coral-framework/graphql/queries/loadMore.graphql +++ b/client/coral-framework/graphql/queries/loadMore.graphql @@ -1,7 +1,7 @@ #import "../fragments/commentView.graphql" -query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER) { - new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort}) { +query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $notIgnoredBy: String) { + new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, notIgnoredBy: $notIgnoredBy}) { ...commentView replyCount replies(limit: 3) { diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index 102c656e5..a5e3982c6 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -1,6 +1,6 @@ #import "../fragments/commentView.graphql" -query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!) { +query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!, $notIgnoredBy: String) { # the comment here is for loading one comment and it's children, probably after following a permalink # $has_comment is derived from the comment_id query param in the iframe url, # which is in turn pulled from the host page url @@ -38,10 +38,10 @@ query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comme } commentCount totalCommentCount - comments(limit: 10) { + comments(limit: 10, notIgnoredBy: $notIgnoredBy) { ...commentView replyCount - replies(limit: 3) { + replies(limit: 3, notIgnoredBy: $notIgnoredBy) { ...commentView } } diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index e2ae29f65..684821345 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -6,6 +6,7 @@ const { const DataLoader = require('dataloader'); const CommentModel = require('../../models/comment'); +const UsersService = require('../../services/users'); /** * Returns the comment count for all comments that are public based on their @@ -142,7 +143,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id}) = * @param {Object} context graph context * @param {Object} query query terms to apply to the comments query */ -const getCommentsByQuery = ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort}) => { +const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, notIgnoredBy}) => { let comments = CommentModel.find(); // Only administrators can search for comments with statuses that are not @@ -184,6 +185,19 @@ const getCommentsByQuery = ({user}, {ids, statuses, asset_id, parent_id, author_ comments = comments.where({parent_id}); } + if (notIgnoredBy) { + if (user.id !== notIgnoredBy) { + throw new Error(`You are not authorized to query for comments notIgnoredBy ${notIgnoredBy}`); + } + + // load afresh, as `user` may be from cache and not have recent ignores + const freshUser = await UsersService.findById(user.id); + const ignoredUsers = freshUser.ignoresUsers; + comments = comments.where({ + author_id: {$nin: ignoredUsers} + }); + } + if (cursor) { if (sort === 'REVERSE_CHRONOLOGICAL') { comments = comments.where({ diff --git a/graph/mutators/user.js b/graph/mutators/user.js index fd888a402..d68351701 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -18,7 +18,6 @@ const ignoreUser = async ({user}, userToIgnore) => { }; const stopIgnoringUser = async ({user}, userToStopIgnoring) => { - console.log('stopIgnoringUser!!'); return await UsersService.stopIgnoringUsers(user.id, [userToStopIgnoring.id]); }; diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index c2a0a2b66..6459f53cd 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -2,12 +2,13 @@ const Asset = { recentComments({id}, _, {loaders: {Comments}}) { return Comments.genRecentComments.load(id); }, - comments({id}, {sort, limit}, {loaders: {Comments}}) { + comments({id}, {sort, limit, notIgnoredBy}, {loaders: {Comments}}) { return Comments.getByQuery({ asset_id: id, sort, limit, - parent_id: null + parent_id: null, + notIgnoredBy, }); }, commentCount({id, commentCount}, _, {loaders: {Comments}}) { diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 2752ad0e3..b99aeeee5 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -12,12 +12,14 @@ const Comment = { recentReplies({id}, _, {loaders: {Comments}}) { return Comments.genRecentReplies.load(id); }, - replies({id, asset_id}, {sort, limit}, {loaders: {Comments}}) { + replies({id, asset_id}, {sort, limit, notIgnoredBy}, {loaders: {Comments}}) { + console.log('replies notIgnoredBy', notIgnoredBy); return Comments.getByQuery({ asset_id, parent_id: id, sort, - limit + limit, + notIgnoredBy, }); }, replyCount({id}, _, {loaders: {Comments}}) { diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 42f5a021d..530ee74d6 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -19,15 +19,15 @@ const RootQuery = { // This endpoint is used for loading moderation queues, so hide it in the // event that we aren't an admin. - comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort}}, {user, loaders: {Comments, Actions}}) { - let query = {statuses, asset_id, parent_id, limit, cursor, sort}; + comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, notIgnoredBy}}, {user, loaders: {Comments, Actions}}) { + let query = {statuses, asset_id, parent_id, limit, cursor, sort, notIgnoredBy}; if (user != null && user.hasRoles('ADMIN') && action_type) { return Actions.getByTypes({action_type, item_type: 'COMMENTS'}) .then((ids) => { // Perform the query using the available resolver. - return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort}); + return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort, notIgnoredBy}); }); } diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 87926a5da..6bc68c599 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -140,6 +140,9 @@ input CommentsQuery { # Sort the results by created_at. sort: SORT_ORDER = REVERSE_CHRONOLOGICAL + + # Exclude comments ignored by this user ID + notIgnoredBy: String } # CommentCountQuery allows the ability to query comment counts by specific @@ -185,7 +188,7 @@ type Comment { recentReplies: [Comment] # the replies that were made to the comment. - replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3): [Comment] + replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, notIgnoredBy: String): [Comment] # The count of replies on a comment. replyCount: Int @@ -417,7 +420,7 @@ type Asset { recentComments: [Comment] # The top level comments that are attached to the asset. - comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10): [Comment] + comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10, notIgnoredBy: String): [Comment] # The count of top level comments on the asset. commentCount: Int diff --git a/test/graph/queries/asset.js b/test/graph/queries/asset.js new file mode 100644 index 000000000..5ee19697a --- /dev/null +++ b/test/graph/queries/asset.js @@ -0,0 +1,93 @@ +const expect = require('chai').expect; +const {graphql} = require('graphql'); + +const schema = require('../../../graph/schema'); +const Context = require('../../../graph/context'); +const UsersService = require('../../../services/users'); +const SettingsService = require('../../../services/settings'); +const Asset = require('../../../models/asset'); +const CommentsService = require('../../../services/comments'); + +describe('graph.queries.asset', () => { + beforeEach(async () => { + await SettingsService.init(); + }); + + it('can get comments edge', async () => { + const assetId = 'fakeAssetId'; + const assetUrl = 'https://bengo.is'; + await Asset.create({id: assetId, url: assetUrl}); + + const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + const context = new Context({user}); + + await CommentsService.publicCreate([1, 2].map(() => ({ + author_id: user.id, + asset_id: assetId, + body: `hello there! ${ String(Math.random()).slice(2)}`, + }))); + + const assetCommentsQuery = ` + query assetCommentsQuery($assetId: ID!, $assetUrl: String!) { + asset(id: $assetId, url: $assetUrl) { + comments(limit: 10) { + id, + body, + } + } + } + `; + const assetCommentsResponse = await graphql(schema, assetCommentsQuery, {}, context, {assetId, assetUrl}); + const comments = assetCommentsResponse.data.asset.comments; + expect(comments.length).to.equal(2); + }); + + it('can query comments edge to exclude comments ignored by user', async () => { + const assetId = 'fakeAssetId1'; + const assetUrl = 'https://bengo.is/1'; + await Asset.create({id: assetId, url: assetUrl}); + + const userA = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + const userB = await UsersService.createLocalUser('usernameB@example.com', 'password', 'usernameB'); + const userC = await UsersService.createLocalUser('usernameC@example.com', 'password', 'usernameC'); + const context = new Context({user: userA}); + + // create 2 comments each for userB, userC + await Promise.all([userB, userC].map(user => CommentsService.publicCreate([1, 2].map(() => ({ + author_id: user.id, + asset_id: assetId, + body: `hello there! ${ String(Math.random()).slice(2)}`, + }))))); + + // ignore userB + const ignoreUserMutation = ` + mutation ignoreUser ($id: ID!) { + ignoreUser(id:$id) { + errors { + translation_key + } + } + } + `; + const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: userB.id}); + if (ignoreUserResponse.errors && ignoreUserResponse.errors.length) { + console.error(ignoreUserResponse.errors); + } + expect(ignoreUserResponse.errors).to.be.empty; + + const assetCommentsWithoutIgnoredQuery = ` + query assetCommentsQuery($assetId: ID!, $assetUrl: String!, $notIgnoredBy: String!) { + asset(id: $assetId, url: $assetUrl) { + comments(limit: 10, notIgnoredBy: $notIgnoredBy) { + id, + body, + } + } + } + `; + const assetCommentsResponse = await graphql(schema, assetCommentsWithoutIgnoredQuery, {}, context, {assetId, assetUrl, notIgnoredBy: userA.id}); + const comments = assetCommentsResponse.data.asset.comments; + expect(comments.length).to.equal(2); + }); + +}); From b561e9f643748453bf3a1245af0e46f69b5cdaa1 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 15:13:01 -0700 Subject: [PATCH 14/44] Comment replyCount edge can now be filtered by notIgnoredBy --- .../graphql/queries/loadMore.graphql | 2 +- .../graphql/queries/streamQuery.graphql | 6 ++-- graph/loaders/comments.js | 31 +++++++++++++++++++ graph/resolvers/comment.js | 8 +++-- graph/typeDefs.graphql | 2 +- 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/client/coral-framework/graphql/queries/loadMore.graphql b/client/coral-framework/graphql/queries/loadMore.graphql index 6b15117da..d4ee818a1 100644 --- a/client/coral-framework/graphql/queries/loadMore.graphql +++ b/client/coral-framework/graphql/queries/loadMore.graphql @@ -3,7 +3,7 @@ query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $notIgnoredBy: String) { new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, notIgnoredBy: $notIgnoredBy}) { ...commentView - replyCount + replyCount(notIgnoredBy: $notIgnoredBy) replies(limit: 3) { ...commentView } diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index a5e3982c6..9b95a9c2b 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -6,13 +6,13 @@ query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comme # which is in turn pulled from the host page url comment(id: $comment_id) @include(if: $has_comment) { ...commentView - replyCount + replyCount(notIgnoredBy: $notIgnoredBy) replies { ...commentView } parent { ...commentView - replyCount + replyCount(notIgnoredBy: $notIgnoredBy) replies { ...commentView } @@ -40,7 +40,7 @@ query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comme totalCommentCount comments(limit: 10, notIgnoredBy: $notIgnoredBy) { ...commentView - replyCount + replyCount(notIgnoredBy: $notIgnoredBy) replies(limit: 3, notIgnoredBy: $notIgnoredBy) { ...commentView } diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 684821345..13aa47fb4 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -105,6 +105,36 @@ const getCountsByParentID = (context, parent_ids) => { .then((results) => results.map((result) => result ? result.count : 0)); }; +/** + * Returns the count of comments for the provided parent_id, also filtering by personalization options. + * + * @param {Array} id The ID of the parent comment + * @param {Array} notIgnoredBy Exclude comments ignored by this User ID + */ +const getCountByParentIDPersonalized = async (context, {id, notIgnoredBy}) => { + const query = { + parent_id: { + $in: [id] + }, + status: { + $in: ['NONE', 'ACCEPTED'] + } + }; + if (notIgnoredBy) { + const user = context.user; + if (user.id !== notIgnoredBy) { + throw new Error(`You are not authorized to query for comments counts notIgnoredBy ${notIgnoredBy}`); + } + + // load afresh, as `user` may be from cache and not have recent ignores + const freshUser = await UsersService.findById(user.id); + const ignoredUsers = freshUser.ignoresUsers; + query.author_id = {$nin: ignoredUsers}; + } + const count = await CommentModel.where(query).count(); + return count; +}; + /** * Retrieves the count of comments based on the passed in query. * @param {Object} context graph context @@ -360,6 +390,7 @@ module.exports = (context) => ({ countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', 3600, (ids) => getCountsByAssetID(context, ids)), parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', 3600, (ids) => getParentCountsByAssetID(context, ids)), countByParentID: new SharedCounterDataLoader('Comments.countByParentID', 3600, (ids) => getCountsByParentID(context, ids)), + countByParentIDPersonalized: (query) => getCountByParentIDPersonalized(context, query), genRecentReplies: new DataLoader((ids) => genRecentReplies(context, ids)), genRecentComments: new DataLoader((ids) => genRecentComments(context, ids)) } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index b99aeeee5..1f57cb939 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -13,7 +13,6 @@ const Comment = { return Comments.genRecentReplies.load(id); }, replies({id, asset_id}, {sort, limit, notIgnoredBy}, {loaders: {Comments}}) { - console.log('replies notIgnoredBy', notIgnoredBy); return Comments.getByQuery({ asset_id, parent_id: id, @@ -22,8 +21,11 @@ const Comment = { notIgnoredBy, }); }, - replyCount({id}, _, {loaders: {Comments}}) { - return Comments.countByParentID.load(id); + replyCount({id}, {notIgnoredBy}, {loaders: {Comments}}) { + if ( ! notIgnoredBy) { + return Comments.countByParentID.load(id); + } + return Comments.countByParentIDPersonalized({id, notIgnoredBy}); }, actions({id}, _, {user, loaders: {Actions}}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 6bc68c599..1980a3bbc 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -191,7 +191,7 @@ type Comment { replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, notIgnoredBy: String): [Comment] # The count of replies on a comment. - replyCount: Int + replyCount(notIgnoredBy: String): Int # Actions completed on the parent. Requires the `ADMIN` role. actions: [Action] From 58736586e49e2ea9d03adee4d16100492e3aa5a2 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 15:41:53 -0700 Subject: [PATCH 15/44] Asset commentCount and totalCommentCount can be personalized by notIgnoredBy --- .../graphql/queries/streamQuery.graphql | 4 +- graph/loaders/comments.js | 59 +++++++++++++++++++ graph/resolvers/asset.js | 12 ++-- graph/typeDefs.graphql | 4 +- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index 9b95a9c2b..a96f7f634 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -36,8 +36,8 @@ query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comme charCount requireEmailConfirmation } - commentCount - totalCommentCount + commentCount(notIgnoredBy: $notIgnoredBy) + totalCommentCount(notIgnoredBy: $notIgnoredBy) comments(limit: 10, notIgnoredBy: $notIgnoredBy) { ...commentView replyCount(notIgnoredBy: $notIgnoredBy) diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 13aa47fb4..c9fe1c302 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -40,6 +40,34 @@ const getCountsByAssetID = (context, asset_ids) => { .then((results) => results.map((result) => result ? result.count : 0)); }; +/** + * Returns the count of all public comments on an asset id, also filtering by personalization options. + * + * @param {Array} id The ID of the asset + * @param {Array} notIgnoredBy Exclude comments ignored by this User ID + */ +const getCountsByAssetIDPersonalized = async (context, {assetId, notIgnoredBy}) => { + const query = { + asset_id: assetId, + status: { + $in: ['NONE', 'ACCEPTED'], + }, + }; + if (notIgnoredBy) { + const user = context.user; + if (user.id !== notIgnoredBy) { + throw new Error(`You are not authorized to query for comments counts notIgnoredBy ${notIgnoredBy}`); + } + + // load afresh, as `user` may be from cache and not have recent ignores + const freshUser = await UsersService.findById(user.id); + const ignoredUsers = freshUser.ignoresUsers; + query.author_id = {$nin: ignoredUsers}; + } + const count = await CommentModel.where(query).count(); + return count; +}; + /** * Returns the comment count for all comments that are public based on their * asset ids. @@ -73,6 +101,35 @@ const getParentCountsByAssetID = (context, asset_ids) => { .then((results) => results.map((result) => result ? result.count : 0)); }; +/** + * Returns the count of top-level comments on an asset id, also filtering by personalization options. + * + * @param {Array} id The ID of the asset + * @param {Array} notIgnoredBy Exclude comments ignored by this User ID + */ +const getParentCountByAssetIDPersonalized = async (context, {assetId, notIgnoredBy}) => { + const query = { + asset_id: assetId, + parent_id: null, + status: { + $in: ['NONE', 'ACCEPTED'], + }, + }; + if (notIgnoredBy) { + const user = context.user; + if (user.id !== notIgnoredBy) { + throw new Error(`You are not authorized to query for comments counts notIgnoredBy ${notIgnoredBy}`); + } + + // load afresh, as `user` may be from cache and not have recent ignores + const freshUser = await UsersService.findById(user.id); + const ignoredUsers = freshUser.ignoresUsers; + query.author_id = {$nin: ignoredUsers}; + } + const count = await CommentModel.where(query).count(); + return count; +}; + /** * Returns the comment count for all comments that are public based on their * parent ids. @@ -388,7 +445,9 @@ module.exports = (context) => ({ getByQuery: (query) => getCommentsByQuery(context, query), getCountByQuery: (query) => getCommentCountByQuery(context, query), countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', 3600, (ids) => getCountsByAssetID(context, ids)), + countByAssetIDPersonalized: (query) => getCountsByAssetIDPersonalized(context, query), parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', 3600, (ids) => getParentCountsByAssetID(context, ids)), + parentCountByAssetIDPersonalized: (query) => getParentCountByAssetIDPersonalized(context, query), countByParentID: new SharedCounterDataLoader('Comments.countByParentID', 3600, (ids) => getCountsByParentID(context, ids)), countByParentIDPersonalized: (query) => getCountByParentIDPersonalized(context, query), genRecentReplies: new DataLoader((ids) => genRecentReplies(context, ids)), diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index 6459f53cd..811181aa6 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -11,18 +11,22 @@ const Asset = { notIgnoredBy, }); }, - commentCount({id, commentCount}, _, {loaders: {Comments}}) { + commentCount({id, commentCount}, {notIgnoredBy}, {loaders: {Comments}}) { + if (notIgnoredBy) { + return Comments.parentCountByAssetIDPersonalized({assetId: id, notIgnoredBy}); + } if (commentCount != null) { return commentCount; } - return Comments.parentCountByAssetID.load(id); }, - totalCommentCount({id, totalCommentCount}, _, {loaders: {Comments}}) { + totalCommentCount({id, totalCommentCount}, {notIgnoredBy}, {loaders: {Comments}}) { + if (notIgnoredBy) { + return Comments.countByAssetIDPersonalized({assetId: id, notIgnoredBy}); + } if (totalCommentCount != null) { return totalCommentCount; } - return Comments.countByAssetID.load(id); }, settings({settings = null}, _, {loaders: {Settings}}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 1980a3bbc..7f988054b 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -423,10 +423,10 @@ type Asset { comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10, notIgnoredBy: String): [Comment] # The count of top level comments on the asset. - commentCount: Int + commentCount(notIgnoredBy: String): Int # The total count of all comments made on the asset. - totalCommentCount: Int + totalCommentCount(notIgnoredBy: String): Int # The settings (rectified with the global settings) that should be applied to # this asset. From 7dfe12b87429441ba9068af67b9e922138ab00ed Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 15:46:13 -0700 Subject: [PATCH 16/44] Users cannot ignore themselves at service layer --- services/users.js | 3 +++ test/graph/mutations/ignoreUser.js | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/services/users.js b/services/users.js index da3b23bc6..de9678089 100644 --- a/services/users.js +++ b/services/users.js @@ -844,6 +844,9 @@ module.exports = class UsersService { static ignoreUsers(userId, usersToIgnore) { assert(Array.isArray(usersToIgnore), 'usersToIgnore is an array'); assert(usersToIgnore.every(u => typeof u === 'string'), 'usersToIgnore is an array of string user IDs'); + if (usersToIgnore.includes(userId)) { + throw new Error('Users cannot ignore themselves'); + } // TODO: For each usersToIgnore, make sure they exist? return UserModel.update({id: userId}, { diff --git a/test/graph/mutations/ignoreUser.js b/test/graph/mutations/ignoreUser.js index fc6035839..c5633bfb4 100644 --- a/test/graph/mutations/ignoreUser.js +++ b/test/graph/mutations/ignoreUser.js @@ -53,6 +53,22 @@ describe('graph.mutations.ignoreUser', () => { expect(myIgnoredUsers[0].username).to.equal(userToIgnore.username); }); + it('users cannot ignore themselves', async () => { + const user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA'); + const context = new Context({user}); + const ignoreUserResponse = await graphql(schema, ignoreUserMutation, {}, context, {id: user.id}); + expect(ignoreUserResponse.errors).to.not.be.empty; + + // now check my ignored users + const myIgnoredUsersResponse = await graphql(schema, getMyIgnoredUsersQuery, {}, context, {}); + if (myIgnoredUsersResponse.errors && myIgnoredUsersResponse.errors.length) { + console.error(myIgnoredUsersResponse.errors); + } + expect(myIgnoredUsersResponse.errors).to.be.empty; + const myIgnoredUsers = myIgnoredUsersResponse.data.myIgnoredUsers; + expect(myIgnoredUsers.length).to.equal(0); + }); + }); describe('graph.mutations.stopIgnoringUser', () => { From 4d2e5ed81fee9d76e5ca24b44eb692e782aa8c17 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 15:51:38 -0700 Subject: [PATCH 17/44] Comments by logged in user dont show TopRightMenu + IgnoreUserWizard --- client/coral-embed-stream/src/Comment.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index a087f133b..9ed0bee23 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -291,12 +291,15 @@ class Comment extends React.Component { - - - + { (currentUser && (comment.user.id !== currentUser.id)) + ? + + + : null + }
    From 58ffd61884b7597fdb4672d552dfc2c8a235c15d Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 16:13:53 -0700 Subject: [PATCH 18/44] Move TopRightMenu component out of Comment and into own file --- client/coral-embed-stream/src/Comment.css | 64 +------ client/coral-embed-stream/src/Comment.js | 166 +----------------- .../coral-embed-stream/src/TopRightMenu.css | 39 ++++ client/coral-embed-stream/src/TopRightMenu.js | 156 ++++++++++++++++ 4 files changed, 197 insertions(+), 228 deletions(-) create mode 100644 client/coral-embed-stream/src/TopRightMenu.css create mode 100644 client/coral-embed-stream/src/TopRightMenu.js diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index 6f89208bc..b463aa247 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -16,71 +16,9 @@ .topRightMenu { float: right; text-align: right; + cursor: pointer; } .topRightMenu > * { text-align: initial; } - -.topRightMenu .toggler { - cursor: pointer; -} - -.topRightMenu .Menu { - background-color: white; - text-align: initial; -} - -.Toggleable:focus { - outline: none; -} - -.Menu { - border: 1px solid #ddd; - margin: 0; -} -ul.Menu { - list-style-type: none; - padding: 0; -} - -.MenuItem { - cursor: pointer; - padding: 1em; -} - -.IgnoreUserWizard { - background-color: #2E343B; - color: white; - padding: 1em; - max-width: 220px; -} - -.IgnoreUserWizard header { - font-weight: bold; -} - -.IgnoreUserWizard .textAlignRight { - text-align: right; -} - -/** - * Up/Down Chevrons for the top right menu - */ -.chevron { -} -.chevron:before { - content: '⌃'; - display: inline-block; - position: relative; - top: 0.25em; -} - -/* Down Arrow */ -.chevron.down:before { - display: inline-block; - position: relative; - transform: rotate(180deg); - top: 0; - /*top: -0.25em;*/ -} diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 9ed0bee23..4b01e19a7 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -11,7 +11,6 @@ import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton'; import AuthorName from 'coral-plugin-author-name/AuthorName'; -import {Button} from 'coral-ui'; import TagLabel from 'coral-plugin-tag-label/TagLabel'; import Content from 'coral-plugin-commentcontent/CommentContent'; import PubDate from 'coral-plugin-pubdate/PubDate'; @@ -22,9 +21,9 @@ import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} import LoadMore from 'coral-embed-stream/src/LoadMore'; import {Slot} from 'coral-framework'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; +import {TopRightMenu} from './TopRightMenu'; import styles from './Comment.css'; -import classnames from 'classnames'; const getActionSummary = (type, comment) => comment.action_summaries .filter((a) => a.__typename === type)[0]; @@ -152,126 +151,6 @@ class Comment extends React.Component { tag: BEST_TAG, }), () => 'Failed to remove best comment tag'); - class IgnoreUserWizard extends React.Component { - static propTypes = { - - // comment on which this menu appears - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }).isRequired, - cancel: PropTypes.func.isRequired, - - // actually submit the ignore. Provide {id: user id to ignore} - ignoreUser: PropTypes.func.isRequired, - } - constructor(props) { - super(props); - this.state = { - - // what step of the wizard is the user on - step: 1 - }; - this.onClickCancel = this.onClickCancel.bind(this); - } - onClickCancel() { - this.props.cancel(); - } - render() { - const {user, ignoreUser} = this.props; - const goToStep = (stepNum) => this.setState({step: stepNum}); - const step1 = ( -
    -
    Ignore User
    -

    When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.

    -
    - - -
    -
    - ); - const onClickIgnoreUser = async () => { - await ignoreUser({id: user.id}); - }; - const step2Confirmation = ( -
    -
    Ignore User
    -

    Are you sure you want to ignore { user.name }?

    -
    - - -
    -
    - ); - const elsForStep = [step1, step2Confirmation]; - const {step} = this.state; - const elForThisStep = elsForStep[step - 1]; - return ( -
    - { elForThisStep } -
    - ); - } - } - - // TopRightMenu appears as a dropdown in the top right of the comment. - // when you click the down cehvron, it expands and shows IgnoreUserWizard - // when you click 'cancel' in the wizard, it closes the menu - class TopRightMenu extends React.Component { - static propTypes = { - - // comment on which this menu appears - comment: PropTypes.shape({ - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }).isRequired - }).isRequired, - ignoreUser: PropTypes.func, - - // show notification to the user (e.g. for errors) - addNotification: PropTypes.func.isRequired, - } - constructor(props) { - super(props); - this.state = { - timesReset: 0 - }; - } - render() { - const {comment, ignoreUser, addNotification} = this.props; - - // timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable) - const reset = () => this.setState({timesReset: this.state.timesReset + 1}); - const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => { - - // close menu - reset(); - - // ignore user - let errorToThrow; - try { - await ignoreUser({id}); - } catch (error) { - addNotification('error', 'Failed to ignore user'); - errorToThrow = error; - } - throw errorToThrow; - }; - return ( - -
    - -
    -
    - ); - } - } - return (
    ; -const downArrow = ; -class Toggleable extends React.Component { - constructor(props) { - super(props); - this.toggle = this.toggle.bind(this); - this.close = this.close.bind(this); - this.state = { - isOpen: false - }; - } - toggle() { - this.setState({isOpen: ! this.state.isOpen}); - } - close() { - this.setState({isOpen: false}); - } - render() { - const {children} = this.props; - const {isOpen} = this.state; - return ( - - // /*onBlur={ this.close } */ - - {isOpen ? upArrow : downArrow} - {isOpen ? children : null} - - ); - } -} -const Menu = ({children}) => ( -
      - { children } -
    -); -Menu.Item = ({children, onClick}) => ( -
  • - { children } -
  • -); - export default Comment; diff --git a/client/coral-embed-stream/src/TopRightMenu.css b/client/coral-embed-stream/src/TopRightMenu.css new file mode 100644 index 000000000..587920c5f --- /dev/null +++ b/client/coral-embed-stream/src/TopRightMenu.css @@ -0,0 +1,39 @@ +.Toggleable:focus { + outline: none; +} + +.IgnoreUserWizard { + background-color: #2E343B; + color: white; + padding: 1em; + max-width: 220px; +} + +.IgnoreUserWizard header { + font-weight: bold; +} + +.IgnoreUserWizard .textAlignRight { + text-align: right; +} + +/** + * Up/Down Chevrons for the top right menu + */ +.chevron { +} +.chevron:before { + content: '⌃'; + display: inline-block; + position: relative; + top: 0.25em; +} + +/* Down Arrow */ +.chevron.down:before { + display: inline-block; + position: relative; + transform: rotate(180deg); + top: 0; + /*top: -0.25em;*/ +} diff --git a/client/coral-embed-stream/src/TopRightMenu.js b/client/coral-embed-stream/src/TopRightMenu.js new file mode 100644 index 000000000..1e06997dd --- /dev/null +++ b/client/coral-embed-stream/src/TopRightMenu.js @@ -0,0 +1,156 @@ +import React, {PropTypes} from 'react'; +import classnames from 'classnames'; + +import {Button} from 'coral-ui'; +import styles from './TopRightMenu.css'; + +// TopRightMenu appears as a dropdown in the top right of the comment. +// when you click the down cehvron, it expands and shows IgnoreUserWizard +// when you click 'cancel' in the wizard, it closes the menu +export class TopRightMenu extends React.Component { + static propTypes = { + + // comment on which this menu appears + comment: PropTypes.shape({ + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired + }).isRequired, + ignoreUser: PropTypes.func, + + // show notification to the user (e.g. for errors) + addNotification: PropTypes.func.isRequired, + } + constructor(props) { + super(props); + this.state = { + timesReset: 0 + }; + } + render() { + const {comment, ignoreUser, addNotification} = this.props; + + // timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable) + const reset = () => this.setState({timesReset: this.state.timesReset + 1}); + const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => { + + // close menu + reset(); + + // ignore user + try { + await ignoreUser({id}); + } catch (error) { + addNotification('error', 'Failed to ignore user'); + throw error; + } + }; + return ( + +
    + +
    +
    + ); + } +} + +class IgnoreUserWizard extends React.Component { + static propTypes = { + + // comment on which this menu appears + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + cancel: PropTypes.func.isRequired, + + // actually submit the ignore. Provide {id: user id to ignore} + ignoreUser: PropTypes.func.isRequired, + } + constructor(props) { + super(props); + this.state = { + + // what step of the wizard is the user on + step: 1 + }; + this.onClickCancel = this.onClickCancel.bind(this); + } + onClickCancel() { + this.props.cancel(); + } + render() { + const {user, ignoreUser} = this.props; + const goToStep = (stepNum) => this.setState({step: stepNum}); + const step1 = ( +
    +
    Ignore User
    +

    When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.

    +
    + + +
    +
    + ); + const onClickIgnoreUser = async () => { + await ignoreUser({id: user.id}); + }; + const step2Confirmation = ( +
    +
    Ignore User
    +

    Are you sure you want to ignore { user.name }?

    +
    + + +
    +
    + ); + const elsForStep = [step1, step2Confirmation]; + const {step} = this.state; + const elForThisStep = elsForStep[step - 1]; + return ( +
    + { elForThisStep } +
    + ); + } +} + +const upArrow = ; +const downArrow = ; +class Toggleable extends React.Component { + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + this.close = this.close.bind(this); + this.state = { + isOpen: false + }; + } + toggle() { + this.setState({isOpen: ! this.state.isOpen}); + } + close() { + this.setState({isOpen: false}); + } + render() { + const {children} = this.props; + const {isOpen} = this.state; + return ( + + // /*onBlur={ this.close } */ + + {isOpen ? upArrow : downArrow} + {isOpen ? children : null} + + ); + } +} + From 0dff670b7096a8978e4a4ae46773131752dcf589 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 16:38:29 -0700 Subject: [PATCH 19/44] Slot component wraps in inline element, not block (which can affect other styles) --- client/coral-framework/components/Slot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 3d1d61328..0e5cf86c2 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -5,9 +5,9 @@ class Slot extends Component { render() { const {fill, ...rest} = this.props; return ( -
    + {getSlotElements(fill, rest)} -
    + ); } } From a43e713ac00f39a127a0478c48c1ba61693bc617 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 16:39:20 -0700 Subject: [PATCH 20/44] Move TopRightMenu down 5px --- client/coral-embed-stream/src/Comment.css | 1 + 1 file changed, 1 insertion(+) diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css index b463aa247..8d7c7ebf9 100644 --- a/client/coral-embed-stream/src/Comment.css +++ b/client/coral-embed-stream/src/Comment.css @@ -17,6 +17,7 @@ float: right; text-align: right; cursor: pointer; + margin-top: 5px; } .topRightMenu > * { From 12e383ef13ca8285393cab701294f5724ab9da5f Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Mon, 10 Apr 2017 17:28:01 -0700 Subject: [PATCH 21/44] refetch streamQuery on stopIgnoringUser --- .../graphql/mutations/index.js | 33 ++++++++++++------- .../coral-framework/graphql/queries/index.js | 28 +++++++++------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/client/coral-framework/graphql/mutations/index.js b/client/coral-framework/graphql/mutations/index.js index 64a39d108..da1603f9e 100644 --- a/client/coral-framework/graphql/mutations/index.js +++ b/client/coral-framework/graphql/mutations/index.js @@ -10,6 +10,8 @@ import IGNORE_USER from './ignoreUser.graphql'; import STOP_IGNORING_USER from './stopIgnoringUser.graphql'; import MY_IGNORED_USERS from '../queries/myIgnoredUsers.graphql'; +import STREAM_QUERY from '../queries/streamQuery.graphql'; +import {variablesForStreamQuery} from '../queries'; import commentView from '../fragments/commentView.graphql'; @@ -168,15 +170,24 @@ export const ignoreUser = graphql(IGNORE_USER, { }); export const stopIgnoringUser = graphql(STOP_IGNORING_USER, { - props: ({mutate}) => ({ - stopIgnoringUser: ({id}) => { - return mutate({ - variables: { - id, - }, - refetchQueries: [{ - query: MY_IGNORED_USERS, - }] - }); - }}), + props: ({mutate, ownProps}) => { + return { + stopIgnoringUser: ({id}) => { + return mutate({ + variables: { + id, + }, + refetchQueries: [ + { + query: MY_IGNORED_USERS, + }, + { + query: STREAM_QUERY, + variables: variablesForStreamQuery(ownProps), + } + ] + }); + } + }; + } }); diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index a1f8aecf0..bcf397d3c 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -121,21 +121,25 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s }); }; +export const variablesForStreamQuery = ({auth}) => { + + // where the query string is from the embeded iframe url + let comment_id = getQueryVariable('comment_id'); + let has_comment = comment_id != null; + return { + asset_id: getQueryVariable('asset_id'), + asset_url: getQueryVariable('asset_url'), + comment_id: has_comment ? comment_id : 'no-comment', + has_comment, + notIgnoredBy: (auth && auth.user) ? auth.user.id : undefined, + }; +}; + // load the comment stream. export const queryStream = graphql(STREAM_QUERY, { - options: ({auth} = {}) => { - - // where the query string is from the embeded iframe url - let comment_id = getQueryVariable('comment_id'); - let has_comment = comment_id != null; + options: (props) => { return { - variables: { - asset_id: getQueryVariable('asset_id'), - asset_url: getQueryVariable('asset_url'), - comment_id: has_comment ? comment_id : 'no-comment', - has_comment, - notIgnoredBy: (auth && auth.user) ? auth.user.id : undefined, - } + variables: variablesForStreamQuery(props) }; }, props: ({data}) => ({ From eb1c1db09b8e446575740a37cf7e405b471df190 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Tue, 11 Apr 2017 09:50:08 -0700 Subject: [PATCH 22/44] Ignore users queries are excludeIgnored: Boolean instead of notIgnoredBy: String --- .../coral-framework/graphql/queries/index.js | 6 +-- .../graphql/queries/loadMore.graphql | 6 +-- .../graphql/queries/streamQuery.graphql | 16 ++++---- graph/loaders/comments.js | 40 +++++++------------ graph/resolvers/asset.js | 16 ++++---- graph/resolvers/comment.js | 12 +++--- graph/resolvers/root_query.js | 6 +-- graph/typeDefs.graphql | 14 +++---- test/graph/queries/asset.js | 6 +-- 9 files changed, 55 insertions(+), 67 deletions(-) diff --git a/client/coral-framework/graphql/queries/index.js b/client/coral-framework/graphql/queries/index.js index bcf397d3c..5dbea5822 100644 --- a/client/coral-framework/graphql/queries/index.js +++ b/client/coral-framework/graphql/queries/index.js @@ -30,7 +30,7 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => { asset_id, limit, sort, - notIgnoredBy: data.variables.notIgnoredBy, + excludeIgnored: data.variables.excludeIgnored, }, updateQuery: (oldData, {fetchMoreResult:{asset}}) => { return { @@ -54,7 +54,7 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment asset_id, // the id of the asset we're currently on sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL - notIgnoredBy: data.variables.notIgnoredBy, + excludeIgnored: data.variables.excludeIgnored, }, updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => { let updatedAsset; @@ -131,7 +131,7 @@ export const variablesForStreamQuery = ({auth}) => { asset_url: getQueryVariable('asset_url'), comment_id: has_comment ? comment_id : 'no-comment', has_comment, - notIgnoredBy: (auth && auth.user) ? auth.user.id : undefined, + excludeIgnored: Boolean(auth && auth.user && auth.user.id), }; }; diff --git a/client/coral-framework/graphql/queries/loadMore.graphql b/client/coral-framework/graphql/queries/loadMore.graphql index d4ee818a1..b18f4d84e 100644 --- a/client/coral-framework/graphql/queries/loadMore.graphql +++ b/client/coral-framework/graphql/queries/loadMore.graphql @@ -1,9 +1,9 @@ #import "../fragments/commentView.graphql" -query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $notIgnoredBy: String) { - new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, notIgnoredBy: $notIgnoredBy}) { +query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { + new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) { ...commentView - replyCount(notIgnoredBy: $notIgnoredBy) + replyCount(excludeIgnored: $excludeIgnored) replies(limit: 3) { ...commentView } diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index a96f7f634..06e36cebd 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -1,18 +1,18 @@ #import "../fragments/commentView.graphql" -query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!, $notIgnoredBy: String) { +query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comment: Boolean!, $excludeIgnored: Boolean) { # the comment here is for loading one comment and it's children, probably after following a permalink # $has_comment is derived from the comment_id query param in the iframe url, # which is in turn pulled from the host page url comment(id: $comment_id) @include(if: $has_comment) { ...commentView - replyCount(notIgnoredBy: $notIgnoredBy) + replyCount(excludeIgnored: $excludeIgnored) replies { ...commentView } parent { ...commentView - replyCount(notIgnoredBy: $notIgnoredBy) + replyCount(excludeIgnored: $excludeIgnored) replies { ...commentView } @@ -36,12 +36,12 @@ query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comme charCount requireEmailConfirmation } - commentCount(notIgnoredBy: $notIgnoredBy) - totalCommentCount(notIgnoredBy: $notIgnoredBy) - comments(limit: 10, notIgnoredBy: $notIgnoredBy) { + commentCount(excludeIgnored: $excludeIgnored) + totalCommentCount(excludeIgnored: $excludeIgnored) + comments(limit: 10, excludeIgnored: $excludeIgnored) { ...commentView - replyCount(notIgnoredBy: $notIgnoredBy) - replies(limit: 3, notIgnoredBy: $notIgnoredBy) { + replyCount(excludeIgnored: $excludeIgnored) + replies(limit: 3, excludeIgnored: $excludeIgnored) { ...commentView } } diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index c9fe1c302..0faa124da 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -44,20 +44,17 @@ const getCountsByAssetID = (context, asset_ids) => { * Returns the count of all public comments on an asset id, also filtering by personalization options. * * @param {Array} id The ID of the asset - * @param {Array} notIgnoredBy Exclude comments ignored by this User ID + * @param {Array} excludeIgnored Exclude comments ignored by the requesting user */ -const getCountsByAssetIDPersonalized = async (context, {assetId, notIgnoredBy}) => { +const getCountsByAssetIDPersonalized = async (context, {assetId, excludeIgnored}) => { const query = { asset_id: assetId, status: { $in: ['NONE', 'ACCEPTED'], }, }; - if (notIgnoredBy) { - const user = context.user; - if (user.id !== notIgnoredBy) { - throw new Error(`You are not authorized to query for comments counts notIgnoredBy ${notIgnoredBy}`); - } + const user = context.user; + if (excludeIgnored && user) { // load afresh, as `user` may be from cache and not have recent ignores const freshUser = await UsersService.findById(user.id); @@ -105,9 +102,9 @@ const getParentCountsByAssetID = (context, asset_ids) => { * Returns the count of top-level comments on an asset id, also filtering by personalization options. * * @param {Array} id The ID of the asset - * @param {Array} notIgnoredBy Exclude comments ignored by this User ID + * @param {Array} excludeIgnored Exclude comments ignored by the requesting user */ -const getParentCountByAssetIDPersonalized = async (context, {assetId, notIgnoredBy}) => { +const getParentCountByAssetIDPersonalized = async (context, {assetId, excludeIgnored}) => { const query = { asset_id: assetId, parent_id: null, @@ -115,11 +112,8 @@ const getParentCountByAssetIDPersonalized = async (context, {assetId, notIgnored $in: ['NONE', 'ACCEPTED'], }, }; - if (notIgnoredBy) { - const user = context.user; - if (user.id !== notIgnoredBy) { - throw new Error(`You are not authorized to query for comments counts notIgnoredBy ${notIgnoredBy}`); - } + const user = context.user; + if (excludeIgnored && user) { // load afresh, as `user` may be from cache and not have recent ignores const freshUser = await UsersService.findById(user.id); @@ -166,9 +160,9 @@ const getCountsByParentID = (context, parent_ids) => { * Returns the count of comments for the provided parent_id, also filtering by personalization options. * * @param {Array} id The ID of the parent comment - * @param {Array} notIgnoredBy Exclude comments ignored by this User ID + * @param {Array} excludeIgnored Exclude comments ignored by context.user */ -const getCountByParentIDPersonalized = async (context, {id, notIgnoredBy}) => { +const getCountByParentIDPersonalized = async (context, {id, excludeIgnored}) => { const query = { parent_id: { $in: [id] @@ -177,11 +171,8 @@ const getCountByParentIDPersonalized = async (context, {id, notIgnoredBy}) => { $in: ['NONE', 'ACCEPTED'] } }; - if (notIgnoredBy) { - const user = context.user; - if (user.id !== notIgnoredBy) { - throw new Error(`You are not authorized to query for comments counts notIgnoredBy ${notIgnoredBy}`); - } + const user = context.user; + if (excludeIgnored && user) { // load afresh, as `user` may be from cache and not have recent ignores const freshUser = await UsersService.findById(user.id); @@ -230,7 +221,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id}) = * @param {Object} context graph context * @param {Object} query query terms to apply to the comments query */ -const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, notIgnoredBy}) => { +const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, author_id, limit, cursor, sort, excludeIgnored}) => { let comments = CommentModel.find(); // Only administrators can search for comments with statuses that are not @@ -272,10 +263,7 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a comments = comments.where({parent_id}); } - if (notIgnoredBy) { - if (user.id !== notIgnoredBy) { - throw new Error(`You are not authorized to query for comments notIgnoredBy ${notIgnoredBy}`); - } + if (excludeIgnored && user) { // load afresh, as `user` may be from cache and not have recent ignores const freshUser = await UsersService.findById(user.id); diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js index 811181aa6..96bc8c1bc 100644 --- a/graph/resolvers/asset.js +++ b/graph/resolvers/asset.js @@ -2,27 +2,27 @@ const Asset = { recentComments({id}, _, {loaders: {Comments}}) { return Comments.genRecentComments.load(id); }, - comments({id}, {sort, limit, notIgnoredBy}, {loaders: {Comments}}) { + comments({id}, {sort, limit, excludeIgnored}, {loaders: {Comments}}) { return Comments.getByQuery({ asset_id: id, sort, limit, parent_id: null, - notIgnoredBy, + excludeIgnored, }); }, - commentCount({id, commentCount}, {notIgnoredBy}, {loaders: {Comments}}) { - if (notIgnoredBy) { - return Comments.parentCountByAssetIDPersonalized({assetId: id, notIgnoredBy}); + commentCount({id, commentCount}, {excludeIgnored}, {user, loaders: {Comments}}) { + if (user && excludeIgnored) { + return Comments.parentCountByAssetIDPersonalized({assetId: id, excludeIgnored}); } if (commentCount != null) { return commentCount; } return Comments.parentCountByAssetID.load(id); }, - totalCommentCount({id, totalCommentCount}, {notIgnoredBy}, {loaders: {Comments}}) { - if (notIgnoredBy) { - return Comments.countByAssetIDPersonalized({assetId: id, notIgnoredBy}); + totalCommentCount({id, totalCommentCount}, {excludeIgnored}, {user, loaders: {Comments}}) { + if (user && excludeIgnored) { + return Comments.countByAssetIDPersonalized({assetId: id, excludeIgnored}); } if (totalCommentCount != null) { return totalCommentCount; diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 1f57cb939..19ea11efe 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -12,20 +12,20 @@ const Comment = { recentReplies({id}, _, {loaders: {Comments}}) { return Comments.genRecentReplies.load(id); }, - replies({id, asset_id}, {sort, limit, notIgnoredBy}, {loaders: {Comments}}) { + replies({id, asset_id}, {sort, limit, excludeIgnored}, {loaders: {Comments}}) { return Comments.getByQuery({ asset_id, parent_id: id, sort, limit, - notIgnoredBy, + excludeIgnored, }); }, - replyCount({id}, {notIgnoredBy}, {loaders: {Comments}}) { - if ( ! notIgnoredBy) { - return Comments.countByParentID.load(id); + replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { + if (user && excludeIgnored) { + return Comments.countByParentIDPersonalized({id, excludeIgnored}); } - return Comments.countByParentIDPersonalized({id, notIgnoredBy}); + return Comments.countByParentID.load(id); }, actions({id}, _, {user, loaders: {Actions}}) { diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 530ee74d6..b26ecabca 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -19,15 +19,15 @@ const RootQuery = { // This endpoint is used for loading moderation queues, so hide it in the // event that we aren't an admin. - comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, notIgnoredBy}}, {user, loaders: {Comments, Actions}}) { - let query = {statuses, asset_id, parent_id, limit, cursor, sort, notIgnoredBy}; + comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}}, {user, loaders: {Comments, Actions}}) { + let query = {statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}; if (user != null && user.hasRoles('ADMIN') && action_type) { return Actions.getByTypes({action_type, item_type: 'COMMENTS'}) .then((ids) => { // Perform the query using the available resolver. - return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort, notIgnoredBy}); + return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}); }); } diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 7f988054b..aeae1537c 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -141,8 +141,8 @@ input CommentsQuery { # Sort the results by created_at. sort: SORT_ORDER = REVERSE_CHRONOLOGICAL - # Exclude comments ignored by this user ID - notIgnoredBy: String + # Exclude comments ignored by the requesting user + excludeIgnored: Boolean } # CommentCountQuery allows the ability to query comment counts by specific @@ -188,10 +188,10 @@ type Comment { recentReplies: [Comment] # the replies that were made to the comment. - replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, notIgnoredBy: String): [Comment] + replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, excludeIgnored: Boolean): [Comment] # The count of replies on a comment. - replyCount(notIgnoredBy: String): Int + replyCount(excludeIgnored: Boolean): Int # Actions completed on the parent. Requires the `ADMIN` role. actions: [Action] @@ -420,13 +420,13 @@ type Asset { recentComments: [Comment] # The top level comments that are attached to the asset. - comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10, notIgnoredBy: String): [Comment] + comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10, excludeIgnored: Boolean): [Comment] # The count of top level comments on the asset. - commentCount(notIgnoredBy: String): Int + commentCount(excludeIgnored: Boolean): Int # The total count of all comments made on the asset. - totalCommentCount(notIgnoredBy: String): Int + totalCommentCount(excludeIgnored: Boolean): Int # The settings (rectified with the global settings) that should be applied to # this asset. diff --git a/test/graph/queries/asset.js b/test/graph/queries/asset.js index 5ee19697a..ba31fe330 100644 --- a/test/graph/queries/asset.js +++ b/test/graph/queries/asset.js @@ -76,16 +76,16 @@ describe('graph.queries.asset', () => { expect(ignoreUserResponse.errors).to.be.empty; const assetCommentsWithoutIgnoredQuery = ` - query assetCommentsQuery($assetId: ID!, $assetUrl: String!, $notIgnoredBy: String!) { + query assetCommentsQuery($assetId: ID!, $assetUrl: String!, $excludeIgnored: Boolean!) { asset(id: $assetId, url: $assetUrl) { - comments(limit: 10, notIgnoredBy: $notIgnoredBy) { + comments(limit: 10, excludeIgnored: $excludeIgnored) { id, body, } } } `; - const assetCommentsResponse = await graphql(schema, assetCommentsWithoutIgnoredQuery, {}, context, {assetId, assetUrl, notIgnoredBy: userA.id}); + const assetCommentsResponse = await graphql(schema, assetCommentsWithoutIgnoredQuery, {}, context, {assetId, assetUrl, excludeIgnored: true}); const comments = assetCommentsResponse.data.asset.comments; expect(comments.length).to.equal(2); }); From 847ad2263b8ee92b3b2eb54e93adcb72ea29eb26 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Tue, 11 Apr 2017 10:34:00 -0700 Subject: [PATCH 23/44] Split IgnoreUserWizard into own file --- .../src/IgnoreUserWizard.css | 14 ++++ .../src/IgnoreUserWizard.js | 66 +++++++++++++++++++ .../coral-embed-stream/src/TopRightMenu.css | 15 ----- client/coral-embed-stream/src/TopRightMenu.js | 64 +----------------- 4 files changed, 81 insertions(+), 78 deletions(-) create mode 100644 client/coral-embed-stream/src/IgnoreUserWizard.css create mode 100644 client/coral-embed-stream/src/IgnoreUserWizard.js diff --git a/client/coral-embed-stream/src/IgnoreUserWizard.css b/client/coral-embed-stream/src/IgnoreUserWizard.css new file mode 100644 index 000000000..838f2f76a --- /dev/null +++ b/client/coral-embed-stream/src/IgnoreUserWizard.css @@ -0,0 +1,14 @@ +.IgnoreUserWizard { + background-color: #2E343B; + color: white; + padding: 1em; + max-width: 220px; +} + +.IgnoreUserWizard header { + font-weight: bold; +} + +.IgnoreUserWizard .textAlignRight { + text-align: right; +} diff --git a/client/coral-embed-stream/src/IgnoreUserWizard.js b/client/coral-embed-stream/src/IgnoreUserWizard.js new file mode 100644 index 000000000..371e6d48b --- /dev/null +++ b/client/coral-embed-stream/src/IgnoreUserWizard.js @@ -0,0 +1,66 @@ +import React, {PropTypes} from 'react'; +import styles from './IgnoreUserWizard.css'; +import {Button} from 'coral-ui'; + +// Guides the user through ignoring another user, including confirming their decision +export class IgnoreUserWizard extends React.Component { + static propTypes = { + + // comment on which this menu appears + user: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + cancel: PropTypes.func.isRequired, + + // actually submit the ignore. Provide {id: user id to ignore} + ignoreUser: PropTypes.func.isRequired, + } + constructor(props) { + super(props); + this.state = { + + // what step of the wizard is the user on + step: 1 + }; + this.onClickCancel = this.onClickCancel.bind(this); + } + onClickCancel() { + this.props.cancel(); + } + render() { + const {user, ignoreUser} = this.props; + const goToStep = (stepNum) => this.setState({step: stepNum}); + const step1 = ( +
    +
    Ignore User
    +

    When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.

    +
    + + +
    +
    + ); + const onClickIgnoreUser = async () => { + await ignoreUser({id: user.id}); + }; + const step2Confirmation = ( +
    +
    Ignore User
    +

    Are you sure you want to ignore { user.name }?

    +
    + + +
    +
    + ); + const elsForStep = [step1, step2Confirmation]; + const {step} = this.state; + const elForThisStep = elsForStep[step - 1]; + return ( +
    + { elForThisStep } +
    + ); + } +} diff --git a/client/coral-embed-stream/src/TopRightMenu.css b/client/coral-embed-stream/src/TopRightMenu.css index 587920c5f..5cddae125 100644 --- a/client/coral-embed-stream/src/TopRightMenu.css +++ b/client/coral-embed-stream/src/TopRightMenu.css @@ -2,21 +2,6 @@ outline: none; } -.IgnoreUserWizard { - background-color: #2E343B; - color: white; - padding: 1em; - max-width: 220px; -} - -.IgnoreUserWizard header { - font-weight: bold; -} - -.IgnoreUserWizard .textAlignRight { - text-align: right; -} - /** * Up/Down Chevrons for the top right menu */ diff --git a/client/coral-embed-stream/src/TopRightMenu.js b/client/coral-embed-stream/src/TopRightMenu.js index 1e06997dd..e6b264c27 100644 --- a/client/coral-embed-stream/src/TopRightMenu.js +++ b/client/coral-embed-stream/src/TopRightMenu.js @@ -1,7 +1,7 @@ import React, {PropTypes} from 'react'; import classnames from 'classnames'; -import {Button} from 'coral-ui'; +import {IgnoreUserWizard} from './IgnoreUserWizard'; import styles from './TopRightMenu.css'; // TopRightMenu appears as a dropdown in the top right of the comment. @@ -60,68 +60,6 @@ export class TopRightMenu extends React.Component { } } -class IgnoreUserWizard extends React.Component { - static propTypes = { - - // comment on which this menu appears - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }).isRequired, - cancel: PropTypes.func.isRequired, - - // actually submit the ignore. Provide {id: user id to ignore} - ignoreUser: PropTypes.func.isRequired, - } - constructor(props) { - super(props); - this.state = { - - // what step of the wizard is the user on - step: 1 - }; - this.onClickCancel = this.onClickCancel.bind(this); - } - onClickCancel() { - this.props.cancel(); - } - render() { - const {user, ignoreUser} = this.props; - const goToStep = (stepNum) => this.setState({step: stepNum}); - const step1 = ( -
    -
    Ignore User
    -

    When you ignore a user, all comments they wrote on the site will be hidden from you. You can undo this later from the Profile tab.

    -
    - - -
    -
    - ); - const onClickIgnoreUser = async () => { - await ignoreUser({id: user.id}); - }; - const step2Confirmation = ( -
    -
    Ignore User
    -

    Are you sure you want to ignore { user.name }?

    -
    - - -
    -
    - ); - const elsForStep = [step1, step2Confirmation]; - const {step} = this.state; - const elForThisStep = elsForStep[step - 1]; - return ( -
    - { elForThisStep } -
    - ); - } -} - const upArrow = ; const downArrow = ; class Toggleable extends React.Component { From a7be2772c31a6c1b467201e8ad0ea3ca78dc6b32 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Tue, 11 Apr 2017 14:18:46 -0700 Subject: [PATCH 24/44] IgnoredCommentTombstone body text is more i18n --- client/coral-embed-stream/src/IgnoredCommentTombstone.js | 6 +++++- client/coral-framework/translations.json | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/IgnoredCommentTombstone.js b/client/coral-embed-stream/src/IgnoredCommentTombstone.js index aca235a02..0ed746976 100644 --- a/client/coral-embed-stream/src/IgnoredCommentTombstone.js +++ b/client/coral-embed-stream/src/IgnoredCommentTombstone.js @@ -1,5 +1,9 @@ import React from 'react'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-framework/translations'; +const lang = new I18n(translations); + // Render in place of a Comment when the author of the comment is ignored const IgnoredCommentTombstone = () => (
    @@ -10,7 +14,7 @@ const IgnoredCommentTombstone = () => ( padding: '1em', color: '#3E4F71', }}> - This comment is hidden because you ignored this user. + {lang.t('commentIsIgnored')}

    ); diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index f13db92a8..f6e8b82e4 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -21,6 +21,7 @@ "newCount": "View {0} new {1}", "comment": "comment", "comments": "comments", + "commentIsIgnored": "This comment is hidden because you ignored this user.", "error": { "emailNotVerified": "Email address {0} not verified.", "email": "Not a valid E-Mail", @@ -58,6 +59,7 @@ "newCount": "Ver {0} {1} más", "comment": "commentario", "comments": "commentarios", + "commentIsIgnored": "Este comentario está oculto porque ignoró a este usuario.", "showAllComments": "Mostrar todos los comentarios", "error": { "emailNotVerified": "E-mail {0} no verificado.", From 8cf2130e253dfed5b194fd53768cd25c3b57fc83 Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Tue, 11 Apr 2017 14:31:46 -0700 Subject: [PATCH 25/44] Update spanish translation for commentIsIgnored --- client/coral-framework/translations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index f6e8b82e4..88ead57af 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -59,7 +59,7 @@ "newCount": "Ver {0} {1} más", "comment": "commentario", "comments": "commentarios", - "commentIsIgnored": "Este comentario está oculto porque ignoró a este usuario.", + "commentIsIgnored": "Este comentario está escondido porque has ignorado al usuario.", "showAllComments": "Mostrar todos los comentarios", "error": { "emailNotVerified": "E-mail {0} no verificado.", From 6c0e00155f8bccdd63c86c455fa13355a76dcb2b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 12 Apr 2017 13:29:01 -0600 Subject: [PATCH 26/44] Initial update --- Dockerfile | 2 +- INSTALL.md | 88 ++++++++++++++++++++++++++++++++-------------- circle.yml | 2 +- docker-compose.yml | 25 ------------- package.json | 2 +- 5 files changed, 65 insertions(+), 54 deletions(-) delete mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index 5bc38013a..90c08057f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:7 +FROM node:7.9 # Create app directory RUN mkdir -p /usr/src/app diff --git a/INSTALL.md b/INSTALL.md index 33b634de8..e28a559f1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,11 +2,11 @@ ## Requirements -### System - - Any flavour of Linux, OSX or Windows - 1GB memory (minimum) - 5GB storage (minimum) +- [MongoDB](https://www.mongodb.com/) v3.4 or later +- [Redis](https://redis.io/) v3.2 or later ## Installation From Source @@ -14,24 +14,31 @@ There are some runtime requirements for running Talk from source: -- [Node](https://nodejs.org/) v7 or later -- [MongoDB](https://www.mongodb.com/) v3.4 or later -- [Redis](https://redis.io/) v3.2 or later -- [Yarn](https://yarnpkg.com/) v0.19.1 or later +- [Node](https://nodejs.org/) v7.9 or later +- [Yarn](https://yarnpkg.com/) v0.22.0 or later -_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_ +_Please be sure to check the versions of these requirements. Incorrect versions +of these may lead to unexpected errors!_ ### Installing +#### Download + +It is highly recommended that you download a released version as the code +available in `master` may not be stable. You can download the latest release +from the [releases page](https://github.com/coralproject/talk/releases). + +You can also clone the git repository via: + ```bash -# Download the tarball containing the repository -curl -L https://github.com/coralproject/talk/tarball/master -o coralproject-talk.tar.gz +git clone https://github.com/coralproject/talk.git +``` -# Untar that file and change to that directory -tar xpf coralproject-talk.tar.gz -mv coralproject-talk-* coralproject-talk -cd coralproject-talk +#### Setup +We now have to install the dependancies and build the static assets. + +```bash # Install package dependancies yarn @@ -50,6 +57,8 @@ You can start the server after configuring the server using the command: yarn start ``` +This will setup the server to serve everything on a single node.js process. + You can see other scripts we've made available by consulting the `package.json` file under the `scripts` key including: @@ -58,33 +67,60 @@ file under the `scripts` key including: - `yarn build-watch` watch for changes to client files and build static assets - `yarn dev-start` watch for changes to server files and reload the server -## Installation From Docker Hub +## Installation From Docker + +We currently support packaging the Talk application via Docker, which automates +the dependancy install and asset build process. ### Requirements There are some runtime requirements for running Talk for Docker: -- [MongoDB](https://www.mongodb.com/) v3.2 or later -- [Redis](https://redis.io/) v3.2 or later - [Docker](https://www.docker.com/) v1.13.0 or later - [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later -_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_ +_Please be sure to check the versions of these requirements. Incorrect versions +of these may lead to unexpected errors!_ ### Installing -```bash -# Create a directory for talk -mkdir coralproject-talk -cd coralproject-talk +An example docker-compose.yml: -# Download the docker-compose.yml file from the repository -curl -LO https://raw.githubusercontent.com/coralproject/talk/master/docker-compose.yml +```yaml +version: '2' +services: + talk: + image: coralproject/talk:1.5 + restart: always + ports: + - "5000:5000" + depends_on: + - mongo + - redis + environment: + - TALK_MONGO_URL=mongodb://mongo/talk + - TALK_REDIS_URL=redis://redis + mongo: + image: mongo:3.2 + restart: always + volumes: + - mongo:/data/db + redis: + image: redis:3.2 + restart: always + volumes: + - redis:/data +volumes: + mongo: + external: false + redis: + external: false ``` -At this stage, you should refer to the `README.md` file for required -configuration variables to add to the environment key for the `talk` service -listed in the `docker-compose.yml` file. +At this stage, you should refer to the `README.md` for configuration variables +that are specific to your installation. Some pre-defined fields have been filled +in the above example which are consistent with Docker Compose naming conventions +for [Docker Links](https://docs.docker.com/compose/networking/#links). ### Running diff --git a/circle.yml b/circle.yml index 013ef28b8..e7427094b 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 7 + version: 7.9 services: - docker - redis diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 76c28d20f..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: '2' -services: - talk: - image: coralproject/talk:latest - restart: always - ports: - - "5000:5000" - depends_on: - - mongo - - redis - mongo: - image: mongo:3.2 - restart: always - volumes: - - mongo:/data/db - redis: - image: redis:3.2 - restart: always - volumes: - - redis:/data -volumes: - mongo: - external: false - redis: - external: false diff --git a/package.json b/package.json index 41d8732de..1b09c78c8 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,6 @@ "webpack": "^2.3.1" }, "engines": { - "node": "^7.7.0" + "node": "^7.9.0" } } From 31960d67f37d5a686f5d2c3e827a7b343ab19a02 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 12 Apr 2017 14:25:43 -0600 Subject: [PATCH 27/44] Added more to docs --- INSTALL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index e28a559f1..942b99377 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -72,6 +72,8 @@ file under the `scripts` key including: We currently support packaging the Talk application via Docker, which automates the dependancy install and asset build process. +https://hub.docker.com/r/coralproject/talk/ + ### Requirements There are some runtime requirements for running Talk for Docker: From f7ee08bd3aa98b90a73d334d937b8d261a877503 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 12 Apr 2017 14:39:11 -0600 Subject: [PATCH 28/44] Ignore lifecycle hooks during plugin install --- bin/cli-plugins | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/cli-plugins b/bin/cli-plugins index 94ce74904..b1dee381b 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -67,7 +67,7 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) { console.log(' +missing (m) packages are not found'); console.log(); } - + for (let i in plugins) { let section = itteratePlugins(plugins[i]); @@ -118,7 +118,7 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) { if (!quiet) { console.log(` e ${name}`); } - + if (upgradeRemote) { upgradable.push({name, version}); } @@ -141,12 +141,13 @@ async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) { if (fetchable.length > 0) { - console.log(`$ yarn add ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`); + console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`); if (!dryRun) { let args = [ 'add', + '--ignore-scripts', ...fetchable.map(({name, version}) => `${name}@${version}`) ]; From 2fcfaa8a99c77f239ac7750ee744b6f6df07f20e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 12 Apr 2017 15:08:07 -0600 Subject: [PATCH 29/44] More updates --- INSTALL.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 942b99377..fd2c5175f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -46,6 +46,17 @@ yarn yarn build ``` +After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin +docs) file, you can re-run the following to install their dependancies: + +```bash +# Reconcile plugins +./bin/cli plugins reconcile + +# Build static files +yarn build +``` + ### Running Refer to the `README.md` file for required configuration variables to add to the @@ -57,7 +68,8 @@ You can start the server after configuring the server using the command: yarn start ``` -This will setup the server to serve everything on a single node.js process. +This will setup the server to serve everything on a single node.js process and +is designed to be used in production. You can see other scripts we've made available by consulting the `package.json` file under the `scripts` key including: @@ -65,7 +77,8 @@ file under the `scripts` key including: - `yarn test` run unit tests - `yarn e2e` run end to end tests - `yarn build-watch` watch for changes to client files and build static assets -- `yarn dev-start` watch for changes to server files and reload the server +- `yarn dev-start` watch for changes to server files and reload the server while + also sourcing a `.env` file in your local directory for configuration ## Installation From Docker @@ -74,6 +87,17 @@ the dependancy install and asset build process. https://hub.docker.com/r/coralproject/talk/ +Images are tagged using the following notation: + +- `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting + new features occationally and all the bug fixes, this is the tag for you. +- `x.y` (where `y` is the minor version number): +- `x.y.z` (where `z` is the patch version): + +We provide tags with `*-onbuild` that can be used for easy plugin integration and +acts as a customization endpoint. Instructions are provided in the `PLUGINS.md` +document as to how to use it. + ### Requirements There are some runtime requirements for running Talk for Docker: @@ -126,7 +150,15 @@ for [Docker Links](https://docs.docker.com/compose/networking/#links). ### Running +If you're using docker compose: + ```bash # Start the services using compose docker-compose up -d ``` + +If you're using plain docker: + +```bash +docker run -d -P coralproject/talk:latest +``` \ No newline at end of file From dbc95db8059d4345714a9794f3e68d918cb74a5f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 12 Apr 2017 15:15:51 -0600 Subject: [PATCH 30/44] Updates for docker images --- INSTALL.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index fd2c5175f..8d332b77a 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -91,8 +91,12 @@ Images are tagged using the following notation: - `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting new features occationally and all the bug fixes, this is the tag for you. -- `x.y` (where `y` is the minor version number): -- `x.y.z` (where `z` is the patch version): +- `x.y` (where `y` is the minor version number): any patch updates will be + included with this tag. If you like getting fixes and having features change + only when you want, this is the tag for you. **(recommended)** +- `x.y.z` (where `z` is the patch version): this tag never gets updated, and + essentially freezes your version, this should only be used when you are either + extending Talk or are sure of a specific version you want to freeze. We provide tags with `*-onbuild` that can be used for easy plugin integration and acts as a customization endpoint. Instructions are provided in the `PLUGINS.md` From d5016dff58524d5e74b89b320152c7d19a9897d5 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 12 Apr 2017 16:37:56 -0600 Subject: [PATCH 31/44] Updates to metadata service docs --- PLUGINS.md | 17 ++++++++++------- services/metadata.js | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 8bf00229d..399034b53 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -53,13 +53,6 @@ mismatch._ ## Plugin Dependencies -From your plugins you may import any component of server code relative to the -project root. An example could be: - -```js -const cache = require('services/cache'); -``` - You may also include additional external depenancies in your local packages by specifying a `package.json` at your plugin root which will result in a `node_modules` folder being generated at the plugin root with your specific @@ -67,6 +60,16 @@ dependencies. ## Server Plugins +### API + +You can access any API available inside the talk directory in a plugin by simply +importing the file relative to the talk project root. An example would be if you +wanted to import the `MetadataService`, you would simply write: + +```javascript +const MetadataService = require('services/metadata'); +``` + ### Specification Each plugin should export a single object with all hooks available on it. diff --git a/services/metadata.js b/services/metadata.js index 2cabed8b8..511429e24 100644 --- a/services/metadata.js +++ b/services/metadata.js @@ -36,7 +36,14 @@ class MetadataService { } /** - * Sets an object on the metadata field of an object. + * Sets an object on the metadata field of an object. An example could be: + * + * @example + * const MetadataService = require('services/metadata'); + * const CommentModel = require('models/comment'); + * + * // Sets the property `loaded` on the comment with `id=1`. + * MetadataService.set(CommentModel, '1', 'loaded', true); * * @static * @param {mongoose.Model} model the mongoose model for the object @@ -60,6 +67,13 @@ class MetadataService { /** * Removes the value for the metadata field as the specific key. * + * @example + * const MetadataService = require('services/metadata'); + * const CommentModel = require('models/comment'); + * + * // Removes the property `loaded` on the comment with `id=1`. + * MetadataService.unset(CommentModel, '1', 'loaded'); + * * @static * @param {mongoose.Model} model the mongoose model for the object * @param {String} id the value for the field `id` of the model From 941db338d0abce6a3fd519c5de5b482aed50f73e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 12 Apr 2017 16:51:20 -0600 Subject: [PATCH 32/44] Updates for plugin deployment --- PLUGINS.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/PLUGINS.md b/PLUGINS.md index 399034b53..4339e9ab9 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -58,6 +58,43 @@ specifying a `package.json` at your plugin root which will result in a `node_modules` folder being generated at the plugin root with your specific dependencies. +## Deployment Solutions + +Plugins can be deployed with a production instance of Talk. + +### Source + +Source deployments can just modify the `plugins.json` file and include any +local plugins into the `plugins/` directory. After including the config, you +need to reconcile the plugins and build the static assets: + +```bash +# get plugin dependancies and remote plugins +./bin/cli plugins reconcile + +# build staic assets (including enabled client side plugins) +yarn build +``` + +Then the application can be started as is. + +### Docker + +If you deploy using Docker, you can extend from the `*-onbuild` image, an +example `Dockerfile` for your project could be: + +```Dockerfile +FROM coralproject/talk:latest-onbuild +``` + +Where the directory for your instance would contain a `plugins.json` file +describing the plugin requirements and a `plugins` directory containing any +other local plugins that should be included. + +Onbuild triggers will execute when the image is building with your custom +configuration and will ensure that the image is ready to use by building all +assets inside the image as well. + ## Server Plugins ### API From 90db03bc29e573ca67c16a762a01a9305e33d830 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 13 Apr 2017 13:40:13 -0600 Subject: [PATCH 33/44] rearranged docs --- INSTALL.md | 148 ++++++++++++++++++++++++++--------------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 8d332b77a..a764693ad 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -8,84 +8,12 @@ - [MongoDB](https://www.mongodb.com/) v3.4 or later - [Redis](https://redis.io/) v3.2 or later -## Installation From Source - -### Requirements - -There are some runtime requirements for running Talk from source: - -- [Node](https://nodejs.org/) v7.9 or later -- [Yarn](https://yarnpkg.com/) v0.22.0 or later - -_Please be sure to check the versions of these requirements. Incorrect versions -of these may lead to unexpected errors!_ - -### Installing - -#### Download - -It is highly recommended that you download a released version as the code -available in `master` may not be stable. You can download the latest release -from the [releases page](https://github.com/coralproject/talk/releases). - -You can also clone the git repository via: - -```bash -git clone https://github.com/coralproject/talk.git -``` - -#### Setup - -We now have to install the dependancies and build the static assets. - -```bash -# Install package dependancies -yarn - -# Build static files -yarn build -``` - -After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin -docs) file, you can re-run the following to install their dependancies: - -```bash -# Reconcile plugins -./bin/cli plugins reconcile - -# Build static files -yarn build -``` - -### Running - -Refer to the `README.md` file for required configuration variables to add to the -environment. - -You can start the server after configuring the server using the command: - -```bash -yarn start -``` - -This will setup the server to serve everything on a single node.js process and -is designed to be used in production. - -You can see other scripts we've made available by consulting the `package.json` -file under the `scripts` key including: - -- `yarn test` run unit tests -- `yarn e2e` run end to end tests -- `yarn build-watch` watch for changes to client files and build static assets -- `yarn dev-start` watch for changes to server files and reload the server while - also sourcing a `.env` file in your local directory for configuration - ## Installation From Docker We currently support packaging the Talk application via Docker, which automates the dependancy install and asset build process. -https://hub.docker.com/r/coralproject/talk/ +Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Docker Hub. Images are tagged using the following notation: @@ -165,4 +93,76 @@ If you're using plain docker: ```bash docker run -d -P coralproject/talk:latest -``` \ No newline at end of file +``` + +## Installation From Source + +### Requirements + +There are some runtime requirements for running Talk from source: + +- [Node](https://nodejs.org/) v7.9 or later +- [Yarn](https://yarnpkg.com/) v0.22.0 or later + +_Please be sure to check the versions of these requirements. Incorrect versions +of these may lead to unexpected errors!_ + +### Installing + +#### Download + +It is highly recommended that you download a released version as the code +available in `master` may not be stable. You can download the latest release +from the [releases page](https://github.com/coralproject/talk/releases). + +You can also clone the git repository via: + +```bash +git clone https://github.com/coralproject/talk.git +``` + +#### Setup + +We now have to install the dependancies and build the static assets. + +```bash +# Install package dependancies +yarn + +# Build static files +yarn build +``` + +After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin +docs) file, you can re-run the following to install their dependancies: + +```bash +# Reconcile plugins +./bin/cli plugins reconcile + +# Build static files +yarn build +``` + +### Running + +Refer to the `README.md` file for required configuration variables to add to the +environment. + +You can start the server after configuring the server using the command: + +```bash +yarn start +``` + +This will setup the server to serve everything on a single node.js process and +is designed to be used in production. + +You can see other scripts we've made available by consulting the `package.json` +file under the `scripts` key including: + +- `yarn test` run unit tests +- `yarn e2e` run end to end tests +- `yarn build-watch` watch for changes to client files and build static assets +- `yarn dev-start` watch for changes to server files and reload the server while + also sourcing a `.env` file in your local directory for configuration \ No newline at end of file From 7aa262e97ff291909c660290588c8f4493266f18 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 13 Apr 2017 13:46:37 -0600 Subject: [PATCH 34/44] Added new cluster style deployment docs --- INSTALL.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index a764693ad..1fec24865 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -80,6 +80,61 @@ that are specific to your installation. Some pre-defined fields have been filled in the above example which are consistent with Docker Compose naming conventions for [Docker Links](https://docs.docker.com/compose/networking/#links). +### Scaling + +If you are interested in splitting apart services, you can simply adjust the +command being executed in the container to optimize for your use case. An +example would be if you wanted to run the API server and the job processor +on different machines. You can acheive this easily with docker compose: + +```yaml +version: '2' +services: + talk-api: + image: coralproject/talk:1.5 + command: cli serve + restart: always + ports: + - "5000:5000" + depends_on: + - mongo + - redis + environment: + - TALK_MONGO_URL=mongodb://mongo/talk + - TALK_REDIS_URL=redis://redis + talk-jobs: + image: coralproject/talk:1.5 + command: cli jobs process + restart: always + ports: + - "5001:5000" + depends_on: + - mongo + - redis + environment: + - TALK_MONGO_URL=mongodb://mongo/talk + - TALK_REDIS_URL=redis://redis + mongo: + image: mongo:3.2 + restart: always + volumes: + - mongo:/data/db + redis: + image: redis:3.2 + restart: always + volumes: + - redis:/data +volumes: + mongo: + external: false + redis: + external: false +``` + +Note that the only difference is in the `command` key. From this, you are able +to discretly control which modules are running in order to have the maximum +flexibility when managing your application. + ### Running If you're using docker compose: From d5552df41f4a3bb5a9f561eedbca75b36220a83c Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Thu, 13 Apr 2017 14:46:11 -0600 Subject: [PATCH 35/44] ignore node_modules in plugins --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8982613ac..f4014b561 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ plugins.json plugins/* !plugins/coral-plugin-facebook-auth !plugins/coral-plugin-respect +**/node_modules/* From c395bc88497055e66fa5a45ec600dd493b833c6f Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 13 Apr 2017 14:21:45 -0700 Subject: [PATCH 36/44] Adds closedAt to state... and check that closedTimeout settings exists for the Asset. --- .../containers/ConfigureStreamContainer.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/client/coral-configure/containers/ConfigureStreamContainer.js b/client/coral-configure/containers/ConfigureStreamContainer.js index a621f0cbd..0d9d024c6 100644 --- a/client/coral-configure/containers/ConfigureStreamContainer.js +++ b/client/coral-configure/containers/ConfigureStreamContainer.js @@ -15,7 +15,8 @@ class ConfigureStreamContainer extends Component { super(props); this.state = { - changed: false + changed: false, + closedAt: (props.asset.closedAt === null ? 'open' : 'closed') }; this.toggleStatus = this.toggleStatus.bind(this); @@ -66,21 +67,28 @@ class ConfigureStreamContainer extends Component { } toggleStatus () { + + // update the closedAt status for the asset this.props.updateStatus( - this.props.asset.closedAt === null ? 'closed' : 'open' + this.state.closedAt === 'open' ? 'closed' : 'open' ); + this.setState({ + closedAt: (this.state.closedAt === 'open' ? 'closed' : 'open') + }); } getClosedIn () { const {closedTimeout} = this.props.asset.settings; const {created_at} = this.props.asset; + return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout)); } render () { - const {settings, closedAt} = this.props.asset; - const status = closedAt === null ? 'open' : 'closed'; + const {settings} = this.props.asset; + const {closedAt} = this.state; const premod = settings.moderation === 'PRE'; + const closedTimeout = settings.closedTimeout; return (
    @@ -95,11 +103,11 @@ class ConfigureStreamContainer extends Component { questionBoxContent={settings.questionBoxContent} />
    -

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

    - {status === 'open' ?

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

    : ''} +

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

    + {(closedAt === 'open' && closedTimeout) ?

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

    : ''}
    ); From 902071c0feab69f99e11313a21f18e4ff4a75872 Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 13 Apr 2017 16:11:25 -0700 Subject: [PATCH 37/44] Adds title of most flagged in dashboard. --- client/coral-admin/src/containers/Dashboard/FlagWidget.js | 2 +- client/coral-admin/src/translations.json | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/coral-admin/src/containers/Dashboard/FlagWidget.js b/client/coral-admin/src/containers/Dashboard/FlagWidget.js index 17be2acbd..92de6c4d2 100644 --- a/client/coral-admin/src/containers/Dashboard/FlagWidget.js +++ b/client/coral-admin/src/containers/Dashboard/FlagWidget.js @@ -10,7 +10,7 @@ const FlagWidget = ({assets}) => { return (
    -

    Articles with the most flags

    +

    {lang.t('dashboard.most_flags')}

    {lang.t('streams.article')}

    {lang.t('dashboard.flags')}

    diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 1021978f1..5e465e527 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -139,7 +139,8 @@ "no_likes": "There have been no likes in the last 5 minutes. All quiet.", "flags": "Flags", "no_activity": "There haven't been any comments anywhere in the last five minutes.", - "comment_count": "comments" + "comment_count": "comments", + "most_flags": "Articles with the most flags" }, "streams": { "empty_result": "No assets match this search. Maybe try widening your search?", @@ -283,14 +284,15 @@ "cancel": "Cancelar", "yes_ban_user": "Si, Suspendan el usuario" }, - "dashbord": { + "dashboard": { "next-update": "{0} minutos hasta la siguiente actualización.", "auto-update": "Los datos se actualizan automaticamente cada 5 minutos o cuando recargas.", "no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!", "no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.", "flags": "Marcados", "no_activity": "No hubo comentarios en los ultimos 5 minutos", - "comment_count": "comentarios" + "comment_count": "comentarios", + "most_flags": "Articulos con más reportes" }, "streams": { "empty_result": "No se encuentro articulo con esta busqueda. ¿Tal vez puedas extender la busqueda?", From d9c8882d8fd4f8c872e54be7989e68c0906f5c93 Mon Sep 17 00:00:00 2001 From: gaba Date: Thu, 13 Apr 2017 16:19:21 -0700 Subject: [PATCH 38/44] More translations in the configuration. --- client/coral-admin/src/translations.json | 8 ++++++++ .../components/CloseCommentsInfo.js | 16 +++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 5e465e527..911411523 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -69,6 +69,10 @@ }, "configure": { "closed-stream-settings": "Closed Stream Message", + "open-stream-configuration": "This comment stream is currently open. By closing this comment stream, no new comments may be submitted and all previous comments will still be displayed.", + "close-stream-configuration": "This comment stream is currently closed. By opening this comment stream, new comments may be submitted and displayed", + "close-stream": "Close Stream", + "open-stream": "Open Stream", "stream-settings": "Stream Settings", "moderation-settings": "Moderation Settings", "tech-settings": "Tech Settings", @@ -232,6 +236,10 @@ }, "configure": { "closed-stream-settings": "Mensaje a enviar cuando los comentarios están cerrados en el artículo", + "open-stream-configuration": "Este hilo de comentarios esta abierto. Al cerrarlo, ningún nuevo comentario será publicado y todos los comentarios anteriores serán mostrados.", + "close-stream-configuration": "Este hilo de comentario está en este momento cerrado. Al abrirlo, nuevos comentarios serán publicaods y mostrados.", + "close-stream": "Cerrar Comentarios", + "open-stream": "Abrir Comentarios", "stream-settings": "Configuración de Comentarios", "moderation-settings": "Configuración de Moderación", "tech-settings": "Configuración Técnica", diff --git a/client/coral-configure/components/CloseCommentsInfo.js b/client/coral-configure/components/CloseCommentsInfo.js index 627328f9b..fae7bc5e5 100644 --- a/client/coral-configure/components/CloseCommentsInfo.js +++ b/client/coral-configure/components/CloseCommentsInfo.js @@ -1,23 +1,25 @@ import React from 'react'; import {Button} from 'coral-ui'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations'; + +const lang = new I18n(translations); + export default ({status, onClick}) => ( status === 'open' ? (

    - This comment stream is currently open. By closing this comment stream, - no new comments may be submitted and all previous comments will still - be displayed. + {lang.t('configure.open-stream-configuration')}

    - +
    ) : (

    - This comment stream is currently closed. By opening this comment stream, - new comments may be submitted and displayed + {lang.t('configure.close-stream-configuration')}

    - +
    ) ); From 061e775227dd5d05df6f5b41c4a4cc2f1c7e28ae Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 14 Apr 2017 10:26:06 -0600 Subject: [PATCH 39/44] Spell checks + doc updates --- INSTALL.md | 12 ++++++------ PLUGINS.md | 16 +++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 1fec24865..a7ccf302d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -11,14 +11,14 @@ ## Installation From Docker We currently support packaging the Talk application via Docker, which automates -the dependancy install and asset build process. +the dependency install and asset build process. Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Docker Hub. Images are tagged using the following notation: - `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting - new features occationally and all the bug fixes, this is the tag for you. + new features occasionally and all the bug fixes, this is the tag for you. - `x.y` (where `y` is the minor version number): any patch updates will be included with this tag. If you like getting fixes and having features change only when you want, this is the tag for you. **(recommended)** @@ -85,7 +85,7 @@ for [Docker Links](https://docs.docker.com/compose/networking/#links). If you are interested in splitting apart services, you can simply adjust the command being executed in the container to optimize for your use case. An example would be if you wanted to run the API server and the job processor -on different machines. You can acheive this easily with docker compose: +on different machines. You can achieve this easily with docker compose: ```yaml version: '2' @@ -132,7 +132,7 @@ volumes: ``` Note that the only difference is in the `command` key. From this, you are able -to discretly control which modules are running in order to have the maximum +to discretely control which modules are running in order to have the maximum flexibility when managing your application. ### Running @@ -178,7 +178,7 @@ git clone https://github.com/coralproject/talk.git #### Setup -We now have to install the dependancies and build the static assets. +We now have to install the dependencies and build the static assets. ```bash # Install package dependancies @@ -189,7 +189,7 @@ yarn build ``` After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin -docs) file, you can re-run the following to install their dependancies: +docs) file, you can re-run the following to install their dependencies: ```bash # Reconcile plugins diff --git a/PLUGINS.md b/PLUGINS.md index 4339e9ab9..068a0886f 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -46,14 +46,20 @@ External plugins can be resolved by running: ./bin/cli plugins reconcile ``` -This will also traverse into local plugin folders and install their -dependancies. _Note that if the plugin is already installed and available in the -node_modules folder, it will not be fetched again unless there is a version -mismatch._ +This achieves two things: + +1. It will traverse into local plugin folders and install their dependencies. + _Note that if the plugin is already installed and available in the node_modules folder, it will not be + fetched again unless there is a version mismatch._ This will result in the + project `package.json` and `yarn.lock` files to be modified, this is normal as + this ensures that repeated deployments (with the same config) will have the + same config, these changes should not be committed to source control. +2. It will seek out dependencies that are listed in the object notation and try + to install them from npm. ## Plugin Dependencies -You may also include additional external depenancies in your local packages by +You may also include additional external dependencies in your local packages by specifying a `package.json` at your plugin root which will result in a `node_modules` folder being generated at the plugin root with your specific dependencies. From 9cf31e0bfa57d3c278d08801d7c7b72df76d6804 Mon Sep 17 00:00:00 2001 From: gaba Date: Fri, 14 Apr 2017 10:37:05 -0700 Subject: [PATCH 40/44] A note on the from/to parameters. --- graph/typeDefs.graphql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 3f61273d4..2c410ce3c 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -544,11 +544,11 @@ type RootQuery { users(query: UsersQuery): [User] # Asset metrics related to user actions are saturated into the assets - # returned. + # returned. Parameters `from` and `to` are related to the action created_at field. assetMetrics(from: Date!, to: Date!, sort: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!] # Comment metrics related to user actions are saturated into the comments - # returned. + # returned. Parameters `from` and `to` are related to the action created_at field. commentMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Comment!] } From a19fe02782f80da67da3fff6d99be95c2e5643b7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 14 Apr 2017 14:48:44 -0600 Subject: [PATCH 41/44] Added setup/usage docs --- INSTALL.md | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index a7ccf302d..49e78ee7d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,3 +1,11 @@ +## Contents + +- [Installation](#installation) - install the application on a machine + - [Via Docker](#installation-from-docker) + - [Via Source](#installation-from-source) +- [Setup](#setup) - setup the application for first use +- [Usage](#usage) - connect the application to a website + # Installation ## Requirements @@ -176,7 +184,7 @@ You can also clone the git repository via: git clone https://github.com/coralproject/talk.git ``` -#### Setup +#### Building We now have to install the dependencies and build the static assets. @@ -220,4 +228,38 @@ file under the `scripts` key including: - `yarn e2e` run end to end tests - `yarn build-watch` watch for changes to client files and build static assets - `yarn dev-start` watch for changes to server files and reload the server while - also sourcing a `.env` file in your local directory for configuration \ No newline at end of file + also sourcing a `.env` file in your local directory for configuration + +# Setup + +Once you've installed Talk (either via Docker or source), you still need to +setup the application. If you are unfamiliar with any terminoligy used in the +setup process, refer to the `TERMINOLOGY.md` document. + +## Via Web + +If you want to perform your setup via the web, you can navigate to your +installation of Talk at the path `/admin/install`. There you will be asked a +series of questions for your installation. + +## Via CLI + +If you want to perform your setup through the terminal, you can simply run: + +```bash +cli setup +``` + +And follow the instructions to perform initial setup and create your first user +account. + + +# Usage + +After setup is complete, you can then refer to the `/admin/configure` path to +get the embed code that you can copy/paste onto your blog or website in order to +start using Talk. + +_In order for the embed to work correctly, you will need to whitelist the domain +that is allowed to embed your site on the `/admin/configure` page, failure to do +so will result in the comment stream not loading._ From d4fc8a474d0cdca7ea47d513002f9b4207952b79 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 14 Apr 2017 14:57:13 -0600 Subject: [PATCH 42/44] Should resolve bug relating to usernames being set as "undefined" --- services/users.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/services/users.js b/services/users.js index de9678089..14301ccf4 100644 --- a/services/users.js +++ b/services/users.js @@ -254,26 +254,28 @@ module.exports = class UsersService { * @param {Boolean} checkAgainstWordlist enables cheching against the wordlist * @return {Promise} */ - static isValidUsername(username, checkAgainstWordlist = true) { + static async isValidUsername(username, checkAgainstWordlist = true) { const onlyLettersNumbersUnderscore = /^[A-Za-z0-9_]+$/; if (!username) { - return Promise.reject(errors.ErrMissingUsername); + throw errors.ErrMissingUsername; } if (!onlyLettersNumbersUnderscore.test(username)) { - - return Promise.reject(errors.ErrSpecialChars); + throw errors.ErrSpecialChars; } if (checkAgainstWordlist) { // check for profanity - console.log('Username profanity check disabled: ', Wordlist.usernameCheck(username)); + let err = await Wordlist.usernameCheck(username); + if (err) { + throw err; + } } // No errors found! - return Promise.resolve(username); + return username; } /** From e028e30afdcf3261d56debb0a8f98d370be5cf17 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 14 Apr 2017 15:04:35 -0600 Subject: [PATCH 43/44] Added line about SSL certificates --- INSTALL.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 49e78ee7d..57ef3712d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -15,6 +15,11 @@ - 5GB storage (minimum) - [MongoDB](https://www.mongodb.com/) v3.4 or later - [Redis](https://redis.io/) v3.2 or later +- SSL Certificate + - This application assumes that you will be serving this application in a + production environment, and therefore requires that you serve it behind a + webserver with a valid SSL certificate. This is chosen in order to secure + user's sessions. ## Installation From Docker From bee27da7255532ee9f34b2f59218968307c6b7da Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 14 Apr 2017 15:16:42 -0600 Subject: [PATCH 44/44] Added note for production based deploy --- INSTALL.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 57ef3712d..18244f635 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -24,7 +24,8 @@ ## Installation From Docker We currently support packaging the Talk application via Docker, which automates -the dependency install and asset build process. +the dependency install and asset build process. This is the recommended way to +deploy the application when used in production. Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Docker Hub. @@ -165,6 +166,10 @@ docker run -d -P coralproject/talk:latest ## Installation From Source +This provides information on how to setup the application from source. Note that +this is not recommended for production deploys, but will work for development +and testing purposes. + ### Requirements There are some runtime requirements for running Talk from source: