From 1fc07e8649cf9b3e72dc284000fcaffa365733a3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 24 Jan 2018 23:31:31 +0100 Subject: [PATCH] Implement activity tracking for flagged accounts --- client/coral-admin/src/actions/community.js | 6 + client/coral-admin/src/components/Header.js | 6 +- client/coral-admin/src/constants/community.js | 2 + client/coral-admin/src/containers/Header.js | 7 +- client/coral-admin/src/graphql/index.js | 16 +- client/coral-admin/src/reducers/community.js | 10 ++ .../Community/containers/FlaggedAccounts.js | 21 ++- .../routes/Community/containers/Indicator.js | 152 +++++++++++++++++- .../src/routes/Community/graphql.js | 24 ++- 9 files changed, 217 insertions(+), 27 deletions(-) diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index d2b8f1a3f..82a74daf3 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -11,6 +11,7 @@ import { HIDE_BANUSER_DIALOG, SHOW_REJECT_USERNAME_DIALOG, HIDE_REJECT_USERNAME_DIALOG, + SET_INDICATOR_TRACK, } from '../constants/community'; import t from 'coral-framework/services/i18n'; @@ -68,3 +69,8 @@ export const showRejectUsernameDialog = user => ({ export const hideRejectUsernameDialog = () => ({ type: HIDE_REJECT_USERNAME_DIALOG, }); + +export const setIndicatorTrack = track => ({ + type: SET_INDICATOR_TRACK, + track, +}); diff --git a/client/coral-admin/src/components/Header.js b/client/coral-admin/src/components/Header.js index e6bd73f52..6b2274d27 100644 --- a/client/coral-admin/src/components/Header.js +++ b/client/coral-admin/src/components/Header.js @@ -15,6 +15,7 @@ const CoralHeader = ({ showShortcuts = () => {}, auth, root, + data, }) => { return (
@@ -31,7 +32,7 @@ const CoralHeader = ({ activeClassName={styles.active} > {t('configure.moderate')} - + )} {t('configure.community')} - + {can(auth.user, 'UPDATE_CONFIG') && ( @@ -119,6 +120,7 @@ CoralHeader.propTypes = { showShortcuts: PropTypes.func, handleLogout: PropTypes.func.isRequired, root: PropTypes.object.isRequired, + data: PropTypes.object.isRequired, }; export default CoralHeader; diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index a9cc2f1e3..1ffe3f8d6 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -18,3 +18,5 @@ export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALO export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`; export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`; + +export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`; diff --git a/client/coral-admin/src/containers/Header.js b/client/coral-admin/src/containers/Header.js index dae096517..dd16c17c6 100644 --- a/client/coral-admin/src/containers/Header.js +++ b/client/coral-admin/src/containers/Header.js @@ -13,10 +13,5 @@ export default withQuery( } ${ModerationIndicator.fragments.root} ${CommunityIndicator.fragments.root} - `, - { - options: { - pollInterval: 10000, - }, - } + ` )(Header); diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 3a24702cd..95595c265 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -173,13 +173,17 @@ export default { }, updateQueries: { TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => { + const decrement = { + flaggedUsernamesCount: { $apply: count => count - 1 }, + }; + // Remove from list after the mutation was "really" completed. if (get(mutationResult, 'data.approveUsername.isOptimistic')) { - return prev; + return update(prev, decrement); } const updated = update(prev, { - flaggedUsernamesCount: { $apply: count => count - 1 }, + ...decrement, flaggedUsers: { nodes: { $apply: nodes => nodes.filter(node => node.id !== id) }, }, @@ -227,13 +231,17 @@ export default { }, updateQueries: { TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => { + const decrement = { + flaggedUsernamesCount: { $apply: count => count - 1 }, + }; + // Remove from list after the mutation was "really" completed. if (get(mutationResult, 'data.rejectUsername.isOptimistic')) { - return prev; + return update(prev, decrement); } const updated = update(prev, { - flaggedUsernamesCount: { $apply: count => count - 1 }, + ...decrement, flaggedUsers: { nodes: { $apply: nodes => nodes.filter(node => node.id !== id), diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 9fe17ad8f..68e471935 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -9,6 +9,7 @@ import { HIDE_BANUSER_DIALOG, SHOW_REJECT_USERNAME_DIALOG, HIDE_REJECT_USERNAME_DIALOG, + SET_INDICATOR_TRACK, } from '../constants/community'; const initialState = { @@ -24,6 +25,10 @@ const initialState = { user: {}, banDialog: false, rejectUsernameDialog: false, + // If true the activity indicator will track flagged account changes + // in order to determine the current queue count. Set this to false + // if the queue count is determined by other means. + indicatorTrack: true, }; export default function community(state = initialState, action) { @@ -91,6 +96,11 @@ export default function community(state = initialState, action) { ...state, searchValue: action.value, }; + case SET_INDICATOR_TRACK: + return { + ...state, + indicatorTrack: action.track, + }; default: return state; } diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js index 09240b8f3..f5fd36263 100644 --- a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js @@ -6,12 +6,15 @@ import withQuery from 'coral-framework/hocs/withQuery'; import { Spinner } from 'coral-ui'; import PropTypes from 'prop-types'; import { withApproveUsername } from 'coral-framework/graphql/mutations'; -import { showRejectUsernameDialog } from '../../../actions/community'; +import { + showRejectUsernameDialog, + setIndicatorTrack, +} from '../../../actions/community'; 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 { handleFlaggedUsernameChange } from '../graphql'; +import { handleFlaggedAccountsChange } from '../graphql'; import { notify } from 'coral-framework/actions/notification'; import { isFlaggedUserDangling } from '../utils'; import t from 'coral-framework/services/i18n'; @@ -50,7 +53,7 @@ class FlaggedAccountsContainer extends Component { prev, { subscriptionData: { data: { usernameFlagged: user } } } ) => { - return handleFlaggedUsernameChange(prev, user, () => { + return handleFlaggedAccountsChange(prev, user, () => { const msg = t( 'flagged_usernames.notify_flagged', whoFlagged(user), @@ -66,7 +69,7 @@ class FlaggedAccountsContainer extends Component { prev, { subscriptionData: { data: { usernameApproved: user } } } ) => { - return handleFlaggedUsernameChange(prev, user, () => { + return handleFlaggedAccountsChange(prev, user, () => { const msg = t( 'flagged_usernames.notify_approved', whoChangedTheStatus(user.state.status.username), @@ -82,7 +85,7 @@ class FlaggedAccountsContainer extends Component { prev, { subscriptionData: { data: { usernameRejected: user } } } ) => { - return handleFlaggedUsernameChange(prev, user, () => { + return handleFlaggedAccountsChange(prev, user, () => { const msg = t( 'flagged_usernames.notify_rejected', whoChangedTheStatus(user.state.status.username), @@ -102,7 +105,7 @@ class FlaggedAccountsContainer extends Component { }, } ) => { - return handleFlaggedUsernameChange(prev, user, () => { + return handleFlaggedAccountsChange(prev, user, () => { const msg = t( 'flagged_usernames.notify_changed', previousUsername, @@ -125,10 +128,14 @@ class FlaggedAccountsContainer extends Component { } componentWillMount() { + // Stop activity indicator tracking, as we'll handle it here. + this.props.setIndicatorTrack(false); this.subscribeToUpdates(); } componentWillUnmount() { + // Restart activity indicator tracking. + this.props.setIndicatorTrack(true); this.unsubscribe(); } @@ -197,6 +204,7 @@ FlaggedAccountsContainer.propTypes = { approveUsername: PropTypes.func, data: PropTypes.object, root: PropTypes.object, + setIndicatorTrack: PropTypes.func, }; const LOAD_MORE_QUERY = gql` @@ -288,6 +296,7 @@ const mapDispatchToProps = dispatch => showRejectUsernameDialog, viewUserDetail, notify, + setIndicatorTrack, }, dispatch ); diff --git a/client/coral-admin/src/routes/Community/containers/Indicator.js b/client/coral-admin/src/routes/Community/containers/Indicator.js index 04a351fb6..eeb5e20f7 100644 --- a/client/coral-admin/src/routes/Community/containers/Indicator.js +++ b/client/coral-admin/src/routes/Community/containers/Indicator.js @@ -1,24 +1,164 @@ +import React, { Component } from 'react'; import { compose, gql } from 'react-apollo'; import Indicator from '../../../components/Indicator'; import { withFragments } from 'plugin-api/beta/client/hocs'; -import { branch, renderNothing } from 'recompose'; +import { handleIndicatorChange } from '../graphql'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; -const hideIfNoData = hasNoData => branch(hasNoData, renderNothing); +class IndicatorContainer extends Component { + subscriptions = []; + + subscribeToUpdates() { + const parameters = [ + { + document: USERNAME_FLAGGED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameFlagged: user } } } + ) => { + return handleIndicatorChange(prev, user); + }, + }, + { + document: USERNAME_APPROVED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameApproved: user } } } + ) => { + return handleIndicatorChange(prev, user); + }, + }, + { + document: USERNAME_REJECTED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameRejected: user } } } + ) => { + return handleIndicatorChange(prev, user); + }, + }, + { + document: USERNAME_CHANGED_SUBSCRIPTION, + updateQuery: ( + prev, + { subscriptionData: { data: { usernameChanged: { user } } } } + ) => { + return handleIndicatorChange(prev, user); + }, + }, + ]; + + this.subscriptions = parameters.map(param => + this.props.data.subscribeToMore(param) + ); + } + + unsubscribe() { + this.subscriptions.forEach(unsubscribe => unsubscribe()); + this.subscriptions = []; + } + + componentWillMount() { + if (this.props.track) { + this.subscribeToUpdates(); + } + } + + componentWillUnmount() { + this.unsubscribe(); + } + + componentWillReceiveProps(nextProps) { + if (!this.props.track && nextProps.track) { + this.subscribeToUpdates(); + } + if (this.props.track && !nextProps.track) { + this.unsubscribe(); + } + } + + render() { + if (!this.props.root || !this.props.root.flaggedUsernamesCount) { + return null; + } + + return ; + } +} + +IndicatorContainer.propTypes = { + data: PropTypes.object, + root: PropTypes.object, + track: PropTypes.bool, +}; + +const fields = ` + state { + status { + username { + status + } + } + } +`; + +const USERNAME_FLAGGED_SUBSCRIPTION = gql` + subscription TalkAdmin_CommunityIndicator_UsernameFlagged { + usernameFlagged { + ${fields} + } + } +`; + +const USERNAME_APPROVED_SUBSCRIPTION = gql` + subscription TalkAdmin_ComunityIndicator_UsernameApproved { + usernameApproved { + ${fields} + } + } +`; + +const USERNAME_REJECTED_SUBSCRIPTION = gql` + subscription TalkAdmin_CommunityIndicator_UsernameRejected { + usernameRejected { + ${fields} + } + } +`; + +const USERNAME_CHANGED_SUBSCRIPTION = gql` + subscription TalkAdmin_ComunityIndicator_UsernameChanged { + usernameChanged { + previousUsername + user { + ${fields} + } + } + } +`; + +const mapStateToProps = state => ({ + track: state.community.indicatorTrack, +}); const enhance = compose( + connect(mapStateToProps), withFragments({ root: gql` - fragment TalkAdmin_Community_Indicator_root on RootQuery { + fragment TalkAdmin_CommunityIndicator_root on RootQuery { flaggedUsernamesCount: userCount( query: { action_type: FLAG state: { status: { username: [SET, CHANGED] } } } ) + me { + id + } } `, - }), - hideIfNoData(props => !props.root.flaggedUsernamesCount) + }) ); -export default enhance(Indicator); +export default enhance(IndicatorContainer); diff --git a/client/coral-admin/src/routes/Community/graphql.js b/client/coral-admin/src/routes/Community/graphql.js index af10c8e86..8d6941c41 100644 --- a/client/coral-admin/src/routes/Community/graphql.js +++ b/client/coral-admin/src/routes/Community/graphql.js @@ -49,13 +49,13 @@ function decrementFlaggedUserCount(root) { } /** - * Assimilate flagged user changes into current store. + * Assimilate flagged acount 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 handleFlaggedUsernameChange(root, user, notify) { +export function handleFlaggedAccountsChange(root, user, notify) { if (user.state.status.username.status !== 'SET') { // Check if change came from current user, if so ignore it. const lastChange = @@ -87,7 +87,7 @@ export function handleFlaggedUsernameChange(root, user, notify) { break; case 'APPROVED': case 'REJECTED': - return root; + return decrementFlaggedUserCount(root); default: } } @@ -105,3 +105,21 @@ export function handleFlaggedUsernameChange(root, user, notify) { } } } + +/** + * Track indicator status + * @param {Object} root current state of the store + * @param {Object} user user that was changed + * @return {Object} next state of the store + */ +export function handleIndicatorChange(root, user) { + switch (user.state.status.username.status) { + case 'SET': + case 'CHANGED': + return incrementFlaggedUserCount(root); + case 'APPROVED': + case 'REJECTED': + return decrementFlaggedUserCount(root); + default: + } +}