import { Left, None, Option, Some } from "@sniptt/monads"; import classNames from "classnames"; import { Component, linkEvent } from "inferno"; import { Link } from "inferno-router"; import { AddAdmin, AddModToCommunity, BanFromCommunity, BanPerson, BlockPerson, CommentNode as CommentNodeI, CommentReplyView, CommentView, CommunityModeratorView, CreateCommentLike, CreateCommentReport, DeleteComment, GetComments, ListingType, MarkCommentReplyAsRead, MarkPersonMentionAsRead, PersonMentionView, PersonViewSafe, PurgeComment, PurgePerson, RemoveComment, SaveComment, toUndefined, TransferCommunity, } from "lemmy-js-client"; import moment from "moment"; import { i18n } from "../../i18next"; import { BanType, CommentViewType, PurgeType } from "../../interfaces"; import { UserService, WebSocketService } from "../../services"; import { amCommunityCreator, auth, canAdmin, canMod, colorList, commentTreeMaxDepth, futureDaysToUnixTime, isAdmin, isBanned, isMod, mdToHtml, numToSI, setupTippy, showScores, wsClient, } from "../../utils"; import { Icon, PurgeWarning, Spinner } from "../common/icon"; import { MomentTime } from "../common/moment-time"; 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: Option; showBanDialog: boolean; removeData: boolean; banReason: Option; banExpireDays: Option; banType: BanType; showPurgeDialog: boolean; purgeReason: Option; purgeType: PurgeType; purgeLoading: boolean; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; showConfirmAppointAsMod: boolean; showConfirmAppointAsAdmin: boolean; collapsed: boolean; viewSource: boolean; showAdvanced: boolean; showReportDialog: boolean; reportReason: string; my_vote: Option; score: number; upvotes: number; downvotes: number; readLoading: boolean; saveLoading: boolean; } interface CommentNodeProps { node: CommentNodeI; moderators: Option; admins: Option; noBorder?: boolean; noIndent?: boolean; viewOnly?: boolean; locked?: boolean; markable?: boolean; showContext?: boolean; showCommunity?: boolean; enableDownvotes: boolean; viewType: CommentViewType; } export class CommentNode extends Component { private emptyState: CommentNodeState = { showReply: false, showEdit: false, showRemoveDialog: false, removeReason: None, showBanDialog: false, removeData: false, banReason: None, banExpireDays: None, banType: BanType.Community, showPurgeDialog: false, purgeLoading: false, purgeReason: None, purgeType: PurgeType.Person, collapsed: false, viewSource: false, showAdvanced: false, showConfirmTransferSite: false, showConfirmTransferCommunity: false, showConfirmAppointAsMod: false, showConfirmAppointAsAdmin: false, showReportDialog: false, reportReason: null, my_vote: this.props.node.comment_view.my_vote, score: this.props.node.comment_view.counts.score, upvotes: this.props.node.comment_view.counts.upvotes, downvotes: this.props.node.comment_view.counts.downvotes, readLoading: false, saveLoading: false, }; constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleCommentUpvote = this.handleCommentUpvote.bind(this); this.handleCommentDownvote = this.handleCommentDownvote.bind(this); } // TODO see if there's a better way to do this, and all willReceiveProps componentWillReceiveProps(nextProps: CommentNodeProps) { let cv = nextProps.node.comment_view; this.state.my_vote = cv.my_vote; this.state.upvotes = cv.counts.upvotes; this.state.downvotes = cv.counts.downvotes; this.state.score = cv.counts.score; this.state.readLoading = false; this.state.saveLoading = false; this.setState(this.state); } render() { let node = this.props.node; let cv = this.props.node.comment_view; let purgeTypeText: string; if (this.state.purgeType == PurgeType.Comment) { purgeTypeText = i18n.t("purge_comment"); } else if (this.state.purgeType == PurgeType.Person) { purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`; } let canMod_ = canMod( this.props.moderators, this.props.admins, cv.creator.id ); let canAdmin_ = canAdmin(this.props.admins, cv.creator.id); let isMod_ = isMod(this.props.moderators, cv.creator.id); let isAdmin_ = isAdmin(this.props.admins, cv.creator.id); let amCommunityCreator_ = amCommunityCreator( this.props.moderators, cv.creator.id ); let borderColor = this.props.node.depth ? colorList[(this.props.node.depth - 1) % colorList.length] : colorList[0]; let moreRepliesBorderColor = this.props.node.depth ? colorList[this.props.node.depth % colorList.length] : colorList[0]; let showMoreChildren = this.props.viewType == CommentViewType.Tree && !this.state.collapsed && node.children.length == 0 && node.comment_view.counts.child_count > 0; return (
{isMod_ && (
{i18n.t("mod")}
)} {isAdmin_ && (
{i18n.t("admin")}
)} {this.isPostCreator && (
{i18n.t("creator")}
)} {cv.creator.bot_account && (
{i18n.t("bot_account").toLowerCase()}
)} {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
{i18n.t("banned")}
)} {this.props.showCommunity && ( <> {i18n.t("to")} {cv.post.name} )} {this.linkBtn(true)} {/* This is an expanding spacer for mobile */}
{showScores() && ( <> {numToSI(this.state.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.isSome() && !this.props.viewOnly && ( <> {this.props.enableDownvotes && ( )} {!this.state.showAdvanced ? ( ) : ( <> {!this.myComment && ( <> )} {this.myComment && ( <> )} {/* 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 { let cv = this.props.node.comment_view; 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) { let cv = this.props.node.comment_view; let classnames = classNames("btn btn-link btn-animate text-muted", { "btn-sm": small, }); let title = this.props.showContext ? i18n.t("show_context") : i18n.t("link"); return ( <> { } ); } get loadingIcon() { return ; } get myComment(): boolean { return UserService.Instance.myUserInfo .map( m => m.local_user_view.person.id == this.props.node.comment_view.creator.id ) .unwrapOr(false); } get isPostCreator(): boolean { return ( this.props.node.comment_view.creator.id == this.props.node.comment_view.post.creator_id ); } get commentUnlessRemoved(): string { let comment = this.props.node.comment_view.comment; return comment.removed ? `*${i18n.t("removed")}*` : comment.deleted ? `*${i18n.t("deleted")}*` : comment.content; } handleReplyClick(i: CommentNode) { i.state.showReply = true; i.setState(i.state); } handleEditClick(i: CommentNode) { i.state.showEdit = true; i.setState(i.state); } handleBlockUserClick(i: CommentNode) { let blockUserForm = new BlockPerson({ person_id: i.props.node.comment_view.creator.id, block: true, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); } handleDeleteClick(i: CommentNode) { let comment = i.props.node.comment_view.comment; let deleteForm = new DeleteComment({ comment_id: comment.id, deleted: !comment.deleted, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.deleteComment(deleteForm)); } handleSaveCommentClick(i: CommentNode) { let cv = i.props.node.comment_view; let save = cv.saved == undefined ? true : !cv.saved; let form = new SaveComment({ comment_id: cv.comment.id, save, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.saveComment(form)); i.state.saveLoading = true; i.setState(this.state); } handleReplyCancel() { this.state.showReply = false; this.state.showEdit = false; this.setState(this.state); } handleCommentUpvote(event: any) { event.preventDefault(); let myVote = this.state.my_vote.unwrapOr(0); let newVote = myVote == 1 ? 0 : 1; if (myVote == 1) { this.state.score--; this.state.upvotes--; } else if (myVote == -1) { this.state.downvotes--; this.state.upvotes++; this.state.score += 2; } else { this.state.upvotes++; this.state.score++; } this.state.my_vote = Some(newVote); let form = new CreateCommentLike({ comment_id: this.props.node.comment_view.comment.id, score: newVote, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.likeComment(form)); this.setState(this.state); setupTippy(); } handleCommentDownvote(event: any) { event.preventDefault(); let myVote = this.state.my_vote.unwrapOr(0); let newVote = myVote == -1 ? 0 : -1; if (myVote == 1) { this.state.score -= 2; this.state.upvotes--; this.state.downvotes++; } else if (myVote == -1) { this.state.downvotes--; this.state.score++; } else { this.state.downvotes++; this.state.score--; } this.state.my_vote = Some(newVote); let form = new CreateCommentLike({ comment_id: this.props.node.comment_view.comment.id, score: newVote, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.likeComment(form)); this.setState(this.state); setupTippy(); } handleShowReportDialog(i: CommentNode) { i.state.showReportDialog = !i.state.showReportDialog; i.setState(i.state); } handleReportReasonChange(i: CommentNode, event: any) { i.state.reportReason = event.target.value; i.setState(i.state); } handleReportSubmit(i: CommentNode) { let comment = i.props.node.comment_view.comment; let form = new CreateCommentReport({ comment_id: comment.id, reason: i.state.reportReason, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.createCommentReport(form)); i.state.showReportDialog = false; i.setState(i.state); } handleModRemoveShow(i: CommentNode) { i.state.showRemoveDialog = !i.state.showRemoveDialog; i.state.showBanDialog = false; i.setState(i.state); } handleModRemoveReasonChange(i: CommentNode, event: any) { i.state.removeReason = Some(event.target.value); i.setState(i.state); } handleModRemoveDataChange(i: CommentNode, event: any) { i.state.removeData = event.target.checked; i.setState(i.state); } handleModRemoveSubmit(i: CommentNode) { let comment = i.props.node.comment_view.comment; let form = new RemoveComment({ comment_id: comment.id, removed: !comment.removed, reason: i.state.removeReason, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.removeComment(form)); i.state.showRemoveDialog = false; i.setState(i.state); } 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; } handleMarkRead(i: CommentNode) { if (i.isPersonMentionType(i.props.node.comment_view)) { let form = new MarkPersonMentionAsRead({ person_mention_id: i.props.node.comment_view.person_mention.id, read: !i.props.node.comment_view.person_mention.read, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form)); } else if (i.isCommentReplyType(i.props.node.comment_view)) { let form = new MarkCommentReplyAsRead({ comment_reply_id: i.props.node.comment_view.comment_reply.id, read: !i.props.node.comment_view.comment_reply.read, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form)); } i.state.readLoading = true; i.setState(this.state); } handleModBanFromCommunityShow(i: CommentNode) { i.state.showBanDialog = true; i.state.banType = BanType.Community; i.state.showRemoveDialog = false; i.setState(i.state); } handleModBanShow(i: CommentNode) { i.state.showBanDialog = true; i.state.banType = BanType.Site; i.state.showRemoveDialog = false; i.setState(i.state); } handleModBanReasonChange(i: CommentNode, event: any) { i.state.banReason = Some(event.target.value); i.setState(i.state); } handleModBanExpireDaysChange(i: CommentNode, event: any) { i.state.banExpireDays = Some(event.target.value); i.setState(i.state); } handleModBanFromCommunitySubmit(i: CommentNode) { i.state.banType = BanType.Community; i.setState(i.state); i.handleModBanBothSubmit(i); } handleModBanSubmit(i: CommentNode) { i.state.banType = BanType.Site; i.setState(i.state); i.handleModBanBothSubmit(i); } handleModBanBothSubmit(i: CommentNode) { let cv = i.props.node.comment_view; if (i.state.banType == BanType.Community) { // If its an unban, restore all their data let ban = !cv.creator_banned_from_community; if (ban == false) { i.state.removeData = false; } let form = new BanFromCommunity({ person_id: cv.creator.id, community_id: cv.community.id, ban, remove_data: Some(i.state.removeData), reason: i.state.banReason, expires: i.state.banExpireDays.map(futureDaysToUnixTime), auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.banFromCommunity(form)); } else { // If its an unban, restore all their data let ban = !cv.creator.banned; if (ban == false) { i.state.removeData = false; } let form = new BanPerson({ person_id: cv.creator.id, ban, remove_data: Some(i.state.removeData), reason: i.state.banReason, expires: i.state.banExpireDays.map(futureDaysToUnixTime), auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.banPerson(form)); } i.state.showBanDialog = false; i.setState(i.state); } handlePurgePersonShow(i: CommentNode) { i.state.showPurgeDialog = true; i.state.purgeType = PurgeType.Person; i.state.showRemoveDialog = false; i.setState(i.state); } handlePurgeCommentShow(i: CommentNode) { i.state.showPurgeDialog = true; i.state.purgeType = PurgeType.Comment; i.state.showRemoveDialog = false; i.setState(i.state); } handlePurgeReasonChange(i: CommentNode, event: any) { i.state.purgeReason = Some(event.target.value); i.setState(i.state); } handlePurgeSubmit(i: CommentNode, event: any) { event.preventDefault(); if (i.state.purgeType == PurgeType.Person) { let form = new PurgePerson({ person_id: i.props.node.comment_view.creator.id, reason: i.state.purgeReason, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.purgePerson(form)); } else if (i.state.purgeType == PurgeType.Comment) { let form = new PurgeComment({ comment_id: i.props.node.comment_view.comment.id, reason: i.state.purgeReason, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.purgeComment(form)); } i.state.purgeLoading = true; i.setState(i.state); } handleShowConfirmAppointAsMod(i: CommentNode) { i.state.showConfirmAppointAsMod = true; i.setState(i.state); } handleCancelConfirmAppointAsMod(i: CommentNode) { i.state.showConfirmAppointAsMod = false; i.setState(i.state); } handleAddModToCommunity(i: CommentNode) { let cv = i.props.node.comment_view; let form = new AddModToCommunity({ person_id: cv.creator.id, community_id: cv.community.id, added: !isMod(i.props.moderators, cv.creator.id), auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.addModToCommunity(form)); i.state.showConfirmAppointAsMod = false; i.setState(i.state); } handleShowConfirmAppointAsAdmin(i: CommentNode) { i.state.showConfirmAppointAsAdmin = true; i.setState(i.state); } handleCancelConfirmAppointAsAdmin(i: CommentNode) { i.state.showConfirmAppointAsAdmin = false; i.setState(i.state); } handleAddAdmin(i: CommentNode) { let creatorId = i.props.node.comment_view.creator.id; let form = new AddAdmin({ person_id: creatorId, added: !isAdmin(i.props.admins, creatorId), auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.addAdmin(form)); i.state.showConfirmAppointAsAdmin = false; i.setState(i.state); } handleShowConfirmTransferCommunity(i: CommentNode) { i.state.showConfirmTransferCommunity = true; i.setState(i.state); } handleCancelShowConfirmTransferCommunity(i: CommentNode) { i.state.showConfirmTransferCommunity = false; i.setState(i.state); } handleTransferCommunity(i: CommentNode) { let cv = i.props.node.comment_view; let form = new TransferCommunity({ community_id: cv.community.id, person_id: cv.creator.id, auth: auth().unwrap(), }); WebSocketService.Instance.send(wsClient.transferCommunity(form)); i.state.showConfirmTransferCommunity = false; i.setState(i.state); } handleShowConfirmTransferSite(i: CommentNode) { i.state.showConfirmTransferSite = true; i.setState(i.state); } handleCancelShowConfirmTransferSite(i: CommentNode) { i.state.showConfirmTransferSite = false; i.setState(i.state); } get isCommentNew(): boolean { let now = moment.utc().subtract(10, "minutes"); let then = moment.utc(this.props.node.comment_view.comment.published); return now.isBefore(then); } handleCommentCollapse(i: CommentNode) { i.state.collapsed = !i.state.collapsed; i.setState(i.state); setupTippy(); } handleViewSource(i: CommentNode) { i.state.viewSource = !i.state.viewSource; i.setState(i.state); } handleShowAdvanced(i: CommentNode) { i.state.showAdvanced = !i.state.showAdvanced; i.setState(i.state); setupTippy(); } handleFetchChildren(i: CommentNode) { let form = new GetComments({ post_id: Some(i.props.node.comment_view.post.id), parent_id: Some(i.props.node.comment_view.comment.id), max_depth: Some(commentTreeMaxDepth), page: None, sort: None, limit: Some(999), type_: Some(ListingType.All), community_name: None, community_id: None, saved_only: Some(false), auth: auth(false).ok(), }); WebSocketService.Instance.send(wsClient.getComments(form)); } get scoreColor() { if (this.state.my_vote.unwrapOr(0) == 1) { return "text-info"; } else if (this.state.my_vote.unwrapOr(0) == -1) { return "text-danger"; } else { return "text-muted"; } } get pointsTippy(): string { let points = i18n.t("number_of_points", { count: this.state.score, formattedCount: this.state.score, }); let upvotes = i18n.t("number_of_upvotes", { count: this.state.upvotes, formattedCount: this.state.upvotes, }); let downvotes = i18n.t("number_of_downvotes", { count: this.state.downvotes, formattedCount: this.state.downvotes, }); return `${points} • ${upvotes} • ${downvotes}`; } get expandText(): string { return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse"); } }