diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js
index d2b8f1a3f..9a0452ea1 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,9 @@ export const showRejectUsernameDialog = user => ({
export const hideRejectUsernameDialog = () => ({
type: HIDE_REJECT_USERNAME_DIALOG,
});
+
+// Enable or disable the activity indicator subscriptions.
+export const setIndicatorTrack = track => ({
+ type: SET_INDICATOR_TRACK,
+ track,
+});
diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js
index cd09b04b2..7899b7685 100644
--- a/client/coral-admin/src/actions/moderation.js
+++ b/client/coral-admin/src/actions/moderation.js
@@ -31,10 +31,16 @@ export const storySearchChange = value => ({
});
export const clearState = () => ({
- type: actions.MODERATION_CLEAR_STATE,
+ type: actions.CLEAR_STATE,
});
export const selectCommentId = id => ({
- type: actions.MODERATION_SELECT_COMMENT,
+ type: actions.SELECT_COMMENT,
id,
});
+
+// Enable or disable the activity indicator subscriptions.
+export const setIndicatorTrack = track => ({
+ type: actions.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/constants/moderation.js b/client/coral-admin/src/constants/moderation.js
index 8eb939d76..c233692dc 100644
--- a/client/coral-admin/src/constants/moderation.js
+++ b/client/coral-admin/src/constants/moderation.js
@@ -1,9 +1,12 @@
-export const TOGGLE_MODAL = 'TOGGLE_MODAL';
-export const SINGLE_VIEW = 'SINGLE_VIEW';
-export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
-export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
-export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
-export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
-export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
-export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
-export const MODERATION_SELECT_COMMENT = 'MODERATION_SELECT_COMMENT';
+const prefix = `MODERATION`;
+
+export const TOGGLE_MODAL = `${prefix}_TOGGLE_MODAL`;
+export const SINGLE_VIEW = `${prefix}_SINGLE_VIEW`;
+export const HIDE_SHORTCUTS_NOTE = `${prefix}_HIDE_SHORTCUTS_NOTE`;
+export const SET_SORT_ORDER = `${prefix}_SET_SORT_ORDER`;
+export const SHOW_STORY_SEARCH = `${prefix}_SHOW_STORY_SEARCH`;
+export const HIDE_STORY_SEARCH = `${prefix}_HIDE_STORY_SEARCH`;
+export const STORY_SEARCH_CHANGE_VALUE = `${prefix}_STORY_SEARCH_CHANGE_VALUE`;
+export const CLEAR_STATE = `${prefix}_CLEAR_STATE`;
+export const SELECT_COMMENT = `${prefix}_SELECT_COMMENT`;
+export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
diff --git a/client/coral-admin/src/containers/CommentLabels.js b/client/coral-admin/src/containers/CommentLabels.js
index 4cee8b1cc..ee65eb636 100644
--- a/client/coral-admin/src/containers/CommentLabels.js
+++ b/client/coral-admin/src/containers/CommentLabels.js
@@ -25,6 +25,7 @@ export default withFragments({
id
role
}
+ created_at
}
${getSlotFragmentSpreads(slots, 'comment')}
}
diff --git a/client/coral-admin/src/containers/Header.js b/client/coral-admin/src/containers/Header.js
index dae096517..d3be66764 100644
--- a/client/coral-admin/src/containers/Header.js
+++ b/client/coral-admin/src/containers/Header.js
@@ -7,7 +7,7 @@ import { getDefinitionName } from 'coral-framework/utils';
export default withQuery(
gql`
- query TalkAdmin_Header {
+ query TalkAdmin_Header($nullID: ID) {
...${getDefinitionName(ModerationIndicator.fragments.root)}
...${getDefinitionName(CommunityIndicator.fragments.root)}
}
@@ -16,7 +16,7 @@ export default withQuery(
`,
{
options: {
- pollInterval: 10000,
+ variables: { nullID: null },
},
}
)(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/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js
index e2c6a32ff..3dee12785 100644
--- a/client/coral-admin/src/reducers/moderation.js
+++ b/client/coral-admin/src/reducers/moderation.js
@@ -8,14 +8,19 @@ const initialState = {
shortcutsNoteVisible: 'show',
sortOrder: 'DESC',
selectedCommentId: '',
+ // If true the activity indicator will turn on subscriptions
+ // in order to determine queue counts. Set this to false
+ // if the queue count is determined by other means.
+ indicatorTrack: true,
};
export default function moderation(state = initialState, action) {
switch (action.type) {
- case actions.MODERATION_CLEAR_STATE:
+ case actions.CLEAR_STATE:
return {
...initialState,
shortcutsNoteVisible: state.shortcutsNoteVisible,
+ indicatorTrack: state.indicatorTrack,
};
case actions.TOGGLE_MODAL:
return {
@@ -52,11 +57,16 @@ export default function moderation(state = initialState, action) {
...state,
sortOrder: action.order,
};
- case actions.MODERATION_SELECT_COMMENT:
+ case actions.SELECT_COMMENT:
return {
...state,
selectedCommentId: action.id,
};
+ case actions.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 1d7e089e2..60ae4ee99 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';
@@ -31,10 +34,6 @@ function whoFlagged(user) {
class FlaggedAccountsContainer extends Component {
subscriptions = [];
- constructor(props) {
- super(props);
- }
-
getCountWithoutDangling() {
return this.props.root.flaggedUsers.nodes.filter(
node => !isFlaggedUserDangling(node)
@@ -49,7 +48,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),
@@ -65,7 +64,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),
@@ -81,7 +80,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),
@@ -101,7 +100,7 @@ class FlaggedAccountsContainer extends Component {
},
}
) => {
- return handleFlaggedUsernameChange(prev, user, () => {
+ return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_changed',
previousUsername,
@@ -124,10 +123,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();
}
@@ -196,6 +199,7 @@ FlaggedAccountsContainer.propTypes = {
approveUsername: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
+ setIndicatorTrack: PropTypes.func,
};
const LOAD_MORE_QUERY = gql`
@@ -287,6 +291,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..9e5be831a 100644
--- a/client/coral-admin/src/routes/Community/containers/Indicator.js
+++ b/client/coral-admin/src/routes/Community/containers/Indicator.js
@@ -1,14 +1,152 @@
+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
@@ -17,8 +155,7 @@ const enhance = compose(
)
}
`,
- }),
- 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:
+ }
+}
diff --git a/client/coral-admin/src/routes/Moderation/containers/Indicator.js b/client/coral-admin/src/routes/Moderation/containers/Indicator.js
index bd33a58ef..1475b6e2d 100644
--- a/client/coral-admin/src/routes/Moderation/containers/Indicator.js
+++ b/client/coral-admin/src/routes/Moderation/containers/Indicator.js
@@ -1,25 +1,207 @@
+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, subscriptionFields } from '../graphql';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import withQueueConfig from '../hoc/withQueueConfig';
+import baseQueueConfig from '../queueConfig';
+import Slot from 'coral-framework/components/Slot';
-const hideIfNoData = hasNoData => branch(hasNoData, renderNothing);
+class IndicatorContainer extends Component {
+ subscriptions = [];
+
+ handleCommentChange = (root, comment) => {
+ return handleIndicatorChange(root, comment, this.props.queueConfig);
+ };
+
+ subscribeToUpdates() {
+ const parameters = [
+ {
+ document: COMMENT_ADDED_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentAdded: comment } } }
+ ) => {
+ return this.handleCommentChange(prev, comment);
+ },
+ },
+ {
+ document: COMMENT_FLAGGED_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentFlagged: comment } } }
+ ) => {
+ return this.handleCommentChange(prev, comment);
+ },
+ },
+ {
+ document: COMMENT_EDITED_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentEdited: comment } } }
+ ) => {
+ return this.handleCommentChange(prev, comment);
+ },
+ },
+ {
+ document: COMMENT_ACCEPTED_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentAccepted: comment } } }
+ ) => {
+ return this.handleCommentChange(prev, comment);
+ },
+ },
+ {
+ document: COMMENT_REJECTED_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentRejected: comment } } }
+ ) => {
+ return this.handleCommentChange(prev, comment);
+ },
+ },
+ {
+ document: COMMENT_RESET_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentReset: comment } } }
+ ) => {
+ return this.handleCommentChange(prev, comment);
+ },
+ },
+ ];
+
+ 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.premodCount && !this.props.root.reportedCount)
+ ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ );
+ }
+}
+
+IndicatorContainer.propTypes = {
+ data: PropTypes.object,
+ root: PropTypes.object,
+ track: PropTypes.bool,
+ queueConfig: PropTypes.object,
+};
+
+const COMMENT_ADDED_SUBSCRIPTION = gql`
+ subscription TalkAdmin_ModerationIndicator_CommentAdded {
+ commentAdded(statuses: null) {
+ ${subscriptionFields}
+ }
+ }
+`;
+
+const COMMENT_FLAGGED_SUBSCRIPTION = gql`
+ subscription TalkAdmin_ModerationIndicator_CommentFlagged {
+ commentFlagged {
+ ${subscriptionFields}
+ }
+ }
+`;
+
+const COMMENT_EDITED_SUBSCRIPTION = gql`
+ subscription TalkAdmin_ModerationIndicator_CommentEdited {
+ commentEdited {
+ ${subscriptionFields}
+ }
+ }
+`;
+
+const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
+ subscription TalkAdmin_ModerationIndicator_CommentAccepted {
+ commentAccepted {
+ ${subscriptionFields}
+ }
+ }
+`;
+
+const COMMENT_REJECTED_SUBSCRIPTION = gql`
+ subscription TalkAdmin_ModerationIndicator_CommentRejected {
+ commentRejected {
+ ${subscriptionFields}
+ }
+ }
+`;
+
+const COMMENT_RESET_SUBSCRIPTION = gql`
+ subscription TalkAdmin_ModerationIndicator_CommentReset {
+ commentReset {
+ ${subscriptionFields}
+ }
+ }
+`;
+
+const mapStateToProps = state => ({
+ track: state.moderation.indicatorTrack,
+});
const enhance = compose(
+ connect(mapStateToProps),
withFragments({
root: gql`
fragment TalkAdmin_Moderation_Indicator_root on RootQuery {
- premodCount: commentCount(query: { statuses: [PREMOD] })
+ premodCount: commentCount(
+ query: { statuses: [PREMOD], asset_id: $nullID }
+ )
reportedCount: commentCount(
query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD]
action_type: FLAG
+ asset_id: $nullID
}
)
}
`,
}),
- hideIfNoData(props => !props.root.premodCount && !props.root.reportedCount)
+ withQueueConfig(baseQueueConfig)
);
-export default enhance(Indicator);
+export default enhance(IndicatorContainer);
diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
index e0e3f14fe..c0e2e60a6 100644
--- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js
+++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
@@ -15,6 +15,7 @@ import {
handleCommentChange,
commentBelongToQueue,
cleanUpQueue,
+ subscriptionFields,
} from '../graphql';
import { viewUserDetail } from '../../../actions/userDetail';
@@ -27,6 +28,7 @@ import {
storySearchChange,
clearState,
selectCommentId,
+ setIndicatorTrack,
} from 'actions/moderation';
import withQueueConfig from '../hoc/withQueueConfig';
import { notify } from 'coral-framework/actions/notification';
@@ -198,21 +200,42 @@ class ModerationContainer extends Component {
}
componentWillMount() {
+ if (!this.props.data.variables.asset_id) {
+ // Stop activity indicator tracking, as we'll handle it here.
+ this.props.setIndicatorTrack(false);
+ }
this.props.clearState();
this.subscribeToUpdates();
}
componentWillUnmount() {
+ if (!this.props.data.variables.asset_id) {
+ // Restart activity indicator tracking.
+ this.props.setIndicatorTrack(true);
+ }
this.unsubscribe();
}
componentWillReceiveProps(nextProps) {
+ const currentAssetId = this.props.data.variables.asset_id;
+ const nextAssetId = nextProps.data.variables.asset_id;
+
// Resubscribe when we change between assets.
- if (
- this.props.data.variables.asset_id !== nextProps.data.variables.asset_id
- ) {
+ if (currentAssetId !== nextAssetId) {
this.resubscribe(nextProps.data.variables);
}
+
+ // We are only subscribing to a specific asset_id, so activity indicator
+ // needs to do its own tracking.
+ if (!currentAssetId && nextAssetId) {
+ this.props.setIndicatorTrack(true);
+ }
+
+ // We are subscribing to all comment changes, and as such there is no
+ // need for the activity indicator to do the same.
+ if (currentAssetId && !nextAssetId) {
+ this.props.setIndicatorTrack(false);
+ }
}
cleanUpQueue = queue => {
@@ -316,10 +339,12 @@ class ModerationContainer extends Component {
);
}
}
+
const COMMENT_ADDED_SUBSCRIPTION = gql`
subscription CommentAdded($asset_id: ID){
commentAdded(asset_id: $asset_id, statuses: null){
...${getDefinitionName(Comment.fragments.comment)}
+ ${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -329,6 +354,7 @@ const COMMENT_EDITED_SUBSCRIPTION = gql`
subscription CommentEdited($asset_id: ID){
commentEdited(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
+ ${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -338,6 +364,7 @@ const COMMENT_FLAGGED_SUBSCRIPTION = gql`
subscription CommentFlagged($asset_id: ID){
commentFlagged(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
+ ${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -347,14 +374,7 @@ const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
subscription CommentAccepted($asset_id: ID){
commentAccepted(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
- status_history {
- type
- created_at
- assigned_by {
- id
- username
- }
- }
+ ${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -364,14 +384,7 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
subscription CommentRejected($asset_id: ID){
commentRejected(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
- status_history {
- type
- created_at
- assigned_by {
- id
- username
- }
- }
+ ${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -381,14 +394,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
subscription CommentReset($asset_id: ID){
commentReset(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
- status_history {
- type
- created_at
- assigned_by {
- id
- username
- }
- }
+ ${subscriptionFields}
}
}
${Comment.fragments.comment}
@@ -525,6 +531,7 @@ const mapDispatchToProps = dispatch => ({
clearState,
notify,
selectCommentId,
+ setIndicatorTrack,
},
dispatch
),
diff --git a/client/coral-admin/src/routes/Moderation/graphql.js b/client/coral-admin/src/routes/Moderation/graphql.js
index 481c42ab9..c542b8f84 100644
--- a/client/coral-admin/src/routes/Moderation/graphql.js
+++ b/client/coral-admin/src/routes/Moderation/graphql.js
@@ -91,6 +91,76 @@ function getCommentQueues(comment, queueConfig) {
return queues;
}
+function getOlderDate(a, b) {
+ if (a) {
+ a = new Date(a);
+ }
+ if (b) {
+ b = new Date(b);
+ }
+
+ if (!b) {
+ return a;
+ }
+
+ if (!a) {
+ return b;
+ }
+ return a < b ? b : a;
+}
+
+function determineLatestChange(comment) {
+ let lc = null;
+
+ comment.status_history.forEach(item => {
+ lc = getOlderDate(lc, item.created_at);
+ });
+
+ comment.actions.forEach(item => {
+ lc = getOlderDate(lc, item.created_at);
+ });
+
+ return lc;
+}
+
+function reconstructPreviousCommentState(comment) {
+ const history = comment.status_history;
+ const actions = comment.actions;
+ const lastChangeDate = determineLatestChange(comment);
+ const previousComment = {
+ ...comment,
+ status_history: history.filter(
+ item => new Date(item.created_at) < lastChangeDate
+ ),
+ actions: actions.filter(item => new Date(item.created_at) < lastChangeDate),
+ };
+
+ // Comment did not exist previously.
+ if (!previousComment.status_history.length) {
+ return null;
+ }
+
+ previousComment.status =
+ previousComment.status_history[
+ previousComment.status_history.length - 1
+ ].type;
+
+ return previousComment;
+}
+
+/**
+ * getPreviousCommentQueues determines queues that this comment previously belonged to.
+ */
+function getPreviousCommentQueues(comment, queueConfig) {
+ const previousCommentState = reconstructPreviousCommentState(comment);
+
+ if (!previousCommentState) {
+ return [];
+ }
+
+ return getCommentQueues(previousCommentState, queueConfig);
+}
+
/**
* Return whether or not the comment belongs to the queue.
*/
@@ -203,17 +273,7 @@ export function handleCommentChange(
let next = root;
// Queues that this comment previously belonged to.
- const prevQueues =
- comment.status_history.length <= 1
- ? []
- : getCommentQueues(
- {
- ...comment,
- status:
- comment.status_history[comment.status_history.length - 2].type,
- },
- queueConfig
- );
+ const prevQueues = getPreviousCommentQueues(comment, queueConfig);
// Queues that this comment needs to be placed.
const nextQueues = getCommentQueues(comment, queueConfig);
@@ -291,3 +351,49 @@ export function handleCommentChange(
});
return next;
}
+
+const indicatorQueues = ['premod', 'reported'];
+
+/**
+ * Track indicator status
+ * @param {Object} root current state of the store
+ * @param {Object} comment comment that was changed
+ * @return {Object} next state of the store
+ */
+export function handleIndicatorChange(root, comment, queueConfig) {
+ let next = root;
+
+ // Queues that this comment previously belonged to.
+ const prevQueues = getPreviousCommentQueues(comment, queueConfig);
+
+ // Queues that this comment needs to be placed.
+ const nextQueues = getCommentQueues(comment, queueConfig);
+
+ for (const queue of indicatorQueues) {
+ if (prevQueues.indexOf(queue) === -1 && nextQueues.indexOf(queue) >= 0) {
+ next = increaseCommentCount(next, queue);
+ }
+ if (prevQueues.indexOf(queue) >= 0 && nextQueues.indexOf(queue) === -1) {
+ next = decreaseCommentCount(next, queue);
+ }
+ }
+
+ return next;
+}
+
+export const subscriptionFields = `
+ status
+ actions {
+ __typename
+ created_at
+ }
+ status_history {
+ type
+ assigned_by {
+ id
+ }
+ created_at
+ }
+ updated_at
+ created_at
+`;
diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js
index ff9ab9c95..992148b4f 100644
--- a/graph/resolvers/util.js
+++ b/graph/resolvers/util.js
@@ -50,8 +50,12 @@ const decorateWithPermissionCheck = (typeResolver, protect) => {
*/
const decorateUserField = (typeResolver, field) => {
// The default resolver for the user decorator is loading the user by id.
- let fieldResolver = (obj, args, ctx) =>
- ctx.loaders.Users.getByID.load(obj[field]);
+ let fieldResolver = (obj, args, ctx) => {
+ if (!obj[field]) {
+ return null;
+ }
+ return ctx.loaders.Users.getByID.load(obj[field]);
+ };
// The resolver can be overridden however. This decorator will simply wrap the
// field with a permission check.
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 79d1c5b0f..550856082 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -502,6 +502,9 @@ type Comment {
# The time when the comment was created
created_at: Date!
+ # The time when the comment was updated.
+ updated_at: Date
+
# describes how the comment can be edited
editing: EditInfo
diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModIndicatorSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModIndicatorSubscription.js
new file mode 100644
index 000000000..86ae6bb2c
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/containers/ModIndicatorSubscription.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import { gql } from 'react-apollo';
+import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql';
+
+class ModIndicatorSubscription extends React.Component {
+ subscriptions = null;
+
+ componentWillMount() {
+ const configs = [
+ {
+ document: COMMENT_FEATURED_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentFeatured: { comment } } } }
+ ) => {
+ return this.props.handleCommentChange(prev, comment);
+ },
+ },
+ {
+ document: COMMENT_UNFEATURED_SUBSCRIPTION,
+ updateQuery: (
+ prev,
+ { subscriptionData: { data: { commentUnfeatured: { comment } } } }
+ ) => {
+ return this.props.handleCommentChange(prev, comment);
+ },
+ },
+ ];
+ this.subscriptions = configs.map(config =>
+ this.props.data.subscribeToMore(config)
+ );
+ }
+
+ componentWillUnmount() {
+ this.subscriptions.forEach(unsubscribe => unsubscribe());
+ }
+
+ render() {
+ return null;
+ }
+}
+
+const COMMENT_FEATURED_SUBSCRIPTION = gql`
+ subscription TalkFeaturedComments_Indicator_CommentFeatured {
+ commentFeatured {
+ comment {
+ ${subscriptionFields}
+ }
+ }
+ }
+`;
+
+const COMMENT_UNFEATURED_SUBSCRIPTION = gql`
+ subscription TalkFeaturedComments_Indicator_CommentUnfeatured {
+ commentUnfeatured {
+ comment {
+ ${subscriptionFields}
+ }
+ }
+ }
+`;
+
+export default ModIndicatorSubscription;
diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js
index 3e7eb3aaf..6d0fdd811 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/ModSubscription.js
@@ -5,6 +5,7 @@ import Comment from 'coral-admin/src/routes/Moderation/containers/Comment';
import { getDefinitionName } from 'coral-framework/utils';
import truncate from 'lodash/truncate';
import t from 'coral-framework/services/i18n';
+import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql';
function prepareNotificationText(text) {
return truncate(text, { length: 50 }).replace('\n', ' ');
@@ -79,14 +80,7 @@ const COMMENT_FEATURED_SUBSCRIPTION = gql`
commentFeatured(asset_id: $assetId) {
comment {
...${getDefinitionName(Comment.fragments.comment)}
- status_history {
- type
- created_at
- assigned_by {
- id
- username
- }
- }
+ ${subscriptionFields}
}
user {
id
@@ -102,6 +96,7 @@ const COMMENT_UNFEATURED_SUBSCRIPTION = gql`
commentUnfeatured(asset_id: $assetId){
comment {
...${getDefinitionName(Comment.fragments.comment)}
+ ${subscriptionFields}
}
user {
id
diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js
index 1f0bac704..2572a54f4 100644
--- a/plugins/talk-plugin-featured-comments/client/index.js
+++ b/plugins/talk-plugin-featured-comments/client/index.js
@@ -6,6 +6,7 @@ import update from 'immutability-helper';
import ModTag from './containers/ModTag';
import ModActionButton from './containers/ModActionButton';
import ModSubscription from './containers/ModSubscription';
+import ModIndicatorSubscription from './containers/ModIndicatorSubscription';
import FeaturedDialog from './containers/FeaturedDialog';
import { gql } from 'react-apollo';
import reducer from './reducer';
@@ -23,6 +24,7 @@ export default {
moderationActions: [ModActionButton],
adminModeration: [ModSubscription, FeaturedDialog],
adminCommentInfoBar: [ModTag],
+ adminModerationIndicator: [ModIndicatorSubscription],
},
mutations: {
IgnoreUser: ({ variables }) => ({
diff --git a/plugins/talk-plugin-featured-comments/index.js b/plugins/talk-plugin-featured-comments/index.js
index f5a9c8ae5..6dd3114e1 100644
--- a/plugins/talk-plugin-featured-comments/index.js
+++ b/plugins/talk-plugin-featured-comments/index.js
@@ -36,7 +36,7 @@ module.exports = {
commentFeatured: (options, args) => ({
commentFeatured: {
filter: ({ comment }, { user }) => {
- if (args.asset_id === null) {
+ if (!args.asset_id) {
return check(user, ['ADMIN', 'MODERATOR']);
}
return comment.asset_id === args.asset_id;
@@ -46,7 +46,7 @@ module.exports = {
commentUnfeatured: (options, args) => ({
commentUnfeatured: {
filter: ({ comment }, { user }) => {
- if (args.asset_id === null) {
+ if (!args.asset_id) {
return check(user, ['ADMIN', 'MODERATOR']);
}
return comment.asset_id === args.asset_id;