import * as classnames from "classnames"; import * as React from "react"; import Icon from "../application/icon.component"; import AddCommentForm from "./add_comment_form.component"; import DownVoteButton from "./down_vote_button.component"; import UpVoteButton from "./up_vote_button.component"; import { AddCommentFormCommentableFragment, AddCommentFormSessionFragment, CommentFragment } from "../support/schema"; import { NetworkStatus } from "apollo-client"; const { I18n } = require("react-i18nify"); interface CommentProps { comment: CommentFragment; session: | AddCommentFormSessionFragment & { user: any; } | null; articleClassName?: string; isRootComment?: boolean; votable?: boolean; rootCommentable: AddCommentFormCommentableFragment; orderBy: string; commentsMaxLength: number; } interface CommentState { showReplies: boolean; showReplyForm: boolean; } interface Dict { [key: string]: boolean | undefined; } /** * A single comment component with the author info and the comment's body * @class * @augments Component */ class Comment extends React.Component { public static defaultProps: any = { articleClassName: "comment", isRootComment: false, session: null, votable: false }; public commentNode: HTMLDivElement; constructor(props: CommentProps) { super(props); const { comment: { id } } = props; const isThreadHidden = !!this.getThreadsStorage()[id]; this.state = { showReplies: !isThreadHidden, showReplyForm: false }; } public componentDidMount() { const { comment: { id } } = this.props; const hash = document.location.hash; const regex = new RegExp(`#comment_${id}`); function scrollTo(element: Element, to: number, duration: number) { if (duration <= 0) { return; } const difference = to - element.scrollTop; const perTick = (difference / duration) * 10; setTimeout(() => { element.scrollTop = element.scrollTop + perTick; if (element.scrollTop === to) { return; } scrollTo(element, to, duration - 10); }, 10); } if (regex.test(hash)) { scrollTo(document.body, this.commentNode.offsetTop, 200); } if (window.$(document).foundation) { window.$(`#flagModalComment${id}`).foundation(); } } public getNodeReference = (commentNode: HTMLDivElement) => (this.commentNode = commentNode) public render(): JSX.Element { const { session, comment: { id, author, formattedBody, createdAt, formattedCreatedAt }, articleClassName } = this.props; let modalName = "loginModal"; if (session && session.user) { modalName = `flagModalComment${id}`; } let singleCommentUrl = `${window.location.pathname}?commentId=${id}`; if (window.location.search && window.location.search !== "") { singleCommentUrl = `${ window.location.pathname }${window.location.search.replace(/commentId=\d*/gi, `commentId=${id}`)}`; } return (
{this._renderAuthorReference()}
{this._renderFlagModal()}
{this._renderAlignmentBadge()}
{this._renderShowHideThreadButton()} {this._renderReplyButton()}
{this._renderVoteButtons()}
{this._renderReplies()} {this._renderAdditionalReplyButton()} {this._renderReplyForm()}
); } private toggleReplyForm = () => { const { showReplyForm } = this.state; this.setState({ showReplyForm: !showReplyForm }); } private getThreadsStorage = (): Dict => { const storage: Dict = JSON.parse(localStorage.hiddenCommentThreads || null) || {}; return storage; } private saveThreadsStorage = (id: string, state: boolean) => { const storage = this.getThreadsStorage(); storage[parseInt(id, 10)] = state; localStorage.hiddenCommentThreads = JSON.stringify(storage); } private toggleReplies = () => { const { comment: { id } } = this.props; const { showReplies } = this.state; const newState = !showReplies; this.saveThreadsStorage(id, !newState); this.setState({ showReplies: newState }); } private countReplies = (comment: CommentFragment): number => { const { comments } = comment; if (!comments) { return 0; } return ( comments.length + comments.map(this.countReplies).reduce((a: number, b: number) => a + b, 0) ); } /** * Render author information as a link to author's profile * @private * @returns {DOMElement} - Render a link with the author information */ private _renderAuthorReference() { const { comment: { author } } = this.props; if (author.profilePath === "") { return this._renderAuthor(); } return {this._renderAuthor()}; } /** * Render author information * @private * @returns {DOMElement} - Render all the author information */ private _renderAuthor() { const { comment: { author } } = this.props; if (author.deleted) { return this._renderDeletedAuthor(); } return this._renderActiveAuthor(); } /** * Render deleted author information * @private * @returns {DOMElement} - Render all the author information */ private _renderDeletedAuthor() { const { comment: { author } } = this.props; return (
author-avatar {I18n.t("components.comment.deleted_user")}
); } /** * Render active author information * @private * @returns {DOMElement} - Render all the author information */ private _renderActiveAuthor() { const { comment: { author } } = this.props; return (
author-avatar {author.name} {author.badge === "" || ( )} {author.nickname}
); } /** * Render reply button if user can reply the comment * @private * @returns {Void|DOMElement} - Render the reply button or not if user can reply */ private _renderReplyButton() { const { comment: { id, acceptsNewComments, userAllowedToComment }, session } = this.props; if (session && acceptsNewComments && userAllowedToComment) { return ( ); } return  ; } /** * Render additional reply button if user can reply the comment at the bottom of a conversation * @private * @returns {Void|DOMElement} - Render the reply button or not if user can reply */ private _renderAdditionalReplyButton() { const { comment: { id, acceptsNewComments, hasComments, userAllowedToComment }, session, isRootComment } = this.props; const { showReplies } = this.state; if (session && acceptsNewComments && userAllowedToComment) { if (hasComments && isRootComment && showReplies) { return (
); } } return null; } /** * Render show/hide thread button if comment is top-level and has children. * @private * @returns {Void|DOMElement} - Render the reply button or not */ private _renderShowHideThreadButton() { const { comment, isRootComment } = this.props; const { id, hasComments } = comment; const { showReplies } = this.state; if (hasComments && isRootComment) { return ( ); } return null; } /** * Render upVote and downVote buttons when the comment is votable * @private * @returns {Void|DOMElement} - Render the upVote and downVote buttons or not */ private _renderVoteButtons() { const { session, comment, votable, rootCommentable, orderBy } = this.props; const { comment: { userAllowedToComment } } = this.props; if (votable && userAllowedToComment) { return (
); } return  ; } /** * Render comment's comments alternating the css class * @private * @returns {Void|DomElement} - A wrapper element with comment's comments inside */ private _renderReplies() { const { comment: { id, hasComments, comments }, session, votable, articleClassName, rootCommentable, orderBy, commentsMaxLength } = this.props; const { showReplies } = this.state; let replyArticleClassName = "comment comment--nested"; if (articleClassName === "comment comment--nested") { replyArticleClassName = `${replyArticleClassName} comment--nested--alt`; } if (hasComments) { return (
{comments.map((reply: CommentFragment) => ( ))}
); } return null; } /** * Render reply form based on the current component state * @private * @returns {Void|ReactElement} - Render the AddCommentForm component or not */ private _renderReplyForm() { const { session, comment, rootCommentable, orderBy, commentsMaxLength } = this.props; const { showReplyForm } = this.state; const { comment: { userAllowedToComment } } = this.props; if (session && showReplyForm && userAllowedToComment) { return ( ); } return null; } /** * Render alignment badge if comment's alignment is 0 or -1 * @private * @returns {Void|DOMElement} - The alignment's badge or not */ private _renderAlignmentBadge() { const { comment: { alignment } } = this.props; const spanClassName = classnames("label alignment", { success: alignment === 1, alert: alignment === -1 }); let label = ""; if (alignment === 1) { label = I18n.t("components.comment.alignment.in_favor"); } else { label = I18n.t("components.comment.alignment.against"); } if (alignment === 1 || alignment === -1) { return ( {label}   ); } return null; } /** * Render a modal to report the comment. * @private * @return {Void|DOMElement} - The comment's report modal or not. */ private _renderFlagModal() { const { session, comment: { id, sgid, alreadyReported, userAllowedToComment } } = this.props; const authenticityToken = this._getAuthenticityToken(); const closeModal = () => { window.$(`#flagModalComment${id}`).foundation("close"); }; if (session && session.user && userAllowedToComment) { return (

{I18n.t("components.comment.report.title")}

{(() => { if (alreadyReported) { return (

{I18n.t("components.comment.report.already_reported")}

); } return [

{I18n.t("components.comment.report.description")}

,