From 6aece16fd01f59b9516a6b32e88c7a330c2ad7f1 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 16 Jan 2018 11:31:44 +0100 Subject: [PATCH 01/19] Add userFlagged subscription --- graph/mutators/action.js | 18 ++++++++++++++---- graph/resolvers/subscription.js | 3 +++ graph/setupFunctions.js | 11 +++++++++++ graph/typeDefs.graphql | 5 +++++ perms/constants/subscription.js | 1 + perms/reducers/subscription.js | 1 + 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/graph/mutators/action.js b/graph/mutators/action.js index 13ccfda6b..58e215916 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -73,10 +73,20 @@ const createAction = async ( metadata, }); - if (action_type === 'FLAG' && item_type === 'COMMENTS') { - // The item is a comment, and this is a flag. Push that the comment was - // flagged, don't wait for it to finish. - pubsub.publish('commentFlagged', item); + if (action_type === 'FLAG') { + switch (item_type) { + case 'COMMENTS': + // The item is a comment, and this is a flag. Push that the comment was + // flagged, don't wait for it to finish. + pubsub.publish('commentFlagged', item); + break; + case 'USERS': + // The item is a user, and this is a flag. Push that the user was + // flagged, don't wait for it to finish. + pubsub.publish('userFlagged', item); + break; + default: + } } return action; diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js index 8be3236c1..17b8b0075 100644 --- a/graph/resolvers/subscription.js +++ b/graph/resolvers/subscription.js @@ -26,6 +26,9 @@ const Subscription = { usernameRejected(user) { return user; }, + usernameFlagged(user) { + return user; + }, }; module.exports = Subscription; diff --git a/graph/setupFunctions.js b/graph/setupFunctions.js index e25b770dd..a1e0d1145 100644 --- a/graph/setupFunctions.js +++ b/graph/setupFunctions.js @@ -9,6 +9,7 @@ const { SUBSCRIBE_ALL_USER_BANNED, SUBSCRIBE_ALL_USERNAME_REJECTED, SUBSCRIBE_ALL_USERNAME_APPROVED, + SUBSCRIBE_ALL_USERNAME_FLAGGED, } = require('../perms/constants'); const merge = require('lodash/merge'); @@ -97,6 +98,16 @@ const setupFunctions = { } return !args.user_id || user.id === args.user_id; }, + usernameFlagged: (options, args, user, context) => { + if ( + !context.user || + (args.user_id !== user.id && + !context.user.can(SUBSCRIBE_ALL_USERNAME_FLAGGED)) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + }, usernameRejected: (options, args, user, context) => { if ( !context.user || diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 002318bc3..e94c8d8de 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1506,6 +1506,11 @@ type Subscription { # users with the `ADMIN` or `MODERATOR` role. userBanned(user_id: ID): User + # Get an update whenever a username was flagged. + # `user_id` must match id of current user except for + # users with the `ADMIN` or `MODERATOR` role. + usernameFlagged(user_id: ID): User + # Get an update whenever a username has been rejected. # `user_id` must match id of current user except for # users with the `ADMIN` or `MODERATOR` role. diff --git a/perms/constants/subscription.js b/perms/constants/subscription.js index 22dbd2fa3..9d87c07eb 100644 --- a/perms/constants/subscription.js +++ b/perms/constants/subscription.js @@ -9,4 +9,5 @@ module.exports = { SUBSCRIBE_ALL_USER_BANNED: 'SUBSCRIBE_ALL_USER_BANNED', SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED', SUBSCRIBE_ALL_USERNAME_APPROVED: 'SUBSCRIBE_ALL_USERNAME_APPROVED', + SUBSCRIBE_ALL_USERNAME_FLAGGED: 'SUBSCRIBE_ALL_USERNAME_FLAGGED', }; diff --git a/perms/reducers/subscription.js b/perms/reducers/subscription.js index 3ae7d1cf8..4f76c8264 100644 --- a/perms/reducers/subscription.js +++ b/perms/reducers/subscription.js @@ -13,6 +13,7 @@ module.exports = (user, perm) => { case types.SUBSCRIBE_ALL_USER_BANNED: case types.SUBSCRIBE_ALL_USERNAME_REJECTED: case types.SUBSCRIBE_ALL_USERNAME_APPROVED: + case types.SUBSCRIBE_ALL_USERNAME_FLAGGED: return check(user, ['ADMIN', 'MODERATOR']); default: break; From 519480c0e469545dd161a8bf7f4acd8999bcc4ef Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 17 Jan 2018 15:12:17 +0100 Subject: [PATCH 02/19] Fix counts not decreasing on mutations --- client/coral-admin/src/graphql/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 66dd21836..5aa355573 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -166,6 +166,7 @@ export default { updateQueries: { TalkAdmin_Community: prev => { const updated = update(prev, { + flaggedUsernamesCount: { $apply: count => count - 1 }, flaggedUsers: { nodes: { $apply: nodes => nodes.filter(node => node.id !== id) }, }, @@ -178,6 +179,7 @@ export default { updateQueries: { TalkAdmin_Community: prev => { const updated = update(prev, { + flaggedUsernamesCount: { $apply: count => count - 1 }, flaggedUsers: { nodes: { $apply: nodes => nodes.filter(node => node.id !== userId), From ee78f085986126ba243697452a7f632a196f53b1 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 17 Jan 2018 15:20:38 +0100 Subject: [PATCH 03/19] Implement live update for reported users --- .../Community/components/FlaggedAccounts.js | 4 +- .../Community/components/FlaggedUser.js | 2 + .../Community/containers/FlaggedAccounts.js | 122 ++++++++++++++++++ .../Community/containers/FlaggedUser.js | 1 + .../src/routes/Community/graphql.js | 91 +++++++++++++ graph/mutators/action.js | 2 +- graph/mutators/user.js | 4 +- graph/resolvers/subscription.js | 6 + graph/setupFunctions.js | 11 ++ graph/typeDefs.graphql | 6 +- perms/constants/subscription.js | 1 + perms/reducers/subscription.js | 1 + 12 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 client/coral-admin/src/routes/Community/graphql.js diff --git a/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js index 7fe858bb9..770cf8300 100644 --- a/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/components/FlaggedAccounts.js @@ -17,6 +17,7 @@ class FlaggedAccounts extends React.Component { approveUser, me, viewUserDetail, + hasMore, } = this.props; const hasResults = users.nodes && !!users.nodes.length; @@ -60,7 +61,7 @@ class FlaggedAccounts extends React.Component { ) : ( {t('community.no_flagged_accounts')} )} - + ); @@ -70,6 +71,7 @@ class FlaggedAccounts extends React.Component { FlaggedAccounts.propTypes = { users: PropTypes.object, loadMore: PropTypes.func, + hasMore: PropTypes.bool, showRejectUsernameDialog: PropTypes.func, approveUser: PropTypes.func, me: PropTypes.object, diff --git a/client/coral-admin/src/routes/Community/components/FlaggedUser.js b/client/coral-admin/src/routes/Community/components/FlaggedUser.js index 3b0e1fa6e..4a892ea1f 100644 --- a/client/coral-admin/src/routes/Community/components/FlaggedUser.js +++ b/client/coral-admin/src/routes/Community/components/FlaggedUser.js @@ -110,10 +110,12 @@ class User extends React.Component {
diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js index 669f30090..a7541f6a4 100644 --- a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js @@ -11,15 +11,88 @@ import { viewUserDetail } from '../../../actions/userDetail'; import { getDefinitionName } from 'coral-framework/utils'; import { appendNewNodes } from 'plugin-api/beta/client/utils'; import update from 'immutability-helper'; +import { handleFlaggedUserChange } from '../graphql'; +import { notify } from 'coral-framework/actions/notification'; import FlaggedAccounts from '../components/FlaggedAccounts'; import FlaggedUser from '../containers/FlaggedUser'; class FlaggedAccountsContainer extends Component { + subscriptions = []; + constructor(props) { super(props); } + subscribeToUpdates() { + const parameters = [ + { + document: USERNAME_FLAGGED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameFlagged: user } } } + ) => { + return handleFlaggedUserChange(prev, user, () => { + this.props.notify('info', `user ${user.username} flagged`); + }); + }, + }, + { + document: USERNAME_APPROVED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameApproved: user } } } + ) => { + console.log(user); + return handleFlaggedUserChange(prev, user, () => { + this.props.notify('info', `user ${user.username} approved`); + }); + }, + }, + { + document: USERNAME_REJECTED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameRejected: user } } } + ) => { + console.log(user); + return handleFlaggedUserChange(prev, user, () => { + this.props.notify('info', `user ${user.username} rejected`); + }); + }, + }, + { + document: USERNAME_CHANGED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameChanged: user } } } + ) => { + console.log(user); + return handleFlaggedUserChange(prev, user, () => { + this.props.notify('info', `user ${user.username} changed`); + }); + }, + }, + ]; + + this.subscriptions = parameters.map(param => + this.props.data.subscribeToMore(param) + ); + } + + unsubscribe() { + this.subscriptions.forEach(unsubscribe => unsubscribe()); + this.subscriptions = []; + } + + componentWillMount() { + this.subscribeToUpdates(); + } + + componentWillUnmount() { + this.unsubscribe(); + } + approveUser = ({ userId: id }) => { return this.props.approveUsername(id); }; @@ -68,6 +141,10 @@ class FlaggedAccountsContainer extends Component { data={this.props.data} root={this.props.root} users={this.props.root.flaggedUsers} + hasMore={ + this.props.root.flaggedUsers.nodes.length < + this.props.root.flaggedUsernamesCount + } me={this.props.root.me} /> ); @@ -77,6 +154,8 @@ class FlaggedAccountsContainer extends Component { FlaggedAccountsContainer.propTypes = { showRejectUsernameDialog: PropTypes.func, viewUserDetail: PropTypes.func, + notify: PropTypes.func, + flaggedUsernamesCount: PropTypes.number, approveUsername: PropTypes.func, data: PropTypes.object, root: PropTypes.object, @@ -105,11 +184,48 @@ const LOAD_MORE_QUERY = gql` ${FlaggedUser.fragments.user} `; +const USERNAME_FLAGGED_SUBSCRIPTION = gql` + subscription TalkAdmin_UsernameFlagged { + usernameFlagged { + ...${getDefinitionName(FlaggedUser.fragments.user)} + } + } + ${FlaggedUser.fragments.user} +`; + +const USERNAME_APPROVED_SUBSCRIPTION = gql` + subscription TalkAdmin_UsernameApproved { + usernameApproved { + ...${getDefinitionName(FlaggedUser.fragments.user)} + } + } + ${FlaggedUser.fragments.user} +`; + +const USERNAME_REJECTED_SUBSCRIPTION = gql` + subscription TalkAdmin_UsernameRejected { + usernameRejected { + ...${getDefinitionName(FlaggedUser.fragments.user)} + } + } + ${FlaggedUser.fragments.user} +`; + +const USERNAME_CHANGED_SUBSCRIPTION = gql` + subscription TalkAdmin_UsernameChanged { + usernameChanged { + ...${getDefinitionName(FlaggedUser.fragments.user)} + } + } + ${FlaggedUser.fragments.user} +`; + const mapDispatchToProps = dispatch => bindActionCreators( { showRejectUsernameDialog, viewUserDetail, + notify, }, dispatch ); @@ -120,6 +236,12 @@ export default compose( withFragments({ root: gql` fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery { + flaggedUsernamesCount: userCount( + query: { + action_type: FLAG + state: { status: { username: [SET, CHANGED] } } + } + ) flaggedUsers: users(query:{ action_type: FLAG, state: { diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedUser.js b/client/coral-admin/src/routes/Community/containers/FlaggedUser.js index 3a30577d4..cf6075796 100644 --- a/client/coral-admin/src/routes/Community/containers/FlaggedUser.js +++ b/client/coral-admin/src/routes/Community/containers/FlaggedUser.js @@ -17,6 +17,7 @@ export default withFragments({ fragment TalkAdminCommunity_FlaggedUser_user on User { id username + created_at state { status { username { diff --git a/client/coral-admin/src/routes/Community/graphql.js b/client/coral-admin/src/routes/Community/graphql.js new file mode 100644 index 000000000..c87a88263 --- /dev/null +++ b/client/coral-admin/src/routes/Community/graphql.js @@ -0,0 +1,91 @@ +import update from 'immutability-helper'; + +function shouldAddFlaggedUser(root, user) { + const isEmpty = !root.flaggedUsers.nodes.length; + if (isEmpty) { + return true; + } + + if (hasFlaggedUser(root, user)) { + return false; + } + + const oldest = root.flaggedUsers.nodes.reduce((cur, node) => { + const createdAt = new Date(node.created_at); + return createdAt < cur ? createdAt : cur; + }, new Date()); + + return new Date(user.created_at) >= oldest; +} + +function hasFlaggedUser(root, user) { + return root.flaggedUsers.nodes.find(u => u.id === user.id); +} + +function applyUserChanges(root, user) { + const index = root.flaggedUsers.nodes.findIndex(({ id }) => id === user.id); + if (index > -1) { + return update(root, { + flaggedUsers: { + nodes: { + [index]: { $merge: user }, + }, + }, + }); + } + return root; +} + +function incrementFlaggedUserCount(root) { + return update(root, { + flaggedUsernamesCount: { $apply: count => count + 1 }, + }); +} + +/** + * Assimilate flagged user changes into current store. + * @param {Object} root current state of the store + * @param {Object} user user that was changed + * @param {function} notify callback to show notification + * @return {Object} next state of the store + */ +export function handleFlaggedUserChange(root, user, notify) { + if (!hasFlaggedUser(root, user)) { + switch (user.state.status.username.status) { + case 'SET': + case 'CHANGED': + root = incrementFlaggedUserCount(root); + + if (!shouldAddFlaggedUser(root, user)) { + return root; + } + + notify(); + + return update(root, { + flaggedUsernamesCount: { $apply: count => count + 1 }, + flaggedUsers: { + nodes: { $push: [user] }, + }, + }); + break; + case 'APPROVED': + case 'REJECTED': + return root; + default: + } + } + + if (hasFlaggedUser(root, user)) { + switch (user.state.status.username.status) { + case 'SET': + case 'CHANGED': + return root; + case 'APPROVED': + case 'REJECTED': + notify(); + return applyUserChanges(root, user); + default: + } + } +} diff --git a/graph/mutators/action.js b/graph/mutators/action.js index 58e215916..20f9db4fe 100644 --- a/graph/mutators/action.js +++ b/graph/mutators/action.js @@ -83,7 +83,7 @@ const createAction = async ( case 'USERS': // The item is a user, and this is a flag. Push that the user was // flagged, don't wait for it to finish. - pubsub.publish('userFlagged', item); + pubsub.publish('usernameFlagged', item); break; default: } diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 5e7df2db8..81340e1ee 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -56,7 +56,9 @@ const stopIgnoringUser = ({ user }, userToStopIgnoring) => { }; const changeUsername = async (ctx, id, username) => { - return UsersService.changeUsername(id, username, ctx.user.id); + const user = await UsersService.changeUsername(id, username, ctx.user.id); + ctx.pubsub.publish('usernameChanged', user); + return user; }; const setUsername = async (ctx, id, username) => { diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js index 17b8b0075..114a395a3 100644 --- a/graph/resolvers/subscription.js +++ b/graph/resolvers/subscription.js @@ -23,12 +23,18 @@ const Subscription = { userSuspended(user) { return user; }, + usernameApproved(user) { + return user; + }, usernameRejected(user) { return user; }, usernameFlagged(user) { return user; }, + usernameChanged(user) { + return user; + }, }; module.exports = Subscription; diff --git a/graph/setupFunctions.js b/graph/setupFunctions.js index a1e0d1145..cdc03fda3 100644 --- a/graph/setupFunctions.js +++ b/graph/setupFunctions.js @@ -10,6 +10,7 @@ const { SUBSCRIBE_ALL_USERNAME_REJECTED, SUBSCRIBE_ALL_USERNAME_APPROVED, SUBSCRIBE_ALL_USERNAME_FLAGGED, + SUBSCRIBE_ALL_USERNAME_CHANGED, } = require('../perms/constants'); const merge = require('lodash/merge'); @@ -128,6 +129,16 @@ const setupFunctions = { } return !args.user_id || user.id === args.user_id; }, + usernameChanged: (options, args, user, context) => { + if ( + !context.user || + (args.user_id !== user.id && + !context.user.can(SUBSCRIBE_ALL_USERNAME_CHANGED)) + ) { + return false; + } + return !args.user_id || user.id === args.user_id; + }, }; /** diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index e94c8d8de..106fc27e6 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -1516,9 +1516,13 @@ type Subscription { # users with the `ADMIN` or `MODERATOR` role. usernameRejected(user_id: ID): User - # Gen an update whenever a username has been approved. `user_id` must match id + # Get an update whenever a username has been approved. `user_id` must match id # of current user except for users with the `ADMIN` or `MODERATOR` role. usernameApproved(user_id: ID): User + + # Get an update whenever a username has been changed. `user_id` must match id + # of current user except for users with the `ADMIN` or `MODERATOR` role. + usernameChanged(user_id: ID): User } ################################################################################ diff --git a/perms/constants/subscription.js b/perms/constants/subscription.js index 9d87c07eb..e27730396 100644 --- a/perms/constants/subscription.js +++ b/perms/constants/subscription.js @@ -10,4 +10,5 @@ module.exports = { SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED', SUBSCRIBE_ALL_USERNAME_APPROVED: 'SUBSCRIBE_ALL_USERNAME_APPROVED', SUBSCRIBE_ALL_USERNAME_FLAGGED: 'SUBSCRIBE_ALL_USERNAME_FLAGGED', + SUBSCRIBE_ALL_USERNAME_CHANGED: 'SUBSCRIBE_ALL_USERNAME_CHANGED', }; diff --git a/perms/reducers/subscription.js b/perms/reducers/subscription.js index 4f76c8264..44fb43af6 100644 --- a/perms/reducers/subscription.js +++ b/perms/reducers/subscription.js @@ -14,6 +14,7 @@ module.exports = (user, perm) => { case types.SUBSCRIBE_ALL_USERNAME_REJECTED: case types.SUBSCRIBE_ALL_USERNAME_APPROVED: case types.SUBSCRIBE_ALL_USERNAME_FLAGGED: + case types.SUBSCRIBE_ALL_USERNAME_CHANGED: return check(user, ['ADMIN', 'MODERATOR']); default: break; From f282aa47c98293b0e8367adce7fd38f2172cc8a1 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 17 Jan 2018 17:31:48 +0100 Subject: [PATCH 04/19] Implement dangling --- client/coral-admin/src/components/ApproveButton.css | 6 +++++- client/coral-admin/src/components/ApproveButton.js | 4 +++- client/coral-admin/src/components/RejectButton.css | 6 +++++- client/coral-admin/src/components/RejectButton.js | 4 +++- .../src/routes/Community/components/FlaggedUser.css | 6 +++++- .../src/routes/Community/components/FlaggedUser.js | 5 +++++ .../src/routes/Community/containers/FlaggedAccounts.js | 10 ++++++++-- client/coral-admin/src/routes/Community/graphql.js | 8 +++++++- 8 files changed, 41 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/components/ApproveButton.css b/client/coral-admin/src/components/ApproveButton.css index 4c42cc233..071be2fcb 100644 --- a/client/coral-admin/src/components/ApproveButton.css +++ b/client/coral-admin/src/components/ApproveButton.css @@ -16,12 +16,16 @@ transform: scale(.8); margin: 0; - &:hover { + &:not(:disabled):hover { box-shadow: none; color: white; background-color: #519954; cursor: pointer; } + + &:disabled { + cursor: not-allowed; + } } .active { diff --git a/client/coral-admin/src/components/ApproveButton.js b/client/coral-admin/src/components/ApproveButton.js index 97a674221..884ac3da5 100644 --- a/client/coral-admin/src/components/ApproveButton.js +++ b/client/coral-admin/src/components/ApproveButton.js @@ -7,7 +7,7 @@ import { Icon } from 'coral-ui'; import t from 'coral-framework/services/i18n'; -const ApproveButton = ({ active, minimal, onClick, className }) => { +const ApproveButton = ({ active, minimal, onClick, className, disabled }) => { const text = active ? t('modqueue.approved') : t('modqueue.approve'); return (