Subscribe to comments

This commit is contained in:
Chi Vinh Le
2017-06-05 20:51:08 +07:00
parent 3c96b7eb1e
commit 6b0cdae183
18 changed files with 418 additions and 217 deletions
@@ -2,7 +2,6 @@ import {pym} from 'coral-framework';
import * as actions from '../constants/stream';
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
export const setCommentCountCache = (amount) => ({type: actions.SET_COMMENT_COUNT_CACHE, amount});
function removeParam(key, sourceURL) {
let rtn = sourceURL.split('?')[0];
@@ -26,6 +26,41 @@ import {getEditableUntilDate} from './util';
import styles from './Comment.css';
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF');
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second newest comment in
// the current reply list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
const replies = props.comment.replies;
if (replies && replies.nodes.length) {
const idCursors = [replies.nodes[replies.nodes.length - 1].id];
if (replies.nodes.length >= 2) {
idCursors.push(replies.nodes[replies.nodes.length - 2].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const replies = props.comment.replies;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = replies.nodes.findIndex((node) => node.id === idCursors[0]);
const prevInLine = replies.nodes[index - 1];
if (prevInLine) {
idCursors.push(prevInLine.id);
}
}
return {idCursors};
}
// hold actions links (e.g. Reply) along the comment footer
const ActionButton = ({children}) => {
@@ -37,6 +72,7 @@ const ActionButton = ({children}) => {
};
class Comment extends React.Component {
constructor(props) {
super(props);
@@ -49,7 +85,29 @@ class Comment extends React.Component {
// Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment)
isEditing: false,
replyBoxVisible: false,
...resetCursors({}, props),
};
}
componentWillReceiveProps(next) {
const {comment: {replies: prevReplies}} = this.props;
const {comment: {replies: nextReplies}} = next;
if (
prevReplies && nextReplies &&
nextReplies.nodes.length < prevReplies.nodes.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextReplies.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextReplies.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
}
}
}
static propTypes = {
@@ -67,6 +125,7 @@ class Comment extends React.Component {
addNotification: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
liveUpdates: PropTypes.bool.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
@@ -127,6 +186,45 @@ class Comment extends React.Component {
}
}
loadNewReplies = () => {
const {replies, replyCount, id} = this.props.comment;
if (replyCount > replies.nodes.length) {
this.props.loadMore(id).then(() => {
this.setState(resetCursors(this.state, this.props));
});
return;
}
this.setState(resetCursors);
};
// getVisibileReplies returns a list containing comments
// which were authored by `userId` or comes before the `idCursor`.
getVisibileReplies() {
const {comment: {replies}, currentUser, liveUpdates} = this.props;
const idCursor = this.state.idCursors[0];
const userId = currentUser ? currentUser.id : null;
if (!replies) {
return [];
}
if (liveUpdates) {
return replies.nodes;
}
const view = [];
let pastCursor = false;
replies.nodes.forEach((comment) => {
if (idCursor && !pastCursor || comment.user.id === userId) {
view.push(comment);
}
if (comment.id === idCursor) {
pastCursor = true;
}
});
return view;
}
componentDidMount() {
this._isMounted = true;
if (this.editWindowExpiryTimeout) {
@@ -162,19 +260,20 @@ class Comment extends React.Component {
highlighted,
postFlag,
postDontAgree,
loadMore,
setActiveReplyBox,
activeReplyBox,
deleteAction,
addCommentTag,
removeCommentTag,
ignoreUser,
liveUpdates,
disableReply,
commentIsIgnored,
maxCharCount,
charCountEnable
} = this.props;
const view = this.getVisibileReplies();
const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary(
'DontAgreeActionSummary',
@@ -369,46 +468,45 @@ class Comment extends React.Component {
assetId={asset.id}
/>
: null}
{comment.replies &&
comment.replies.nodes.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postComment={postComment}
editComment={this.props.editComment}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply}
/>;
})}
{comment.replies &&
<div className="coral-load-more-replies">
<LoadMore
topLevel={false}
replyCount={comment.replyCount}
moreComments={comment.replyCount > comment.replies.nodes.length}
loadMore={() => loadMore(comment.id)}
/>
</div>}
{view.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <Comment
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
activeReplyBox={activeReplyBox}
addNotification={addNotification}
parentId={comment.id}
postComment={postComment}
editComment={this.props.editComment}
depth={depth + 1}
asset={asset}
highlighted={highlighted}
currentUser={currentUser}
postFlag={postFlag}
deleteAction={deleteAction}
addCommentTag={addCommentTag}
removeCommentTag={removeCommentTag}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
showSignInDialog={showSignInDialog}
liveUpdates={liveUpdates}
reactKey={reply.id}
key={reply.id}
comment={reply}
/>;
})}
<div className="coral-load-more-replies">
<LoadMore
topLevel={false}
replyCount={comment.replyCount}
moreComments={comment.replyCount > view.length}
loadMore={this.loadNewReplies}
/>
</div>
</div>
);
}
@@ -2,22 +2,14 @@ import React, {PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
const onLoadMoreClick = ({loadMore, commentCount, setCommentCountCache}) => (e) => {
e.preventDefault();
setCommentCountCache(commentCount);
loadMore();
};
const NewCount = (props) => {
const newComments = props.commentCount - props.commentCountCache;
const NewCount = ({count, loadMore}) => {
return <div className='coral-new-comments coral-load-more'>
{
props.commentCountCache && newComments > 0 ?
<button onClick={onLoadMoreClick(props)}>
{newComments === 1
? t('framework.new_count', newComments, t('framework.comment'))
: t('framework.new_count', newComments, t('framework.comments'))}
count ?
<button onClick={loadMore}>
{count === 1
? t('framework.new_count', count, t('framework.comment'))
: t('framework.new_count', count, t('framework.comments'))}
</button>
: null
}
@@ -25,8 +17,7 @@ const NewCount = (props) => {
};
NewCount.propTypes = {
commentCount: PropTypes.number.isRequired,
commentCountCache: PropTypes.number,
count: PropTypes.number.isRequired,
loadMore: PropTypes.func.isRequired,
};
@@ -1,6 +1,5 @@
import React, {PropTypes} from 'react';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
import Comment from '../containers/Comment';
import SuspendedAccount from './SuspendedAccount';
@@ -13,9 +12,81 @@ import {ModerationLink} from 'coral-plugin-moderation';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import t, {timeago} from 'coral-framework/services/i18n';
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
// resetCursors will return the id cursors of the first and second comment of
// the current comment list. The cursors are used to dertermine which
// comments to show. The spare cursor functions as a backup in case one
// of the comments gets deleted.
function resetCursors(state, props) {
const comments = props.root.asset.comments;
if (comments && comments.nodes.length) {
const idCursors = [comments.nodes[0].id];
if (comments.nodes[1]) {
idCursors.push(comments.nodes[1].id);
}
return {idCursors};
}
return {idCursors: []};
}
// invalidateCursor is called whenever a comment is removed which is referenced
// by one of the 2 id cursors. It returns a new set of id cursors calculated
// using the help of the backup cursor.
function invalidateCursor(invalidated, state, props) {
const alt = invalidated === 1 ? 0 : 1;
const comments = props.root.asset.comments;
const idCursors = [];
if (state.idCursors[alt]) {
idCursors.push(state.idCursors[alt]);
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
const nextInLine = comments.nodes[index + 1];
if (nextInLine) {
idCursors.push(nextInLine.id);
}
}
return {idCursors};
}
class Stream extends React.Component {
constructor(props) {
super(props);
this.state = resetCursors(this.state, props);
}
componentWillReceiveProps(next) {
const {root: {asset: {comments: prevComments}}} = this.props;
const {root: {asset: {comments: nextComments}}} = next;
if (!prevComments && nextComments) {
this.setState(resetCursors);
return;
}
if (
prevComments && nextComments &&
nextComments.nodes.length < prevComments.nodes.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
this.setState(invalidateCursor(0, this.state, next));
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
this.setState(invalidateCursor(1, this.state, next));
}
}
}
viewNewComments = () => {
this.setState(resetCursors);
};
setActiveReplyBox = (reactKey) => {
if (!this.props.auth.user) {
this.props.showSignInDialog();
@@ -24,6 +95,30 @@ class Stream extends React.Component {
}
};
// getVisibileComments returns a list containing comments
// which were authored by current user or comes after the `idCursor`.
getVisibleComments() {
const {root: {asset: {comments}}, auth: {user}} = this.props;
const idCursor = this.state.idCursors[0];
const userId = user ? user.id : null;
if (!comments) {
return [];
}
const view = [];
let pastCursor = false;
comments.nodes.forEach((comment) => {
if (comment.id === idCursor) {
pastCursor = true;
}
if (pastCursor || comment.user.id === userId) {
view.push(comment);
}
});
return view;
}
render() {
const {
root: {asset, asset: {comments}, comment, me},
@@ -38,9 +133,9 @@ class Stream extends React.Component {
pluginProps,
ignoreUser,
auth: {loggedIn, user},
commentCountCache,
editName
} = this.props;
const view = this.getVisibleComments();
const open = asset.closedAt === null;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
@@ -97,8 +192,6 @@ class Stream extends React.Component {
postComment={this.props.postComment}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
setCommentCountCache={this.props.setCommentCountCache}
commentCountCache={commentCountCache}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
@@ -139,16 +232,15 @@ class Stream extends React.Component {
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
liveUpdates={true}
/>
: <div>
<NewCount
commentCount={asset.commentCount}
commentCountCache={commentCountCache}
setCommentCountCache={this.props.setCommentCountCache}
loadMore={this.props.loadNewComments}
count={comments.nodes.length - view.length}
loadMore={this.viewNewComments}
/>
<div className="embed__stream">
{comments && comments.nodes.map((comment) => {
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
@@ -178,6 +270,7 @@ class Stream extends React.Component {
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={this.props.editComment}
liveUpdates={false}
/>;
})}
</div>
@@ -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';
@@ -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
);
@@ -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(
@@ -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);
},
}
}),
+43 -8
View File
@@ -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);
}
+10 -10
View File
@@ -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;
@@ -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;
@@ -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});
+6
View File
@@ -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;
};
+1 -1
View File
@@ -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
+3
View File
@@ -1,6 +1,9 @@
const Subscription = {
commentAdded(comment) {
return comment;
},
commentEdited(comment) {
return comment;
}
};
+5
View File
@@ -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
},
}),
});
/**
+1
View File
@@ -883,6 +883,7 @@ type RootMutation {
type Subscription {
commentAdded(asset_id: ID!): Comment
commentEdited(asset_id: ID!): Comment
}
################################################################################
+4 -1
View File
@@ -104,7 +104,10 @@ const CommentSchema = new Schema({
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
}
},
toJSON: {
virtuals: true,
},
});
CommentSchema.virtual('edited').get(function() {