From 2dc882a12ec58deac3881841abf0de47b08b919d Mon Sep 17 00:00:00 2001 From: Benjamin Goering Date: Tue, 4 Apr 2017 14:56:23 -0700 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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.",