From 30d4562383599cda7db99ef542d7e01ec08a2314 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 01:18:41 +0700 Subject: [PATCH 1/6] Refactor pagination --- client/coral-admin/src/graphql/index.js | 54 ++-- .../routes/Moderation/components/LoadMore.js | 17 +- .../Moderation/components/Moderation.js | 2 +- .../Moderation/components/ModerationQueue.js | 17 +- .../Moderation/containers/Moderation.js | 63 +++-- .../src/components/Comment.js | 18 +- .../src/components/LoadMore.js | 28 +- .../src/components/Stream.js | 23 +- .../src/containers/Stream.js | 260 ++++++++++++------ .../coral-embed-stream/src/graphql/index.js | 126 ++------- .../coral-embed-stream/src/graphql/utils.js | 98 +++++++ .../containers/ProfileContainer.js | 18 +- graph/loaders/assets.js | 3 +- graph/loaders/comments.js | 21 +- graph/mutators/comment.js | 4 +- graph/resolvers/asset.js | 7 - graph/typeDefs.graphql | 18 +- test/server/graph/queries/asset.js | 29 +- 18 files changed, 447 insertions(+), 359 deletions(-) create mode 100644 client/coral-embed-stream/src/graphql/utils.js 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/Stream.js b/client/coral-embed-stream/src/components/Stream.js index d93044c9ee..9615ac0cdc 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,13 +54,9 @@ 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 + const firstCommentDate = comments.nodes[0] + ? comments.nodes[0].created_at : new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(); const commentIsIgnored = (comment) => { return ( @@ -137,7 +132,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} @@ -152,13 +147,13 @@ class Stream extends React.Component {
- {comments.map((comment) => { + {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..8cb880d079 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,18 +173,36 @@ 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) { id commentCount(excludeIgnored: $excludeIgnored) comments(limit: 10) { - id - replyCount(excludeIgnored: $excludeIgnored) + nodes { + id + replyCount(excludeIgnored: $excludeIgnored) + } } } } @@ -146,12 +210,38 @@ const LOAD_COMMENT_COUNTS_QUERY = gql` 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 { ...${getDefinitionName(Comment.fragments.comment)} + replyCount(excludeIgnored: $excludeIgnored) + replies(limit: 3, excludeIgnored: $excludeIgnored) { + nodes { + ...${getDefinitionName(Comment.fragments.comment)} + } + hasNextPage + startCursor + endCursor + } } + hasNextPage + startCursor + endCursor + } + } + ${Comment.fragments.comment} +`; + +const commentFragment = gql` + fragment CoralEmbedStream_Stream_comment on Comment { + ...${getDefinitionName(Comment.fragments.comment)} + replyCount(excludeIgnored: $excludeIgnored) + replies { + nodes { + ...${getDefinitionName(Comment.fragments.comment)} + } + hasNextPage + startCursor + endCursor } } ${Comment.fragments.comment} @@ -161,17 +251,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 +275,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)} + nodes { + ...CoralEmbedStream_Stream_comment } + hasNextPage + startCursor + endCursor } } me { @@ -218,7 +298,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..c1c9f82a48 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 {insertComment, removeComment} 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 } } } @@ -128,114 +140,26 @@ const extension = { action_summaries: [], tags, status: null, - id: 'pending' + 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 insertComment(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 removeComment(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..af5a355bc5 --- /dev/null +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -0,0 +1,98 @@ +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 insertComment(root, id, comment) { + 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 removeComment(root, id, comment) { + if (root.comment) { + if (root.comment.parent) { + return update(root, { + comment: { + parent: { + $apply: (node) => findAndRemoveComment(node, id, comment), + }, + }, + }); + } + return update(root, { + comment: { + $apply: (node) => findAndRemoveComment(node, id, comment), + }, + }); + } + return update(root, { + asset: {$apply: (asset) => findAndRemoveComment(asset, id, comment)}, + }); +} 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..6d0520a507 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,13 @@ type Comment { editing: EditInfo } +type CommentConnection { + hasNextPage: Boolean! + startCursor: Date + endCursor: Date + nodes: [Comment!]! +} + ################################################################################ ## Actions ################################################################################ @@ -462,14 +469,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 +580,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/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); } }); From ebdd586d1e48489965debb96f3f18fb613f0c870 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 18:47:07 +0700 Subject: [PATCH 2/6] Fix mutation funkiness on permlink view --- .../src/components/NewCount.js | 11 ++--------- .../coral-embed-stream/src/components/Stream.js | 10 ++-------- .../coral-embed-stream/src/containers/Stream.js | 14 +++++++++++--- client/coral-embed-stream/src/graphql/index.js | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 20 deletions(-) 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 357262bf3c..11f2b78195 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -54,10 +54,6 @@ class Stream extends React.Component { user.suspension.until && new Date(user.suspension.until) > new Date(); - // Find the created_at date of the first comment. If no comments exist, set the date to a week ago. - const firstCommentDate = comments.nodes[0] - ? comments.nodes[0].created_at - : new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(); const commentIsIgnored = (comment) => { return ( me && @@ -148,13 +144,11 @@ class Stream extends React.Component {
- {comments.nodes.map((comment) => { + {comments && comments.nodes.map((comment) => { return commentIsIgnored(comment) ? : { const descending = (a, b) => ascending(a, b) * -1; const LOAD_COMMENT_COUNTS_QUERY = gql` - query CoralEmbedStream_LoadCommentCounts($assetUrl: String, $assetId: ID, $excludeIgnored: Boolean) { + query CoralEmbedStream_LoadCommentCounts($assetUrl: String, , $commentId: ID!, $assetId: ID, $hasComment: Boolean!, $excludeIgnored: Boolean) { + comment(id: $commentId) @include(if: $hasComment) { + id + parent { + id + replyCount(excludeIgnored: $excludeIgnored) + } + replyCount(excludeIgnored: $excludeIgnored) + } asset(id: $assetId, url: $assetUrl) { id commentCount(excludeIgnored: $excludeIgnored) - comments(limit: 10) { + comments(limit: 10) @skip(if: $hasComment) { nodes { id replyCount(excludeIgnored: $excludeIgnored) @@ -277,7 +285,7 @@ const fragments = { } commentCount(excludeIgnored: $excludeIgnored) totalCommentCount(excludeIgnored: $excludeIgnored) - comments(limit: 10, excludeIgnored: $excludeIgnored) { + comments(limit: 10, excludeIgnored: $excludeIgnored) @skip(if: $hasComment) { nodes { ...CoralEmbedStream_Stream_comment } diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index c1c9f82a48..3e5bcd2595 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -128,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 }, @@ -140,6 +143,19 @@ const extension = { action_summaries: [], tags, status: null, + 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()}`, } } From 5245755d49a01eab1ff2f2be57efa85a5782c6e3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 19:41:08 +0700 Subject: [PATCH 3/6] Add some comments --- client/coral-embed-stream/src/graphql/index.js | 6 +++--- client/coral-embed-stream/src/graphql/utils.js | 10 +++++----- graph/typeDefs.graphql | 9 +++++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 3e5bcd2595..1b4ac500b2 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -2,7 +2,7 @@ import {gql} from 'react-apollo'; import {add} from 'coral-framework/services/graphqlRegistry'; import update from 'immutability-helper'; import uuid from 'uuid/v4'; -import {insertComment, removeComment} from './utils'; +import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils'; const extension = { fragments: { @@ -165,7 +165,7 @@ const extension = { if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') { return prev; } - return insertComment(prev, parent_id, comment); + return insertCommentIntoEmbedQuery(prev, parent_id, comment); }, } }), @@ -175,7 +175,7 @@ const extension = { if (!['PREMOD', 'REJECTED'].includes(comment.status)) { return null; } - return removeComment(prev, comment.id); + 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 index af5a355bc5..3df4c2e519 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -27,7 +27,7 @@ function findAndInsertComment(parent, id, comment) { }); } -export function insertComment(root, id, comment) { +export function insertCommentIntoEmbedQuery(root, id, comment) { if (root.comment) { if (root.comment.parent) { return update(root, { @@ -75,24 +75,24 @@ function findAndRemoveComment(parent, id) { return update(parent, changes); } -export function removeComment(root, id, comment) { +export function removeCommentFromEmbedQuery(root, id) { if (root.comment) { if (root.comment.parent) { return update(root, { comment: { parent: { - $apply: (node) => findAndRemoveComment(node, id, comment), + $apply: (node) => findAndRemoveComment(node, id), }, }, }); } return update(root, { comment: { - $apply: (node) => findAndRemoveComment(node, id, comment), + $apply: (node) => findAndRemoveComment(node, id), }, }); } return update(root, { - asset: {$apply: (asset) => findAndRemoveComment(asset, id, comment)}, + asset: {$apply: (asset) => findAndRemoveComment(asset, id)}, }); } diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 6d0520a507..7f5982779d 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -253,10 +253,19 @@ 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!]! } From 33eff78a64c71ec4561433940fd59effb40a3d15 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 19:49:53 +0700 Subject: [PATCH 4/6] Always request comment id --- client/coral-embed-stream/src/containers/Stream.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 51a838f067..f2f5f67db4 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -220,10 +220,12 @@ const LOAD_MORE_QUERY = gql` query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) { 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 @@ -241,10 +243,12 @@ const LOAD_MORE_QUERY = gql` 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 From d9acbbad967a22078e2a47d4723f611a0363e8d8 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 22:37:25 +0700 Subject: [PATCH 5/6] Fix translation bug in CommentCount --- client/coral-plugin-comment-count/CommentCount.js | 2 +- locales/en.yml | 1 + locales/es.yml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) 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/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\ From fa08450185298d0ac3e47630351cd00f6b6387ed Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 22:39:05 +0700 Subject: [PATCH 6/6] Increase/Decrease totalCommentCount after mutation --- client/coral-embed-stream/src/graphql/utils.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index 3df4c2e519..1634324796 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -28,6 +28,14 @@ function findAndInsertComment(parent, 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, { @@ -76,6 +84,14 @@ function findAndRemoveComment(parent, id) { } 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, {