- {comments && comments.nodes.map((comment) => {
+ {view.map((comment) => {
return commentIsIgnored(comment)
?
: ;
})}
diff --git a/client/coral-embed-stream/src/constants/stream.js b/client/coral-embed-stream/src/constants/stream.js
index cb17edb2f..4be4dc125 100644
--- a/client/coral-embed-stream/src/constants/stream.js
+++ b/client/coral-embed-stream/src/constants/stream.js
@@ -1,5 +1,3 @@
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
-export const SET_COMMENT_COUNT_CACHE = 'SET_COMMENT_COUNT_CACHE';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
-export const NEW_COMMENT_COUNT_POLL_INTERVAL = 20000;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js
index 05dd9fb91..74d4e9d70 100644
--- a/client/coral-embed-stream/src/containers/Embed.js
+++ b/client/coral-embed-stream/src/containers/Embed.js
@@ -14,7 +14,7 @@ import Embed from '../components/Embed';
import Stream from './Stream';
import {setActiveTab} from '../actions/embed';
-import {setCommentCountCache, viewAllComments} from '../actions/stream';
+import {viewAllComments} from '../actions/stream';
const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
@@ -33,13 +33,6 @@ class EmbedContainer extends React.Component {
// TODO: remove asset data from redux store.
fetchAssetSuccess(nextProps.root.asset);
-
- const {setCommentCountCache, commentCountCache} = this.props;
- const {asset} = nextProps.root;
-
- if (commentCountCache === -1) {
- setCommentCountCache(asset.commentCount);
- }
}
}
@@ -87,7 +80,6 @@ export const withEmbedQuery = withQuery(EMBED_QUERY, {
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
- commentCountCache: state.stream.commentCountCache,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
@@ -103,7 +95,6 @@ const mapDispatchToProps = (dispatch) =>
setActiveTab,
viewAllComments,
fetchAssetSuccess,
- setCommentCountCache
},
dispatch
);
diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js
index f2f5f67db..ea42b5bd6 100644
--- a/client/coral-embed-stream/src/containers/Stream.js
+++ b/client/coral-embed-stream/src/containers/Stream.js
@@ -2,7 +2,7 @@ import React from 'react';
import {gql, compose} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
-import {NEW_COMMENT_COUNT_POLL_INTERVAL, ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
+import {ADDTL_COMMENTS_ON_LOAD_MORE} from '../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree, withDeleteAction,
withAddCommentTag, withRemoveCommentTag, withIgnoreUser, withEditComment,
@@ -11,23 +11,68 @@ import update from 'immutability-helper';
import {notificationActions, authActions} from 'coral-framework';
import {editName} from 'coral-framework/actions/user';
-import {setCommentCountCache, setActiveReplyBox} from '../actions/stream';
+import {setActiveReplyBox} from '../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import {withFragments} from 'coral-framework/hocs';
import {getDefinitionName} from 'coral-framework/utils';
+import {findCommentInEmbedQuery, insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from '../graphql/utils';
const {showSignInDialog} = authActions;
const {addNotification} = notificationActions;
class StreamContainer extends React.Component {
- getCounts = (variables) => {
- return this.props.data.fetchMore({
- query: LOAD_COMMENT_COUNTS_QUERY,
- variables,
+ subscribeToUpdates = () => {
+ this.props.data.subscribeToMore({
+ document: COMMENTS_EDITED_SUBSCRIPTION,
+ variables: {
+ assetId: this.props.root.asset.id,
+ },
+ updateQuery: (prev, {subscriptionData: {data: {commentEdited}}}) => {
- // Apollo requires this, even though we don't use it...
- updateQuery: (data) => data,
+ // Ignore mutations from me.
+ // TODO: need way to detect mutations created by this client, and allow mutations from other clients.
+ if (this.props.auth.user && commentEdited.user.id === this.props.auth.user.id) {
+ return prev;
+ }
+
+ // Exit when comment is not in the query.
+ if (!findCommentInEmbedQuery(prev, commentEdited.id)) {
+ return prev;
+ }
+
+ if (['PREMOD', 'REJECTED'].includes(commentEdited.status)) {
+ return removeCommentFromEmbedQuery(prev, commentEdited.id);
+ }
+ },
+ });
+ this.props.data.subscribeToMore({
+ document: COMMENTS_ADDED_SUBSCRIPTION,
+ variables: {
+ assetId: this.props.root.asset.id,
+ },
+ updateQuery: (prev, {subscriptionData: {data: {commentAdded}}}) => {
+
+ // Ignore mutations from me.
+ // TODO: need way to detect mutations created by this client, and allow mutations from other clients.
+ if (this.props.auth.user && commentAdded.user.id === this.props.auth.user.id) {
+ return prev;
+ }
+
+ // Exit if author is ignored.
+ if (
+ this.props.root.me &&
+ this.props.root.me.ignoredUsers.some(({id}) => id === commentAdded.user.id)) {
+ return prev;
+ }
+
+ // Exit when comment is already in the query.
+ if (findCommentInEmbedQuery(prev, commentAdded.id)) {
+ return prev;
+ }
+
+ return insertCommentIntoEmbedQuery(prev, commentAdded);
+ }
});
};
@@ -95,38 +140,6 @@ class StreamContainer extends React.Component {
});
}
- 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,
@@ -157,15 +170,7 @@ class StreamContainer extends React.Component {
};
componentDidMount() {
- if (this.props.previousTab) {
- this.props.data.refetch()
- .then(({data: {asset: {commentCount}}}) => {
- return this.props.setCommentCountCache(commentCount);
- });
- }
- this.countPoll = setInterval(() => {
- this.getCounts(this.props.data.variables);
- }, NEW_COMMENT_COUNT_POLL_INTERVAL);
+ this.subscribeToUpdates();
}
componentWillUnmount() {
@@ -177,7 +182,6 @@ class StreamContainer extends React.Component {
{...this.props}
loadMore={this.loadMore}
loadMoreComments={this.loadMoreComments}
- loadNewComments={this.loadNewComments}
loadNewReplies={this.loadNewReplies}
/>;
}
@@ -191,26 +195,47 @@ const ascending = (a, b) => {
return 0;
};
-const descending = (a, b) => ascending(a, b) * -1;
+const commentFragment = gql`
+ fragment CoralEmbedStream_Stream_comment on Comment {
+ id
+ ...${getDefinitionName(Comment.fragments.comment)}
+ replyCount(excludeIgnored: $excludeIgnored)
+ replies {
+ nodes {
+ id
+ ...${getDefinitionName(Comment.fragments.comment)}
+ }
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ }
+ ${Comment.fragments.comment}
+`;
-const LOAD_COMMENT_COUNTS_QUERY = gql`
- query CoralEmbedStream_LoadCommentCounts($assetUrl: String, , $commentId: ID!, $assetId: ID, $hasComment: Boolean!, $excludeIgnored: Boolean) {
- comment(id: $commentId) @include(if: $hasComment) {
- id
+const COMMENTS_ADDED_SUBSCRIPTION = gql`
+ subscription onCommentAdded($assetId: ID!, $excludeIgnored: Boolean){
+ commentAdded(asset_id: $assetId){
parent {
id
- replyCount(excludeIgnored: $excludeIgnored)
}
- replyCount(excludeIgnored: $excludeIgnored)
+ ...CoralEmbedStream_Stream_comment
}
- asset(id: $assetId, url: $assetUrl) {
+ }
+ ${commentFragment}
+`;
+
+const COMMENTS_EDITED_SUBSCRIPTION = gql`
+ subscription onCommentEdited($assetId: ID!){
+ commentEdited(asset_id: $assetId){
id
- commentCount(excludeIgnored: $excludeIgnored)
- comments(limit: 10) @skip(if: $hasComment) {
- nodes {
- id
- replyCount(excludeIgnored: $excludeIgnored)
- }
+ body
+ status
+ editing {
+ edited
+ }
+ user {
+ id
}
}
}
@@ -241,24 +266,6 @@ const LOAD_MORE_QUERY = gql`
${Comment.fragments.comment}
`;
-const commentFragment = gql`
- fragment CoralEmbedStream_Stream_comment on Comment {
- id
- ...${getDefinitionName(Comment.fragments.comment)}
- replyCount(excludeIgnored: $excludeIgnored)
- replies {
- nodes {
- id
- ...${getDefinitionName(Comment.fragments.comment)}
- }
- hasNextPage
- startCursor
- endCursor
- }
- }
- ${Comment.fragments.comment}
-`;
-
const fragments = {
root: gql`
fragment CoralEmbedStream_Stream_root on RootQuery {
@@ -331,7 +338,6 @@ const mapDispatchToProps = (dispatch) =>
addNotification,
setActiveReplyBox,
editName,
- setCommentCountCache,
}, dispatch);
export default compose(
diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js
index 1b4ac500b..04225eb96 100644
--- a/client/coral-embed-stream/src/graphql/index.js
+++ b/client/coral-embed-stream/src/graphql/index.js
@@ -91,6 +91,9 @@ const extension = {
edited
editableUntil
}
+ parent {
+ id
+ }
}
`,
},
@@ -144,6 +147,9 @@ const extension = {
tags,
status: null,
replyCount: 0,
+ parent: parent_id
+ ? {id: parent_id}
+ : null,
replies: {
__typename: 'CommentConnection',
nodes: [],
@@ -165,7 +171,7 @@ const extension = {
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
return prev;
}
- return insertCommentIntoEmbedQuery(prev, parent_id, comment);
+ return insertCommentIntoEmbedQuery(prev, comment);
},
}
}),
diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js
index 163432479..3ab9c9e3b 100644
--- a/client/coral-embed-stream/src/graphql/utils.js
+++ b/client/coral-embed-stream/src/graphql/utils.js
@@ -1,11 +1,11 @@
import update from 'immutability-helper';
-function findAndInsertComment(parent, id, comment) {
+function findAndInsertComment(parent, comment) {
const [connectionField, countField, action] = parent.comments
? ['comments', 'commentCount', '$unshift']
: ['replies', 'replyCount', '$push'];
- if (!id || parent.id === id) {
+ if (!comment.parent || parent.id === comment.parent.id) {
return update(parent, {
[connectionField]: {
nodes: {[action]: [comment]},
@@ -21,13 +21,13 @@ function findAndInsertComment(parent, id, comment) {
[connectionField]: {
nodes: {
$apply: (nodes) =>
- nodes.map((node) => findAndInsertComment(node, id, comment))
+ nodes.map((node) => findAndInsertComment(node, comment))
},
},
});
}
-export function insertCommentIntoEmbedQuery(root, id, comment) {
+export function insertCommentIntoEmbedQuery(root, comment) {
// Increase total comment count by one.
root = update(root, {
@@ -41,19 +41,19 @@ export function insertCommentIntoEmbedQuery(root, id, comment) {
return update(root, {
comment: {
parent: {
- $apply: (node) => findAndInsertComment(node, id, comment),
+ $apply: (node) => findAndInsertComment(node, comment),
},
},
});
}
return update(root, {
comment: {
- $apply: (node) => findAndInsertComment(node, id, comment),
+ $apply: (node) => findAndInsertComment(node, comment),
},
});
}
return update(root, {
- asset: {$apply: (asset) => findAndInsertComment(asset, id, comment)},
+ asset: {$apply: (asset) => findAndInsertComment(asset, comment)},
});
}
@@ -78,7 +78,7 @@ function findAndRemoveComment(parent, id) {
};
if (parent[countField] && next.length !== connection.nodes.length) {
- changes[countField] = {$set: changes[countField] - 1};
+ changes[countField] = {$set: parent[countField] - 1};
}
return update(parent, changes);
}
@@ -112,3 +112,38 @@ export function removeCommentFromEmbedQuery(root, id) {
asset: {$apply: (asset) => findAndRemoveComment(asset, id)},
});
}
+
+function findComment(nodes, callback) {
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (callback(node)) {
+ return node;
+ }
+ if (node.replies) {
+ const find = findComment(node.replies.nodes, callback);
+ if (find){
+ return find;
+ }
+ }
+ }
+ return false;
+}
+
+export function findCommentInEmbedQuery(root, callbackOrId) {
+ let callback = callbackOrId;
+ if (typeof callbackOrId === 'string') {
+ callback = (node) => node.id === callbackOrId;
+ }
+ if (root.comment) {
+ if (callback(root.comment)) {
+ return root.comment;
+ }
+ if (root.comment.parent && callback(root.comment.parent)) {
+ return root.comment.parent;
+ }
+ }
+ if (!root.asset.comments) {
+ return false;
+ }
+ return findComment(root.asset.comments.nodes, callback);
+}
diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js
index c77bc9848..fd906c75e 100644
--- a/client/coral-framework/services/client.js
+++ b/client/coral-framework/services/client.js
@@ -1,16 +1,16 @@
import ApolloClient, {addTypename} from 'apollo-client';
import {networkInterface} from './transport';
+import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
-// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
+const wsClient = new SubscriptionClient(`ws://${location.host}/api/v1/live`, {
+ reconnect: true
+});
+
+const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
+ networkInterface,
+ wsClient,
+);
-// TODO: replace absolute reference with something loaded from the store/page.
-// const wsClient = new SubscriptionClient('ws://localhost:3000/api/v1/live', {
-// reconnect: true
-// });
-// const networkInterface = addGraphQLSubscriptions(
-// getNetworkInterface(),
-// wsClient,
-// );
export const client = new ApolloClient({
connectToDevTools: true,
addTypename: true,
@@ -21,7 +21,7 @@ export const client = new ApolloClient({
}
return null;
},
- networkInterface
+ networkInterface: networkInterfaceWithSubscriptions,
});
export default client;
diff --git a/client/coral-framework/services/subscriptions.js b/client/coral-framework/services/subscriptions.js
deleted file mode 100644
index 818a6fb33..000000000
--- a/client/coral-framework/services/subscriptions.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import {print} from 'graphql-tag/printer';
-
-// quick way to add the subscribe and unsubscribe functions to the network interface
-const addGraphQLSubscriptions = (networkInterface, wsClient) => {
- return Object.assign(networkInterface, {
- subscribe: (request, handler) => wsClient.subscribe({
- query: print(request.query),
- variables: request.variables,
- }, handler),
- unsubscribe: (id) => {
- wsClient.unsubscribe(id);
- },
- });
-};
-
-export default addGraphQLSubscriptions;
diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js
index 3cd48d2b0..5105667ed 100644
--- a/client/coral-plugin-commentbox/CommentBox.js
+++ b/client/coral-plugin-commentbox/CommentBox.js
@@ -36,18 +36,10 @@ class CommentBox extends React.Component {
}
};
}
- static get defaultProps() {
- return {
- setCommentCountCache: () => {}
- };
- }
postComment = ({body}) => {
const {
commentPostedHandler,
postComment,
- setCommentCountCache,
- commentCountCache,
- isReply,
assetId,
parentId,
addNotification,
@@ -60,8 +52,6 @@ class CommentBox extends React.Component {
...this.props.commentBox
};
- !isReply && setCommentCountCache(commentCountCache + 1);
-
// Execute preSubmit Hooks
this.state.hooks.preSubmit.forEach((hook) => hook());
@@ -74,19 +64,12 @@ class CommentBox extends React.Component {
notifyForNewCommentStatus(addNotification, postedComment.status);
- if (postedComment.status === 'REJECTED') {
- !isReply && setCommentCountCache(commentCountCache);
- } else if (postedComment.status === 'PREMOD') {
- !isReply && setCommentCountCache(commentCountCache);
- }
-
if (commentPostedHandler) {
commentPostedHandler();
}
})
.catch((err) => {
console.error(err);
- !isReply && setCommentCountCache(commentCountCache);
});
this.setState({postedCount: this.state.postedCount + 1});
@@ -190,7 +173,6 @@ CommentBox.propTypes = {
authorId: PropTypes.string.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
- setCommentCountCache: PropTypes.func,
};
const mapStateToProps = ({commentBox}) => ({commentBox});
diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js
index 7d234c755..88c391078 100644
--- a/graph/mutators/comment.js
+++ b/graph/mutators/comment.js
@@ -340,6 +340,12 @@ const edit = async (context, {id, asset_id, edit: {body}}) => {
// Execute the edit.
const comment = await CommentsService.edit(id, context.user.id, {body, status});
+ if (context.pubsub) {
+
+ // Publish the edited comment via the subscription.
+ context.pubsub.publish('commentEdited', comment);
+ }
+
return comment;
};
diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js
index 2e8412169..33ec0ef60 100644
--- a/graph/resolvers/comment.js
+++ b/graph/resolvers/comment.js
@@ -49,7 +49,7 @@ const Comment = {
},
async editing(comment, _, {loaders: {Settings}}) {
const settings = await Settings.load();
- const editableUntil = new Date(Number(comment.created_at) + settings.editCommentWindowLength);
+ const editableUntil = new Date(Number(new Date(comment.created_at)) + settings.editCommentWindowLength);
return {
edited: comment.edited,
editableUntil: editableUntil
diff --git a/graph/resolvers/subscription.js b/graph/resolvers/subscription.js
index b3f5e655c..a593c1eb6 100644
--- a/graph/resolvers/subscription.js
+++ b/graph/resolvers/subscription.js
@@ -1,6 +1,9 @@
const Subscription = {
commentAdded(comment) {
return comment;
+ },
+ commentEdited(comment) {
+ return comment;
}
};
diff --git a/graph/subscriptions.js b/graph/subscriptions.js
index 2eb676cc4..0442c237d 100644
--- a/graph/subscriptions.js
+++ b/graph/subscriptions.js
@@ -25,6 +25,11 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
filter: (comment) => comment.asset_id === args.asset_id
},
}),
+ commentEdited: (options, args) => ({
+ commentEdited: {
+ filter: (comment) => comment.asset_id === args.asset_id
+ },
+ }),
});
/**
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index aed610ad7..187e5bba5 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -883,6 +883,7 @@ type RootMutation {
type Subscription {
commentAdded(asset_id: ID!): Comment
+ commentEdited(asset_id: ID!): Comment
}
################################################################################
diff --git a/models/comment.js b/models/comment.js
index 27a2bfae3..1ffeb3fd6 100644
--- a/models/comment.js
+++ b/models/comment.js
@@ -104,7 +104,10 @@ const CommentSchema = new Schema({
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
- }
+ },
+ toJSON: {
+ virtuals: true,
+ },
});
CommentSchema.virtual('edited').get(function() {