From 30d4562383599cda7db99ef542d7e01ec08a2314 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 01:18:41 +0700 Subject: [PATCH 01/11] 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 | 272 +++++++++++------- .../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, 453 insertions(+), 365 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 740af20a1..59a6d7c8f 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 c90d3cd14..612629647 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 956575ce4..036f8e0c1 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 234c9cbcb..490c2a985 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 63af5a6cd..537aa1d65 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 83a175874..e3f0137b4 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 c9c8a9704..31001fcd2 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 d93044c9e..9615ac0cd 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 f3cb1bfe9..8cb880d07 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) { - - // 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) { - - // 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'); - } - - 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'); - - // 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 - - updatedAsset = { - ...oldData, - asset: { - ...oldData.asset, - comments: newComments ? [...new_top_level_comments.reverse(), ...oldData.asset.comments] - : [...oldData.asset.comments, ...new_top_level_comments] - } - }; + updateQuery: (prev, {fetchMoreResult:{comments}}) => { + if (!comments.nodes.length) { + return prev; } - return updatedAsset; - } + 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) + }, + }, + }); + + // 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)}, + }); + } + + return update(prev, { + asset: { + comments: { + nodes: { + $apply: (nodes) => nodes.map( + (node) => node.id !== parent_id + ? node + : updateNode(node) + ) + }, + }, + }, + }); + }, + }); + } + + 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) + }, + }, + }, + }); + }, + }); + }; + + 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 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 3b40e0028..c1c9f82a4 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; + CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => { + if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') { + return prev; } - - 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] - } - }; - } - - 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 000000000..af5a355bc --- /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 d95a50f13..5e343e72f 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 d74199279..11f77df60 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 92483c0f7..8360f88ce 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 8acb2f8ed..7d234c755 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 747e2534e..41710ba35 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 1abb894b8..6d0520a50 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 757b33874..a11155c47 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 2a3941552d458c629577df095f7d8d656b08797b Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Wed, 31 May 2017 13:38:14 -0400 Subject: [PATCH 02/11] Update README with Trust info --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a514d1ee1..9390f19fd 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,12 @@ sign and verify tokens via a `HS256` algorithm. Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for alternative methods of loading configuration during development. -## Plugins +### Using Trust + +Talk ships with core components we call "Trust". This allow Talk to automatically do special things based on intelligence built into the system. + +Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/talk/services/karma.js). -Talk ships with a plugin architecture that allows developers to significantly extend the platform. For more information, see our [plugin documentation](docs/PLUGINS.md). ## Supported Browsers From c172260d74945e8c3dc94e34574542143868772a Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Wed, 31 May 2017 14:09:53 -0400 Subject: [PATCH 03/11] Fix link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9390f19fd..fc2aba463 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ alternative methods of loading configuration during development. Talk ships with core components we call "Trust". This allow Talk to automatically do special things based on intelligence built into the system. -Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/talk/services/karma.js). +Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/services/karma.js). ## Supported Browsers From ac8e723e7584138ff80e9285b244d3284cef589a Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Wed, 31 May 2017 15:21:02 -0400 Subject: [PATCH 04/11] Copy changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc2aba463..1b2c1590f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ alternative methods of loading configuration during development. ### Using Trust -Talk ships with core components we call "Trust". This allow Talk to automatically do special things based on intelligence built into the system. +Talk ships with core components we call "Trust". This allows Talk to automate certain actions based on previous user behavior. Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/services/karma.js). From 93d8e0015bf5a23a2feecfae0f606b50fd38e863 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Wed, 31 May 2017 15:27:23 -0400 Subject: [PATCH 05/11] Remove space --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1b2c1590f..48055e326 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,6 @@ Talk ships with core components we call "Trust". This allows Talk to automate ce Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/services/karma.js). - ## Supported Browsers ### Web From 9bd75a56704304e6bb1c9938dbce0b9d9b81da03 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Wed, 31 May 2017 16:55:14 -0400 Subject: [PATCH 06/11] Bump version in prep for 1.8.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 388cc2d41..8c783ecb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "1.7.0", + "version": "1.8.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "scripts": { From ebdd586d1e48489965debb96f3f18fb613f0c870 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Thu, 1 Jun 2017 18:47:07 +0700 Subject: [PATCH 07/11] 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 3cde3786b..43dce13e0 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 357262bf3..11f2b7819 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 c1c9f82a4..3e5bcd259 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 08/11] 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 3e5bcd259..1b4ac500b 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 af5a355bc..3df4c2e51 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 6d0520a50..7f5982779 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 09/11] 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 51a838f06..f2f5f67db 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 10/11] 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 8a2517387..cca99e729 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 2f0b41511..ea3248617 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 9f1424e7a..d658bcfcf 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 11/11] 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 3df4c2e51..163432479 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, {