import React, {PropTypes} from 'react'; import LoadMore from './LoadMore'; import Comment from '../components/Comment'; import SuspendedAccount from './SuspendedAccount'; import Slot from 'coral-framework/components/Slot'; import InfoBox from 'coral-plugin-infobox/InfoBox'; import {can} from 'coral-framework/services/perms'; import {ModerationLink} from 'coral-plugin-moderation'; import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox'; import t, {timeago} from 'coral-framework/services/i18n'; import CommentBox from 'coral-plugin-commentbox/CommentBox'; import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import NewCount from './NewCount'; import {TransitionGroup} from 'react-transition-group'; 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 { isLoadingMore = false; constructor(props) { super(props); this.state = { ...resetCursors(this.state, props), keepCommentBox: false, }; } componentWillReceiveProps(next) { const {root: {asset: {comments: prevComments}}} = this.props; const {root: {asset: {comments: nextComments}}} = next; if (!prevComments && nextComments) { this.setState(resetCursors); return; } // Keep comment box when user was live suspended, banned, ... if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) { this.setState({keepCommentBox: true}); } 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(); } else { this.props.setActiveReplyBox(reactKey); } }; loadMoreComments = () => { if (!this.isLoadingMore) { this.isLoadingMore = true; this.props.loadMoreComments() .then(() => this.isLoadingMore = false) .catch((e) => { this.isLoadingMore = false; throw e; }); } } // 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; } userIsDegraged({auth: {user}} = this.props) { return !can(user, 'INTERACT_WITH_COMMUNITY'); } render() { const { commentClassNames, root: {asset, asset: {comments}, comment, me}, postComment, addNotification, postFlag, postDontAgree, deleteAction, showSignInDialog, addTag, ignoreUser, auth: {loggedIn, user}, removeTag, pluginProps, editName } = this.props; const {keepCommentBox} = this.state; const view = this.getVisibleComments(); const open = asset.closedAt === null; // even though the permalinked comment is the highlighted one, we're displaying its parent + replies const highlightedComment = comment && comment.parent ? comment.parent : comment; const banned = user && user.status === 'BANNED'; const temporarilySuspended = user && user.suspension.until && new Date(user.suspension.until) > new Date(); const commentIsIgnored = (comment) => { return ( me && me.ignoredUsers && me.ignoredUsers.find((u) => u.id === comment.user.id) ); }; const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox); return (
{open ?
{!banned && temporarilySuspended && {t( 'stream.temporarily_suspended', this.props.root.settings.organizationName, timeago(user.suspension.until) )} } {banned && } {showCommentBox && }
:

{asset.settings.closedMessage}

} {loggedIn && ( )}
{/* the highlightedComment is isolated after the user followed a permalink */} {highlightedComment ? :
{view.map((comment) => { return commentIsIgnored(comment) ? : ; })}
}
); } } Stream.propTypes = { addNotification: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, // dispatch action to add a tag to a comment addTag: PropTypes.func, // dispatch action to remove a tag from a comment removeTag: PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, // edit a comment, passed (id, asset_id, { body }) editComment: React.PropTypes.func }; export default Stream;