mirror of
https://github.com/wassname/talk.git
synced 2026-07-20 12:40:47 +08:00
Merge branch 'master' into cleanup
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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')}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user