From 6b0cdae183e93e6b5dbd18e68f65ff62205407fd Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 5 Jun 2017 20:51:08 +0700 Subject: [PATCH 01/17] Subscribe to comments --- .../coral-embed-stream/src/actions/stream.js | 1 - .../src/components/Comment.js | 180 ++++++++++++++---- .../src/components/NewCount.js | 23 +-- .../src/components/Stream.js | 111 ++++++++++- .../src/constants/stream.js | 2 - .../src/containers/Embed.js | 11 +- .../src/containers/Stream.js | 172 +++++++++-------- .../coral-embed-stream/src/graphql/index.js | 8 +- .../coral-embed-stream/src/graphql/utils.js | 51 ++++- client/coral-framework/services/client.js | 20 +- .../coral-framework/services/subscriptions.js | 16 -- client/coral-plugin-commentbox/CommentBox.js | 18 -- graph/mutators/comment.js | 6 + graph/resolvers/comment.js | 2 +- graph/resolvers/subscription.js | 3 + graph/subscriptions.js | 5 + graph/typeDefs.graphql | 1 + models/comment.js | 5 +- 18 files changed, 418 insertions(+), 217 deletions(-) delete mode 100644 client/coral-framework/services/subscriptions.js diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index 81b57a0fe..6a9e47838 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -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]; diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 48392cc51..2c69f3979 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -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) - ? - : ; - })} - {comment.replies && -
- comment.replies.nodes.length} - loadMore={() => loadMore(comment.id)} - /> -
} + {view.map((reply) => { + return commentIsIgnored(reply) + ? + : ; + })} +
+ view.length} + loadMore={this.loadNewReplies} + /> +
); } diff --git a/client/coral-embed-stream/src/components/NewCount.js b/client/coral-embed-stream/src/components/NewCount.js index 43dce13e0..1c135ab9b 100644 --- a/client/coral-embed-stream/src/components/NewCount.js +++ b/client/coral-embed-stream/src/components/NewCount.js @@ -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
{ - props.commentCountCache && newComments > 0 ? - : 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, }; diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 11f2b7819..725d5c488 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -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} /> :
- {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() { From 9993350b7ee6ec05b2e14c7c272bbdaf840a6a8f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 00:54:12 +0700 Subject: [PATCH 02/17] Animate flash for new comments --- .../src/components/Comment.css | 10 +++++ .../src/components/Comment.js | 39 ++++++++++++++++--- .../src/components/Stream.js | 7 ++-- .../src/containers/Embed.js | 2 +- package.json | 1 + yarn.lock | 2 +- 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 5fab57d95..b0fca75c8 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -97,3 +97,13 @@ .Wizard .textAlignRight { text-align: right; } + +@keyframes enter { + 0% {background-color: rgba(0, 0, 0, 0);} + 50% {background-color: rgba(255,255,0, 0.2);} + 100% {background-color: rgba(0, 0, 0, 0);} +} + +.enter { + animation: enter 1000ms; +} diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 2c69f3979..c853e5018 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -8,6 +8,9 @@ import Content from 'coral-plugin-commentcontent/CommentContent'; import PubDate from 'coral-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; import FlagComment from 'coral-plugin-flags/FlagComment'; +import {TransitionGroup} from 'react-transition-group'; +import cn from 'classnames'; + import { BestButton, IfUserCanModifyBest, @@ -19,7 +22,6 @@ import Slot from 'coral-framework/components/Slot'; import LoadMore from './LoadMore'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import {TopRightMenu} from './TopRightMenu'; -import classnames from 'classnames'; import {EditableCommentContent} from './EditableCommentContent'; import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import {getEditableUntilDate} from './util'; @@ -85,9 +87,9 @@ 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, + animateEnter: false, ...resetCursors({}, props), }; - } componentWillReceiveProps(next) { @@ -110,6 +112,28 @@ class Comment extends React.Component { } } + componentWillAppear(callback) { + callback(); + } + componentWillEnter(callback) { + callback(); + const userId = this.props.currentUser ? this.props.currentUser.id : null; + if (this.props.comment.id.indexOf('pending') >= 0) { + return; + } + if (userId && this.props.comment.user.id === userId) { + + // This comment was just added by currentUser. + if (Date.now() - Number(new Date(this.props.comment.created_at)) < 30 * 1000) { + return; + } + } + this.setState({animateEnter: true}); + } + componentWillLeave(callback) { + callback(); + } + static propTypes = { reactKey: PropTypes.string.isRequired, @@ -325,7 +349,7 @@ class Comment extends React.Component { return (
@@ -362,17 +386,17 @@ class Comment extends React.Component { (comment.user.id === currentUser.id)) /* User can edit/delete their own comment for a short window after posting */ - ? + ? { commentIsStillEditable(comment) && Edit } /* TopRightMenu allows currentUser to ignore other users' comments */ - : + : : null} + + {view.map((reply) => { return commentIsIgnored(reply) ? @@ -499,6 +525,7 @@ class Comment extends React.Component { comment={reply} />; })} +
nodes.some((node) => node.id === id); @@ -239,7 +240,7 @@ class Stream extends React.Component { count={comments.nodes.length - view.length} loadMore={this.viewNewComments} /> -
+ {view.map((comment) => { return commentIsIgnored(comment) ? @@ -273,7 +274,7 @@ class Stream extends React.Component { liveUpdates={false} />; })} -
+ pym.scrollParentToChildEl('coralStream'), 0); diff --git a/package.json b/package.json index fe87f17f5..c3c08a2db 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "react-apollo": "^1.1.0", "react-recaptcha": "^2.2.6", "react-toastify": "^1.5.0", + "react-transition-group": "^1.1.3", "recompose": "^0.23.1", "redis": "^2.7.1", "resolve": "^1.3.2", diff --git a/yarn.lock b/yarn.lock index 9855d91e8..6a069df84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6909,7 +6909,7 @@ react-toastify@^1.5.0: prop-types "^15.5.8" react-transition-group "^1.1.2" -react-transition-group@^1.1.2: +react-transition-group@^1.1.2, react-transition-group@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.1.3.tgz#5e02cf6e44a863314ff3c68a0c826c2d9d70b221" dependencies: From 3ad17f008feed884a410c95fbe27a5ab759ec751 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 01:44:17 +0700 Subject: [PATCH 03/17] Refactor fetchMore updaters --- .../src/components/Comment.js | 2 + .../src/containers/Stream.js | 75 ++--------- .../coral-embed-stream/src/graphql/utils.js | 119 +++++++++++------- 3 files changed, 87 insertions(+), 109 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index c853e5018..daefc4c4e 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -115,6 +115,7 @@ class Comment extends React.Component { componentWillAppear(callback) { callback(); } + componentWillEnter(callback) { callback(); const userId = this.props.currentUser ? this.props.currentUser.id : null; @@ -130,6 +131,7 @@ class Comment extends React.Component { } this.setState({animateEnter: true}); } + componentWillLeave(callback) { callback(); } diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index ea42b5bd6..fa21c1ed2 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -7,7 +7,6 @@ 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'; @@ -16,7 +15,12 @@ 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'; +import { + findCommentInEmbedQuery, + insertCommentIntoEmbedQuery, + removeCommentFromEmbedQuery, + insertFetchedCommentsIntoEmbedQuery, +} from '../graphql/utils'; const {showSignInDialog} = authActions; const {addNotification} = notificationActions; @@ -92,50 +96,7 @@ class StreamContainer extends React.Component { excludeIgnored: this.props.data.variables.excludeIgnored, }, updateQuery: (prev, {fetchMoreResult:{comments}}) => { - if (!comments.nodes.length) { - return prev; - } - - 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) - ) - }, - }, - }, - }); + return insertFetchedCommentsIntoEmbedQuery(prev, comments, parent_id); }, }); } @@ -152,19 +113,7 @@ class StreamContainer extends React.Component { 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}, - }, - }, - }); + return insertFetchedCommentsIntoEmbedQuery(prev, comments); }, }); }; @@ -187,14 +136,6 @@ class StreamContainer extends React.Component { } } -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 commentFragment = gql` fragment CoralEmbedStream_Stream_comment on Comment { id diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index 3ab9c9e3b..d2a557f23 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -1,7 +1,29 @@ import update from 'immutability-helper'; +function applyToCommentsOrigin(root, callback) { + if (root.comment) { + if (root.comment.parent) { + return update(root, { + comment: { + parent: { + $apply: (node) => callback(node), + }, + }, + }); + } + return update(root, { + comment: { + $apply: (node) => callback(node), + }, + }); + } + return update(root, { + asset: {$apply: (asset) => callback(asset)}, + }); +} + function findAndInsertComment(parent, comment) { - const [connectionField, countField, action] = parent.comments + const [connectionField, countField, action] = parent.__typename === 'Asset' ? ['comments', 'commentCount', '$unshift'] : ['replies', 'replyCount', '$push']; @@ -35,30 +57,11 @@ export function insertCommentIntoEmbedQuery(root, comment) { totalCommentCount: {$apply: (c) => c + 1}, }, }); - - if (root.comment) { - if (root.comment.parent) { - return update(root, { - comment: { - parent: { - $apply: (node) => findAndInsertComment(node, comment), - }, - }, - }); - } - return update(root, { - comment: { - $apply: (node) => findAndInsertComment(node, comment), - }, - }); - } - return update(root, { - asset: {$apply: (asset) => findAndInsertComment(asset, comment)}, - }); + return applyToCommentsOrigin(root, (origin) => findAndInsertComment(origin, comment)); } function findAndRemoveComment(parent, id) { - const [connectionField, countField] = parent.comments + const [connectionField, countField] = parent.__typename === 'Asset' ? ['comments', 'commentCount'] : ['replies', 'replyCount']; @@ -91,26 +94,7 @@ export function removeCommentFromEmbedQuery(root, id) { totalCommentCount: {$apply: (c) => c - 1}, }, }); - - if (root.comment) { - if (root.comment.parent) { - return update(root, { - comment: { - parent: { - $apply: (node) => findAndRemoveComment(node, id), - }, - }, - }); - } - return update(root, { - comment: { - $apply: (node) => findAndRemoveComment(node, id), - }, - }); - } - return update(root, { - asset: {$apply: (asset) => findAndRemoveComment(asset, id)}, - }); + return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id)); } function findComment(nodes, callback) { @@ -147,3 +131,54 @@ export function findCommentInEmbedQuery(root, callbackOrId) { } return findComment(root.asset.comments.nodes, callback); } + +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; +}; + +function findAndInsertFetchedComments(parent, comments, parent_id) { + const isAsset = parent.__typename === 'Asset'; + const connectionField = isAsset ? 'comments' : 'replies'; + if (!parent_id && connectionField === 'comments' || parent.id === parent_id) { + return update(parent, { + [connectionField]: { + hasNextPage: {$set: comments.hasNextPage}, + endCursor: {$set: comments.endCursor}, + nodes: {$apply: (nodes) => { + if (isAsset) { + return nodes.concat(comments.nodes); + } + return nodes + .concat(comments.nodes.filter( + (comment) => !nodes.some((node) => node.id === comment.id) + )) + .sort(ascending); + }}, + }, + }); + } + + const connection = parent[connectionField]; + if (!connection) { + return parent; + } + return update(parent, { + [connectionField]: { + nodes: { + $apply: (nodes) => + nodes.map((node) => findAndInsertFetchedComments(node, comments, parent_id)) + }, + }, + }); +} + +export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) { + if (!comments.nodes.length) { + return root; + } + return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id)); +} From 2be73d1df25af1a75ec6a54eb146838c65437103 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 02:10:56 +0700 Subject: [PATCH 04/17] Unsubscribe on unmount --- .../src/containers/Stream.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index fa21c1ed2..893c15dc1 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -26,8 +26,10 @@ const {showSignInDialog} = authActions; const {addNotification} = notificationActions; class StreamContainer extends React.Component { - subscribeToUpdates = () => { - this.props.data.subscribeToMore({ + subscriptions = []; + + subscribeToUpdates() { + const sub1 = this.props.data.subscribeToMore({ document: COMMENTS_EDITED_SUBSCRIPTION, variables: { assetId: this.props.root.asset.id, @@ -50,7 +52,8 @@ class StreamContainer extends React.Component { } }, }); - this.props.data.subscribeToMore({ + + const sub2 = this.props.data.subscribeToMore({ document: COMMENTS_ADDED_SUBSCRIPTION, variables: { assetId: this.props.root.asset.id, @@ -78,7 +81,14 @@ class StreamContainer extends React.Component { return insertCommentIntoEmbedQuery(prev, commentAdded); } }); - }; + + this.subscriptions.push(sub1, sub2); + } + + unsubscribe() { + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.subscriptions = []; + } loadNewReplies = (parent_id) => { const comment = this.props.root.comment @@ -123,6 +133,7 @@ class StreamContainer extends React.Component { } componentWillUnmount() { + this.unsubscribe(); clearInterval(this.countPoll); } From 2d0eec15c185f6833c64c21db3d879eb92ddf729 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 02:25:32 +0700 Subject: [PATCH 05/17] Readd falsely deleted part --- client/coral-embed-stream/src/containers/Stream.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 893c15dc1..593fa1eb0 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -129,6 +129,9 @@ class StreamContainer extends React.Component { }; componentDidMount() { + if (this.props.previousTab) { + this.props.data.refetch(); + } this.subscribeToUpdates(); } From 056ddc7010bb3ac64832aae559ae76bae53fc04a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 02:25:42 +0700 Subject: [PATCH 06/17] Meaningful comment --- client/coral-embed-stream/src/components/Comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index daefc4c4e..e99755f9c 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -224,7 +224,7 @@ class Comment extends React.Component { }; // getVisibileReplies returns a list containing comments - // which were authored by `userId` or comes before the `idCursor`. + // which were authored by current user or comes before the `idCursor`. getVisibileReplies() { const {comment: {replies}, currentUser, liveUpdates} = this.props; const idCursor = this.state.idCursors[0]; From 728d523b9e6d8d53101eb9ca9f571eaad9a5d28a Mon Sep 17 00:00:00 2001 From: David Erwin Date: Mon, 5 Jun 2017 17:25:27 -0400 Subject: [PATCH 07/17] Begin adding css override classes --- client/coral-embed-stream/src/components/Comment.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 48392cc51..f56c2ec0e 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -226,7 +226,7 @@ class Comment extends React.Component { return (
@@ -234,15 +234,15 @@ class Comment extends React.Component {
- + {isStaff(comment.tags) ? Staff : null} {commentIsBest(comment) ? : null } - - + + { (comment.editing && comment.editing.edited) ?  (Edited) From ad4c4a0a5b3e203ed1ff7655983893d458d152d6 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Mon, 5 Jun 2017 16:46:58 -0600 Subject: [PATCH 08/17] fix for active tab not showing. user box classNames --- client/coral-embed-stream/src/components/Embed.js | 8 +++++--- client/coral-ui/components/TabBar.js | 2 +- plugins/coral-plugin-auth/client/components/UserBox.js | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 264b1b2aa..bf1aa2292 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -38,13 +38,15 @@ export default class Embed extends React.Component { const {asset: {totalCommentCount}} = this.props.root; const {user} = this.props.auth; + console.log('activeTab', activeTab); + return (
- - {t('framework.my_profile')} - {t('framework.configure_stream')} + + {t('framework.my_profile')} + {t('framework.configure_stream')} {commentId &&
); From 124ea19b525715ac6c626417e1646c11325ac6a3 Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Tue, 6 Jun 2017 12:03:12 -0400 Subject: [PATCH 15/17] Update en.yml --- locales/en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 03fad3bc5..90182abb7 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -202,7 +202,7 @@ en: flag_reason: "Reason for reporting (Optional)" flag_username: "Report username" framework: - banned_account_msg: "Your account is currently suspended. This means that you cannot Like Report or write comments. Please contact us if you have any questions." + banned_account_msg: "Your account is currently banned. This means that you cannot Like, Report, or write comments. Please contact us if you have any questions." because_you_ignored: "Because you ignored the following commenters, their comments are hidden." comment: comment comment_is_ignored: "This comment is hidden because you ignored this user." @@ -322,7 +322,7 @@ en: bio: bio cancel: "Cancel" days: "{0} days" - description_0: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}." + description_0: "Would you like to temporarily suspend this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}." description_1: "Suspending this user will temporarily disable their account and hide all of their comments on the site." description_notify: "Suspending this user will temporarily disable their account and hide all of their comments on the site." description_reject: "Would you like to temporarily ban this user because of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}." From afdcdff51e058524aa8ed728cdbda323eddc320d Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 6 Jun 2017 23:18:37 +0700 Subject: [PATCH 16/17] Handle ignored user case --- client/coral-embed-stream/src/components/Comment.js | 11 +++++++++-- client/coral-embed-stream/src/components/LoadMore.js | 3 +++ client/coral-embed-stream/src/containers/Stream.js | 2 +- client/coral-embed-stream/src/graphql/utils.js | 3 --- locales/en.yml | 1 + locales/es.yml | 1 + 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 177ac457d..730c2ca4a 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -204,6 +204,11 @@ class Comment extends React.Component { } } + hasIgnoredReplies() { + return this.props.comment.replies && + this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply)); + } + loadNewReplies = () => { const {replies, replyCount, id} = this.props.comment; if (replyCount > replies.nodes.length) { @@ -292,6 +297,8 @@ class Comment extends React.Component { } = this.props; const view = this.getVisibileReplies(); + const hasMoreComments = comment.replies && (comment.replies.hasNextPage || comment.replies.nodes.length > view.length); + const replyCount = this.hasIgnoredReplies() ? '' : comment.replyCount; const flagSummary = getActionSummary('FlagActionSummary', comment); const dontAgreeSummary = getActionSummary( 'DontAgreeActionSummary', @@ -524,8 +531,8 @@ class Comment extends React.Component {
view.length} + replyCount={replyCount} + moreComments={hasMoreComments} loadMore={this.loadNewReplies} />
diff --git a/client/coral-embed-stream/src/components/LoadMore.js b/client/coral-embed-stream/src/components/LoadMore.js index 31001fcd2..ca2816d10 100644 --- a/client/coral-embed-stream/src/components/LoadMore.js +++ b/client/coral-embed-stream/src/components/LoadMore.js @@ -9,6 +9,9 @@ class LoadMore extends React.Component { } replyCountFormat = (count) => { + if (!count) { + return t('framework.view_all_replies_unknown_number'); + } if (count === 1) { return t('framework.view_reply'); } diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 360ee2ef9..6b4ae71ed 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -159,7 +159,7 @@ const commentFragment = gql` id ...${getDefinitionName(Comment.fragments.comment)} replyCount(excludeIgnored: $excludeIgnored) - replies { + replies(excludeIgnored: $excludeIgnored) { nodes { id ...${getDefinitionName(Comment.fragments.comment)} diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js index d2a557f23..eff1e2439 100644 --- a/client/coral-embed-stream/src/graphql/utils.js +++ b/client/coral-embed-stream/src/graphql/utils.js @@ -177,8 +177,5 @@ function findAndInsertFetchedComments(parent, comments, parent_id) { } export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) { - if (!comments.nodes.length) { - return root; - } return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id)); } diff --git a/locales/en.yml b/locales/en.yml index 03fad3bc5..3f3b7fc0d 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -224,6 +224,7 @@ en: success_bio_update: "Your biography has been updated" success_name_update: "Your username has been updated" success_update_settings: "The changes you have made have been applied to the comment stream on this article" + view_all_replies_unknown_number: "view all replies" view_all_replies: "view {0} replies" view_all_replies_initial: "view all {0} replies" view_more_comments: "view more comments" diff --git a/locales/es.yml b/locales/es.yml index 9e7a8bcba..0802a7604 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -223,6 +223,7 @@ es: success_bio_update: "Tu biografia fue actualizada" success_name_update: "Tu nombre de usuario ha sido actualizado" success_update_settings: "La configuración de este articulo fue actualizada" + view_all_replies_unknown_number: "ver todas las respuestas" view_all_replies: "ver {0} respuestas" view_all_replies_initial: "ver todas las {0} respuestas" view_more_comments: "Ver más comentarios" From 6fcd003e76e495492d3cb554996f155c322cfe40 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 6 Jun 2017 10:32:14 -0600 Subject: [PATCH 17/17] remove console.log --- client/coral-embed-stream/src/components/Embed.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index bf1aa2292..145e8340a 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -38,8 +38,6 @@ export default class Embed extends React.Component { const {asset: {totalCommentCount}} = this.props.root; const {user} = this.props.auth; - console.log('activeTab', activeTab); - return (