mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 08:19:42 +08:00
Live update modqueue on new comments
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user