import { colorList, getCommentParentId, myAuth, myAuthRequired, showScores, } from "@utils/app"; import { futureDaysToUnixTime, numToSI } from "@utils/helpers"; import { amCommunityCreator, canAdmin, canMod, isAdmin, isBanned, isMod, } from "@utils/roles"; import classNames from "classnames"; import isBefore from "date-fns/isBefore"; import parseISO from "date-fns/parseISO"; import subMinutes from "date-fns/subMinutes"; import { Component, InfernoNode, linkEvent } from "inferno"; import { Link } from "inferno-router"; import { AddAdmin, AddModToCommunity, BanFromCommunity, BanPerson, BlockPerson, CommentId, CommentReplyView, CommentView, CommunityModeratorView, CreateComment, CreateCommentLike, CreateCommentReport, DeleteComment, DistinguishComment, EditComment, GetComments, Language, MarkCommentReplyAsRead, MarkPersonMentionAsRead, PersonMentionView, PersonView, PurgeComment, PurgePerson, RemoveComment, SaveComment, TransferCommunity, } from "lemmy-js-client"; import deepEqual from "lodash.isequal"; import { commentTreeMaxDepth } from "../../config"; import { BanType, CommentNodeI, CommentViewType, PurgeType, VoteContentType, } from "../../interfaces"; import { mdToHtml, mdToHtmlNoImages } from "../../markdown"; import { I18NextService, UserService } from "../../services"; import { setupTippy } from "../../tippy"; import { Icon, PurgeWarning, Spinner } from "../common/icon"; import { MomentTime } from "../common/moment-time"; import { VoteButtonsCompact } from "../common/vote-buttons"; import { CommunityLink } from "../community/community-link"; import { PersonListing } from "../person/person-listing"; import { CommentForm } from "./comment-form"; import { CommentNodes } from "./comment-nodes"; interface CommentNodeState { showReply: boolean; showEdit: boolean; showRemoveDialog: boolean; removeReason?: string; showBanDialog: boolean; removeData: boolean; banReason?: string; banExpireDays?: number; banType: BanType; showPurgeDialog: boolean; purgeReason?: string; purgeType: PurgeType; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; showConfirmAppointAsMod: boolean; showConfirmAppointAsAdmin: boolean; collapsed: boolean; viewSource: boolean; showAdvanced: boolean; showReportDialog: boolean; reportReason?: string; createOrEditCommentLoading: boolean; upvoteLoading: boolean; downvoteLoading: boolean; saveLoading: boolean; readLoading: boolean; blockPersonLoading: boolean; deleteLoading: boolean; removeLoading: boolean; distinguishLoading: boolean; banLoading: boolean; addModLoading: boolean; addAdminLoading: boolean; transferCommunityLoading: boolean; fetchChildrenLoading: boolean; reportLoading: boolean; purgeLoading: boolean; } interface CommentNodeProps { node: CommentNodeI; moderators?: CommunityModeratorView[]; admins?: PersonView[]; noBorder?: boolean; noIndent?: boolean; viewOnly?: boolean; locked?: boolean; markable?: boolean; showContext?: boolean; showCommunity?: boolean; enableDownvotes?: boolean; viewType: CommentViewType; allLanguages: Language[]; siteLanguages: number[]; hideImages?: boolean; finished: Map; onSaveComment(form: SaveComment): void; onCommentReplyRead(form: MarkCommentReplyAsRead): void; onPersonMentionRead(form: MarkPersonMentionAsRead): void; onCreateComment(form: EditComment | CreateComment): void; onEditComment(form: EditComment | CreateComment): void; onCommentVote(form: CreateCommentLike): void; onBlockPerson(form: BlockPerson): void; onDeleteComment(form: DeleteComment): void; onRemoveComment(form: RemoveComment): void; onDistinguishComment(form: DistinguishComment): void; onAddModToCommunity(form: AddModToCommunity): void; onAddAdmin(form: AddAdmin): void; onBanPersonFromCommunity(form: BanFromCommunity): void; onBanPerson(form: BanPerson): void; onTransferCommunity(form: TransferCommunity): void; onFetchChildren?(form: GetComments): void; onCommentReport(form: CreateCommentReport): void; onPurgePerson(form: PurgePerson): void; onPurgeComment(form: PurgeComment): void; } export class CommentNode extends Component { state: CommentNodeState = { showReply: false, showEdit: false, showRemoveDialog: false, showBanDialog: false, removeData: false, banType: BanType.Community, showPurgeDialog: false, purgeType: PurgeType.Person, collapsed: false, viewSource: false, showAdvanced: false, showConfirmTransferSite: false, showConfirmTransferCommunity: false, showConfirmAppointAsMod: false, showConfirmAppointAsAdmin: false, showReportDialog: false, createOrEditCommentLoading: false, upvoteLoading: false, downvoteLoading: false, saveLoading: false, readLoading: false, blockPersonLoading: false, deleteLoading: false, removeLoading: false, distinguishLoading: false, banLoading: false, addModLoading: false, addAdminLoading: false, transferCommunityLoading: false, fetchChildrenLoading: false, reportLoading: false, purgeLoading: false, }; constructor(props: any, context: any) { super(props, context); this.handleReplyCancel = this.handleReplyCancel.bind(this); } get commentView(): CommentView { return this.props.node.comment_view; } get commentId(): CommentId { return this.commentView.comment.id; } componentWillReceiveProps( nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps> ): void { if (!deepEqual(this.props, nextProps)) { this.setState({ showReply: false, showEdit: false, showRemoveDialog: false, showBanDialog: false, removeData: false, banType: BanType.Community, showPurgeDialog: false, purgeType: PurgeType.Person, collapsed: false, viewSource: false, showAdvanced: false, showConfirmTransferSite: false, showConfirmTransferCommunity: false, showConfirmAppointAsMod: false, showConfirmAppointAsAdmin: false, showReportDialog: false, createOrEditCommentLoading: false, upvoteLoading: false, downvoteLoading: false, saveLoading: false, readLoading: false, blockPersonLoading: false, deleteLoading: false, removeLoading: false, distinguishLoading: false, banLoading: false, addModLoading: false, addAdminLoading: false, transferCommunityLoading: false, fetchChildrenLoading: false, reportLoading: false, purgeLoading: false, }); } } render() { const node = this.props.node; const cv = this.commentView; const purgeTypeText = this.state.purgeType == PurgeType.Comment ? I18NextService.i18n.t("purge_comment") : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`; const canMod_ = canMod( cv.creator.id, this.props.moderators, this.props.admins ); const canModOnSelf = canMod( cv.creator.id, this.props.moderators, this.props.admins, UserService.Instance.myUserInfo, true ); const canAdmin_ = canAdmin(cv.creator.id, this.props.admins); const canAdminOnSelf = canAdmin( cv.creator.id, this.props.admins, UserService.Instance.myUserInfo, true ); const isMod_ = isMod(cv.creator.id, this.props.moderators); const isAdmin_ = isAdmin(cv.creator.id, this.props.admins); const amCommunityCreator_ = amCommunityCreator( cv.creator.id, this.props.moderators ); const moreRepliesBorderColor = this.props.node.depth ? colorList[this.props.node.depth % colorList.length] : colorList[0]; const showMoreChildren = this.props.viewType == CommentViewType.Tree && !this.state.collapsed && node.children.length == 0 && node.comment_view.counts.child_count > 0; return (
  • {cv.comment.distinguished && ( )} {this.isPostCreator && (
    {I18NextService.i18n.t("op").toUpperCase()}
    )} {isMod_ && (
    {I18NextService.i18n.t("mod")}
    )} {isAdmin_ && (
    {I18NextService.i18n.t("admin")}
    )} {cv.creator.bot_account && (
    {I18NextService.i18n.t("bot_account").toLowerCase()}
    )} {this.props.showCommunity && ( <> {I18NextService.i18n.t("to")} {cv.post.name} )} {this.linkBtn(true)} {cv.comment.language_id !== 0 && ( { this.props.allLanguages.find( lang => lang.id === cv.comment.language_id )?.name } )} {/* This is an expanding spacer for mobile */}
    {showScores() && ( <> {numToSI(this.commentView.counts.score)} )}
    {/* end of user row */} {this.state.showEdit && ( )} {!this.state.showEdit && !this.state.collapsed && (
    {this.state.viewSource ? (
    {this.commentUnlessRemoved}
    ) : (
    )}
    {this.props.showContext && this.linkBtn()} {this.props.markable && ( )} {UserService.Instance.myUserInfo && !this.props.viewOnly && ( <> {!this.state.showAdvanced ? ( ) : ( <> {!this.myComment && ( <> )} {this.myComment && ( <> {(canModOnSelf || canAdminOnSelf) && ( )} )} {/* Admins and mods can remove comments */} {(canMod_ || canAdmin_) && ( <> {!cv.comment.removed ? ( ) : ( )} )} {/* Mods can ban from community, and appoint as mods to community */} {canMod_ && ( <> {!isMod_ && (!cv.creator_banned_from_community ? ( ) : ( ))} {!cv.creator_banned_from_community && (!this.state.showConfirmAppointAsMod ? ( ) : ( <> ))} )} {/* Community creators and admins can transfer community to another mod */} {(amCommunityCreator_ || canAdmin_) && isMod_ && cv.creator.local && (!this.state.showConfirmTransferCommunity ? ( ) : ( <> ))} {/* Admins can ban from all, and appoint other admins */} {canAdmin_ && ( <> {!isAdmin_ && ( <> {!isBanned(cv.creator) ? ( ) : ( )} )} {!isBanned(cv.creator) && cv.creator.local && (!this.state.showConfirmAppointAsAdmin ? ( ) : ( <> ))} )} )} )}
    {/* end of button group */}
    )}
    {showMoreChildren && (
    )} {/* end of details */} {this.state.showRemoveDialog && (
    )} {this.state.showReportDialog && (
    )} {this.state.showBanDialog && (
    {/* TODO hold off on expires until later */} {/*
    */} {/* */} {/* */} {/*
    */}
    )} {this.state.showPurgeDialog && (
    {this.state.purgeLoading ? ( ) : ( )}
    )} {this.state.showReply && ( )} {!this.state.collapsed && node.children.length > 0 && ( )} {/* A collapsed clearfix */} {this.state.collapsed &&
    }
  • ); } get commentReplyOrMentionRead(): boolean { const cv = this.commentView; if (this.isPersonMentionType(cv)) { return cv.person_mention.read; } else if (this.isCommentReplyType(cv)) { return cv.comment_reply.read; } else { return false; } } linkBtn(small = false) { const cv = this.commentView; const classnames = classNames("btn btn-link btn-animate text-muted", { "btn-sm": small, }); const title = this.props.showContext ? I18NextService.i18n.t("show_context") : I18NextService.i18n.t("link"); // The context button should show the parent comment by default const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id; return ( <> { } ); } get myComment(): boolean { return ( UserService.Instance.myUserInfo?.local_user_view.person.id == this.commentView.creator.id ); } get isPostCreator(): boolean { return this.commentView.creator.id == this.commentView.post.creator_id; } get scoreColor() { if (this.commentView.my_vote == 1) { return "text-info"; } else if (this.commentView.my_vote == -1) { return "text-danger"; } else { return "text-muted"; } } get pointsTippy(): string { const points = I18NextService.i18n.t("number_of_points", { count: Number(this.commentView.counts.score), formattedCount: numToSI(this.commentView.counts.score), }); const upvotes = I18NextService.i18n.t("number_of_upvotes", { count: Number(this.commentView.counts.upvotes), formattedCount: numToSI(this.commentView.counts.upvotes), }); const downvotes = I18NextService.i18n.t("number_of_downvotes", { count: Number(this.commentView.counts.downvotes), formattedCount: numToSI(this.commentView.counts.downvotes), }); return `${points} • ${upvotes} • ${downvotes}`; } get expandText(): string { return this.state.collapsed ? I18NextService.i18n.t("expand") : I18NextService.i18n.t("collapse"); } get commentUnlessRemoved(): string { const comment = this.commentView.comment; return comment.removed ? `*${I18NextService.i18n.t("removed")}*` : comment.deleted ? `*${I18NextService.i18n.t("deleted")}*` : comment.content; } handleReplyClick(i: CommentNode) { i.setState({ showReply: true }); } handleEditClick(i: CommentNode) { i.setState({ showEdit: true }); } handleReplyCancel() { this.setState({ showReply: false, showEdit: false }); } handleShowReportDialog(i: CommentNode) { i.setState({ showReportDialog: !i.state.showReportDialog }); } handleReportReasonChange(i: CommentNode, event: any) { i.setState({ reportReason: event.target.value }); } handleModRemoveShow(i: CommentNode) { i.setState({ showRemoveDialog: !i.state.showRemoveDialog, showBanDialog: false, }); } handleModRemoveReasonChange(i: CommentNode, event: any) { i.setState({ removeReason: event.target.value }); } handleModRemoveDataChange(i: CommentNode, event: any) { i.setState({ removeData: event.target.checked }); } isPersonMentionType( item: CommentView | PersonMentionView | CommentReplyView ): item is PersonMentionView { return (item as PersonMentionView).person_mention?.id !== undefined; } isCommentReplyType( item: CommentView | PersonMentionView | CommentReplyView ): item is CommentReplyView { return (item as CommentReplyView).comment_reply?.id !== undefined; } handleModBanFromCommunityShow(i: CommentNode) { i.setState({ showBanDialog: true, banType: BanType.Community, showRemoveDialog: false, }); } handleModBanShow(i: CommentNode) { i.setState({ showBanDialog: true, banType: BanType.Site, showRemoveDialog: false, }); } handleModBanReasonChange(i: CommentNode, event: any) { i.setState({ banReason: event.target.value }); } handleModBanExpireDaysChange(i: CommentNode, event: any) { i.setState({ banExpireDays: event.target.value }); } handlePurgePersonShow(i: CommentNode) { i.setState({ showPurgeDialog: true, purgeType: PurgeType.Person, showRemoveDialog: false, }); } handlePurgeCommentShow(i: CommentNode) { i.setState({ showPurgeDialog: true, purgeType: PurgeType.Comment, showRemoveDialog: false, }); } handlePurgeReasonChange(i: CommentNode, event: any) { i.setState({ purgeReason: event.target.value }); } handleShowConfirmAppointAsMod(i: CommentNode) { i.setState({ showConfirmAppointAsMod: true }); } handleCancelConfirmAppointAsMod(i: CommentNode) { i.setState({ showConfirmAppointAsMod: false }); } handleShowConfirmAppointAsAdmin(i: CommentNode) { i.setState({ showConfirmAppointAsAdmin: true }); } handleCancelConfirmAppointAsAdmin(i: CommentNode) { i.setState({ showConfirmAppointAsAdmin: false }); } handleShowConfirmTransferCommunity(i: CommentNode) { i.setState({ showConfirmTransferCommunity: true }); } handleCancelShowConfirmTransferCommunity(i: CommentNode) { i.setState({ showConfirmTransferCommunity: false }); } handleShowConfirmTransferSite(i: CommentNode) { i.setState({ showConfirmTransferSite: true }); } handleCancelShowConfirmTransferSite(i: CommentNode) { i.setState({ showConfirmTransferSite: false }); } get isCommentNew(): boolean { const now = subMinutes(new Date(), 10); const then = parseISO(this.commentView.comment.published); return isBefore(now, then); } handleCommentCollapse(i: CommentNode) { i.setState({ collapsed: !i.state.collapsed }); setupTippy(); } handleViewSource(i: CommentNode) { i.setState({ viewSource: !i.state.viewSource }); } handleShowAdvanced(i: CommentNode) { i.setState({ showAdvanced: !i.state.showAdvanced }); setupTippy(); } handleSaveComment(i: CommentNode) { i.setState({ saveLoading: true }); i.props.onSaveComment({ comment_id: i.commentView.comment.id, save: !i.commentView.saved, auth: myAuthRequired(), }); } handleBlockPerson(i: CommentNode) { i.setState({ blockPersonLoading: true }); i.props.onBlockPerson({ person_id: i.commentView.creator.id, block: true, auth: myAuthRequired(), }); } handleMarkAsRead(i: CommentNode) { i.setState({ readLoading: true }); const cv = i.commentView; if (i.isPersonMentionType(cv)) { i.props.onPersonMentionRead({ person_mention_id: cv.person_mention.id, read: !cv.person_mention.read, auth: myAuthRequired(), }); } else if (i.isCommentReplyType(cv)) { i.props.onCommentReplyRead({ comment_reply_id: cv.comment_reply.id, read: !cv.comment_reply.read, auth: myAuthRequired(), }); } } handleDeleteComment(i: CommentNode) { i.setState({ deleteLoading: true }); i.props.onDeleteComment({ comment_id: i.commentId, deleted: !i.commentView.comment.deleted, auth: myAuthRequired(), }); } handleRemoveComment(i: CommentNode, event: any) { event.preventDefault(); i.setState({ removeLoading: true }); i.props.onRemoveComment({ comment_id: i.commentId, removed: !i.commentView.comment.removed, auth: myAuthRequired(), }); } handleDistinguishComment(i: CommentNode) { i.setState({ distinguishLoading: true }); i.props.onDistinguishComment({ comment_id: i.commentId, distinguished: !i.commentView.comment.distinguished, auth: myAuthRequired(), }); } handleBanPersonFromCommunity(i: CommentNode) { i.setState({ banLoading: true }); i.props.onBanPersonFromCommunity({ community_id: i.commentView.community.id, person_id: i.commentView.creator.id, ban: !i.commentView.creator_banned_from_community, reason: i.state.banReason, remove_data: i.state.removeData, expires: futureDaysToUnixTime(i.state.banExpireDays), auth: myAuthRequired(), }); } handleBanPerson(i: CommentNode) { i.setState({ banLoading: true }); i.props.onBanPerson({ person_id: i.commentView.creator.id, ban: !i.commentView.creator_banned_from_community, reason: i.state.banReason, remove_data: i.state.removeData, expires: futureDaysToUnixTime(i.state.banExpireDays), auth: myAuthRequired(), }); } handleModBanBothSubmit(i: CommentNode, event: any) { event.preventDefault(); if (i.state.banType == BanType.Community) { i.handleBanPersonFromCommunity(i); } else { i.handleBanPerson(i); } } handleAddModToCommunity(i: CommentNode) { i.setState({ addModLoading: true }); const added = !isMod(i.commentView.comment.creator_id, i.props.moderators); i.props.onAddModToCommunity({ community_id: i.commentView.community.id, person_id: i.commentView.creator.id, added, auth: myAuthRequired(), }); } handleAddAdmin(i: CommentNode) { i.setState({ addAdminLoading: true }); const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins); i.props.onAddAdmin({ person_id: i.commentView.creator.id, added, auth: myAuthRequired(), }); } handleTransferCommunity(i: CommentNode) { i.setState({ transferCommunityLoading: true }); i.props.onTransferCommunity({ community_id: i.commentView.community.id, person_id: i.commentView.creator.id, auth: myAuthRequired(), }); } handleReportComment(i: CommentNode, event: any) { event.preventDefault(); i.setState({ reportLoading: true }); i.props.onCommentReport({ comment_id: i.commentId, reason: i.state.reportReason ?? "", auth: myAuthRequired(), }); } handlePurgeBothSubmit(i: CommentNode, event: any) { event.preventDefault(); i.setState({ purgeLoading: true }); if (i.state.purgeType == PurgeType.Person) { i.props.onPurgePerson({ person_id: i.commentView.creator.id, reason: i.state.purgeReason, auth: myAuthRequired(), }); } else { i.props.onPurgeComment({ comment_id: i.commentId, reason: i.state.purgeReason, auth: myAuthRequired(), }); } } handleFetchChildren(i: CommentNode) { i.setState({ fetchChildrenLoading: true }); i.props.onFetchChildren?.({ parent_id: i.commentId, max_depth: commentTreeMaxDepth, limit: 999, // TODO type_: "All", saved_only: false, auth: myAuth(), }); } }