9 import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
17 } from "@utils/roles";
18 import classNames from "classnames";
19 import { Component, InfernoNode, linkEvent } from "inferno";
20 import { Link } from "inferno-router";
30 CommunityModeratorView,
39 MarkCommentReplyAsRead,
40 MarkPersonMentionAsRead,
48 } from "lemmy-js-client";
49 import moment from "moment";
50 import { commentTreeMaxDepth } from "../../config";
57 } from "../../interfaces";
58 import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
59 import { I18NextService, UserService } from "../../services";
60 import { setupTippy } from "../../tippy";
61 import { Icon, PurgeWarning, Spinner } from "../common/icon";
62 import { MomentTime } from "../common/moment-time";
63 import { CommunityLink } from "../community/community-link";
64 import { PersonListing } from "../person/person-listing";
65 import { CommentForm } from "./comment-form";
66 import { CommentNodes } from "./comment-nodes";
68 interface CommentNodeState {
71 showRemoveDialog: boolean;
72 removeReason?: string;
73 showBanDialog: boolean;
76 banExpireDays?: number;
78 showPurgeDialog: boolean;
81 showConfirmTransferSite: boolean;
82 showConfirmTransferCommunity: boolean;
83 showConfirmAppointAsMod: boolean;
84 showConfirmAppointAsAdmin: boolean;
87 showAdvanced: boolean;
88 showReportDialog: boolean;
89 reportReason?: string;
90 createOrEditCommentLoading: boolean;
91 upvoteLoading: boolean;
92 downvoteLoading: boolean;
95 blockPersonLoading: boolean;
96 deleteLoading: boolean;
97 removeLoading: boolean;
98 distinguishLoading: boolean;
100 addModLoading: boolean;
101 addAdminLoading: boolean;
102 transferCommunityLoading: boolean;
103 fetchChildrenLoading: boolean;
104 reportLoading: boolean;
105 purgeLoading: boolean;
108 interface CommentNodeProps {
110 moderators?: CommunityModeratorView[];
111 admins?: PersonView[];
117 showContext?: boolean;
118 showCommunity?: boolean;
119 enableDownvotes?: boolean;
120 viewType: CommentViewType;
121 allLanguages: Language[];
122 siteLanguages: number[];
123 hideImages?: boolean;
124 finished: Map<CommentId, boolean | undefined>;
125 onSaveComment(form: SaveComment): void;
126 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
127 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
128 onCreateComment(form: EditComment | CreateComment): void;
129 onEditComment(form: EditComment | CreateComment): void;
130 onCommentVote(form: CreateCommentLike): void;
131 onBlockPerson(form: BlockPerson): void;
132 onDeleteComment(form: DeleteComment): void;
133 onRemoveComment(form: RemoveComment): void;
134 onDistinguishComment(form: DistinguishComment): void;
135 onAddModToCommunity(form: AddModToCommunity): void;
136 onAddAdmin(form: AddAdmin): void;
137 onBanPersonFromCommunity(form: BanFromCommunity): void;
138 onBanPerson(form: BanPerson): void;
139 onTransferCommunity(form: TransferCommunity): void;
140 onFetchChildren?(form: GetComments): void;
141 onCommentReport(form: CreateCommentReport): void;
142 onPurgePerson(form: PurgePerson): void;
143 onPurgeComment(form: PurgeComment): void;
146 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
147 state: CommentNodeState = {
150 showRemoveDialog: false,
151 showBanDialog: false,
153 banType: BanType.Community,
154 showPurgeDialog: false,
155 purgeType: PurgeType.Person,
159 showConfirmTransferSite: false,
160 showConfirmTransferCommunity: false,
161 showConfirmAppointAsMod: false,
162 showConfirmAppointAsAdmin: false,
163 showReportDialog: false,
164 createOrEditCommentLoading: false,
165 upvoteLoading: false,
166 downvoteLoading: false,
169 blockPersonLoading: false,
170 deleteLoading: false,
171 removeLoading: false,
172 distinguishLoading: false,
174 addModLoading: false,
175 addAdminLoading: false,
176 transferCommunityLoading: false,
177 fetchChildrenLoading: false,
178 reportLoading: false,
182 constructor(props: any, context: any) {
183 super(props, context);
185 this.handleReplyCancel = this.handleReplyCancel.bind(this);
188 get commentView(): CommentView {
189 return this.props.node.comment_view;
192 get commentId(): CommentId {
193 return this.commentView.comment.id;
196 componentWillReceiveProps(
197 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
199 if (this.props != nextProps) {
203 showRemoveDialog: false,
204 showBanDialog: false,
206 banType: BanType.Community,
207 showPurgeDialog: false,
208 purgeType: PurgeType.Person,
212 showConfirmTransferSite: false,
213 showConfirmTransferCommunity: false,
214 showConfirmAppointAsMod: false,
215 showConfirmAppointAsAdmin: false,
216 showReportDialog: false,
217 createOrEditCommentLoading: false,
218 upvoteLoading: false,
219 downvoteLoading: false,
222 blockPersonLoading: false,
223 deleteLoading: false,
224 removeLoading: false,
225 distinguishLoading: false,
227 addModLoading: false,
228 addAdminLoading: false,
229 transferCommunityLoading: false,
230 fetchChildrenLoading: false,
231 reportLoading: false,
238 const node = this.props.node;
239 const cv = this.commentView;
241 const purgeTypeText =
242 this.state.purgeType == PurgeType.Comment
243 ? I18NextService.i18n.t("purge_comment")
244 : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
246 const canMod_ = canMod(
248 this.props.moderators,
251 const canModOnSelf = canMod(
253 this.props.moderators,
255 UserService.Instance.myUserInfo,
258 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
259 const canAdminOnSelf = canAdmin(
262 UserService.Instance.myUserInfo,
265 const isMod_ = isMod(cv.creator.id, this.props.moderators);
266 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
267 const amCommunityCreator_ = amCommunityCreator(
269 this.props.moderators
272 const moreRepliesBorderColor = this.props.node.depth
273 ? colorList[this.props.node.depth % colorList.length]
276 const showMoreChildren =
277 this.props.viewType == CommentViewType.Tree &&
278 !this.state.collapsed &&
279 node.children.length == 0 &&
280 node.comment_view.counts.child_count > 0;
283 <li className="comment" role="comment">
285 id={`comment-${cv.comment.id}`}
286 className={classNames(`details comment-node py-2`, {
287 "border-top border-light": !this.props.noBorder,
288 mark: this.isCommentNew || this.commentView.comment.distinguished,
292 className={classNames({
293 "ms-2": !this.props.noIndent,
296 <div className="d-flex flex-wrap align-items-center text-muted small">
298 className="btn btn-sm text-muted me-2"
299 onClick={linkEvent(this, this.handleCommentCollapse)}
300 aria-label={this.expandText}
301 data-tippy-content={this.expandText}
304 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
305 classes="icon-inline"
308 <span className="me-2">
309 <PersonListing person={cv.creator} />
311 {cv.comment.distinguished && (
312 <Icon icon="shield" inline classes={`text-danger me-2`} />
314 {this.isPostCreator && (
315 <div className="badge text-bg-light d-none d-sm-inline me-2">
316 {I18NextService.i18n.t("creator")}
320 <div className="badge text-bg-light d-none d-sm-inline me-2">
321 {I18NextService.i18n.t("mod")}
325 <div className="badge text-bg-light d-none d-sm-inline me-2">
326 {I18NextService.i18n.t("admin")}
329 {cv.creator.bot_account && (
330 <div className="badge text-bg-light d-none d-sm-inline me-2">
331 {I18NextService.i18n.t("bot_account").toLowerCase()}
334 {this.props.showCommunity && (
336 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
337 <CommunityLink community={cv.community} />
338 <span className="mx-2">•</span>
339 <Link className="me-2" to={`/post/${cv.post.id}`}>
345 {cv.comment.language_id !== 0 && (
346 <span className="badge text-bg-light d-none d-sm-inline me-2">
348 this.props.allLanguages.find(
349 lang => lang.id === cv.comment.language_id
354 {/* This is an expanding spacer for mobile */}
355 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
359 className={`unselectable pointer ${this.scoreColor}`}
360 onClick={linkEvent(this, this.handleUpvote)}
361 data-tippy-content={this.pointsTippy}
363 {this.state.upvoteLoading ? (
367 className="me-1 font-weight-bold"
368 aria-label={I18NextService.i18n.t("number_of_points", {
369 count: Number(this.commentView.counts.score),
370 formattedCount: numToSI(
371 this.commentView.counts.score
375 {numToSI(this.commentView.counts.score)}
379 <span className="me-1">•</span>
384 published={cv.comment.published}
385 updated={cv.comment.updated}
389 {/* end of user row */}
390 {this.state.showEdit && (
394 onReplyCancel={this.handleReplyCancel}
395 disabled={this.props.locked}
396 finished={this.props.finished.get(
397 this.props.node.comment_view.comment.id
400 allLanguages={this.props.allLanguages}
401 siteLanguages={this.props.siteLanguages}
402 containerClass="comment-comment-container"
403 onUpsertComment={this.props.onEditComment}
406 {!this.state.showEdit && !this.state.collapsed && (
408 {this.state.viewSource ? (
409 <pre>{this.commentUnlessRemoved}</pre>
413 dangerouslySetInnerHTML={
414 this.props.hideImages
415 ? mdToHtmlNoImages(this.commentUnlessRemoved)
416 : mdToHtml(this.commentUnlessRemoved)
420 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
421 {this.props.showContext && this.linkBtn()}
422 {this.props.markable && (
424 className="btn btn-link btn-animate text-muted"
425 onClick={linkEvent(this, this.handleMarkAsRead)}
427 this.commentReplyOrMentionRead
428 ? I18NextService.i18n.t("mark_as_unread")
429 : I18NextService.i18n.t("mark_as_read")
432 this.commentReplyOrMentionRead
433 ? I18NextService.i18n.t("mark_as_unread")
434 : I18NextService.i18n.t("mark_as_read")
437 {this.state.readLoading ? (
442 classes={`icon-inline ${
443 this.commentReplyOrMentionRead && "text-success"
449 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
452 className={`btn btn-link btn-animate ${
453 this.commentView.my_vote === 1
457 onClick={linkEvent(this, this.handleUpvote)}
458 data-tippy-content={I18NextService.i18n.t("upvote")}
459 aria-label={I18NextService.i18n.t("upvote")}
460 aria-pressed={this.commentView.my_vote === 1}
462 {this.state.upvoteLoading ? (
466 <Icon icon="arrow-up1" classes="icon-inline" />
468 this.commentView.counts.upvotes !==
469 this.commentView.counts.score && (
470 <span className="ms-1">
471 {numToSI(this.commentView.counts.upvotes)}
477 {this.props.enableDownvotes && (
479 className={`btn btn-link btn-animate ${
480 this.commentView.my_vote === -1
484 onClick={linkEvent(this, this.handleDownvote)}
485 data-tippy-content={I18NextService.i18n.t("downvote")}
486 aria-label={I18NextService.i18n.t("downvote")}
487 aria-pressed={this.commentView.my_vote === -1}
489 {this.state.downvoteLoading ? (
493 <Icon icon="arrow-down1" classes="icon-inline" />
495 this.commentView.counts.upvotes !==
496 this.commentView.counts.score && (
497 <span className="ms-1">
498 {numToSI(this.commentView.counts.downvotes)}
506 className="btn btn-link btn-animate text-muted"
507 onClick={linkEvent(this, this.handleReplyClick)}
508 data-tippy-content={I18NextService.i18n.t("reply")}
509 aria-label={I18NextService.i18n.t("reply")}
511 <Icon icon="reply1" classes="icon-inline" />
513 {!this.state.showAdvanced ? (
515 className="btn btn-link btn-animate text-muted btn-more"
516 onClick={linkEvent(this, this.handleShowAdvanced)}
517 data-tippy-content={I18NextService.i18n.t("more")}
518 aria-label={I18NextService.i18n.t("more")}
520 <Icon icon="more-vertical" classes="icon-inline" />
524 {!this.myComment && (
527 className="btn btn-link btn-animate text-muted"
528 to={`/create_private_message/${cv.creator.id}`}
529 title={I18NextService.i18n
536 className="btn btn-link btn-animate text-muted"
539 this.handleShowReportDialog
541 data-tippy-content={I18NextService.i18n.t(
544 aria-label={I18NextService.i18n.t(
551 className="btn btn-link btn-animate text-muted"
554 this.handleBlockPerson
556 data-tippy-content={I18NextService.i18n.t(
559 aria-label={I18NextService.i18n.t("block_user")}
561 {this.state.blockPersonLoading ? (
564 <Icon icon="slash" />
570 className="btn btn-link btn-animate text-muted"
571 onClick={linkEvent(this, this.handleSaveComment)}
574 ? I18NextService.i18n.t("unsave")
575 : I18NextService.i18n.t("save")
579 ? I18NextService.i18n.t("unsave")
580 : I18NextService.i18n.t("save")
583 {this.state.saveLoading ? (
588 classes={`icon-inline ${
589 cv.saved && "text-warning"
595 className="btn btn-link btn-animate text-muted"
596 onClick={linkEvent(this, this.handleViewSource)}
597 data-tippy-content={I18NextService.i18n.t(
600 aria-label={I18NextService.i18n.t("view_source")}
604 classes={`icon-inline ${
605 this.state.viewSource && "text-success"
612 className="btn btn-link btn-animate text-muted"
613 onClick={linkEvent(this, this.handleEditClick)}
614 data-tippy-content={I18NextService.i18n.t(
617 aria-label={I18NextService.i18n.t("edit")}
619 <Icon icon="edit" classes="icon-inline" />
622 className="btn btn-link btn-animate text-muted"
625 this.handleDeleteComment
629 ? I18NextService.i18n.t("delete")
630 : I18NextService.i18n.t("restore")
634 ? I18NextService.i18n.t("delete")
635 : I18NextService.i18n.t("restore")
638 {this.state.deleteLoading ? (
643 classes={`icon-inline ${
644 cv.comment.deleted && "text-danger"
650 {(canModOnSelf || canAdminOnSelf) && (
652 className="btn btn-link btn-animate text-muted"
655 this.handleDistinguishComment
658 !cv.comment.distinguished
659 ? I18NextService.i18n.t("distinguish")
660 : I18NextService.i18n.t("undistinguish")
663 !cv.comment.distinguished
664 ? I18NextService.i18n.t("distinguish")
665 : I18NextService.i18n.t("undistinguish")
670 classes={`icon-inline ${
671 cv.comment.distinguished && "text-danger"
678 {/* Admins and mods can remove comments */}
679 {(canMod_ || canAdmin_) && (
681 {!cv.comment.removed ? (
683 className="btn btn-link btn-animate text-muted"
686 this.handleModRemoveShow
688 aria-label={I18NextService.i18n.t("remove")}
690 {I18NextService.i18n.t("remove")}
694 className="btn btn-link btn-animate text-muted"
697 this.handleRemoveComment
699 aria-label={I18NextService.i18n.t("restore")}
701 {this.state.removeLoading ? (
704 I18NextService.i18n.t("restore")
710 {/* Mods can ban from community, and appoint as mods to community */}
714 (!cv.creator_banned_from_community ? (
716 className="btn btn-link btn-animate text-muted"
719 this.handleModBanFromCommunityShow
721 aria-label={I18NextService.i18n.t(
725 {I18NextService.i18n.t(
731 className="btn btn-link btn-animate text-muted"
734 this.handleBanPersonFromCommunity
736 aria-label={I18NextService.i18n.t("unban")}
738 {this.state.banLoading ? (
741 I18NextService.i18n.t("unban")
745 {!cv.creator_banned_from_community &&
746 (!this.state.showConfirmAppointAsMod ? (
748 className="btn btn-link btn-animate text-muted"
751 this.handleShowConfirmAppointAsMod
755 ? I18NextService.i18n.t("remove_as_mod")
756 : I18NextService.i18n.t(
762 ? I18NextService.i18n.t("remove_as_mod")
763 : I18NextService.i18n.t("appoint_as_mod")}
768 className="btn btn-link btn-animate text-muted"
769 aria-label={I18NextService.i18n.t(
773 {I18NextService.i18n.t("are_you_sure")}
776 className="btn btn-link btn-animate text-muted"
779 this.handleAddModToCommunity
781 aria-label={I18NextService.i18n.t("yes")}
783 {this.state.addModLoading ? (
786 I18NextService.i18n.t("yes")
790 className="btn btn-link btn-animate text-muted"
793 this.handleCancelConfirmAppointAsMod
795 aria-label={I18NextService.i18n.t("no")}
797 {I18NextService.i18n.t("no")}
803 {/* Community creators and admins can transfer community to another mod */}
804 {(amCommunityCreator_ || canAdmin_) &&
807 (!this.state.showConfirmTransferCommunity ? (
809 className="btn btn-link btn-animate text-muted"
812 this.handleShowConfirmTransferCommunity
814 aria-label={I18NextService.i18n.t(
818 {I18NextService.i18n.t("transfer_community")}
823 className="btn btn-link btn-animate text-muted"
824 aria-label={I18NextService.i18n.t(
828 {I18NextService.i18n.t("are_you_sure")}
831 className="btn btn-link btn-animate text-muted"
834 this.handleTransferCommunity
836 aria-label={I18NextService.i18n.t("yes")}
838 {this.state.transferCommunityLoading ? (
841 I18NextService.i18n.t("yes")
845 className="btn btn-link btn-animate text-muted"
849 .handleCancelShowConfirmTransferCommunity
851 aria-label={I18NextService.i18n.t("no")}
853 {I18NextService.i18n.t("no")}
857 {/* Admins can ban from all, and appoint other admins */}
863 className="btn btn-link btn-animate text-muted"
866 this.handlePurgePersonShow
868 aria-label={I18NextService.i18n.t(
872 {I18NextService.i18n.t("purge_user")}
875 className="btn btn-link btn-animate text-muted"
878 this.handlePurgeCommentShow
880 aria-label={I18NextService.i18n.t(
884 {I18NextService.i18n.t("purge_comment")}
887 {!isBanned(cv.creator) ? (
889 className="btn btn-link btn-animate text-muted"
892 this.handleModBanShow
894 aria-label={I18NextService.i18n.t(
898 {I18NextService.i18n.t("ban_from_site")}
902 className="btn btn-link btn-animate text-muted"
907 aria-label={I18NextService.i18n.t(
911 {this.state.banLoading ? (
914 I18NextService.i18n.t("unban_from_site")
920 {!isBanned(cv.creator) &&
922 (!this.state.showConfirmAppointAsAdmin ? (
924 className="btn btn-link btn-animate text-muted"
927 this.handleShowConfirmAppointAsAdmin
931 ? I18NextService.i18n.t(
934 : I18NextService.i18n.t(
940 ? I18NextService.i18n.t("remove_as_admin")
941 : I18NextService.i18n.t(
947 <button className="btn btn-link btn-animate text-muted">
948 {I18NextService.i18n.t("are_you_sure")}
951 className="btn btn-link btn-animate text-muted"
956 aria-label={I18NextService.i18n.t("yes")}
958 {this.state.addAdminLoading ? (
961 I18NextService.i18n.t("yes")
965 className="btn btn-link btn-animate text-muted"
968 this.handleCancelConfirmAppointAsAdmin
970 aria-label={I18NextService.i18n.t("no")}
972 {I18NextService.i18n.t("no")}
983 {/* end of button group */}
988 {showMoreChildren && (
990 className={classNames("details ms-1 comment-node py-2", {
991 "border-top border-light": !this.props.noBorder,
993 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
996 className="btn btn-link text-muted"
997 onClick={linkEvent(this, this.handleFetchChildren)}
999 {this.state.fetchChildrenLoading ? (
1003 {I18NextService.i18n.t("x_more_replies", {
1004 count: node.comment_view.counts.child_count,
1005 formattedCount: numToSI(
1006 node.comment_view.counts.child_count
1015 {/* end of details */}
1016 {this.state.showRemoveDialog && (
1018 className="form-inline"
1019 onSubmit={linkEvent(this, this.handleRemoveComment)}
1022 className="visually-hidden"
1023 htmlFor={`mod-remove-reason-${cv.comment.id}`}
1025 {I18NextService.i18n.t("reason")}
1029 id={`mod-remove-reason-${cv.comment.id}`}
1030 className="form-control me-2"
1031 placeholder={I18NextService.i18n.t("reason")}
1032 value={this.state.removeReason}
1033 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1037 className="btn btn-secondary"
1038 aria-label={I18NextService.i18n.t("remove_comment")}
1040 {I18NextService.i18n.t("remove_comment")}
1044 {this.state.showReportDialog && (
1046 className="form-inline"
1047 onSubmit={linkEvent(this, this.handleReportComment)}
1050 className="visually-hidden"
1051 htmlFor={`report-reason-${cv.comment.id}`}
1053 {I18NextService.i18n.t("reason")}
1058 id={`report-reason-${cv.comment.id}`}
1059 className="form-control me-2"
1060 placeholder={I18NextService.i18n.t("reason")}
1061 value={this.state.reportReason}
1062 onInput={linkEvent(this, this.handleReportReasonChange)}
1066 className="btn btn-secondary"
1067 aria-label={I18NextService.i18n.t("create_report")}
1069 {I18NextService.i18n.t("create_report")}
1073 {this.state.showBanDialog && (
1074 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1075 <div className="mb-3 row col-12">
1077 className="col-form-label"
1078 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1080 {I18NextService.i18n.t("reason")}
1084 id={`mod-ban-reason-${cv.comment.id}`}
1085 className="form-control me-2"
1086 placeholder={I18NextService.i18n.t("reason")}
1087 value={this.state.banReason}
1088 onInput={linkEvent(this, this.handleModBanReasonChange)}
1091 className="col-form-label"
1092 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1094 {I18NextService.i18n.t("expires")}
1098 id={`mod-ban-expires-${cv.comment.id}`}
1099 className="form-control me-2"
1100 placeholder={I18NextService.i18n.t("number_of_days")}
1101 value={this.state.banExpireDays}
1102 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1104 <div className="input-group mb-3">
1105 <div className="form-check">
1107 className="form-check-input"
1108 id="mod-ban-remove-data"
1110 checked={this.state.removeData}
1111 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1114 className="form-check-label"
1115 htmlFor="mod-ban-remove-data"
1116 title={I18NextService.i18n.t("remove_content_more")}
1118 {I18NextService.i18n.t("remove_content")}
1123 {/* TODO hold off on expires until later */}
1124 {/* <div class="mb-3 row"> */}
1125 {/* <label class="col-form-label">Expires</label> */}
1126 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1128 <div className="mb-3 row">
1131 className="btn btn-secondary"
1132 aria-label={I18NextService.i18n.t("ban")}
1134 {this.state.banLoading ? (
1138 {I18NextService.i18n.t("ban")} {cv.creator.name}
1146 {this.state.showPurgeDialog && (
1147 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1149 <label className="visually-hidden" htmlFor="purge-reason">
1150 {I18NextService.i18n.t("reason")}
1155 className="form-control my-3"
1156 placeholder={I18NextService.i18n.t("reason")}
1157 value={this.state.purgeReason}
1158 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1160 <div className="mb-3 row col-12">
1161 {this.state.purgeLoading ? (
1166 className="btn btn-secondary"
1167 aria-label={purgeTypeText}
1175 {this.state.showReply && (
1178 onReplyCancel={this.handleReplyCancel}
1179 disabled={this.props.locked}
1180 finished={this.props.finished.get(
1181 this.props.node.comment_view.comment.id
1184 allLanguages={this.props.allLanguages}
1185 siteLanguages={this.props.siteLanguages}
1186 containerClass="comment-comment-container"
1187 onUpsertComment={this.props.onCreateComment}
1190 {!this.state.collapsed && node.children.length > 0 && (
1192 nodes={node.children}
1193 locked={this.props.locked}
1194 moderators={this.props.moderators}
1195 admins={this.props.admins}
1196 enableDownvotes={this.props.enableDownvotes}
1197 viewType={this.props.viewType}
1198 allLanguages={this.props.allLanguages}
1199 siteLanguages={this.props.siteLanguages}
1200 hideImages={this.props.hideImages}
1201 isChild={!this.props.noIndent}
1202 depth={this.props.node.depth + 1}
1203 finished={this.props.finished}
1204 onCommentReplyRead={this.props.onCommentReplyRead}
1205 onPersonMentionRead={this.props.onPersonMentionRead}
1206 onCreateComment={this.props.onCreateComment}
1207 onEditComment={this.props.onEditComment}
1208 onCommentVote={this.props.onCommentVote}
1209 onBlockPerson={this.props.onBlockPerson}
1210 onSaveComment={this.props.onSaveComment}
1211 onDeleteComment={this.props.onDeleteComment}
1212 onRemoveComment={this.props.onRemoveComment}
1213 onDistinguishComment={this.props.onDistinguishComment}
1214 onAddModToCommunity={this.props.onAddModToCommunity}
1215 onAddAdmin={this.props.onAddAdmin}
1216 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1217 onBanPerson={this.props.onBanPerson}
1218 onTransferCommunity={this.props.onTransferCommunity}
1219 onFetchChildren={this.props.onFetchChildren}
1220 onCommentReport={this.props.onCommentReport}
1221 onPurgePerson={this.props.onPurgePerson}
1222 onPurgeComment={this.props.onPurgeComment}
1225 {/* A collapsed clearfix */}
1226 {this.state.collapsed && <div className="row col-12" />}
1231 get commentReplyOrMentionRead(): boolean {
1232 const cv = this.commentView;
1234 if (this.isPersonMentionType(cv)) {
1235 return cv.person_mention.read;
1236 } else if (this.isCommentReplyType(cv)) {
1237 return cv.comment_reply.read;
1243 linkBtn(small = false) {
1244 const cv = this.commentView;
1246 const classnames = classNames("btn btn-link btn-animate text-muted", {
1250 const title = this.props.showContext
1251 ? I18NextService.i18n.t("show_context")
1252 : I18NextService.i18n.t("link");
1254 // The context button should show the parent comment by default
1255 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1260 className={classnames}
1261 to={`/comment/${parentCommentId}`}
1264 <Icon icon="link" classes="icon-inline" />
1267 <a className={classnames} title={title} href={cv.comment.ap_id}>
1268 <Icon icon="fedilink" classes="icon-inline" />
1275 get myComment(): boolean {
1277 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1278 this.commentView.creator.id
1282 get isPostCreator(): boolean {
1283 return this.commentView.creator.id == this.commentView.post.creator_id;
1287 if (this.commentView.my_vote == 1) {
1289 } else if (this.commentView.my_vote == -1) {
1290 return "text-danger";
1292 return "text-muted";
1296 get pointsTippy(): string {
1297 const points = I18NextService.i18n.t("number_of_points", {
1298 count: Number(this.commentView.counts.score),
1299 formattedCount: numToSI(this.commentView.counts.score),
1302 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1303 count: Number(this.commentView.counts.upvotes),
1304 formattedCount: numToSI(this.commentView.counts.upvotes),
1307 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1308 count: Number(this.commentView.counts.downvotes),
1309 formattedCount: numToSI(this.commentView.counts.downvotes),
1312 return `${points} • ${upvotes} • ${downvotes}`;
1315 get expandText(): string {
1316 return this.state.collapsed
1317 ? I18NextService.i18n.t("expand")
1318 : I18NextService.i18n.t("collapse");
1321 get commentUnlessRemoved(): string {
1322 const comment = this.commentView.comment;
1323 return comment.removed
1324 ? `*${I18NextService.i18n.t("removed")}*`
1326 ? `*${I18NextService.i18n.t("deleted")}*`
1330 handleReplyClick(i: CommentNode) {
1331 i.setState({ showReply: true });
1334 handleEditClick(i: CommentNode) {
1335 i.setState({ showEdit: true });
1338 handleReplyCancel() {
1339 this.setState({ showReply: false, showEdit: false });
1342 handleShowReportDialog(i: CommentNode) {
1343 i.setState({ showReportDialog: !i.state.showReportDialog });
1346 handleReportReasonChange(i: CommentNode, event: any) {
1347 i.setState({ reportReason: event.target.value });
1350 handleModRemoveShow(i: CommentNode) {
1352 showRemoveDialog: !i.state.showRemoveDialog,
1353 showBanDialog: false,
1357 handleModRemoveReasonChange(i: CommentNode, event: any) {
1358 i.setState({ removeReason: event.target.value });
1361 handleModRemoveDataChange(i: CommentNode, event: any) {
1362 i.setState({ removeData: event.target.checked });
1365 isPersonMentionType(
1366 item: CommentView | PersonMentionView | CommentReplyView
1367 ): item is PersonMentionView {
1368 return (item as PersonMentionView).person_mention?.id !== undefined;
1372 item: CommentView | PersonMentionView | CommentReplyView
1373 ): item is CommentReplyView {
1374 return (item as CommentReplyView).comment_reply?.id !== undefined;
1377 handleModBanFromCommunityShow(i: CommentNode) {
1379 showBanDialog: true,
1380 banType: BanType.Community,
1381 showRemoveDialog: false,
1385 handleModBanShow(i: CommentNode) {
1387 showBanDialog: true,
1388 banType: BanType.Site,
1389 showRemoveDialog: false,
1393 handleModBanReasonChange(i: CommentNode, event: any) {
1394 i.setState({ banReason: event.target.value });
1397 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1398 i.setState({ banExpireDays: event.target.value });
1401 handlePurgePersonShow(i: CommentNode) {
1403 showPurgeDialog: true,
1404 purgeType: PurgeType.Person,
1405 showRemoveDialog: false,
1409 handlePurgeCommentShow(i: CommentNode) {
1411 showPurgeDialog: true,
1412 purgeType: PurgeType.Comment,
1413 showRemoveDialog: false,
1417 handlePurgeReasonChange(i: CommentNode, event: any) {
1418 i.setState({ purgeReason: event.target.value });
1421 handleShowConfirmAppointAsMod(i: CommentNode) {
1422 i.setState({ showConfirmAppointAsMod: true });
1425 handleCancelConfirmAppointAsMod(i: CommentNode) {
1426 i.setState({ showConfirmAppointAsMod: false });
1429 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1430 i.setState({ showConfirmAppointAsAdmin: true });
1433 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1434 i.setState({ showConfirmAppointAsAdmin: false });
1437 handleShowConfirmTransferCommunity(i: CommentNode) {
1438 i.setState({ showConfirmTransferCommunity: true });
1441 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1442 i.setState({ showConfirmTransferCommunity: false });
1445 handleShowConfirmTransferSite(i: CommentNode) {
1446 i.setState({ showConfirmTransferSite: true });
1449 handleCancelShowConfirmTransferSite(i: CommentNode) {
1450 i.setState({ showConfirmTransferSite: false });
1453 get isCommentNew(): boolean {
1454 const now = moment.utc().subtract(10, "minutes");
1455 const then = moment.utc(this.commentView.comment.published);
1456 return now.isBefore(then);
1459 handleCommentCollapse(i: CommentNode) {
1460 i.setState({ collapsed: !i.state.collapsed });
1464 handleViewSource(i: CommentNode) {
1465 i.setState({ viewSource: !i.state.viewSource });
1468 handleShowAdvanced(i: CommentNode) {
1469 i.setState({ showAdvanced: !i.state.showAdvanced });
1473 handleSaveComment(i: CommentNode) {
1474 i.setState({ saveLoading: true });
1476 i.props.onSaveComment({
1477 comment_id: i.commentView.comment.id,
1478 save: !i.commentView.saved,
1479 auth: myAuthRequired(),
1483 handleUpvote(i: CommentNode) {
1484 i.setState({ upvoteLoading: true });
1485 i.props.onCommentVote({
1486 comment_id: i.commentId,
1487 score: newVote(VoteType.Upvote, i.commentView.my_vote),
1488 auth: myAuthRequired(),
1492 handleDownvote(i: CommentNode) {
1493 i.setState({ downvoteLoading: true });
1494 i.props.onCommentVote({
1495 comment_id: i.commentId,
1496 score: newVote(VoteType.Downvote, i.commentView.my_vote),
1497 auth: myAuthRequired(),
1501 handleBlockPerson(i: CommentNode) {
1502 i.setState({ blockPersonLoading: true });
1503 i.props.onBlockPerson({
1504 person_id: i.commentView.creator.id,
1506 auth: myAuthRequired(),
1510 handleMarkAsRead(i: CommentNode) {
1511 i.setState({ readLoading: true });
1512 const cv = i.commentView;
1513 if (i.isPersonMentionType(cv)) {
1514 i.props.onPersonMentionRead({
1515 person_mention_id: cv.person_mention.id,
1516 read: !cv.person_mention.read,
1517 auth: myAuthRequired(),
1519 } else if (i.isCommentReplyType(cv)) {
1520 i.props.onCommentReplyRead({
1521 comment_reply_id: cv.comment_reply.id,
1522 read: !cv.comment_reply.read,
1523 auth: myAuthRequired(),
1528 handleDeleteComment(i: CommentNode) {
1529 i.setState({ deleteLoading: true });
1530 i.props.onDeleteComment({
1531 comment_id: i.commentId,
1532 deleted: !i.commentView.comment.deleted,
1533 auth: myAuthRequired(),
1537 handleRemoveComment(i: CommentNode, event: any) {
1538 event.preventDefault();
1539 i.setState({ removeLoading: true });
1540 i.props.onRemoveComment({
1541 comment_id: i.commentId,
1542 removed: !i.commentView.comment.removed,
1543 auth: myAuthRequired(),
1547 handleDistinguishComment(i: CommentNode) {
1548 i.setState({ distinguishLoading: true });
1549 i.props.onDistinguishComment({
1550 comment_id: i.commentId,
1551 distinguished: !i.commentView.comment.distinguished,
1552 auth: myAuthRequired(),
1556 handleBanPersonFromCommunity(i: CommentNode) {
1557 i.setState({ banLoading: true });
1558 i.props.onBanPersonFromCommunity({
1559 community_id: i.commentView.community.id,
1560 person_id: i.commentView.creator.id,
1561 ban: !i.commentView.creator_banned_from_community,
1562 reason: i.state.banReason,
1563 remove_data: i.state.removeData,
1564 expires: futureDaysToUnixTime(i.state.banExpireDays),
1565 auth: myAuthRequired(),
1569 handleBanPerson(i: CommentNode) {
1570 i.setState({ banLoading: true });
1571 i.props.onBanPerson({
1572 person_id: i.commentView.creator.id,
1573 ban: !i.commentView.creator_banned_from_community,
1574 reason: i.state.banReason,
1575 remove_data: i.state.removeData,
1576 expires: futureDaysToUnixTime(i.state.banExpireDays),
1577 auth: myAuthRequired(),
1581 handleModBanBothSubmit(i: CommentNode, event: any) {
1582 event.preventDefault();
1583 if (i.state.banType == BanType.Community) {
1584 i.handleBanPersonFromCommunity(i);
1586 i.handleBanPerson(i);
1590 handleAddModToCommunity(i: CommentNode) {
1591 i.setState({ addModLoading: true });
1593 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1594 i.props.onAddModToCommunity({
1595 community_id: i.commentView.community.id,
1596 person_id: i.commentView.creator.id,
1598 auth: myAuthRequired(),
1602 handleAddAdmin(i: CommentNode) {
1603 i.setState({ addAdminLoading: true });
1605 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1606 i.props.onAddAdmin({
1607 person_id: i.commentView.creator.id,
1609 auth: myAuthRequired(),
1613 handleTransferCommunity(i: CommentNode) {
1614 i.setState({ transferCommunityLoading: true });
1615 i.props.onTransferCommunity({
1616 community_id: i.commentView.community.id,
1617 person_id: i.commentView.creator.id,
1618 auth: myAuthRequired(),
1622 handleReportComment(i: CommentNode, event: any) {
1623 event.preventDefault();
1624 i.setState({ reportLoading: true });
1625 i.props.onCommentReport({
1626 comment_id: i.commentId,
1627 reason: i.state.reportReason ?? "",
1628 auth: myAuthRequired(),
1632 handlePurgeBothSubmit(i: CommentNode, event: any) {
1633 event.preventDefault();
1634 i.setState({ purgeLoading: true });
1636 if (i.state.purgeType == PurgeType.Person) {
1637 i.props.onPurgePerson({
1638 person_id: i.commentView.creator.id,
1639 reason: i.state.purgeReason,
1640 auth: myAuthRequired(),
1643 i.props.onPurgeComment({
1644 comment_id: i.commentId,
1645 reason: i.state.purgeReason,
1646 auth: myAuthRequired(),
1651 handleFetchChildren(i: CommentNode) {
1652 i.setState({ fetchChildrenLoading: true });
1653 i.props.onFetchChildren?.({
1654 parent_id: i.commentId,
1655 max_depth: commentTreeMaxDepth,