From ee78f085986126ba243697452a7f632a196f53b1 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 17 Jan 2018 15:20:38 +0100 Subject: [PATCH] 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;