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;