mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 18:49:28 +08:00
Subscribe to comments
This commit is contained in:
@@ -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);
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const Subscription = {
|
||||
commentAdded(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentEdited(comment) {
|
||||
return comment;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -883,6 +883,7 @@ type RootMutation {
|
||||
|
||||
type Subscription {
|
||||
commentAdded(asset_id: ID!): Comment
|
||||
commentEdited(asset_id: ID!): Comment
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
+4
-1
@@ -104,7 +104,10 @@ const CommentSchema = new Schema({
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
},
|
||||
toJSON: {
|
||||
virtuals: true,
|
||||
},
|
||||
});
|
||||
|
||||
CommentSchema.virtual('edited').get(function() {
|
||||
|
||||
Reference in New Issue
Block a user