Live update modqueue on new comments

This commit is contained in:
Chi Vinh Le
2017-09-21 23:39:04 +07:00
parent abdb4cbe3e
commit 143adc7c6f
9 changed files with 225 additions and 74 deletions
@@ -132,6 +132,7 @@ export default class Moderation extends Component {
activeTab={activeTab}
/>
<ModerationQueue
key={`${activeTab}_${this.props.moderation.sortOrder}`}
data={this.props.data}
root={this.props.root}
currentAsset={asset}
@@ -6,9 +6,44 @@ import styles from './styles.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
import LoadMore from '../../../components/LoadMore';
import ViewMore from './ViewMore';
import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
if (props.comments && props.comments.length) {
const idCursors = [props.comments[0].id];
if (props.comments[1]) {
idCursors.push(props.comments[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = props.comments.findIndex((node) => node.id === idCursors[0]);
const nextInLine = props.comments[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
class ModerationQueue extends React.Component {
isLoadingMore = false;
@@ -38,6 +73,9 @@ class ModerationQueue extends React.Component {
constructor(props) {
super(props);
this.state = {
...resetCursors(this.state, props),
};
}
componentDidUpdate (prev) {
@@ -51,6 +89,59 @@ class ModerationQueue extends React.Component {
}
}
componentWillReceiveProps(next) {
const {comments: prevComments} = this.props;
const {comments: nextComments} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
}
if (
prevComments && nextComments &&
nextComments.length < prevComments.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
}
}
}
viewNewComments = () => {
this.setState(resetCursors);
};
// getVisibileComments returns a list containing comments
// which comes after the `idCursor`.
getVisibleComments() {
const {comments} = this.props;
const idCursor = this.state.idCursors[0];
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor) {
view.push(comment);
}
});
return view;
}
render () {
const {
comments,
@@ -62,8 +153,14 @@ class ModerationQueue extends React.Component {
...props
} = this.props;
const view = this.getVisibleComments();
return (
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
<ViewMore
viewMore={this.viewNewComments}
count={comments.length - view.length}
/>
<CSSTransitionGroup
key={activeTab}
component={'ul'}
@@ -80,7 +177,7 @@ class ModerationQueue extends React.Component {
transitionLeaveTimeout={1000}
>
{
comments.map((comment, i) => {
view.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
data={this.props.data}
@@ -0,0 +1,21 @@
.viewMoreContainer {
display: flex;
justify-content: center;
width: 100%;
}
.viewMore {
width: 100%;
text-align: center;
color: #FFF;
max-width: 660px;
margin-bottom: 30px;
background-color: #2376D8;
cursor: pointer;
}
.viewMore:hover {
background-color: #4399FF;
}
@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import styles from './ViewMore.css';
import cn from 'classnames';
const ViewMore = ({viewMore, count, className, ...rest}) =>
<div {...rest} className={cn(className, styles.viewMoreContainer)}>
{
count > 0 && <Button
className={styles.viewMore}
onClick={viewMore}>
View {count} New {count > 1 ? 'Comments' : 'Comment'}
</Button>
}
</div>;
ViewMore.propTypes = {
viewMore: PropTypes.func.isRequired,
count: PropTypes.number.isRequired,
className: PropTypes.string
};
export default ViewMore;
@@ -82,50 +82,56 @@ class ModerationContainer extends Component {
}
subscribeToUpdates(variables = this.props.data.variables) {
const sub1 = this.props.data.subscribeToMore({
document: COMMENT_ACCEPTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
const parameters = [
{
document: COMMENT_ADDED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAdded: comment}}}) => {
return this.handleCommentChange(prev, comment);
},
},
});
const sub2 = this.props.data.subscribeToMore({
document: COMMENT_REJECTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
{
document: COMMENT_ACCEPTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
});
const sub3 = this.props.data.subscribeToMore({
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
{
document: COMMENT_REJECTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
});
const sub4 = this.props.data.subscribeToMore({
document: COMMENT_FLAGGED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
const user = comment.actions[comment.actions.length - 1].user;
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
{
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
});
{
document: COMMENT_FLAGGED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
const user = comment.actions[comment.actions.length - 1].user;
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
];
this.subscriptions.push(sub1, sub2, sub3, sub4);
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMore(param));
}
unsubscribe() {
@@ -204,12 +210,9 @@ class ModerationContainer extends Component {
// Not found.
return <NotFoundAsset assetId={assetId} />;
}
if (asset === undefined || asset.id !== assetId) {
}
// Still loading.
return <Spinner />;
}
} else if (asset !== undefined || !('premodCount' in root)) {
if(data.loading) {
// loading.
return <Spinner />;
@@ -240,6 +243,14 @@ 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)}
}
}
${Comment.fragments.comment}
`;
const COMMENT_EDITED_SUBSCRIPTION = gql`
subscription CommentEdited($asset_id: ID){
@@ -369,29 +380,6 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
},
});
const withQueueCountPolling = withQuery(({queueConfig}) => gql`
query CoralAdmin_ModerationCountPoll($asset_id: ID) {
${Object.keys(queueConfig).map((queue) => `
${queue}Count: commentCount(query: {
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
asset_id: $asset_id,
})
`)}
}
`, {
options: (props) => {
const id = getAssetId(props);
return {
pollInterval: 5000,
variables: {
asset_id: id
}
};
}
});
const mapStateToProps = (state) => ({
moderation: state.moderation,
settings: state.settings,
@@ -419,6 +407,5 @@ export default compose(
withQueueConfig(baseQueueConfig),
connect(mapStateToProps, mapDispatchToProps),
withSetCommentStatus,
withQueueCountPolling,
withModQueueQuery,
)(ModerationContainer);
+3 -3
View File
@@ -182,11 +182,11 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
Comments.parentCountByAssetID.incr(asset_id);
}
Comments.countByAssetID.incr(asset_id);
// Publish the newly added comment via the subscription.
pubsub.publish('commentAdded', comment);
}
// Publish the newly added comment via the subscription.
pubsub.publish('commentAdded', comment);
return comment;
};
+21 -1
View File
@@ -26,10 +26,30 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
commentAdded: (options, args) => ({
commentAdded: {
filter: (comment, context) => {
// Only priviledged users can subscribe to all assets.
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) {
return false;
}
return !args.asset_id || comment.asset_id === args.asset_id;
// If user scubsscribes for statuses other than NONE and/or ACCEPTED statuses, it needs
// special priviledges.
if (
(!args.statuses || args.statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))
) {
return false;
}
if (args.asset_id && comment.asset_id !== args.asset_id) {
return false;
}
if (args.statuses && !args.statuses.includes(comment.status)) {
return false;
}
return true;
}
},
}),
+2 -1
View File
@@ -1356,7 +1356,8 @@ type Subscription {
# Get an update whenever a comment was added.
# `asset_id` is required except for users with the `ADMIN` or `MODERATOR` role.
commentAdded(asset_id: ID): Comment
# Non privileged user can only subscribe to 'NONE' and/or 'ACCEPTED' statuses.
commentAdded(asset_id: ID, statuses: [COMMENT_STATUS!] = [NONE, ACCEPTED]): Comment
# Get an update whenever a comment was edited.
# `asset_id` is required except for users with the `ADMIN` or `MODERATOR` role.