1 import classNames from "classnames";
2 import { Component, InfernoNode, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
13 CommunityModeratorView,
22 MarkCommentReplyAsRead,
23 MarkPersonMentionAsRead,
31 } from "lemmy-js-client";
32 import moment from "moment";
33 import { i18n } from "../../i18next";
40 } from "../../interfaces";
41 import { UserService } from "../../services";
62 import { Icon, PurgeWarning, Spinner } from "../common/icon";
63 import { MomentTime } from "../common/moment-time";
64 import { CommunityLink } from "../community/community-link";
65 import { PersonListing } from "../person/person-listing";
66 import { CommentForm } from "./comment-form";
67 import { CommentNodes } from "./comment-nodes";
69 interface CommentNodeState {
72 showRemoveDialog: boolean;
73 removeReason?: string;
74 showBanDialog: boolean;
77 banExpireDays?: number;
79 showPurgeDialog: boolean;
82 showConfirmTransferSite: boolean;
83 showConfirmTransferCommunity: boolean;
84 showConfirmAppointAsMod: boolean;
85 showConfirmAppointAsAdmin: boolean;
88 showAdvanced: boolean;
89 showReportDialog: boolean;
90 reportReason?: string;
91 createOrEditCommentLoading: boolean;
92 upvoteLoading: boolean;
93 downvoteLoading: boolean;
96 blockPersonLoading: boolean;
97 deleteLoading: boolean;
98 removeLoading: boolean;
99 distinguishLoading: boolean;
101 addModLoading: boolean;
102 addAdminLoading: boolean;
103 transferCommunityLoading: boolean;
104 fetchChildrenLoading: boolean;
105 reportLoading: boolean;
106 purgeLoading: boolean;
109 interface CommentNodeProps {
111 moderators?: CommunityModeratorView[];
112 admins?: PersonView[];
118 showContext?: boolean;
119 showCommunity?: boolean;
120 enableDownvotes?: boolean;
121 viewType: CommentViewType;
122 allLanguages: Language[];
123 siteLanguages: number[];
124 hideImages?: boolean;
125 finished: Map<CommentId, boolean | undefined>;
126 onSaveComment(form: SaveComment): void;
127 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
128 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
129 onCreateComment(form: EditComment | CreateComment): void;
130 onEditComment(form: EditComment | CreateComment): void;
131 onCommentVote(form: CreateCommentLike): void;
132 onBlockPerson(form: BlockPerson): void;
133 onDeleteComment(form: DeleteComment): void;
134 onRemoveComment(form: RemoveComment): void;
135 onDistinguishComment(form: DistinguishComment): void;
136 onAddModToCommunity(form: AddModToCommunity): void;
137 onAddAdmin(form: AddAdmin): void;
138 onBanPersonFromCommunity(form: BanFromCommunity): void;
139 onBanPerson(form: BanPerson): void;
140 onTransferCommunity(form: TransferCommunity): void;
141 onFetchChildren?(form: GetComments): void;
142 onCommentReport(form: CreateCommentReport): void;
143 onPurgePerson(form: PurgePerson): void;
144 onPurgeComment(form: PurgeComment): void;
147 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
148 state: CommentNodeState = {
151 showRemoveDialog: false,
152 showBanDialog: false,
154 banType: BanType.Community,
155 showPurgeDialog: false,
156 purgeType: PurgeType.Person,
160 showConfirmTransferSite: false,
161 showConfirmTransferCommunity: false,
162 showConfirmAppointAsMod: false,
163 showConfirmAppointAsAdmin: false,
164 showReportDialog: false,
165 createOrEditCommentLoading: false,
166 upvoteLoading: false,
167 downvoteLoading: false,
170 blockPersonLoading: false,
171 deleteLoading: false,
172 removeLoading: false,
173 distinguishLoading: false,
175 addModLoading: false,
176 addAdminLoading: false,
177 transferCommunityLoading: false,
178 fetchChildrenLoading: false,
179 reportLoading: false,
183 constructor(props: any, context: any) {
184 super(props, context);
186 this.handleReplyCancel = this.handleReplyCancel.bind(this);
189 get commentView(): CommentView {
190 return this.props.node.comment_view;
193 get commentId(): CommentId {
194 return this.commentView.comment.id;
197 componentWillReceiveProps(
198 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
200 if (this.props != nextProps) {
204 showRemoveDialog: false,
205 showBanDialog: false,
207 banType: BanType.Community,
208 showPurgeDialog: false,
209 purgeType: PurgeType.Person,
213 showConfirmTransferSite: false,
214 showConfirmTransferCommunity: false,
215 showConfirmAppointAsMod: false,
216 showConfirmAppointAsAdmin: false,
217 showReportDialog: false,
218 createOrEditCommentLoading: false,
219 upvoteLoading: false,
220 downvoteLoading: false,
223 blockPersonLoading: false,
224 deleteLoading: false,
225 removeLoading: false,
226 distinguishLoading: false,
228 addModLoading: false,
229 addAdminLoading: false,
230 transferCommunityLoading: false,
231 fetchChildrenLoading: false,
232 reportLoading: false,
239 const node = this.props.node;
240 const cv = this.commentView;
242 const purgeTypeText =
243 this.state.purgeType == PurgeType.Comment
244 ? i18n.t("purge_comment")
245 : `${i18n.t("purge")} ${cv.creator.name}`;
247 const canMod_ = canMod(
249 this.props.moderators,
252 const canModOnSelf = canMod(
254 this.props.moderators,
256 UserService.Instance.myUserInfo,
259 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
260 const canAdminOnSelf = canAdmin(
263 UserService.Instance.myUserInfo,
266 const isMod_ = isMod(cv.creator.id, this.props.moderators);
267 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
268 const amCommunityCreator_ = amCommunityCreator(
270 this.props.moderators
274 const borderColor = this.props.node.depth
275 ? colorList[(this.props.node.depth - 1) % colorList.length]
277 const moreRepliesBorderColor = this.props.node.depth
278 ? colorList[this.props.node.depth % colorList.length]
281 const showMoreChildren =
282 this.props.viewType == CommentViewType.Tree &&
283 !this.state.collapsed &&
284 node.children.length == 0 &&
285 node.comment_view.counts.child_count > 0;
288 <li className="comment" role="comment">
290 id={`comment-${cv.comment.id}`}
291 className={classNames(`details comment-node py-2`, {
292 "border-top border-light": !this.props.noBorder,
293 mark: this.isCommentNew || this.commentView.comment.distinguished,
297 className={classNames({
298 "ml-2": !this.props.noIndent,
301 <div className="d-flex flex-wrap align-items-center text-muted small">
303 className="btn btn-sm text-muted mr-2"
304 onClick={linkEvent(this, this.handleCommentCollapse)}
305 aria-label={this.expandText}
306 data-tippy-content={this.expandText}
309 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
310 classes="icon-inline"
313 <span className="mr-2">
314 <PersonListing person={cv.creator} />
316 {cv.comment.distinguished && (
317 <Icon icon="shield" inline classes={`text-danger mr-2`} />
319 {this.isPostCreator && (
320 <div className="badge badge-light d-none d-sm-inline mr-2">
325 <div className="badge d-none d-sm-inline mr-2">
330 <div className="badge d-none d-sm-inline mr-2">
334 {cv.creator.bot_account && (
335 <div className="badge d-none d-sm-inline mr-2">
336 {i18n.t("bot_account").toLowerCase()}
339 {this.props.showCommunity && (
341 <span className="mx-1">{i18n.t("to")}</span>
342 <CommunityLink community={cv.community} />
343 <span className="mx-2">•</span>
344 <Link className="mr-2" to={`/post/${cv.post.id}`}>
350 {cv.comment.language_id !== 0 && (
351 <span className="badge d-none d-sm-inline mr-2">
353 this.props.allLanguages.find(
354 lang => lang.id === cv.comment.language_id
359 {/* This is an expanding spacer for mobile */}
360 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
364 className={`unselectable pointer ${this.scoreColor}`}
365 onClick={linkEvent(this, this.handleUpvote)}
366 data-tippy-content={this.pointsTippy}
368 {this.state.upvoteLoading ? (
372 className="mr-1 font-weight-bold"
373 aria-label={i18n.t("number_of_points", {
374 count: Number(this.commentView.counts.score),
375 formattedCount: numToSI(
376 this.commentView.counts.score
380 {numToSI(this.commentView.counts.score)}
384 <span className="mr-1">•</span>
389 published={cv.comment.published}
390 updated={cv.comment.updated}
394 {/* end of user row */}
395 {this.state.showEdit && (
399 onReplyCancel={this.handleReplyCancel}
400 disabled={this.props.locked}
401 finished={this.props.finished.get(
402 this.props.node.comment_view.comment.id
405 allLanguages={this.props.allLanguages}
406 siteLanguages={this.props.siteLanguages}
407 onUpsertComment={this.props.onEditComment}
410 {!this.state.showEdit && !this.state.collapsed && (
412 {this.state.viewSource ? (
413 <pre>{this.commentUnlessRemoved}</pre>
417 dangerouslySetInnerHTML={
418 this.props.hideImages
419 ? mdToHtmlNoImages(this.commentUnlessRemoved)
420 : mdToHtml(this.commentUnlessRemoved)
424 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
425 {this.props.showContext && this.linkBtn()}
426 {this.props.markable && (
428 className="btn btn-link btn-animate text-muted"
429 onClick={linkEvent(this, this.handleMarkAsRead)}
431 this.commentReplyOrMentionRead
432 ? i18n.t("mark_as_unread")
433 : i18n.t("mark_as_read")
436 this.commentReplyOrMentionRead
437 ? i18n.t("mark_as_unread")
438 : i18n.t("mark_as_read")
441 {this.state.readLoading ? (
446 classes={`icon-inline ${
447 this.commentReplyOrMentionRead && "text-success"
453 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
456 className={`btn btn-link btn-animate ${
457 this.commentView.my_vote === 1
461 onClick={linkEvent(this, this.handleUpvote)}
462 data-tippy-content={i18n.t("upvote")}
463 aria-label={i18n.t("upvote")}
464 aria-pressed={this.commentView.my_vote === 1}
466 {this.state.upvoteLoading ? (
470 <Icon icon="arrow-up1" classes="icon-inline" />
472 this.commentView.counts.upvotes !==
473 this.commentView.counts.score && (
474 <span className="ml-1">
475 {numToSI(this.commentView.counts.upvotes)}
481 {this.props.enableDownvotes && (
483 className={`btn btn-link btn-animate ${
484 this.commentView.my_vote === -1
488 onClick={linkEvent(this, this.handleDownvote)}
489 data-tippy-content={i18n.t("downvote")}
490 aria-label={i18n.t("downvote")}
491 aria-pressed={this.commentView.my_vote === -1}
493 {this.state.downvoteLoading ? (
497 <Icon icon="arrow-down1" classes="icon-inline" />
499 this.commentView.counts.upvotes !==
500 this.commentView.counts.score && (
501 <span className="ml-1">
502 {numToSI(this.commentView.counts.downvotes)}
510 className="btn btn-link btn-animate text-muted"
511 onClick={linkEvent(this, this.handleReplyClick)}
512 data-tippy-content={i18n.t("reply")}
513 aria-label={i18n.t("reply")}
515 <Icon icon="reply1" classes="icon-inline" />
517 {!this.state.showAdvanced ? (
519 className="btn btn-link btn-animate text-muted"
520 onClick={linkEvent(this, this.handleShowAdvanced)}
521 data-tippy-content={i18n.t("more")}
522 aria-label={i18n.t("more")}
524 <Icon icon="more-vertical" classes="icon-inline" />
528 {!this.myComment && (
531 className="btn btn-link btn-animate text-muted"
532 to={`/create_private_message/${cv.creator.id}`}
533 title={i18n.t("message").toLowerCase()}
538 className="btn btn-link btn-animate text-muted"
541 this.handleShowReportDialog
543 data-tippy-content={i18n.t(
546 aria-label={i18n.t("show_report_dialog")}
551 className="btn btn-link btn-animate text-muted"
554 this.handleBlockPerson
556 data-tippy-content={i18n.t("block_user")}
557 aria-label={i18n.t("block_user")}
559 {this.state.blockPersonLoading ? (
562 <Icon icon="slash" />
568 className="btn btn-link btn-animate text-muted"
569 onClick={linkEvent(this, this.handleSaveComment)}
571 cv.saved ? i18n.t("unsave") : i18n.t("save")
574 cv.saved ? i18n.t("unsave") : i18n.t("save")
577 {this.state.saveLoading ? (
582 classes={`icon-inline ${
583 cv.saved && "text-warning"
589 className="btn btn-link btn-animate text-muted"
590 onClick={linkEvent(this, this.handleViewSource)}
591 data-tippy-content={i18n.t("view_source")}
592 aria-label={i18n.t("view_source")}
596 classes={`icon-inline ${
597 this.state.viewSource && "text-success"
604 className="btn btn-link btn-animate text-muted"
605 onClick={linkEvent(this, this.handleEditClick)}
606 data-tippy-content={i18n.t("edit")}
607 aria-label={i18n.t("edit")}
609 <Icon icon="edit" classes="icon-inline" />
612 className="btn btn-link btn-animate text-muted"
615 this.handleDeleteComment
628 {this.state.deleteLoading ? (
633 classes={`icon-inline ${
634 cv.comment.deleted && "text-danger"
640 {(canModOnSelf || canAdminOnSelf) && (
642 className="btn btn-link btn-animate text-muted"
645 this.handleDistinguishComment
648 !cv.comment.distinguished
649 ? i18n.t("distinguish")
650 : i18n.t("undistinguish")
653 !cv.comment.distinguished
654 ? i18n.t("distinguish")
655 : i18n.t("undistinguish")
660 classes={`icon-inline ${
661 cv.comment.distinguished && "text-danger"
668 {/* Admins and mods can remove comments */}
669 {(canMod_ || canAdmin_) && (
671 {!cv.comment.removed ? (
673 className="btn btn-link btn-animate text-muted"
676 this.handleModRemoveShow
678 aria-label={i18n.t("remove")}
684 className="btn btn-link btn-animate text-muted"
687 this.handleRemoveComment
689 aria-label={i18n.t("restore")}
691 {this.state.removeLoading ? (
700 {/* Mods can ban from community, and appoint as mods to community */}
704 (!cv.creator_banned_from_community ? (
706 className="btn btn-link btn-animate text-muted"
709 this.handleModBanFromCommunityShow
711 aria-label={i18n.t("ban_from_community")}
713 {i18n.t("ban_from_community")}
717 className="btn btn-link btn-animate text-muted"
720 this.handleBanPersonFromCommunity
722 aria-label={i18n.t("unban")}
724 {this.state.banLoading ? (
731 {!cv.creator_banned_from_community &&
732 (!this.state.showConfirmAppointAsMod ? (
734 className="btn btn-link btn-animate text-muted"
737 this.handleShowConfirmAppointAsMod
741 ? i18n.t("remove_as_mod")
742 : i18n.t("appoint_as_mod")
746 ? i18n.t("remove_as_mod")
747 : i18n.t("appoint_as_mod")}
752 className="btn btn-link btn-animate text-muted"
753 aria-label={i18n.t("are_you_sure")}
755 {i18n.t("are_you_sure")}
758 className="btn btn-link btn-animate text-muted"
761 this.handleAddModToCommunity
763 aria-label={i18n.t("yes")}
765 {this.state.addModLoading ? (
772 className="btn btn-link btn-animate text-muted"
775 this.handleCancelConfirmAppointAsMod
777 aria-label={i18n.t("no")}
785 {/* Community creators and admins can transfer community to another mod */}
786 {(amCommunityCreator_ || canAdmin_) &&
789 (!this.state.showConfirmTransferCommunity ? (
791 className="btn btn-link btn-animate text-muted"
794 this.handleShowConfirmTransferCommunity
796 aria-label={i18n.t("transfer_community")}
798 {i18n.t("transfer_community")}
803 className="btn btn-link btn-animate text-muted"
804 aria-label={i18n.t("are_you_sure")}
806 {i18n.t("are_you_sure")}
809 className="btn btn-link btn-animate text-muted"
812 this.handleTransferCommunity
814 aria-label={i18n.t("yes")}
816 {this.state.transferCommunityLoading ? (
823 className="btn btn-link btn-animate text-muted"
827 .handleCancelShowConfirmTransferCommunity
829 aria-label={i18n.t("no")}
835 {/* Admins can ban from all, and appoint other admins */}
841 className="btn btn-link btn-animate text-muted"
844 this.handlePurgePersonShow
846 aria-label={i18n.t("purge_user")}
848 {i18n.t("purge_user")}
851 className="btn btn-link btn-animate text-muted"
854 this.handlePurgeCommentShow
856 aria-label={i18n.t("purge_comment")}
858 {i18n.t("purge_comment")}
861 {!isBanned(cv.creator) ? (
863 className="btn btn-link btn-animate text-muted"
866 this.handleModBanShow
868 aria-label={i18n.t("ban_from_site")}
870 {i18n.t("ban_from_site")}
874 className="btn btn-link btn-animate text-muted"
879 aria-label={i18n.t("unban_from_site")}
881 {this.state.banLoading ? (
884 i18n.t("unban_from_site")
890 {!isBanned(cv.creator) &&
892 (!this.state.showConfirmAppointAsAdmin ? (
894 className="btn btn-link btn-animate text-muted"
897 this.handleShowConfirmAppointAsAdmin
901 ? i18n.t("remove_as_admin")
902 : i18n.t("appoint_as_admin")
906 ? i18n.t("remove_as_admin")
907 : i18n.t("appoint_as_admin")}
911 <button className="btn btn-link btn-animate text-muted">
912 {i18n.t("are_you_sure")}
915 className="btn btn-link btn-animate text-muted"
920 aria-label={i18n.t("yes")}
922 {this.state.addAdminLoading ? (
929 className="btn btn-link btn-animate text-muted"
932 this.handleCancelConfirmAppointAsAdmin
934 aria-label={i18n.t("no")}
947 {/* end of button group */}
952 {showMoreChildren && (
954 className={`details ml-1 comment-node py-2 ${
955 !this.props.noBorder ? "border-top border-light" : ""
957 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
960 className="btn btn-link text-muted"
961 onClick={linkEvent(this, this.handleFetchChildren)}
963 {this.state.fetchChildrenLoading ? (
967 {i18n.t("x_more_replies", {
968 count: node.comment_view.counts.child_count,
969 formattedCount: numToSI(
970 node.comment_view.counts.child_count
979 {/* end of details */}
980 {this.state.showRemoveDialog && (
982 className="form-inline"
983 onSubmit={linkEvent(this, this.handleRemoveComment)}
987 htmlFor={`mod-remove-reason-${cv.comment.id}`}
993 id={`mod-remove-reason-${cv.comment.id}`}
994 className="form-control mr-2"
995 placeholder={i18n.t("reason")}
996 value={this.state.removeReason}
997 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1001 className="btn btn-secondary"
1002 aria-label={i18n.t("remove_comment")}
1004 {i18n.t("remove_comment")}
1008 {this.state.showReportDialog && (
1010 className="form-inline"
1011 onSubmit={linkEvent(this, this.handleReportComment)}
1015 htmlFor={`report-reason-${cv.comment.id}`}
1022 id={`report-reason-${cv.comment.id}`}
1023 className="form-control mr-2"
1024 placeholder={i18n.t("reason")}
1025 value={this.state.reportReason}
1026 onInput={linkEvent(this, this.handleReportReasonChange)}
1030 className="btn btn-secondary"
1031 aria-label={i18n.t("create_report")}
1033 {i18n.t("create_report")}
1037 {this.state.showBanDialog && (
1038 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1039 <div className="form-group row col-12">
1041 className="col-form-label"
1042 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1048 id={`mod-ban-reason-${cv.comment.id}`}
1049 className="form-control mr-2"
1050 placeholder={i18n.t("reason")}
1051 value={this.state.banReason}
1052 onInput={linkEvent(this, this.handleModBanReasonChange)}
1055 className="col-form-label"
1056 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1062 id={`mod-ban-expires-${cv.comment.id}`}
1063 className="form-control mr-2"
1064 placeholder={i18n.t("number_of_days")}
1065 value={this.state.banExpireDays}
1066 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1068 <div className="form-group">
1069 <div className="form-check">
1071 className="form-check-input"
1072 id="mod-ban-remove-data"
1074 checked={this.state.removeData}
1075 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1078 className="form-check-label"
1079 htmlFor="mod-ban-remove-data"
1080 title={i18n.t("remove_content_more")}
1082 {i18n.t("remove_content")}
1087 {/* TODO hold off on expires until later */}
1088 {/* <div class="form-group row"> */}
1089 {/* <label class="col-form-label">Expires</label> */}
1090 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1092 <div className="form-group row">
1095 className="btn btn-secondary"
1096 aria-label={i18n.t("ban")}
1098 {this.state.banLoading ? (
1102 {i18n.t("ban")} {cv.creator.name}
1110 {this.state.showPurgeDialog && (
1111 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1113 <label className="sr-only" htmlFor="purge-reason">
1119 className="form-control my-3"
1120 placeholder={i18n.t("reason")}
1121 value={this.state.purgeReason}
1122 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1124 <div className="form-group row col-12">
1125 {this.state.purgeLoading ? (
1130 className="btn btn-secondary"
1131 aria-label={purgeTypeText}
1139 {this.state.showReply && (
1142 onReplyCancel={this.handleReplyCancel}
1143 disabled={this.props.locked}
1144 finished={this.props.finished.get(
1145 this.props.node.comment_view.comment.id
1148 allLanguages={this.props.allLanguages}
1149 siteLanguages={this.props.siteLanguages}
1150 onUpsertComment={this.props.onCreateComment}
1153 {!this.state.collapsed && node.children.length > 0 && (
1155 nodes={node.children}
1156 locked={this.props.locked}
1157 moderators={this.props.moderators}
1158 admins={this.props.admins}
1159 enableDownvotes={this.props.enableDownvotes}
1160 viewType={this.props.viewType}
1161 allLanguages={this.props.allLanguages}
1162 siteLanguages={this.props.siteLanguages}
1163 hideImages={this.props.hideImages}
1164 isChild={!this.props.noIndent}
1165 depth={this.props.node.depth + 1}
1166 finished={this.props.finished}
1167 onCommentReplyRead={this.props.onCommentReplyRead}
1168 onPersonMentionRead={this.props.onPersonMentionRead}
1169 onCreateComment={this.props.onCreateComment}
1170 onEditComment={this.props.onEditComment}
1171 onCommentVote={this.props.onCommentVote}
1172 onBlockPerson={this.props.onBlockPerson}
1173 onSaveComment={this.props.onSaveComment}
1174 onDeleteComment={this.props.onDeleteComment}
1175 onRemoveComment={this.props.onRemoveComment}
1176 onDistinguishComment={this.props.onDistinguishComment}
1177 onAddModToCommunity={this.props.onAddModToCommunity}
1178 onAddAdmin={this.props.onAddAdmin}
1179 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1180 onBanPerson={this.props.onBanPerson}
1181 onTransferCommunity={this.props.onTransferCommunity}
1182 onFetchChildren={this.props.onFetchChildren}
1183 onCommentReport={this.props.onCommentReport}
1184 onPurgePerson={this.props.onPurgePerson}
1185 onPurgeComment={this.props.onPurgeComment}
1188 {/* A collapsed clearfix */}
1189 {this.state.collapsed && <div className="row col-12" />}
1194 get commentReplyOrMentionRead(): boolean {
1195 const cv = this.commentView;
1197 if (this.isPersonMentionType(cv)) {
1198 return cv.person_mention.read;
1199 } else if (this.isCommentReplyType(cv)) {
1200 return cv.comment_reply.read;
1206 linkBtn(small = false) {
1207 const cv = this.commentView;
1208 const classnames = classNames("btn btn-link btn-animate text-muted", {
1212 const title = this.props.showContext
1213 ? i18n.t("show_context")
1216 // The context button should show the parent comment by default
1217 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1222 className={classnames}
1223 to={`/comment/${parentCommentId}`}
1226 <Icon icon="link" classes="icon-inline" />
1229 <a className={classnames} title={title} href={cv.comment.ap_id}>
1230 <Icon icon="fedilink" classes="icon-inline" />
1237 get myComment(): boolean {
1239 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1240 this.commentView.creator.id
1244 get isPostCreator(): boolean {
1245 return this.commentView.creator.id == this.commentView.post.creator_id;
1249 if (this.commentView.my_vote == 1) {
1251 } else if (this.commentView.my_vote == -1) {
1252 return "text-danger";
1254 return "text-muted";
1258 get pointsTippy(): string {
1259 const points = i18n.t("number_of_points", {
1260 count: Number(this.commentView.counts.score),
1261 formattedCount: numToSI(this.commentView.counts.score),
1264 const upvotes = i18n.t("number_of_upvotes", {
1265 count: Number(this.commentView.counts.upvotes),
1266 formattedCount: numToSI(this.commentView.counts.upvotes),
1269 const downvotes = i18n.t("number_of_downvotes", {
1270 count: Number(this.commentView.counts.downvotes),
1271 formattedCount: numToSI(this.commentView.counts.downvotes),
1274 return `${points} • ${upvotes} • ${downvotes}`;
1277 get expandText(): string {
1278 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
1281 get commentUnlessRemoved(): string {
1282 const comment = this.commentView.comment;
1283 return comment.removed
1284 ? `*${i18n.t("removed")}*`
1286 ? `*${i18n.t("deleted")}*`
1290 handleReplyClick(i: CommentNode) {
1291 i.setState({ showReply: true });
1294 handleEditClick(i: CommentNode) {
1295 i.setState({ showEdit: true });
1298 handleReplyCancel() {
1299 this.setState({ showReply: false, showEdit: false });
1302 handleShowReportDialog(i: CommentNode) {
1303 i.setState({ showReportDialog: !i.state.showReportDialog });
1306 handleReportReasonChange(i: CommentNode, event: any) {
1307 i.setState({ reportReason: event.target.value });
1310 handleModRemoveShow(i: CommentNode) {
1312 showRemoveDialog: !i.state.showRemoveDialog,
1313 showBanDialog: false,
1317 handleModRemoveReasonChange(i: CommentNode, event: any) {
1318 i.setState({ removeReason: event.target.value });
1321 handleModRemoveDataChange(i: CommentNode, event: any) {
1322 i.setState({ removeData: event.target.checked });
1325 isPersonMentionType(
1326 item: CommentView | PersonMentionView | CommentReplyView
1327 ): item is PersonMentionView {
1328 return (item as PersonMentionView).person_mention?.id !== undefined;
1332 item: CommentView | PersonMentionView | CommentReplyView
1333 ): item is CommentReplyView {
1334 return (item as CommentReplyView).comment_reply?.id !== undefined;
1337 handleModBanFromCommunityShow(i: CommentNode) {
1339 showBanDialog: true,
1340 banType: BanType.Community,
1341 showRemoveDialog: false,
1345 handleModBanShow(i: CommentNode) {
1347 showBanDialog: true,
1348 banType: BanType.Site,
1349 showRemoveDialog: false,
1353 handleModBanReasonChange(i: CommentNode, event: any) {
1354 i.setState({ banReason: event.target.value });
1357 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1358 i.setState({ banExpireDays: event.target.value });
1361 handlePurgePersonShow(i: CommentNode) {
1363 showPurgeDialog: true,
1364 purgeType: PurgeType.Person,
1365 showRemoveDialog: false,
1369 handlePurgeCommentShow(i: CommentNode) {
1371 showPurgeDialog: true,
1372 purgeType: PurgeType.Comment,
1373 showRemoveDialog: false,
1377 handlePurgeReasonChange(i: CommentNode, event: any) {
1378 i.setState({ purgeReason: event.target.value });
1381 handleShowConfirmAppointAsMod(i: CommentNode) {
1382 i.setState({ showConfirmAppointAsMod: true });
1385 handleCancelConfirmAppointAsMod(i: CommentNode) {
1386 i.setState({ showConfirmAppointAsMod: false });
1389 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1390 i.setState({ showConfirmAppointAsAdmin: true });
1393 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1394 i.setState({ showConfirmAppointAsAdmin: false });
1397 handleShowConfirmTransferCommunity(i: CommentNode) {
1398 i.setState({ showConfirmTransferCommunity: true });
1401 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1402 i.setState({ showConfirmTransferCommunity: false });
1405 handleShowConfirmTransferSite(i: CommentNode) {
1406 i.setState({ showConfirmTransferSite: true });
1409 handleCancelShowConfirmTransferSite(i: CommentNode) {
1410 i.setState({ showConfirmTransferSite: false });
1413 get isCommentNew(): boolean {
1414 const now = moment.utc().subtract(10, "minutes");
1415 const then = moment.utc(this.commentView.comment.published);
1416 return now.isBefore(then);
1419 handleCommentCollapse(i: CommentNode) {
1420 i.setState({ collapsed: !i.state.collapsed });
1424 handleViewSource(i: CommentNode) {
1425 i.setState({ viewSource: !i.state.viewSource });
1428 handleShowAdvanced(i: CommentNode) {
1429 i.setState({ showAdvanced: !i.state.showAdvanced });
1433 handleSaveComment(i: CommentNode) {
1434 i.setState({ saveLoading: true });
1436 i.props.onSaveComment({
1437 comment_id: i.commentView.comment.id,
1438 save: !i.commentView.saved,
1439 auth: myAuthRequired(),
1443 handleUpvote(i: CommentNode) {
1444 i.setState({ upvoteLoading: true });
1445 i.props.onCommentVote({
1446 comment_id: i.commentId,
1447 score: newVote(VoteType.Upvote, i.commentView.my_vote),
1448 auth: myAuthRequired(),
1452 handleDownvote(i: CommentNode) {
1453 i.setState({ downvoteLoading: true });
1454 i.props.onCommentVote({
1455 comment_id: i.commentId,
1456 score: newVote(VoteType.Downvote, i.commentView.my_vote),
1457 auth: myAuthRequired(),
1461 handleBlockPerson(i: CommentNode) {
1462 i.setState({ blockPersonLoading: true });
1463 i.props.onBlockPerson({
1464 person_id: i.commentView.creator.id,
1466 auth: myAuthRequired(),
1470 handleMarkAsRead(i: CommentNode) {
1471 i.setState({ readLoading: true });
1472 const cv = i.commentView;
1473 if (i.isPersonMentionType(cv)) {
1474 i.props.onPersonMentionRead({
1475 person_mention_id: cv.person_mention.id,
1476 read: !cv.person_mention.read,
1477 auth: myAuthRequired(),
1479 } else if (i.isCommentReplyType(cv)) {
1480 i.props.onCommentReplyRead({
1481 comment_reply_id: cv.comment_reply.id,
1482 read: !cv.comment_reply.read,
1483 auth: myAuthRequired(),
1488 handleDeleteComment(i: CommentNode) {
1489 i.setState({ deleteLoading: true });
1490 i.props.onDeleteComment({
1491 comment_id: i.commentId,
1492 deleted: !i.commentView.comment.deleted,
1493 auth: myAuthRequired(),
1497 handleRemoveComment(i: CommentNode, event: any) {
1498 event.preventDefault();
1499 i.setState({ removeLoading: true });
1500 i.props.onRemoveComment({
1501 comment_id: i.commentId,
1502 removed: !i.commentView.comment.removed,
1503 auth: myAuthRequired(),
1507 handleDistinguishComment(i: CommentNode) {
1508 i.setState({ distinguishLoading: true });
1509 i.props.onDistinguishComment({
1510 comment_id: i.commentId,
1511 distinguished: !i.commentView.comment.distinguished,
1512 auth: myAuthRequired(),
1516 handleBanPersonFromCommunity(i: CommentNode) {
1517 i.setState({ banLoading: true });
1518 i.props.onBanPersonFromCommunity({
1519 community_id: i.commentView.community.id,
1520 person_id: i.commentView.creator.id,
1521 ban: !i.commentView.creator_banned_from_community,
1522 reason: i.state.banReason,
1523 remove_data: i.state.removeData,
1524 expires: futureDaysToUnixTime(i.state.banExpireDays),
1525 auth: myAuthRequired(),
1529 handleBanPerson(i: CommentNode) {
1530 i.setState({ banLoading: true });
1531 i.props.onBanPerson({
1532 person_id: i.commentView.creator.id,
1533 ban: !i.commentView.creator_banned_from_community,
1534 reason: i.state.banReason,
1535 remove_data: i.state.removeData,
1536 expires: futureDaysToUnixTime(i.state.banExpireDays),
1537 auth: myAuthRequired(),
1541 handleModBanBothSubmit(i: CommentNode, event: any) {
1542 event.preventDefault();
1543 if (i.state.banType == BanType.Community) {
1544 i.handleBanPersonFromCommunity(i);
1546 i.handleBanPerson(i);
1550 handleAddModToCommunity(i: CommentNode) {
1551 i.setState({ addModLoading: true });
1553 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1554 i.props.onAddModToCommunity({
1555 community_id: i.commentView.community.id,
1556 person_id: i.commentView.creator.id,
1558 auth: myAuthRequired(),
1562 handleAddAdmin(i: CommentNode) {
1563 i.setState({ addAdminLoading: true });
1565 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1566 i.props.onAddAdmin({
1567 person_id: i.commentView.creator.id,
1569 auth: myAuthRequired(),
1573 handleTransferCommunity(i: CommentNode) {
1574 i.setState({ transferCommunityLoading: true });
1575 i.props.onTransferCommunity({
1576 community_id: i.commentView.community.id,
1577 person_id: i.commentView.creator.id,
1578 auth: myAuthRequired(),
1582 handleReportComment(i: CommentNode, event: any) {
1583 event.preventDefault();
1584 i.setState({ reportLoading: true });
1585 i.props.onCommentReport({
1586 comment_id: i.commentId,
1587 reason: i.state.reportReason ?? "",
1588 auth: myAuthRequired(),
1592 handlePurgeBothSubmit(i: CommentNode, event: any) {
1593 event.preventDefault();
1594 i.setState({ purgeLoading: true });
1596 if (i.state.purgeType == PurgeType.Person) {
1597 i.props.onPurgePerson({
1598 person_id: i.commentView.creator.id,
1599 reason: i.state.purgeReason,
1600 auth: myAuthRequired(),
1603 i.props.onPurgeComment({
1604 comment_id: i.commentId,
1605 reason: i.state.purgeReason,
1606 auth: myAuthRequired(),
1611 handleFetchChildren(i: CommentNode) {
1612 i.setState({ fetchChildrenLoading: true });
1613 i.props.onFetchChildren?.({
1614 parent_id: i.commentId,
1615 max_depth: commentTreeMaxDepth,