9 import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
17 } from "@utils/roles";
18 import classNames from "classnames";
19 import isBefore from "date-fns/isBefore";
20 import parseISO from "date-fns/parseISO";
21 import subMinutes from "date-fns/subMinutes";
22 import { Component, InfernoNode, linkEvent } from "inferno";
23 import { Link } from "inferno-router";
33 CommunityModeratorView,
42 MarkCommentReplyAsRead,
43 MarkPersonMentionAsRead,
51 } from "lemmy-js-client";
52 import { commentTreeMaxDepth } from "../../config";
59 } from "../../interfaces";
60 import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
61 import { I18NextService, UserService } from "../../services";
62 import { setupTippy } from "../../tippy";
63 import { Icon, PurgeWarning, Spinner } from "../common/icon";
64 import { MomentTime } from "../common/moment-time";
65 import { CommunityLink } from "../community/community-link";
66 import { PersonListing } from "../person/person-listing";
67 import { CommentForm } from "./comment-form";
68 import { CommentNodes } from "./comment-nodes";
70 interface CommentNodeState {
73 showRemoveDialog: boolean;
74 removeReason?: string;
75 showBanDialog: boolean;
78 banExpireDays?: number;
80 showPurgeDialog: boolean;
83 showConfirmTransferSite: boolean;
84 showConfirmTransferCommunity: boolean;
85 showConfirmAppointAsMod: boolean;
86 showConfirmAppointAsAdmin: boolean;
89 showAdvanced: boolean;
90 showReportDialog: boolean;
91 reportReason?: string;
92 createOrEditCommentLoading: boolean;
93 upvoteLoading: boolean;
94 downvoteLoading: boolean;
97 blockPersonLoading: boolean;
98 deleteLoading: boolean;
99 removeLoading: boolean;
100 distinguishLoading: boolean;
102 addModLoading: boolean;
103 addAdminLoading: boolean;
104 transferCommunityLoading: boolean;
105 fetchChildrenLoading: boolean;
106 reportLoading: boolean;
107 purgeLoading: boolean;
110 interface CommentNodeProps {
112 moderators?: CommunityModeratorView[];
113 admins?: PersonView[];
119 showContext?: boolean;
120 showCommunity?: boolean;
121 enableDownvotes?: boolean;
122 viewType: CommentViewType;
123 allLanguages: Language[];
124 siteLanguages: number[];
125 hideImages?: boolean;
126 finished: Map<CommentId, boolean | undefined>;
127 onSaveComment(form: SaveComment): void;
128 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
129 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
130 onCreateComment(form: EditComment | CreateComment): void;
131 onEditComment(form: EditComment | CreateComment): void;
132 onCommentVote(form: CreateCommentLike): void;
133 onBlockPerson(form: BlockPerson): void;
134 onDeleteComment(form: DeleteComment): void;
135 onRemoveComment(form: RemoveComment): void;
136 onDistinguishComment(form: DistinguishComment): void;
137 onAddModToCommunity(form: AddModToCommunity): void;
138 onAddAdmin(form: AddAdmin): void;
139 onBanPersonFromCommunity(form: BanFromCommunity): void;
140 onBanPerson(form: BanPerson): void;
141 onTransferCommunity(form: TransferCommunity): void;
142 onFetchChildren?(form: GetComments): void;
143 onCommentReport(form: CreateCommentReport): void;
144 onPurgePerson(form: PurgePerson): void;
145 onPurgeComment(form: PurgeComment): void;
148 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
149 state: CommentNodeState = {
152 showRemoveDialog: false,
153 showBanDialog: false,
155 banType: BanType.Community,
156 showPurgeDialog: false,
157 purgeType: PurgeType.Person,
161 showConfirmTransferSite: false,
162 showConfirmTransferCommunity: false,
163 showConfirmAppointAsMod: false,
164 showConfirmAppointAsAdmin: false,
165 showReportDialog: false,
166 createOrEditCommentLoading: false,
167 upvoteLoading: false,
168 downvoteLoading: false,
171 blockPersonLoading: false,
172 deleteLoading: false,
173 removeLoading: false,
174 distinguishLoading: false,
176 addModLoading: false,
177 addAdminLoading: false,
178 transferCommunityLoading: false,
179 fetchChildrenLoading: false,
180 reportLoading: false,
184 constructor(props: any, context: any) {
185 super(props, context);
187 this.handleReplyCancel = this.handleReplyCancel.bind(this);
190 get commentView(): CommentView {
191 return this.props.node.comment_view;
194 get commentId(): CommentId {
195 return this.commentView.comment.id;
198 componentWillReceiveProps(
199 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
201 if (this.props != nextProps) {
205 showRemoveDialog: false,
206 showBanDialog: false,
208 banType: BanType.Community,
209 showPurgeDialog: false,
210 purgeType: PurgeType.Person,
214 showConfirmTransferSite: false,
215 showConfirmTransferCommunity: false,
216 showConfirmAppointAsMod: false,
217 showConfirmAppointAsAdmin: false,
218 showReportDialog: false,
219 createOrEditCommentLoading: false,
220 upvoteLoading: false,
221 downvoteLoading: false,
224 blockPersonLoading: false,
225 deleteLoading: false,
226 removeLoading: false,
227 distinguishLoading: false,
229 addModLoading: false,
230 addAdminLoading: false,
231 transferCommunityLoading: false,
232 fetchChildrenLoading: false,
233 reportLoading: false,
240 const node = this.props.node;
241 const cv = this.commentView;
243 const purgeTypeText =
244 this.state.purgeType == PurgeType.Comment
245 ? I18NextService.i18n.t("purge_comment")
246 : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
248 const canMod_ = canMod(
250 this.props.moderators,
253 const canModOnSelf = canMod(
255 this.props.moderators,
257 UserService.Instance.myUserInfo,
260 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
261 const canAdminOnSelf = canAdmin(
264 UserService.Instance.myUserInfo,
267 const isMod_ = isMod(cv.creator.id, this.props.moderators);
268 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
269 const amCommunityCreator_ = amCommunityCreator(
271 this.props.moderators
274 const moreRepliesBorderColor = this.props.node.depth
275 ? colorList[this.props.node.depth % colorList.length]
278 const showMoreChildren =
279 this.props.viewType == CommentViewType.Tree &&
280 !this.state.collapsed &&
281 node.children.length == 0 &&
282 node.comment_view.counts.child_count > 0;
285 <li className="comment" role="comment">
287 id={`comment-${cv.comment.id}`}
288 className={classNames(`details comment-node py-2`, {
289 "border-top border-light": !this.props.noBorder,
290 mark: this.isCommentNew || this.commentView.comment.distinguished,
294 className={classNames({
295 "ms-2": !this.props.noIndent,
298 <div className="d-flex flex-wrap align-items-center text-muted small">
300 className="btn btn-sm text-muted me-2"
301 onClick={linkEvent(this, this.handleCommentCollapse)}
302 aria-label={this.expandText}
303 data-tippy-content={this.expandText}
306 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
307 classes="icon-inline"
310 <span className="me-2">
311 <PersonListing person={cv.creator} />
313 {cv.comment.distinguished && (
314 <Icon icon="shield" inline classes={`text-danger me-2`} />
316 {this.isPostCreator && (
317 <div className="badge text-bg-light d-none d-sm-inline me-2">
318 {I18NextService.i18n.t("creator")}
322 <div className="badge text-bg-light d-none d-sm-inline me-2">
323 {I18NextService.i18n.t("mod")}
327 <div className="badge text-bg-light d-none d-sm-inline me-2">
328 {I18NextService.i18n.t("admin")}
331 {cv.creator.bot_account && (
332 <div className="badge text-bg-light d-none d-sm-inline me-2">
333 {I18NextService.i18n.t("bot_account").toLowerCase()}
336 {this.props.showCommunity && (
338 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
339 <CommunityLink community={cv.community} />
340 <span className="mx-2">•</span>
341 <Link className="me-2" to={`/post/${cv.post.id}`}>
347 {cv.comment.language_id !== 0 && (
348 <span className="badge text-bg-light d-none d-sm-inline me-2">
350 this.props.allLanguages.find(
351 lang => lang.id === cv.comment.language_id
356 {/* This is an expanding spacer for mobile */}
357 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
361 className={`unselectable pointer ${this.scoreColor}`}
362 onClick={linkEvent(this, this.handleUpvote)}
363 data-tippy-content={this.pointsTippy}
365 {this.state.upvoteLoading ? (
369 className="me-1 font-weight-bold"
370 aria-label={I18NextService.i18n.t("number_of_points", {
371 count: Number(this.commentView.counts.score),
372 formattedCount: numToSI(
373 this.commentView.counts.score
377 {numToSI(this.commentView.counts.score)}
381 <span className="me-1">•</span>
386 published={cv.comment.published}
387 updated={cv.comment.updated}
391 {/* end of user row */}
392 {this.state.showEdit && (
396 onReplyCancel={this.handleReplyCancel}
397 disabled={this.props.locked}
398 finished={this.props.finished.get(
399 this.props.node.comment_view.comment.id
402 allLanguages={this.props.allLanguages}
403 siteLanguages={this.props.siteLanguages}
404 containerClass="comment-comment-container"
405 onUpsertComment={this.props.onEditComment}
408 {!this.state.showEdit && !this.state.collapsed && (
410 {this.state.viewSource ? (
411 <pre>{this.commentUnlessRemoved}</pre>
415 dangerouslySetInnerHTML={
416 this.props.hideImages
417 ? mdToHtmlNoImages(this.commentUnlessRemoved)
418 : mdToHtml(this.commentUnlessRemoved)
422 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
423 {this.props.showContext && this.linkBtn()}
424 {this.props.markable && (
426 className="btn btn-link btn-animate text-muted"
427 onClick={linkEvent(this, this.handleMarkAsRead)}
429 this.commentReplyOrMentionRead
430 ? I18NextService.i18n.t("mark_as_unread")
431 : I18NextService.i18n.t("mark_as_read")
434 this.commentReplyOrMentionRead
435 ? I18NextService.i18n.t("mark_as_unread")
436 : I18NextService.i18n.t("mark_as_read")
439 {this.state.readLoading ? (
444 classes={`icon-inline ${
445 this.commentReplyOrMentionRead && "text-success"
451 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
454 className={`btn btn-link btn-animate ${
455 this.commentView.my_vote === 1
459 onClick={linkEvent(this, this.handleUpvote)}
460 data-tippy-content={I18NextService.i18n.t("upvote")}
461 aria-label={I18NextService.i18n.t("upvote")}
462 aria-pressed={this.commentView.my_vote === 1}
464 {this.state.upvoteLoading ? (
468 <Icon icon="arrow-up1" classes="icon-inline" />
470 this.commentView.counts.upvotes !==
471 this.commentView.counts.score && (
472 <span className="ms-1">
473 {numToSI(this.commentView.counts.upvotes)}
479 {this.props.enableDownvotes && (
481 className={`btn btn-link btn-animate ${
482 this.commentView.my_vote === -1
486 onClick={linkEvent(this, this.handleDownvote)}
487 data-tippy-content={I18NextService.i18n.t("downvote")}
488 aria-label={I18NextService.i18n.t("downvote")}
489 aria-pressed={this.commentView.my_vote === -1}
491 {this.state.downvoteLoading ? (
495 <Icon icon="arrow-down1" classes="icon-inline" />
497 this.commentView.counts.upvotes !==
498 this.commentView.counts.score && (
499 <span className="ms-1">
500 {numToSI(this.commentView.counts.downvotes)}
508 className="btn btn-link btn-animate text-muted"
509 onClick={linkEvent(this, this.handleReplyClick)}
510 data-tippy-content={I18NextService.i18n.t("reply")}
511 aria-label={I18NextService.i18n.t("reply")}
513 <Icon icon="reply1" classes="icon-inline" />
515 {!this.state.showAdvanced ? (
517 className="btn btn-link btn-animate text-muted btn-more"
518 onClick={linkEvent(this, this.handleShowAdvanced)}
519 data-tippy-content={I18NextService.i18n.t("more")}
520 aria-label={I18NextService.i18n.t("more")}
522 <Icon icon="more-vertical" classes="icon-inline" />
526 {!this.myComment && (
529 className="btn btn-link btn-animate text-muted"
530 to={`/create_private_message/${cv.creator.id}`}
531 title={I18NextService.i18n
538 className="btn btn-link btn-animate text-muted"
541 this.handleShowReportDialog
543 data-tippy-content={I18NextService.i18n.t(
546 aria-label={I18NextService.i18n.t(
553 className="btn btn-link btn-animate text-muted"
556 this.handleBlockPerson
558 data-tippy-content={I18NextService.i18n.t(
561 aria-label={I18NextService.i18n.t("block_user")}
563 {this.state.blockPersonLoading ? (
566 <Icon icon="slash" />
572 className="btn btn-link btn-animate text-muted"
573 onClick={linkEvent(this, this.handleSaveComment)}
576 ? I18NextService.i18n.t("unsave")
577 : I18NextService.i18n.t("save")
581 ? I18NextService.i18n.t("unsave")
582 : I18NextService.i18n.t("save")
585 {this.state.saveLoading ? (
590 classes={`icon-inline ${
591 cv.saved && "text-warning"
597 className="btn btn-link btn-animate text-muted"
598 onClick={linkEvent(this, this.handleViewSource)}
599 data-tippy-content={I18NextService.i18n.t(
602 aria-label={I18NextService.i18n.t("view_source")}
606 classes={`icon-inline ${
607 this.state.viewSource && "text-success"
614 className="btn btn-link btn-animate text-muted"
615 onClick={linkEvent(this, this.handleEditClick)}
616 data-tippy-content={I18NextService.i18n.t(
619 aria-label={I18NextService.i18n.t("edit")}
621 <Icon icon="edit" classes="icon-inline" />
624 className="btn btn-link btn-animate text-muted"
627 this.handleDeleteComment
631 ? I18NextService.i18n.t("delete")
632 : I18NextService.i18n.t("restore")
636 ? I18NextService.i18n.t("delete")
637 : I18NextService.i18n.t("restore")
640 {this.state.deleteLoading ? (
645 classes={`icon-inline ${
646 cv.comment.deleted && "text-danger"
652 {(canModOnSelf || canAdminOnSelf) && (
654 className="btn btn-link btn-animate text-muted"
657 this.handleDistinguishComment
660 !cv.comment.distinguished
661 ? I18NextService.i18n.t("distinguish")
662 : I18NextService.i18n.t("undistinguish")
665 !cv.comment.distinguished
666 ? I18NextService.i18n.t("distinguish")
667 : I18NextService.i18n.t("undistinguish")
672 classes={`icon-inline ${
673 cv.comment.distinguished && "text-danger"
680 {/* Admins and mods can remove comments */}
681 {(canMod_ || canAdmin_) && (
683 {!cv.comment.removed ? (
685 className="btn btn-link btn-animate text-muted"
688 this.handleModRemoveShow
690 aria-label={I18NextService.i18n.t("remove")}
692 {I18NextService.i18n.t("remove")}
696 className="btn btn-link btn-animate text-muted"
699 this.handleRemoveComment
701 aria-label={I18NextService.i18n.t("restore")}
703 {this.state.removeLoading ? (
706 I18NextService.i18n.t("restore")
712 {/* Mods can ban from community, and appoint as mods to community */}
716 (!cv.creator_banned_from_community ? (
718 className="btn btn-link btn-animate text-muted"
721 this.handleModBanFromCommunityShow
723 aria-label={I18NextService.i18n.t(
727 {I18NextService.i18n.t(
733 className="btn btn-link btn-animate text-muted"
736 this.handleBanPersonFromCommunity
738 aria-label={I18NextService.i18n.t("unban")}
740 {this.state.banLoading ? (
743 I18NextService.i18n.t("unban")
747 {!cv.creator_banned_from_community &&
748 (!this.state.showConfirmAppointAsMod ? (
750 className="btn btn-link btn-animate text-muted"
753 this.handleShowConfirmAppointAsMod
757 ? I18NextService.i18n.t("remove_as_mod")
758 : I18NextService.i18n.t(
764 ? I18NextService.i18n.t("remove_as_mod")
765 : I18NextService.i18n.t("appoint_as_mod")}
770 className="btn btn-link btn-animate text-muted"
771 aria-label={I18NextService.i18n.t(
775 {I18NextService.i18n.t("are_you_sure")}
778 className="btn btn-link btn-animate text-muted"
781 this.handleAddModToCommunity
783 aria-label={I18NextService.i18n.t("yes")}
785 {this.state.addModLoading ? (
788 I18NextService.i18n.t("yes")
792 className="btn btn-link btn-animate text-muted"
795 this.handleCancelConfirmAppointAsMod
797 aria-label={I18NextService.i18n.t("no")}
799 {I18NextService.i18n.t("no")}
805 {/* Community creators and admins can transfer community to another mod */}
806 {(amCommunityCreator_ || canAdmin_) &&
809 (!this.state.showConfirmTransferCommunity ? (
811 className="btn btn-link btn-animate text-muted"
814 this.handleShowConfirmTransferCommunity
816 aria-label={I18NextService.i18n.t(
820 {I18NextService.i18n.t("transfer_community")}
825 className="btn btn-link btn-animate text-muted"
826 aria-label={I18NextService.i18n.t(
830 {I18NextService.i18n.t("are_you_sure")}
833 className="btn btn-link btn-animate text-muted"
836 this.handleTransferCommunity
838 aria-label={I18NextService.i18n.t("yes")}
840 {this.state.transferCommunityLoading ? (
843 I18NextService.i18n.t("yes")
847 className="btn btn-link btn-animate text-muted"
851 .handleCancelShowConfirmTransferCommunity
853 aria-label={I18NextService.i18n.t("no")}
855 {I18NextService.i18n.t("no")}
859 {/* Admins can ban from all, and appoint other admins */}
865 className="btn btn-link btn-animate text-muted"
868 this.handlePurgePersonShow
870 aria-label={I18NextService.i18n.t(
874 {I18NextService.i18n.t("purge_user")}
877 className="btn btn-link btn-animate text-muted"
880 this.handlePurgeCommentShow
882 aria-label={I18NextService.i18n.t(
886 {I18NextService.i18n.t("purge_comment")}
889 {!isBanned(cv.creator) ? (
891 className="btn btn-link btn-animate text-muted"
894 this.handleModBanShow
896 aria-label={I18NextService.i18n.t(
900 {I18NextService.i18n.t("ban_from_site")}
904 className="btn btn-link btn-animate text-muted"
909 aria-label={I18NextService.i18n.t(
913 {this.state.banLoading ? (
916 I18NextService.i18n.t("unban_from_site")
922 {!isBanned(cv.creator) &&
924 (!this.state.showConfirmAppointAsAdmin ? (
926 className="btn btn-link btn-animate text-muted"
929 this.handleShowConfirmAppointAsAdmin
933 ? I18NextService.i18n.t(
936 : I18NextService.i18n.t(
942 ? I18NextService.i18n.t("remove_as_admin")
943 : I18NextService.i18n.t(
949 <button className="btn btn-link btn-animate text-muted">
950 {I18NextService.i18n.t("are_you_sure")}
953 className="btn btn-link btn-animate text-muted"
958 aria-label={I18NextService.i18n.t("yes")}
960 {this.state.addAdminLoading ? (
963 I18NextService.i18n.t("yes")
967 className="btn btn-link btn-animate text-muted"
970 this.handleCancelConfirmAppointAsAdmin
972 aria-label={I18NextService.i18n.t("no")}
974 {I18NextService.i18n.t("no")}
985 {/* end of button group */}
990 {showMoreChildren && (
992 className={classNames("details ms-1 comment-node py-2", {
993 "border-top border-light": !this.props.noBorder,
995 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
998 className="btn btn-link text-muted"
999 onClick={linkEvent(this, this.handleFetchChildren)}
1001 {this.state.fetchChildrenLoading ? (
1005 {I18NextService.i18n.t("x_more_replies", {
1006 count: node.comment_view.counts.child_count,
1007 formattedCount: numToSI(
1008 node.comment_view.counts.child_count
1017 {/* end of details */}
1018 {this.state.showRemoveDialog && (
1020 className="form-inline"
1021 onSubmit={linkEvent(this, this.handleRemoveComment)}
1024 className="visually-hidden"
1025 htmlFor={`mod-remove-reason-${cv.comment.id}`}
1027 {I18NextService.i18n.t("reason")}
1031 id={`mod-remove-reason-${cv.comment.id}`}
1032 className="form-control me-2"
1033 placeholder={I18NextService.i18n.t("reason")}
1034 value={this.state.removeReason}
1035 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1039 className="btn btn-secondary"
1040 aria-label={I18NextService.i18n.t("remove_comment")}
1042 {I18NextService.i18n.t("remove_comment")}
1046 {this.state.showReportDialog && (
1048 className="form-inline"
1049 onSubmit={linkEvent(this, this.handleReportComment)}
1052 className="visually-hidden"
1053 htmlFor={`report-reason-${cv.comment.id}`}
1055 {I18NextService.i18n.t("reason")}
1060 id={`report-reason-${cv.comment.id}`}
1061 className="form-control me-2"
1062 placeholder={I18NextService.i18n.t("reason")}
1063 value={this.state.reportReason}
1064 onInput={linkEvent(this, this.handleReportReasonChange)}
1068 className="btn btn-secondary"
1069 aria-label={I18NextService.i18n.t("create_report")}
1071 {I18NextService.i18n.t("create_report")}
1075 {this.state.showBanDialog && (
1076 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1077 <div className="mb-3 row col-12">
1079 className="col-form-label"
1080 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1082 {I18NextService.i18n.t("reason")}
1086 id={`mod-ban-reason-${cv.comment.id}`}
1087 className="form-control me-2"
1088 placeholder={I18NextService.i18n.t("reason")}
1089 value={this.state.banReason}
1090 onInput={linkEvent(this, this.handleModBanReasonChange)}
1093 className="col-form-label"
1094 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1096 {I18NextService.i18n.t("expires")}
1100 id={`mod-ban-expires-${cv.comment.id}`}
1101 className="form-control me-2"
1102 placeholder={I18NextService.i18n.t("number_of_days")}
1103 value={this.state.banExpireDays}
1104 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1106 <div className="input-group mb-3">
1107 <div className="form-check">
1109 className="form-check-input"
1110 id="mod-ban-remove-data"
1112 checked={this.state.removeData}
1113 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1116 className="form-check-label"
1117 htmlFor="mod-ban-remove-data"
1118 title={I18NextService.i18n.t("remove_content_more")}
1120 {I18NextService.i18n.t("remove_content")}
1125 {/* TODO hold off on expires until later */}
1126 {/* <div class="mb-3 row"> */}
1127 {/* <label class="col-form-label">Expires</label> */}
1128 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1130 <div className="mb-3 row">
1133 className="btn btn-secondary"
1134 aria-label={I18NextService.i18n.t("ban")}
1136 {this.state.banLoading ? (
1140 {I18NextService.i18n.t("ban")} {cv.creator.name}
1148 {this.state.showPurgeDialog && (
1149 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1151 <label className="visually-hidden" htmlFor="purge-reason">
1152 {I18NextService.i18n.t("reason")}
1157 className="form-control my-3"
1158 placeholder={I18NextService.i18n.t("reason")}
1159 value={this.state.purgeReason}
1160 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1162 <div className="mb-3 row col-12">
1163 {this.state.purgeLoading ? (
1168 className="btn btn-secondary"
1169 aria-label={purgeTypeText}
1177 {this.state.showReply && (
1180 onReplyCancel={this.handleReplyCancel}
1181 disabled={this.props.locked}
1182 finished={this.props.finished.get(
1183 this.props.node.comment_view.comment.id
1186 allLanguages={this.props.allLanguages}
1187 siteLanguages={this.props.siteLanguages}
1188 containerClass="comment-comment-container"
1189 onUpsertComment={this.props.onCreateComment}
1192 {!this.state.collapsed && node.children.length > 0 && (
1194 nodes={node.children}
1195 locked={this.props.locked}
1196 moderators={this.props.moderators}
1197 admins={this.props.admins}
1198 enableDownvotes={this.props.enableDownvotes}
1199 viewType={this.props.viewType}
1200 allLanguages={this.props.allLanguages}
1201 siteLanguages={this.props.siteLanguages}
1202 hideImages={this.props.hideImages}
1203 isChild={!this.props.noIndent}
1204 depth={this.props.node.depth + 1}
1205 finished={this.props.finished}
1206 onCommentReplyRead={this.props.onCommentReplyRead}
1207 onPersonMentionRead={this.props.onPersonMentionRead}
1208 onCreateComment={this.props.onCreateComment}
1209 onEditComment={this.props.onEditComment}
1210 onCommentVote={this.props.onCommentVote}
1211 onBlockPerson={this.props.onBlockPerson}
1212 onSaveComment={this.props.onSaveComment}
1213 onDeleteComment={this.props.onDeleteComment}
1214 onRemoveComment={this.props.onRemoveComment}
1215 onDistinguishComment={this.props.onDistinguishComment}
1216 onAddModToCommunity={this.props.onAddModToCommunity}
1217 onAddAdmin={this.props.onAddAdmin}
1218 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1219 onBanPerson={this.props.onBanPerson}
1220 onTransferCommunity={this.props.onTransferCommunity}
1221 onFetchChildren={this.props.onFetchChildren}
1222 onCommentReport={this.props.onCommentReport}
1223 onPurgePerson={this.props.onPurgePerson}
1224 onPurgeComment={this.props.onPurgeComment}
1227 {/* A collapsed clearfix */}
1228 {this.state.collapsed && <div className="row col-12" />}
1233 get commentReplyOrMentionRead(): boolean {
1234 const cv = this.commentView;
1236 if (this.isPersonMentionType(cv)) {
1237 return cv.person_mention.read;
1238 } else if (this.isCommentReplyType(cv)) {
1239 return cv.comment_reply.read;
1245 linkBtn(small = false) {
1246 const cv = this.commentView;
1248 const classnames = classNames("btn btn-link btn-animate text-muted", {
1252 const title = this.props.showContext
1253 ? I18NextService.i18n.t("show_context")
1254 : I18NextService.i18n.t("link");
1256 // The context button should show the parent comment by default
1257 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1262 className={classnames}
1263 to={`/comment/${parentCommentId}`}
1266 <Icon icon="link" classes="icon-inline" />
1269 <a className={classnames} title={title} href={cv.comment.ap_id}>
1270 <Icon icon="fedilink" classes="icon-inline" />
1277 get myComment(): boolean {
1279 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1280 this.commentView.creator.id
1284 get isPostCreator(): boolean {
1285 return this.commentView.creator.id == this.commentView.post.creator_id;
1289 if (this.commentView.my_vote == 1) {
1291 } else if (this.commentView.my_vote == -1) {
1292 return "text-danger";
1294 return "text-muted";
1298 get pointsTippy(): string {
1299 const points = I18NextService.i18n.t("number_of_points", {
1300 count: Number(this.commentView.counts.score),
1301 formattedCount: numToSI(this.commentView.counts.score),
1304 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1305 count: Number(this.commentView.counts.upvotes),
1306 formattedCount: numToSI(this.commentView.counts.upvotes),
1309 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1310 count: Number(this.commentView.counts.downvotes),
1311 formattedCount: numToSI(this.commentView.counts.downvotes),
1314 return `${points} • ${upvotes} • ${downvotes}`;
1317 get expandText(): string {
1318 return this.state.collapsed
1319 ? I18NextService.i18n.t("expand")
1320 : I18NextService.i18n.t("collapse");
1323 get commentUnlessRemoved(): string {
1324 const comment = this.commentView.comment;
1325 return comment.removed
1326 ? `*${I18NextService.i18n.t("removed")}*`
1328 ? `*${I18NextService.i18n.t("deleted")}*`
1332 handleReplyClick(i: CommentNode) {
1333 i.setState({ showReply: true });
1336 handleEditClick(i: CommentNode) {
1337 i.setState({ showEdit: true });
1340 handleReplyCancel() {
1341 this.setState({ showReply: false, showEdit: false });
1344 handleShowReportDialog(i: CommentNode) {
1345 i.setState({ showReportDialog: !i.state.showReportDialog });
1348 handleReportReasonChange(i: CommentNode, event: any) {
1349 i.setState({ reportReason: event.target.value });
1352 handleModRemoveShow(i: CommentNode) {
1354 showRemoveDialog: !i.state.showRemoveDialog,
1355 showBanDialog: false,
1359 handleModRemoveReasonChange(i: CommentNode, event: any) {
1360 i.setState({ removeReason: event.target.value });
1363 handleModRemoveDataChange(i: CommentNode, event: any) {
1364 i.setState({ removeData: event.target.checked });
1367 isPersonMentionType(
1368 item: CommentView | PersonMentionView | CommentReplyView
1369 ): item is PersonMentionView {
1370 return (item as PersonMentionView).person_mention?.id !== undefined;
1374 item: CommentView | PersonMentionView | CommentReplyView
1375 ): item is CommentReplyView {
1376 return (item as CommentReplyView).comment_reply?.id !== undefined;
1379 handleModBanFromCommunityShow(i: CommentNode) {
1381 showBanDialog: true,
1382 banType: BanType.Community,
1383 showRemoveDialog: false,
1387 handleModBanShow(i: CommentNode) {
1389 showBanDialog: true,
1390 banType: BanType.Site,
1391 showRemoveDialog: false,
1395 handleModBanReasonChange(i: CommentNode, event: any) {
1396 i.setState({ banReason: event.target.value });
1399 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1400 i.setState({ banExpireDays: event.target.value });
1403 handlePurgePersonShow(i: CommentNode) {
1405 showPurgeDialog: true,
1406 purgeType: PurgeType.Person,
1407 showRemoveDialog: false,
1411 handlePurgeCommentShow(i: CommentNode) {
1413 showPurgeDialog: true,
1414 purgeType: PurgeType.Comment,
1415 showRemoveDialog: false,
1419 handlePurgeReasonChange(i: CommentNode, event: any) {
1420 i.setState({ purgeReason: event.target.value });
1423 handleShowConfirmAppointAsMod(i: CommentNode) {
1424 i.setState({ showConfirmAppointAsMod: true });
1427 handleCancelConfirmAppointAsMod(i: CommentNode) {
1428 i.setState({ showConfirmAppointAsMod: false });
1431 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1432 i.setState({ showConfirmAppointAsAdmin: true });
1435 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1436 i.setState({ showConfirmAppointAsAdmin: false });
1439 handleShowConfirmTransferCommunity(i: CommentNode) {
1440 i.setState({ showConfirmTransferCommunity: true });
1443 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1444 i.setState({ showConfirmTransferCommunity: false });
1447 handleShowConfirmTransferSite(i: CommentNode) {
1448 i.setState({ showConfirmTransferSite: true });
1451 handleCancelShowConfirmTransferSite(i: CommentNode) {
1452 i.setState({ showConfirmTransferSite: false });
1455 get isCommentNew(): boolean {
1456 const now = subMinutes(new Date(), 10);
1457 const then = parseISO(this.commentView.comment.published);
1458 return isBefore(now, then);
1461 handleCommentCollapse(i: CommentNode) {
1462 i.setState({ collapsed: !i.state.collapsed });
1466 handleViewSource(i: CommentNode) {
1467 i.setState({ viewSource: !i.state.viewSource });
1470 handleShowAdvanced(i: CommentNode) {
1471 i.setState({ showAdvanced: !i.state.showAdvanced });
1475 handleSaveComment(i: CommentNode) {
1476 i.setState({ saveLoading: true });
1478 i.props.onSaveComment({
1479 comment_id: i.commentView.comment.id,
1480 save: !i.commentView.saved,
1481 auth: myAuthRequired(),
1485 handleUpvote(i: CommentNode) {
1486 i.setState({ upvoteLoading: true });
1487 i.props.onCommentVote({
1488 comment_id: i.commentId,
1489 score: newVote(VoteType.Upvote, i.commentView.my_vote),
1490 auth: myAuthRequired(),
1494 handleDownvote(i: CommentNode) {
1495 i.setState({ downvoteLoading: true });
1496 i.props.onCommentVote({
1497 comment_id: i.commentId,
1498 score: newVote(VoteType.Downvote, i.commentView.my_vote),
1499 auth: myAuthRequired(),
1503 handleBlockPerson(i: CommentNode) {
1504 i.setState({ blockPersonLoading: true });
1505 i.props.onBlockPerson({
1506 person_id: i.commentView.creator.id,
1508 auth: myAuthRequired(),
1512 handleMarkAsRead(i: CommentNode) {
1513 i.setState({ readLoading: true });
1514 const cv = i.commentView;
1515 if (i.isPersonMentionType(cv)) {
1516 i.props.onPersonMentionRead({
1517 person_mention_id: cv.person_mention.id,
1518 read: !cv.person_mention.read,
1519 auth: myAuthRequired(),
1521 } else if (i.isCommentReplyType(cv)) {
1522 i.props.onCommentReplyRead({
1523 comment_reply_id: cv.comment_reply.id,
1524 read: !cv.comment_reply.read,
1525 auth: myAuthRequired(),
1530 handleDeleteComment(i: CommentNode) {
1531 i.setState({ deleteLoading: true });
1532 i.props.onDeleteComment({
1533 comment_id: i.commentId,
1534 deleted: !i.commentView.comment.deleted,
1535 auth: myAuthRequired(),
1539 handleRemoveComment(i: CommentNode, event: any) {
1540 event.preventDefault();
1541 i.setState({ removeLoading: true });
1542 i.props.onRemoveComment({
1543 comment_id: i.commentId,
1544 removed: !i.commentView.comment.removed,
1545 auth: myAuthRequired(),
1549 handleDistinguishComment(i: CommentNode) {
1550 i.setState({ distinguishLoading: true });
1551 i.props.onDistinguishComment({
1552 comment_id: i.commentId,
1553 distinguished: !i.commentView.comment.distinguished,
1554 auth: myAuthRequired(),
1558 handleBanPersonFromCommunity(i: CommentNode) {
1559 i.setState({ banLoading: true });
1560 i.props.onBanPersonFromCommunity({
1561 community_id: i.commentView.community.id,
1562 person_id: i.commentView.creator.id,
1563 ban: !i.commentView.creator_banned_from_community,
1564 reason: i.state.banReason,
1565 remove_data: i.state.removeData,
1566 expires: futureDaysToUnixTime(i.state.banExpireDays),
1567 auth: myAuthRequired(),
1571 handleBanPerson(i: CommentNode) {
1572 i.setState({ banLoading: true });
1573 i.props.onBanPerson({
1574 person_id: i.commentView.creator.id,
1575 ban: !i.commentView.creator_banned_from_community,
1576 reason: i.state.banReason,
1577 remove_data: i.state.removeData,
1578 expires: futureDaysToUnixTime(i.state.banExpireDays),
1579 auth: myAuthRequired(),
1583 handleModBanBothSubmit(i: CommentNode, event: any) {
1584 event.preventDefault();
1585 if (i.state.banType == BanType.Community) {
1586 i.handleBanPersonFromCommunity(i);
1588 i.handleBanPerson(i);
1592 handleAddModToCommunity(i: CommentNode) {
1593 i.setState({ addModLoading: true });
1595 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1596 i.props.onAddModToCommunity({
1597 community_id: i.commentView.community.id,
1598 person_id: i.commentView.creator.id,
1600 auth: myAuthRequired(),
1604 handleAddAdmin(i: CommentNode) {
1605 i.setState({ addAdminLoading: true });
1607 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1608 i.props.onAddAdmin({
1609 person_id: i.commentView.creator.id,
1611 auth: myAuthRequired(),
1615 handleTransferCommunity(i: CommentNode) {
1616 i.setState({ transferCommunityLoading: true });
1617 i.props.onTransferCommunity({
1618 community_id: i.commentView.community.id,
1619 person_id: i.commentView.creator.id,
1620 auth: myAuthRequired(),
1624 handleReportComment(i: CommentNode, event: any) {
1625 event.preventDefault();
1626 i.setState({ reportLoading: true });
1627 i.props.onCommentReport({
1628 comment_id: i.commentId,
1629 reason: i.state.reportReason ?? "",
1630 auth: myAuthRequired(),
1634 handlePurgeBothSubmit(i: CommentNode, event: any) {
1635 event.preventDefault();
1636 i.setState({ purgeLoading: true });
1638 if (i.state.purgeType == PurgeType.Person) {
1639 i.props.onPurgePerson({
1640 person_id: i.commentView.creator.id,
1641 reason: i.state.purgeReason,
1642 auth: myAuthRequired(),
1645 i.props.onPurgeComment({
1646 comment_id: i.commentId,
1647 reason: i.state.purgeReason,
1648 auth: myAuthRequired(),
1653 handleFetchChildren(i: CommentNode) {
1654 i.setState({ fetchChildrenLoading: true });
1655 i.props.onFetchChildren?.({
1656 parent_id: i.commentId,
1657 max_depth: commentTreeMaxDepth,