diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js
index 740af20a17..59a6d7c8fc 100644
--- a/client/coral-admin/src/graphql/index.js
+++ b/client/coral-admin/src/graphql/index.js
@@ -1,4 +1,5 @@
import {add} from 'coral-framework/services/graphqlRegistry';
+import update from 'immutability-helper';
const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
const extension = {
@@ -11,54 +12,53 @@ const extension = {
}),
SetCommentStatus: ({variables: {commentId, status}}) => ({
updateQueries: {
- CoralAdmin_Moderation: (oldData) => {
+ CoralAdmin_Moderation: (prev) => {
const comment = queues.reduce((comment, queue) => {
- return comment ? comment : oldData[queue].find((c) => c.id === commentId);
+ return comment ? comment : prev[queue].nodes.find((c) => c.id === commentId);
}, null);
- let accepted = oldData.accepted;
- let acceptedCount = oldData.acceptedCount;
- let rejected = oldData.rejected;
- let rejectedCount = oldData.rejectedCount;
+ let acceptedNodes = prev.accepted.nodes;
+ let acceptedCount = prev.acceptedCount;
+ let rejectedNodes = prev.rejected.nodes;
+ let rejectedCount = prev.rejectedCount;
if (status !== comment.status) {
if (status === 'ACCEPTED') {
comment.status = 'ACCEPTED';
acceptedCount++;
- accepted = [comment, ...accepted];
+ acceptedNodes = [comment, ...acceptedNodes];
}
else if (status === 'REJECTED') {
comment.status = 'REJECTED';
rejectedCount++;
- rejected = [comment, ...rejected];
+ rejectedNodes = [comment, ...rejectedNodes];
}
}
- const premod = oldData.premod.filter((c) => c.id !== commentId);
- const flagged = oldData.flagged.filter((c) => c.id !== commentId);
- const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
- const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
+ const premodNodes = prev.premod.nodes.filter((c) => c.id !== commentId);
+ const flaggedNodes = prev.flagged.nodes.filter((c) => c.id !== commentId);
+ const premodCount = premodNodes.length < prev.premod.nodes.length ? prev.premodCount - 1 : prev.premodCount;
+ const flaggedCount = flaggedNodes.length < prev.flagged.nodes.length ? prev.flaggedCount - 1 : prev.flaggedCount;
if (status === 'REJECTED') {
- accepted = oldData.accepted.filter((c) => c.id !== commentId);
- acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
+ acceptedNodes = prev.accepted.nodes.filter((c) => c.id !== commentId);
+ acceptedCount = acceptedNodes.length < prev.accepted.nodes.length ? prev.acceptedCount - 1 : prev.acceptedCount;
}
else if (status === 'ACCEPTED') {
- rejected = oldData.rejected.filter((c) => c.id !== commentId);
- rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
+ rejectedNodes = prev.rejected.nodes.filter((c) => c.id !== commentId);
+ rejectedCount = rejectedNodes.length < prev.rejected.nodes.length ? prev.rejectedCount - 1 : prev.rejectedCount;
}
- return {
- ...oldData,
- premodCount: Math.max(0, premodCount),
- flaggedCount: Math.max(0, flaggedCount),
- acceptedCount: Math.max(0, acceptedCount),
- rejectedCount: Math.max(0, rejectedCount),
- premod,
- flagged,
- accepted,
- rejected,
- };
+ return update(prev, {
+ premodCount: {$set: Math.max(0, premodCount)},
+ flaggedCount: {$set: Math.max(0, flaggedCount)},
+ acceptedCount: {$set: Math.max(0, acceptedCount)},
+ rejectedCount: {$set: Math.max(0, rejectedCount)},
+ premod: {nodes: {$set: premodNodes}},
+ flagged: {nodes: {$set: flaggedNodes}},
+ accepted: {nodes: {$set: acceptedNodes}},
+ rejected: {nodes: {$set: rejectedNodes}},
+ });
}
}
}),
diff --git a/client/coral-admin/src/routes/Moderation/components/LoadMore.js b/client/coral-admin/src/routes/Moderation/components/LoadMore.js
index c90d3cd14f..6126296478 100644
--- a/client/coral-admin/src/routes/Moderation/components/LoadMore.js
+++ b/client/coral-admin/src/routes/Moderation/components/LoadMore.js
@@ -2,32 +2,19 @@ import React, {PropTypes} from 'react';
import {Button} from 'coral-ui';
import styles from './styles.css';
-const LoadMore = ({comments, loadMore, sort, tab, assetId, showLoadMore}) =>
+const LoadMore = ({loadMore, showLoadMore}) =>
{
showLoadMore &&
}
;
LoadMore.propTypes = {
- comments: PropTypes.array.isRequired,
loadMore: PropTypes.func.isRequired,
- sort: PropTypes.oneOf(['CHRONOLOGICAL', 'REVERSE_CHRONOLOGICAL']).isRequired,
- tab: PropTypes.oneOf(['rejected', 'premod', 'flagged', 'all', 'accepted']).isRequired,
- assetId: PropTypes.string,
showLoadMore: PropTypes.bool.isRequired
};
diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js
index 956575ce4e..036f8e0c17 100644
--- a/client/coral-admin/src/routes/Moderation/components/Moderation.js
+++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js
@@ -143,7 +143,7 @@ export default class Moderation extends Component {
data={this.props.data}
root={this.props.root}
currentAsset={asset}
- comments={comments}
+ comments={comments.nodes}
activeTab={activeTab}
singleView={moderation.singleView}
selectedIndex={this.state.selectedIndex}
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
index 234c9cbcb5..490c2a9859 100644
--- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
+++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
@@ -21,14 +21,18 @@ class ModerationQueue extends React.Component {
comments: PropTypes.array.isRequired
}
+ loadMore = () => {
+ this.props.loadMore(this.props.activeTab);
+ }
+
componentDidUpdate (prev) {
- const {loadMore, comments, commentCount, sort, activeTab: tab, assetId: asset_id} = this.props;
+ const {comments, commentCount} = this.props;
// if the user just moderated the last (visible) comment
// AND there are more comments available on the server,
// go ahead and load more comments
if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) {
- loadMore({sort, tab, asset_id});
+ this.loadMore();
}
}
@@ -38,9 +42,6 @@ class ModerationQueue extends React.Component {
selectedIndex,
commentCount,
singleView,
- loadMore,
- activeTab,
- sort,
viewUserDetail,
...props
} = this.props;
@@ -75,12 +76,8 @@ class ModerationQueue extends React.Component {
}
);
diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
index 63af5a6cd5..537aa1d655 100644
--- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js
+++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
@@ -7,6 +7,7 @@ import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName} from 'coral-framework/utils';
import * as notification from 'coral-admin/src/services/notification';
import t, {timeago} from 'coral-framework/services/i18n';
+import update from 'immutability-helper';
import {withSetUserStatus, withSuspendUser, withSetCommentStatus} from 'coral-framework/graphql/mutations';
@@ -80,12 +81,12 @@ class ModerationContainer extends Component {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
- loadMore = ({limit = 10, cursor, sort, tab, asset_id}) => {
- let variables = {
- limit,
- cursor,
- sort,
- asset_id
+ loadMore = (tab) => {
+ const variables = {
+ limit: 10,
+ cursor: this.props.root[tab].endCursor,
+ sort: this.props.data.variables.sort,
+ asset_id: this.props.data.variables.asset_id,
};
switch(tab) {
case 'all':
@@ -108,14 +109,12 @@ class ModerationContainer extends Component {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
- updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
- return {
- ...oldData,
- [tab]: [
- ...oldData[tab],
- ...comments
- ]
- };
+ updateQuery: (prev, {fetchMoreResult:{comments}}) => {
+ return update(prev, {
+ [tab]: {
+ nodes: {$push: comments.nodes},
+ },
+ });
}
});
};
@@ -145,11 +144,13 @@ class ModerationContainer extends Component {
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
- ...${getDefinitionName(Comment.fragments.comment)}
- action_summaries {
- count
- ... on FlagActionSummary {
- reason
+ nodes {
+ ...${getDefinitionName(Comment.fragments.comment)}
+ action_summaries {
+ count
+ ... on FlagActionSummary {
+ reason
+ }
}
}
}
@@ -157,6 +158,18 @@ const LOAD_MORE_QUERY = gql`
${Comment.fragments.comment}
`;
+const commentConnectionFragment = gql`
+ fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
+ nodes {
+ ...${getDefinitionName(Comment.fragments.comment)}
+ }
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ ${Comment.fragments.comment}
+`;
+
const withModQueueQuery = withQuery(gql`
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER) {
all: comments(query: {
@@ -164,21 +177,21 @@ const withModQueueQuery = withQuery(gql`
asset_id: $asset_id,
sort: $sort
}) {
- ...${getDefinitionName(Comment.fragments.comment)}
+ ...CoralAdmin_Moderation_CommentConnection
}
accepted: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id,
sort: $sort
}) {
- ...${getDefinitionName(Comment.fragments.comment)}
+ ...CoralAdmin_Moderation_CommentConnection
}
premod: comments(query: {
statuses: [PREMOD],
asset_id: $asset_id,
sort: $sort
}) {
- ...${getDefinitionName(Comment.fragments.comment)}
+ ...CoralAdmin_Moderation_CommentConnection
}
flagged: comments(query: {
action_type: FLAG,
@@ -186,14 +199,14 @@ const withModQueueQuery = withQuery(gql`
statuses: [NONE, PREMOD],
sort: $sort
}) {
- ...${getDefinitionName(Comment.fragments.comment)}
+ ...CoralAdmin_Moderation_CommentConnection
}
rejected: comments(query: {
statuses: [REJECTED],
asset_id: $asset_id,
sort: $sort
}) {
- ...${getDefinitionName(Comment.fragments.comment)}
+ ...CoralAdmin_Moderation_CommentConnection
}
assets: assets {
id
@@ -224,7 +237,7 @@ const withModQueueQuery = withQuery(gql`
organizationName
}
}
- ${Comment.fragments.comment}
+ ${commentConnectionFragment}
`, {
options: ({params: {id = null}, moderation: {sortOrder}}) => {
return {
diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js
index 83a175874a..e3f0137b4d 100644
--- a/client/coral-embed-stream/src/components/Comment.js
+++ b/client/coral-embed-stream/src/components/Comment.js
@@ -87,12 +87,7 @@ class Comment extends React.Component {
name: PropTypes.string
})
),
- replies: PropTypes.arrayOf(
- PropTypes.shape({
- body: PropTypes.string.isRequired,
- id: PropTypes.string.isRequired
- })
- ),
+ replies: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
@@ -195,7 +190,7 @@ class Comment extends React.Component {
let commentClass = parentId
? `reply ${styles.Reply}`
: `comment ${styles.Comment}`;
- commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : '';
+ commentClass += comment.id.indexOf('pending') >= 0 ? ` ${styles.pendingComment}` : '';
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
const notifyOnError = (fn, errorToMessage) =>
@@ -375,7 +370,7 @@ class Comment extends React.Component {
/>
: null}
{comment.replies &&
- comment.replies.map((reply) => {
+ comment.replies.nodes.map((reply) => {
return commentIsIgnored(reply)
?
:
comment.replies.length}
- loadMore={loadMore}
+ moreComments={comment.replyCount > comment.replies.nodes.length}
+ loadMore={() => loadMore(comment.id)}
/>
}
diff --git a/client/coral-embed-stream/src/components/LoadMore.js b/client/coral-embed-stream/src/components/LoadMore.js
index c9c8a9704f..31001fcd2c 100644
--- a/client/coral-embed-stream/src/components/LoadMore.js
+++ b/client/coral-embed-stream/src/components/LoadMore.js
@@ -1,26 +1,7 @@
import React, {PropTypes} from 'react';
-import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {Button} from 'coral-ui';
import t from 'coral-framework/services/i18n';
-const loadMoreComments = (assetId, comments, loadMore, parentId, replyCount) => {
-
- let cursor = null;
- if (comments.length) {
- cursor = parentId
- ? comments[0].created_at
- : comments[comments.length - 1].created_at;
- }
-
- loadMore({
- limit: parentId ? replyCount : ADDTL_COMMENTS_ON_LOAD_MORE,
- cursor,
- asset_id: assetId,
- parent_id: parentId,
- sort: parentId ? 'CHRONOLOGICAL' : 'REVERSE_CHRONOLOGICAL'
- });
-};
-
class LoadMore extends React.Component {
componentDidMount () {
@@ -40,13 +21,13 @@ class LoadMore extends React.Component {
}
render () {
- const {assetId, comments, loadMore, moreComments, parentId, replyCount, topLevel} = this.props;
+ const {topLevel, moreComments, loadMore, replyCount} = this.props;
return moreComments
?
@@ -56,11 +37,8 @@ class LoadMore extends React.Component {
}
LoadMore.propTypes = {
- assetId: PropTypes.string.isRequired,
- comments: PropTypes.array.isRequired,
- moreComments: PropTypes.bool.isRequired,
- topLevel: PropTypes.bool.isRequired,
replyCount: PropTypes.number,
+ topLevel: PropTypes.bool.isRequired,
loadMore: PropTypes.func.isRequired
};
diff --git a/client/coral-embed-stream/src/components/NewCount.js b/client/coral-embed-stream/src/components/NewCount.js
index 3cde3786bb..43dce13e03 100644
--- a/client/coral-embed-stream/src/components/NewCount.js
+++ b/client/coral-embed-stream/src/components/NewCount.js
@@ -2,15 +2,10 @@ import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
-const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, setCommentCountCache}) => (e) => {
+const onLoadMoreClick = ({loadMore, commentCount, setCommentCountCache}) => (e) => {
e.preventDefault();
setCommentCountCache(commentCount);
- loadMore({
- asset_id: assetId,
- limit: 500,
- cursor: firstCommentDate,
- sort: 'CHRONOLOGICAL'
- }, true);
+ loadMore();
};
const NewCount = (props) => {
@@ -33,8 +28,6 @@ NewCount.propTypes = {
commentCount: PropTypes.number.isRequired,
commentCountCache: PropTypes.number,
loadMore: PropTypes.func.isRequired,
- assetId: PropTypes.string.isRequired,
- firstCommentDate: PropTypes.string.isRequired
};
export default NewCount;
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js
index 68fb3611ac..11f2b78195 100644
--- a/client/coral-embed-stream/src/components/Stream.js
+++ b/client/coral-embed-stream/src/components/Stream.js
@@ -31,7 +31,6 @@ class Stream extends React.Component {
addNotification,
postFlag,
postDontAgree,
- loadMore,
deleteAction,
showSignInDialog,
addCommentTag,
@@ -55,14 +54,6 @@ class Stream extends React.Component {
user.suspension.until &&
new Date(user.suspension.until) > new Date();
- const hasOlderComments = !!(asset &&
- asset.lastComment &&
- asset.lastComment.id !== asset.comments[asset.comments.length - 1].id);
-
- // Find the created_at date of the first comment. If no comments exist, set the date to a week ago.
- const firstCommentDate = asset.comments[0]
- ? asset.comments[0].created_at
- : new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString();
const commentIsIgnored = (comment) => {
return (
me &&
@@ -138,7 +129,7 @@ class Stream extends React.Component {
highlighted={comment.id}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
- loadMore={this.props.loadMore}
+ loadMore={this.props.loadNewReplies}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
key={highlightedComment.id}
@@ -153,13 +144,11 @@ class Stream extends React.Component {
- {comments.map((comment) => {
+ {comments && comments.nodes.map((comment) => {
return commentIsIgnored(comment)
?
:
}
diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js
index f3cb1bfe9a..f2f5f67db4 100644
--- a/client/coral-embed-stream/src/containers/Stream.js
+++ b/client/coral-embed-stream/src/containers/Stream.js
@@ -2,14 +2,12 @@ import React from 'react';
import {gql, compose} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
-import uniqBy from 'lodash/uniqBy';
-import sortBy from 'lodash/sortBy';
-import isNil from 'lodash/isNil';
-import {NEW_COMMENT_COUNT_POLL_INTERVAL} from '../constants/stream';
+import {NEW_COMMENT_COUNT_POLL_INTERVAL, ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
} from 'coral-framework/graphql/mutations';
+import update from 'immutability-helper';
import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user';
@@ -33,80 +31,128 @@ class StreamContainer extends React.Component {
});
};
- // handle paginated requests for more Comments pertaining to the Asset
- loadMore = ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => {
+ loadNewReplies = (parent_id) => {
+ const comment = this.props.root.comment
+ ? this.props.root.comment.parent || this.props.root.comment // highlighted comment.
+ : this.props.root.asset.comments.nodes.filter((comment) => comment.id === parent_id)[0];
+
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
- limit, // how many comments are we returning
- cursor, // the date of the first/last comment depending on the sort order
- parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
- asset_id, // the id of the asset we're currently on
- sort, // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
+ limit: parent_id ? 999999 : ADDTL_COMMENTS_ON_LOAD_MORE,
+ cursor: comment.replies.endCursor,
+ parent_id,
+ asset_id: this.props.root.asset.id,
+ sort: 'CHRONOLOGICAL',
excludeIgnored: this.props.data.variables.excludeIgnored,
},
- updateQuery: (oldData, {fetchMoreResult:{new_top_level_comments}}) => {
- let updatedAsset;
-
- if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment
-
- let comment = {};
- if (oldData.comment && oldData.comment.parent) {
+ updateQuery: (prev, {fetchMoreResult:{comments}}) => {
+ if (!comments.nodes.length) {
+ return prev;
+ }
- // put comments (replies) onto the oldData.comment.parent object
- // the initial comment permalinked was a reply
- const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id');
- comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')};
- } else if (oldData.comment) {
+ const updateNode = (node) =>
+ update(node, {
+ replies: {
+ endCursor: {$set: comments.endCursor},
+ nodes: {$apply: (nodes) => nodes
+ .concat(comments.nodes.filter(
+ (comment) => !nodes.some((node) => node.id === comment.id)
+ ))
+ .sort(ascending)
+ },
+ },
+ });
- // put the comments (replies) directly onto oldData.comment
- // the initial comment permalinked was a top-level comment
- const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id');
- comment.replies = sortBy(uniqReplies, 'created_at');
+ // highlighted comment.
+ if (prev.comment) {
+ if (prev.comment.parent) {
+ return update(prev, {
+ comment: {
+ parent: {$apply: (comment) => updateNode(comment)},
+ }
+ });
}
+ return update(prev, {
+ comment: {$apply: (comment) => updateNode(comment)},
+ });
+ }
- updatedAsset = {
- ...oldData,
- comment: {
- ...oldData.comment,
- ...comment
- }
- };
-
- } else if (parent_id) { // If loading more replies
-
- updatedAsset = {
- ...oldData,
- asset: {
- ...oldData.asset,
- comments: oldData.asset.comments.map((comment) => {
-
- // since the dipslayed replies and the returned replies can overlap,
- // pull out the unique ones.
- const uniqueReplies = uniqBy([...new_top_level_comments, ...comment.replies], 'id');
+ return update(prev, {
+ asset: {
+ comments: {
+ nodes: {
+ $apply: (nodes) => nodes.map(
+ (node) => node.id !== parent_id
+ ? node
+ : updateNode(node)
+ )
+ },
+ },
+ },
+ });
+ },
+ });
+ }
- // since we just gave the returned replies precedence, they're now out of order.
- // resort according to date.
- return comment.id === parent_id
- ? {...comment, replies: sortBy(uniqueReplies, 'created_at')}
- : comment;
- })
- }
- };
- } else { // If loading more top-level comments
+ loadNewComments = () => {
+ return this.props.data.fetchMore({
+ query: LOAD_MORE_QUERY,
+ variables: {
+ limit: ADDTL_COMMENTS_ON_LOAD_MORE,
+ cursor: this.props.root.asset.comments.startCursor,
+ parent_id: null,
+ asset_id: this.props.root.asset.id,
+ sort: 'CHRONOLOGICAL',
+ excludeIgnored: this.props.data.variables.excludeIgnored,
+ },
+ updateQuery: (prev, {fetchMoreResult:{comments}}) => {
+ if (!comments.nodes.length) {
+ return prev;
+ }
+ return update(prev, {
+ asset: {
+ comments: {
+ startCursor: {$set: comments.endCursor},
+ nodes: {$apply: (nodes) => comments.nodes.filter(
+ (comment) => !nodes.some((node) => node.id === comment.id)
+ )
+ .concat(nodes)
+ .sort(descending)
+ },
+ },
+ },
+ });
+ },
+ });
+ };
- updatedAsset = {
- ...oldData,
- asset: {
- ...oldData.asset,
- comments: newComments ? [...new_top_level_comments.reverse(), ...oldData.asset.comments]
- : [...oldData.asset.comments, ...new_top_level_comments]
- }
- };
+ loadMoreComments = () => {
+ return this.props.data.fetchMore({
+ query: LOAD_MORE_QUERY,
+ variables: {
+ limit: ADDTL_COMMENTS_ON_LOAD_MORE,
+ cursor: this.props.root.asset.comments.endCursor,
+ parent_id: null,
+ asset_id: this.props.root.asset.id,
+ sort: 'REVERSE_CHRONOLOGICAL',
+ excludeIgnored: this.props.data.variables.excludeIgnored,
+ },
+ updateQuery: (prev, {fetchMoreResult:{comments}}) => {
+ if (!comments.nodes.length) {
+ return prev;
}
- return updatedAsset;
- }
+ return update(prev, {
+ asset: {
+ comments: {
+ hasNextPage: {$set: comments.hasNextPage},
+ endCursor: {$set: comments.endCursor},
+ nodes: {$push: comments.nodes},
+ },
+ },
+ });
+ },
});
};
@@ -127,31 +173,87 @@ class StreamContainer extends React.Component {
}
render() {
- return ;
+ return ;
}
}
+const ascending = (a, b) => {
+ const dateA = new Date(a.created_at);
+ const dateB = new Date(b.created_at);
+ if (dateA < dateB) { return -1; }
+ if (dateA > dateB) { return 1; }
+ return 0;
+};
+
+const descending = (a, b) => ascending(a, b) * -1;
+
const LOAD_COMMENT_COUNTS_QUERY = gql`
- query CoralEmbedStream_LoadCommentCounts($assetUrl: String, $assetId: ID, $excludeIgnored: Boolean) {
- asset(id: $assetId, url: $assetUrl) {
+ query CoralEmbedStream_LoadCommentCounts($assetUrl: String, , $commentId: ID!, $assetId: ID, $hasComment: Boolean!, $excludeIgnored: Boolean) {
+ comment(id: $commentId) @include(if: $hasComment) {
id
- commentCount(excludeIgnored: $excludeIgnored)
- comments(limit: 10) {
+ parent {
id
replyCount(excludeIgnored: $excludeIgnored)
}
+ replyCount(excludeIgnored: $excludeIgnored)
+ }
+ asset(id: $assetId, url: $assetUrl) {
+ id
+ commentCount(excludeIgnored: $excludeIgnored)
+ comments(limit: 10) @skip(if: $hasComment) {
+ nodes {
+ id
+ replyCount(excludeIgnored: $excludeIgnored)
+ }
+ }
}
}
`;
const LOAD_MORE_QUERY = gql`
query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
- new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
- ...${getDefinitionName(Comment.fragments.comment)}
- replyCount(excludeIgnored: $excludeIgnored)
- replies(limit: 3) {
+ comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
+ nodes {
+ id
+ ...${getDefinitionName(Comment.fragments.comment)}
+ replyCount(excludeIgnored: $excludeIgnored)
+ replies(limit: 3, excludeIgnored: $excludeIgnored) {
+ nodes {
+ id
+ ...${getDefinitionName(Comment.fragments.comment)}
+ }
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ }
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ }
+ ${Comment.fragments.comment}
+`;
+
+const commentFragment = gql`
+ fragment CoralEmbedStream_Stream_comment on Comment {
+ id
+ ...${getDefinitionName(Comment.fragments.comment)}
+ replyCount(excludeIgnored: $excludeIgnored)
+ replies {
+ nodes {
+ id
...${getDefinitionName(Comment.fragments.comment)}
}
+ hasNextPage
+ startCursor
+ endCursor
}
}
${Comment.fragments.comment}
@@ -161,17 +263,9 @@ const fragments = {
root: gql`
fragment CoralEmbedStream_Stream_root on RootQuery {
comment(id: $commentId) @include(if: $hasComment) {
- ...${getDefinitionName(Comment.fragments.comment)}
- replyCount(excludeIgnored: $excludeIgnored)
- replies {
- ...${getDefinitionName(Comment.fragments.comment)}
- }
+ ...CoralEmbedStream_Stream_comment
parent {
- ...${getDefinitionName(Comment.fragments.comment)}
- replyCount(excludeIgnored: $excludeIgnored)
- replies {
- ...${getDefinitionName(Comment.fragments.comment)}
- }
+ ...CoralEmbedStream_Stream_comment
}
}
asset(id: $assetId, url: $assetUrl) {
@@ -193,17 +287,15 @@ const fragments = {
charCount
requireEmailConfirmation
}
- lastComment {
- id
- }
commentCount(excludeIgnored: $excludeIgnored)
totalCommentCount(excludeIgnored: $excludeIgnored)
- comments(limit: 10, excludeIgnored: $excludeIgnored) {
- ...${getDefinitionName(Comment.fragments.comment)}
- replyCount(excludeIgnored: $excludeIgnored)
- replies(limit: 3, excludeIgnored: $excludeIgnored) {
- ...${getDefinitionName(Comment.fragments.comment)}
+ comments(limit: 10, excludeIgnored: $excludeIgnored) @skip(if: $hasComment) {
+ nodes {
+ ...CoralEmbedStream_Stream_comment
}
+ hasNextPage
+ startCursor
+ endCursor
}
}
me {
@@ -218,7 +310,7 @@ const fragments = {
...${getDefinitionName(Comment.fragments.root)}
}
${Comment.fragments.root}
- ${Comment.fragments.comment}
+ ${commentFragment}
`,
};
diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js
index 3b40e00282..1b4ac500b2 100644
--- a/client/coral-embed-stream/src/graphql/index.js
+++ b/client/coral-embed-stream/src/graphql/index.js
@@ -1,13 +1,20 @@
import {gql} from 'react-apollo';
import {add} from 'coral-framework/services/graphqlRegistry';
import update from 'immutability-helper';
+import uuid from 'uuid/v4';
+import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils';
const extension = {
fragments: {
EditCommentResponse: gql`
fragment CoralEmbedStream_EditCommentResponse on EditCommentResponse {
comment {
+ id
status
+ body
+ editing {
+ edited
+ }
}
}
`,
@@ -50,7 +57,12 @@ const extension = {
comment {
...CoralEmbedStream_CreateCommentResponse_Comment
replies {
- ...CoralEmbedStream_CreateCommentResponse_Comment
+ nodes {
+ ...CoralEmbedStream_CreateCommentResponse_Comment
+ }
+ startCursor
+ endCursor
+ hasNextPage
}
}
}
@@ -116,8 +128,11 @@ const extension = {
}) => ({
optimisticResponse: {
createComment: {
+ __typename: 'CreateCommentResponse',
comment: {
+ __typename: 'Comment',
user: {
+ __typename: 'User',
id: auth.toJS().user.id,
name: auth.toJS().user.username
},
@@ -128,114 +143,39 @@ const extension = {
action_summaries: [],
tags,
status: null,
- id: 'pending'
+ replyCount: 0,
+ replies: {
+ __typename: 'CommentConnection',
+ nodes: [],
+ hasNextPage: false,
+ startCursor: new Date().toISOString(),
+ endCursor: new Date().toISOString(),
+ },
+ editing: {
+ __typename: 'EditInfo',
+ editableUntil: new Date().toISOString(),
+ edited: false,
+ },
+ id: `pending-${uuid()}`,
}
}
},
updateQueries: {
- CoralEmbedStream_Embed: (previousData, {mutationResult: {data: {createComment: {comment}}}}) => {
- if (previousData.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
- return previousData;
- }
-
- let updatedAsset;
-
- // If posting a reply
- if (parent_id) {
- updatedAsset = {
- ...previousData,
- asset: {
- ...previousData.asset,
- comments: previousData.asset.comments.map((oldComment) => {
- return oldComment.id === parent_id
- ? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1}
- : oldComment;
- })
- }
- };
- } else {
-
- // If posting a top-level comment
- updatedAsset = {
- ...previousData,
- asset: {
- ...previousData.asset,
- commentCount: previousData.asset.commentCount + 1,
- comments: [comment, ...previousData.asset.comments]
- }
- };
+ CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
+ if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
+ return prev;
}
-
- return updatedAsset;
- }
+ return insertCommentIntoEmbedQuery(prev, parent_id, comment);
+ },
}
}),
- EditComment: ({
- variables: {id, edit},
- }) => ({
+ EditComment: () => ({
updateQueries: {
- CoralEmbedStream_Embed: (previousData, {mutationResult: {data: {editComment: {comment}}}}) => {
- const {status} = comment;
- const updateCommentWithEdit = (comment, edit) => {
- const {body} = edit;
- const editedComment = update(comment, {
- $merge: {
- body
- },
- editing: {$merge:{edited:true}}
- });
- return editedComment;
- };
- const commentIsStillVisible = (comment) => {
- return !((id === comment.id) && (['PREMOD', 'REJECTED'].includes(status)));
- };
- const resultReflectingEdit = update(previousData, {
- asset: {
- comments: {
- $apply: (comments) => {
- return comments.filter(commentIsStillVisible).map((comment) => {
- let replyWasEditedToBeHidden = false;
- if (comment.id === id) {
- return updateCommentWithEdit(comment, edit);
- }
- const commentWithUpdatedReplies = update(comment, {
- replies: {
- $apply: (comments) => {
- return comments
- .filter((c) => {
- if (commentIsStillVisible(c)) {
- return true;
- }
- replyWasEditedToBeHidden = true;
- return false;
- })
- .map((comment) => {
- if (comment.id === id) {
- return updateCommentWithEdit(comment, edit);
- }
- return comment;
- });
- }
- },
- });
-
- // If a reply was edited to be hdiden, then this parent needs its replyCount to be decremented.
- if (replyWasEditedToBeHidden) {
- return update(commentWithUpdatedReplies, {
- replyCount: {
- $apply: (replyCount) => {
- return replyCount - 1;
- }
- }
- });
- }
- return commentWithUpdatedReplies;
- });
- }
- }
- }
- });
- return resultReflectingEdit;
+ CoralEmbedStream_Embed: (prev, {mutationResult: {data: {editComment: {comment}}}}) => {
+ if (!['PREMOD', 'REJECTED'].includes(comment.status)) {
+ return null;
+ }
+ return removeCommentFromEmbedQuery(prev, comment.id);
},
},
}),
diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js
new file mode 100644
index 0000000000..1634324796
--- /dev/null
+++ b/client/coral-embed-stream/src/graphql/utils.js
@@ -0,0 +1,114 @@
+import update from 'immutability-helper';
+
+function findAndInsertComment(parent, id, comment) {
+ const [connectionField, countField, action] = parent.comments
+ ? ['comments', 'commentCount', '$unshift']
+ : ['replies', 'replyCount', '$push'];
+
+ if (!id || parent.id === id) {
+ return update(parent, {
+ [connectionField]: {
+ nodes: {[action]: [comment]},
+ },
+ [countField]: {$apply: (c) => c + 1},
+ });
+ }
+ const connection = parent[connectionField];
+ if (!connection) {
+ return parent;
+ }
+ return update(parent, {
+ [connectionField]: {
+ nodes: {
+ $apply: (nodes) =>
+ nodes.map((node) => findAndInsertComment(node, id, comment))
+ },
+ },
+ });
+}
+
+export function insertCommentIntoEmbedQuery(root, id, comment) {
+
+ // Increase total comment count by one.
+ root = update(root, {
+ asset: {
+ totalCommentCount: {$apply: (c) => c + 1},
+ },
+ });
+
+ if (root.comment) {
+ if (root.comment.parent) {
+ return update(root, {
+ comment: {
+ parent: {
+ $apply: (node) => findAndInsertComment(node, id, comment),
+ },
+ },
+ });
+ }
+ return update(root, {
+ comment: {
+ $apply: (node) => findAndInsertComment(node, id, comment),
+ },
+ });
+ }
+ return update(root, {
+ asset: {$apply: (asset) => findAndInsertComment(asset, id, comment)},
+ });
+}
+
+function findAndRemoveComment(parent, id) {
+ const [connectionField, countField] = parent.comments
+ ? ['comments', 'commentCount']
+ : ['replies', 'replyCount'];
+
+ const connection = parent[connectionField];
+ if (!connection) {
+ return parent;
+ }
+
+ let next = connection.nodes.filter((node) => node.id !== id);
+ if (next.length === connection.nodes.length) {
+ next = next.map((node) => findAndRemoveComment(node, id));
+ }
+ let changes = {
+ [connectionField]: {
+ nodes: {$set: next},
+ },
+ };
+
+ if (parent[countField] && next.length !== connection.nodes.length) {
+ changes[countField] = {$set: changes[countField] - 1};
+ }
+ return update(parent, changes);
+}
+
+export function removeCommentFromEmbedQuery(root, id) {
+
+ // Decrease total comment by one.
+ root = update(root, {
+ asset: {
+ totalCommentCount: {$apply: (c) => c - 1},
+ },
+ });
+
+ if (root.comment) {
+ if (root.comment.parent) {
+ return update(root, {
+ comment: {
+ parent: {
+ $apply: (node) => findAndRemoveComment(node, id),
+ },
+ },
+ });
+ }
+ return update(root, {
+ comment: {
+ $apply: (node) => findAndRemoveComment(node, id),
+ },
+ });
+ }
+ return update(root, {
+ asset: {$apply: (asset) => findAndRemoveComment(asset, id)},
+ });
+}
diff --git a/client/coral-plugin-comment-count/CommentCount.js b/client/coral-plugin-comment-count/CommentCount.js
index 8a25173875..cca99e7299 100644
--- a/client/coral-plugin-comment-count/CommentCount.js
+++ b/client/coral-plugin-comment-count/CommentCount.js
@@ -6,7 +6,7 @@ const name = 'coral-plugin-comment-count';
const CommentCount = ({count}) => {
return
- {`${count} ${count === 1 ? t('comment.comment') : t('comment_plural')}`}
+ {`${count} ${count === 1 ? t('comment_singular') : t('comment_plural')}`}
;
};
diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js
index d95a50f130..5e343e72ff 100644
--- a/client/coral-settings/containers/ProfileContainer.js
+++ b/client/coral-settings/containers/ProfileContainer.js
@@ -72,8 +72,8 @@ class ProfileContainer extends Component {
{t('framework.my_comments')}
- {me.comments.length
- ?
+ {me.comments.nodes.length
+ ?
: {t('user_no_comment')}
}
);
@@ -90,14 +90,16 @@ const withQuery = graphql(
username,
}
comments {
- id
- body
- asset {
+ nodes {
id
- title
- url
+ body
+ asset {
+ id
+ title
+ url
+ }
+ created_at
}
- created_at
}
}
}`
diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js
index d741992797..11f77df604 100644
--- a/graph/loaders/assets.js
+++ b/graph/loaders/assets.js
@@ -49,7 +49,8 @@ const getAssetsForMetrics = async ({loaders: {Actions, Comments}}) => {
const ids = actions.map(({item_id}) => item_id);
- return Comments.getByQuery({ids});
+ return Comments.getByQuery({ids})
+ .then((connection) => connection.nodes);
};
/**
diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js
index 92483c0f74..8360f88ce5 100644
--- a/graph/loaders/comments.js
+++ b/graph/loaders/comments.js
@@ -293,9 +293,24 @@ const getCommentsByQuery = async ({user}, {ids, statuses, asset_id, parent_id, a
}
}
- return comments
- .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1})
- .limit(limit);
+ let query = comments
+ .sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1});
+ if (limit) {
+ query = query.limit(limit + 1);
+ }
+ return query.then((nodes) => {
+ let hasNextPage = false;
+ if (limit && nodes.length > limit) {
+ hasNextPage = true;
+ nodes.splice(limit, 1);
+ }
+ return Promise.resolve({
+ startCursor: nodes.length ? nodes[0].created_at : null,
+ endCursor: nodes.length ? nodes[nodes.length - 1].created_at : null,
+ hasNextPage,
+ nodes,
+ });
+ });
};
/**
diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js
index 8acb2f8edc..7d234c7558 100644
--- a/graph/mutators/comment.js
+++ b/graph/mutators/comment.js
@@ -338,9 +338,9 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
const status = await resolveNewCommentStatus(context, {asset_id, body}, wordlist, settings);
// Execute the edit.
- await CommentsService.edit(id, context.user.id, {body, status});
+ const comment = await CommentsService.edit(id, context.user.id, {body, status});
- return {status};
+ return comment;
};
module.exports = (context) => {
diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js
index 747e2534ef..41710ba35d 100644
--- a/graph/resolvers/asset.js
+++ b/graph/resolvers/asset.js
@@ -1,11 +1,4 @@
const Asset = {
- lastComment({id}, _, {loaders: {Comments}}) {
- return Comments.getByQuery({
- asset_id: id,
- limit: 1,
- parent_id: null
- }).then((data) => data[0]);
- },
recentComments({id}, _, {loaders: {Comments}}) {
return Comments.genRecentComments.load(id);
},
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 1abb894b85..7f5982779d 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -76,7 +76,7 @@ type User {
ignoredUsers: [User!]
# returns all comments based on a query.
- comments(query: CommentsQuery): [Comment!]
+ comments(query: CommentsQuery): CommentConnection!
# reliable is the reference to a given user's Reliability. If the requesting
# user does not have permission to access the reliability, null will be
@@ -229,7 +229,7 @@ type Comment {
recentReplies: [Comment!]
# the replies that were made to the comment.
- replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, excludeIgnored: Boolean): [Comment!]
+ replies(sort: SORT_ORDER = CHRONOLOGICAL, limit: Int = 3, excludeIgnored: Boolean): CommentConnection!
# The count of replies on a comment.
replyCount(excludeIgnored: Boolean): Int
@@ -253,6 +253,22 @@ type Comment {
editing: EditInfo
}
+# CommentConnection represents a paginable subset of a comment list.
+type CommentConnection {
+
+ # Indicates that there are more comments after this subset.
+ hasNextPage: Boolean!
+
+ # Cursor of first comment in subset.
+ startCursor: Date
+
+ # Cursor of last comment in subset.
+ endCursor: Date
+
+ # Subset of comments.
+ nodes: [Comment!]!
+}
+
################################################################################
## Actions
################################################################################
@@ -462,14 +478,11 @@ type Asset {
# The URL that the asset is located on.
url: String
- # Returns last comment
- lastComment: Comment
-
# Returns recent comments
recentComments: [Comment!]
# The top level comments that are attached to the asset.
- comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10, excludeIgnored: Boolean): [Comment!]
+ comments(sort: SORT_ORDER = REVERSE_CHRONOLOGICAL, limit: Int = 10, excludeIgnored: Boolean): CommentConnection!
# The count of top level comments on the asset.
commentCount(excludeIgnored: Boolean): Int
@@ -576,7 +589,7 @@ type RootQuery {
asset(id: ID, url: String): Asset
# Comments returned based on a query.
- comments(query: CommentsQuery!): [Comment!]
+ comments(query: CommentsQuery!): CommentConnection
# Return the count of comments satisfied by the query. Note that this edge is
# expensive as it is not batched. Requires the `ADMIN` role.
diff --git a/locales/en.yml b/locales/en.yml
index 2f0b415117..ea32486172 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -27,6 +27,7 @@ en:
characters_remaining: "characters remaining"
comment_is_best: "This comment is one of the best"
comment_offensive: "This comment is offensive"
+ comment_singular: Comment
comment_plural: Comments
comment_post_banned_word: "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team."
comment_post_notif: "Your comment has been posted."
diff --git a/locales/es.yml b/locales/es.yml
index 9f1424e7a8..d658bcfcf3 100644
--- a/locales/es.yml
+++ b/locales/es.yml
@@ -31,6 +31,7 @@ es:
characters_remaining: "carácteres restantes"
comment_is_best: "Este comentario es uno de los mejores"
comment_offensive: "Este comentario es ofensivo"
+ comment_singular: Comentario
comment_plural: Comentarios
comment_post_banned_word: "Tu comentario contiene una o más palabras que no está\
n permitidas en nuestro espacio, por lo que no será publicado. Si crees que es\
diff --git a/test/server/graph/queries/asset.js b/test/server/graph/queries/asset.js
index 757b338743..a11155c47b 100644
--- a/test/server/graph/queries/asset.js
+++ b/test/server/graph/queries/asset.js
@@ -45,16 +45,25 @@ describe('graph.queries.asset', () => {
query assetCommentsQuery($id: ID!) {
asset(id: $id) {
comments(limit: 10) {
- id
- body
+ nodes {
+ id
+ body
+ created_at
+ }
+ startCursor
+ endCursor
+ hasNextPage
}
}
}
`;
const res = await graphql(schema, assetCommentsQuery, {}, context, {id: asset.id});
expect(res.erros).is.empty;
- const comments = res.data.asset.comments;
- expect(comments.length).to.equal(2);
+ const {nodes, startCursor, endCursor, hasNextPage} = res.data.asset.comments;
+ expect(nodes.length).to.equal(2);
+ expect(startCursor).to.equal(nodes[0].created_at);
+ expect(endCursor).to.equal(nodes[1].created_at);
+ expect(hasNextPage).to.be.false;
});
it('can query comments edge to exclude comments ignored by user', async () => {
@@ -65,7 +74,7 @@ describe('graph.queries.asset', () => {
asset_id: asset.id,
body: `hello there! ${String(Math.random()).slice(2)}`,
})));
-
+
// Add the second user to the list of ignored users.
context.user.ignoresUsers.push(users[1].id);
@@ -73,8 +82,10 @@ describe('graph.queries.asset', () => {
query assetCommentsQuery($id: ID!, $url: String!, $excludeIgnored: Boolean!) {
asset(id: $id, url: $url) {
comments(limit: 10, excludeIgnored: $excludeIgnored) {
- id
- body
+ nodes {
+ id
+ body
+ }
}
}
}
@@ -90,8 +101,8 @@ describe('graph.queries.asset', () => {
console.error(res.errors);
}
expect(res.errors).is.empty;
- const comments = res.data.asset.comments;
- expect(comments.length).to.equal(1);
+ const nodes = res.data.asset.comments.nodes;
+ expect(nodes.length).to.equal(1);
}
});