Merge branch 'master' into cleanup

This commit is contained in:
Wyatt Johnson
2018-01-29 10:02:52 -07:00
22 changed files with 666 additions and 95 deletions
@@ -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,
});
+8 -2
View File
@@ -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,
});
+4 -2
View File
@@ -15,6 +15,7 @@ const CoralHeader = ({
showShortcuts = () => {},
auth,
root,
data,
}) => {
return (
<div className={styles.headerWrapper}>
@@ -31,7 +32,7 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.moderate')}
<ModerationIndicator root={root} />
<ModerationIndicator root={root} data={data} />
</IndexLink>
)}
<Link
@@ -50,7 +51,7 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.community')}
<CommunityIndicator root={root} />
<CommunityIndicator root={root} data={data} />
</Link>
{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;
@@ -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`;
+12 -9
View File
@@ -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`;
@@ -25,6 +25,7 @@ export default withFragments({
id
role
}
created_at
}
${getSlotFragmentSpreads(slots, 'comment')}
}
+2 -2
View File
@@ -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);
+12 -4
View File
@@ -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),
@@ -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;
}
+12 -2
View File
@@ -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;
}
@@ -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
);
@@ -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 <Indicator />;
}
}
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);
@@ -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:
}
}
@@ -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 (
<span>
<Indicator />
<Slot
data={this.props.data}
handleCommentChange={this.handleCommentChange}
fill="adminModerationIndicator"
/>
</span>
);
}
}
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);
@@ -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
),
@@ -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
`;
+6 -2
View File
@@ -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.
+3
View File
@@ -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
@@ -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;
@@ -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
@@ -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 }) => ({
@@ -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;