8 import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
16 } from "@utils/roles";
17 import classNames from "classnames";
18 import isBefore from "date-fns/isBefore";
19 import parseISO from "date-fns/parseISO";
20 import subMinutes from "date-fns/subMinutes";
21 import { Component, InfernoNode, linkEvent } from "inferno";
22 import { Link } from "inferno-router";
32 CommunityModeratorView,
41 MarkCommentReplyAsRead,
42 MarkPersonMentionAsRead,
50 } from "lemmy-js-client";
51 import deepEqual from "lodash.isequal";
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 { VoteButtonsCompact } from "../common/vote-buttons";
66 import { CommunityLink } from "../community/community-link";
67 import { PersonListing } from "../person/person-listing";
68 import { CommentForm } from "./comment-form";
69 import { CommentNodes } from "./comment-nodes";
71 interface CommentNodeState {
74 showRemoveDialog: boolean;
75 removeReason?: string;
76 showBanDialog: boolean;
79 banExpireDays?: number;
81 showPurgeDialog: boolean;
84 showConfirmTransferSite: boolean;
85 showConfirmTransferCommunity: boolean;
86 showConfirmAppointAsMod: boolean;
87 showConfirmAppointAsAdmin: boolean;
90 showAdvanced: boolean;
91 showReportDialog: boolean;
92 reportReason?: string;
93 createOrEditCommentLoading: boolean;
94 upvoteLoading: boolean;
95 downvoteLoading: boolean;
98 blockPersonLoading: boolean;
99 deleteLoading: boolean;
100 removeLoading: boolean;
101 distinguishLoading: boolean;
103 addModLoading: boolean;
104 addAdminLoading: boolean;
105 transferCommunityLoading: boolean;
106 fetchChildrenLoading: boolean;
107 reportLoading: boolean;
108 purgeLoading: boolean;
111 interface CommentNodeProps {
113 moderators?: CommunityModeratorView[];
114 admins?: PersonView[];
120 showContext?: boolean;
121 showCommunity?: boolean;
122 enableDownvotes?: boolean;
123 viewType: CommentViewType;
124 allLanguages: Language[];
125 siteLanguages: number[];
126 hideImages?: boolean;
127 finished: Map<CommentId, boolean | undefined>;
128 onSaveComment(form: SaveComment): void;
129 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
130 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
131 onCreateComment(form: EditComment | CreateComment): void;
132 onEditComment(form: EditComment | CreateComment): void;
133 onCommentVote(form: CreateCommentLike): void;
134 onBlockPerson(form: BlockPerson): void;
135 onDeleteComment(form: DeleteComment): void;
136 onRemoveComment(form: RemoveComment): void;
137 onDistinguishComment(form: DistinguishComment): void;
138 onAddModToCommunity(form: AddModToCommunity): void;
139 onAddAdmin(form: AddAdmin): void;
140 onBanPersonFromCommunity(form: BanFromCommunity): void;
141 onBanPerson(form: BanPerson): void;
142 onTransferCommunity(form: TransferCommunity): void;
143 onFetchChildren?(form: GetComments): void;
144 onCommentReport(form: CreateCommentReport): void;
145 onPurgePerson(form: PurgePerson): void;
146 onPurgeComment(form: PurgeComment): void;
149 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
150 state: CommentNodeState = {
153 showRemoveDialog: false,
154 showBanDialog: false,
156 banType: BanType.Community,
157 showPurgeDialog: false,
158 purgeType: PurgeType.Person,
162 showConfirmTransferSite: false,
163 showConfirmTransferCommunity: false,
164 showConfirmAppointAsMod: false,
165 showConfirmAppointAsAdmin: false,
166 showReportDialog: false,
167 createOrEditCommentLoading: false,
168 upvoteLoading: false,
169 downvoteLoading: false,
172 blockPersonLoading: false,
173 deleteLoading: false,
174 removeLoading: false,
175 distinguishLoading: false,
177 addModLoading: false,
178 addAdminLoading: false,
179 transferCommunityLoading: false,
180 fetchChildrenLoading: false,
181 reportLoading: false,
185 constructor(props: any, context: any) {
186 super(props, context);
188 this.handleReplyCancel = this.handleReplyCancel.bind(this);
191 get commentView(): CommentView {
192 return this.props.node.comment_view;
195 get commentId(): CommentId {
196 return this.commentView.comment.id;
199 componentWillReceiveProps(
200 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
202 if (!deepEqual(this.props, nextProps)) {
206 showRemoveDialog: false,
207 showBanDialog: false,
209 banType: BanType.Community,
210 showPurgeDialog: false,
211 purgeType: PurgeType.Person,
215 showConfirmTransferSite: false,
216 showConfirmTransferCommunity: false,
217 showConfirmAppointAsMod: false,
218 showConfirmAppointAsAdmin: false,
219 showReportDialog: false,
220 createOrEditCommentLoading: false,
221 upvoteLoading: false,
222 downvoteLoading: false,
225 blockPersonLoading: false,
226 deleteLoading: false,
227 removeLoading: false,
228 distinguishLoading: false,
230 addModLoading: false,
231 addAdminLoading: false,
232 transferCommunityLoading: false,
233 fetchChildrenLoading: false,
234 reportLoading: false,
241 const node = this.props.node;
242 const cv = this.commentView;
244 const purgeTypeText =
245 this.state.purgeType == PurgeType.Comment
246 ? I18NextService.i18n.t("purge_comment")
247 : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
249 const canMod_ = canMod(
251 this.props.moderators,
254 const canModOnSelf = canMod(
256 this.props.moderators,
258 UserService.Instance.myUserInfo,
261 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
262 const canAdminOnSelf = canAdmin(
265 UserService.Instance.myUserInfo,
268 const isMod_ = isMod(cv.creator.id, this.props.moderators);
269 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
270 const amCommunityCreator_ = amCommunityCreator(
272 this.props.moderators
275 const moreRepliesBorderColor = this.props.node.depth
276 ? colorList[this.props.node.depth % colorList.length]
279 const showMoreChildren =
280 this.props.viewType == CommentViewType.Tree &&
281 !this.state.collapsed &&
282 node.children.length == 0 &&
283 node.comment_view.counts.child_count > 0;
286 <li className="comment">
288 id={`comment-${cv.comment.id}`}
289 className={classNames(`details comment-node py-2`, {
290 "border-top border-light": !this.props.noBorder,
291 mark: this.isCommentNew || this.commentView.comment.distinguished,
295 className={classNames({
296 "ms-2": !this.props.noIndent,
299 <div className="d-flex flex-wrap align-items-center text-muted small">
301 className="btn btn-sm text-muted me-2"
302 onClick={linkEvent(this, this.handleCommentCollapse)}
303 aria-label={this.expandText}
304 data-tippy-content={this.expandText}
307 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
308 classes="icon-inline"
311 <span className="me-2">
312 <PersonListing person={cv.creator} />
314 {cv.comment.distinguished && (
315 <Icon icon="shield" inline classes={`text-danger me-2`} />
317 {this.isPostCreator && (
318 <div className="badge text-bg-light d-none d-sm-inline me-2">
319 {I18NextService.i18n.t("creator")}
323 <div className="badge text-bg-light d-none d-sm-inline me-2">
324 {I18NextService.i18n.t("mod")}
328 <div className="badge text-bg-light d-none d-sm-inline me-2">
329 {I18NextService.i18n.t("admin")}
332 {cv.creator.bot_account && (
333 <div className="badge text-bg-light d-none d-sm-inline me-2">
334 {I18NextService.i18n.t("bot_account").toLowerCase()}
337 {this.props.showCommunity && (
339 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
340 <CommunityLink community={cv.community} />
341 <span className="mx-2">•</span>
342 <Link className="me-2" to={`/post/${cv.post.id}`}>
348 {cv.comment.language_id !== 0 && (
349 <span className="badge text-bg-light d-none d-sm-inline me-2">
351 this.props.allLanguages.find(
352 lang => lang.id === cv.comment.language_id
357 {/* This is an expanding spacer for mobile */}
358 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
363 className="me-1 font-weight-bold"
364 aria-label={I18NextService.i18n.t("number_of_points", {
365 count: Number(this.commentView.counts.score),
366 formattedCount: numToSI(this.commentView.counts.score),
369 {numToSI(this.commentView.counts.score)}
371 <span className="me-1">•</span>
376 published={cv.comment.published}
377 updated={cv.comment.updated}
381 {/* end of user row */}
382 {this.state.showEdit && (
386 onReplyCancel={this.handleReplyCancel}
387 disabled={this.props.locked}
388 finished={this.props.finished.get(
389 this.props.node.comment_view.comment.id
392 allLanguages={this.props.allLanguages}
393 siteLanguages={this.props.siteLanguages}
394 containerClass="comment-comment-container"
395 onUpsertComment={this.props.onEditComment}
398 {!this.state.showEdit && !this.state.collapsed && (
400 {this.state.viewSource ? (
401 <pre>{this.commentUnlessRemoved}</pre>
405 dangerouslySetInnerHTML={
406 this.props.hideImages
407 ? mdToHtmlNoImages(this.commentUnlessRemoved)
408 : mdToHtml(this.commentUnlessRemoved)
412 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
413 {this.props.showContext && this.linkBtn()}
414 {this.props.markable && (
416 className="btn btn-link btn-animate text-muted"
417 onClick={linkEvent(this, this.handleMarkAsRead)}
419 this.commentReplyOrMentionRead
420 ? I18NextService.i18n.t("mark_as_unread")
421 : I18NextService.i18n.t("mark_as_read")
424 this.commentReplyOrMentionRead
425 ? I18NextService.i18n.t("mark_as_unread")
426 : I18NextService.i18n.t("mark_as_read")
429 {this.state.readLoading ? (
434 classes={`icon-inline ${
435 this.commentReplyOrMentionRead && "text-success"
441 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
444 voteContentType={VoteContentType.Comment}
445 id={this.commentView.comment.id}
446 onVote={this.props.onCommentVote}
447 enableDownvotes={this.props.enableDownvotes}
448 counts={this.commentView.counts}
449 my_vote={this.commentView.my_vote}
452 className="btn btn-link btn-animate text-muted"
453 onClick={linkEvent(this, this.handleReplyClick)}
454 data-tippy-content={I18NextService.i18n.t("reply")}
455 aria-label={I18NextService.i18n.t("reply")}
457 <Icon icon="reply1" classes="icon-inline" />
459 {!this.state.showAdvanced ? (
461 className="btn btn-link btn-animate text-muted btn-more"
462 onClick={linkEvent(this, this.handleShowAdvanced)}
463 data-tippy-content={I18NextService.i18n.t("more")}
464 aria-label={I18NextService.i18n.t("more")}
466 <Icon icon="more-vertical" classes="icon-inline" />
470 {!this.myComment && (
473 className="btn btn-link btn-animate text-muted"
474 to={`/create_private_message/${cv.creator.id}`}
475 title={I18NextService.i18n
482 className="btn btn-link btn-animate text-muted"
485 this.handleShowReportDialog
487 data-tippy-content={I18NextService.i18n.t(
490 aria-label={I18NextService.i18n.t(
497 className="btn btn-link btn-animate text-muted"
500 this.handleBlockPerson
502 data-tippy-content={I18NextService.i18n.t(
505 aria-label={I18NextService.i18n.t("block_user")}
507 {this.state.blockPersonLoading ? (
510 <Icon icon="slash" />
516 className="btn btn-link btn-animate text-muted"
517 onClick={linkEvent(this, this.handleSaveComment)}
520 ? I18NextService.i18n.t("unsave")
521 : I18NextService.i18n.t("save")
525 ? I18NextService.i18n.t("unsave")
526 : I18NextService.i18n.t("save")
529 {this.state.saveLoading ? (
534 classes={`icon-inline ${
535 cv.saved && "text-warning"
541 className="btn btn-link btn-animate text-muted"
542 onClick={linkEvent(this, this.handleViewSource)}
543 data-tippy-content={I18NextService.i18n.t(
546 aria-label={I18NextService.i18n.t("view_source")}
550 classes={`icon-inline ${
551 this.state.viewSource && "text-success"
558 className="btn btn-link btn-animate text-muted"
559 onClick={linkEvent(this, this.handleEditClick)}
560 data-tippy-content={I18NextService.i18n.t(
563 aria-label={I18NextService.i18n.t("edit")}
565 <Icon icon="edit" classes="icon-inline" />
568 className="btn btn-link btn-animate text-muted"
571 this.handleDeleteComment
575 ? I18NextService.i18n.t("delete")
576 : I18NextService.i18n.t("restore")
580 ? I18NextService.i18n.t("delete")
581 : I18NextService.i18n.t("restore")
584 {this.state.deleteLoading ? (
589 classes={`icon-inline ${
590 cv.comment.deleted && "text-danger"
596 {(canModOnSelf || canAdminOnSelf) && (
598 className="btn btn-link btn-animate text-muted"
601 this.handleDistinguishComment
604 !cv.comment.distinguished
605 ? I18NextService.i18n.t("distinguish")
606 : I18NextService.i18n.t("undistinguish")
609 !cv.comment.distinguished
610 ? I18NextService.i18n.t("distinguish")
611 : I18NextService.i18n.t("undistinguish")
616 classes={`icon-inline ${
617 cv.comment.distinguished && "text-danger"
624 {/* Admins and mods can remove comments */}
625 {(canMod_ || canAdmin_) && (
627 {!cv.comment.removed ? (
629 className="btn btn-link btn-animate text-muted"
632 this.handleModRemoveShow
634 aria-label={I18NextService.i18n.t("remove")}
636 {I18NextService.i18n.t("remove")}
640 className="btn btn-link btn-animate text-muted"
643 this.handleRemoveComment
645 aria-label={I18NextService.i18n.t("restore")}
647 {this.state.removeLoading ? (
650 I18NextService.i18n.t("restore")
656 {/* Mods can ban from community, and appoint as mods to community */}
660 (!cv.creator_banned_from_community ? (
662 className="btn btn-link btn-animate text-muted"
665 this.handleModBanFromCommunityShow
667 aria-label={I18NextService.i18n.t(
671 {I18NextService.i18n.t(
677 className="btn btn-link btn-animate text-muted"
680 this.handleBanPersonFromCommunity
682 aria-label={I18NextService.i18n.t("unban")}
684 {this.state.banLoading ? (
687 I18NextService.i18n.t("unban")
691 {!cv.creator_banned_from_community &&
692 (!this.state.showConfirmAppointAsMod ? (
694 className="btn btn-link btn-animate text-muted"
697 this.handleShowConfirmAppointAsMod
701 ? I18NextService.i18n.t("remove_as_mod")
702 : I18NextService.i18n.t(
708 ? I18NextService.i18n.t("remove_as_mod")
709 : I18NextService.i18n.t("appoint_as_mod")}
714 className="btn btn-link btn-animate text-muted"
715 aria-label={I18NextService.i18n.t(
719 {I18NextService.i18n.t("are_you_sure")}
722 className="btn btn-link btn-animate text-muted"
725 this.handleAddModToCommunity
727 aria-label={I18NextService.i18n.t("yes")}
729 {this.state.addModLoading ? (
732 I18NextService.i18n.t("yes")
736 className="btn btn-link btn-animate text-muted"
739 this.handleCancelConfirmAppointAsMod
741 aria-label={I18NextService.i18n.t("no")}
743 {I18NextService.i18n.t("no")}
749 {/* Community creators and admins can transfer community to another mod */}
750 {(amCommunityCreator_ || canAdmin_) &&
753 (!this.state.showConfirmTransferCommunity ? (
755 className="btn btn-link btn-animate text-muted"
758 this.handleShowConfirmTransferCommunity
760 aria-label={I18NextService.i18n.t(
764 {I18NextService.i18n.t("transfer_community")}
769 className="btn btn-link btn-animate text-muted"
770 aria-label={I18NextService.i18n.t(
774 {I18NextService.i18n.t("are_you_sure")}
777 className="btn btn-link btn-animate text-muted"
780 this.handleTransferCommunity
782 aria-label={I18NextService.i18n.t("yes")}
784 {this.state.transferCommunityLoading ? (
787 I18NextService.i18n.t("yes")
791 className="btn btn-link btn-animate text-muted"
795 .handleCancelShowConfirmTransferCommunity
797 aria-label={I18NextService.i18n.t("no")}
799 {I18NextService.i18n.t("no")}
803 {/* Admins can ban from all, and appoint other admins */}
809 className="btn btn-link btn-animate text-muted"
812 this.handlePurgePersonShow
814 aria-label={I18NextService.i18n.t(
818 {I18NextService.i18n.t("purge_user")}
821 className="btn btn-link btn-animate text-muted"
824 this.handlePurgeCommentShow
826 aria-label={I18NextService.i18n.t(
830 {I18NextService.i18n.t("purge_comment")}
833 {!isBanned(cv.creator) ? (
835 className="btn btn-link btn-animate text-muted"
838 this.handleModBanShow
840 aria-label={I18NextService.i18n.t(
844 {I18NextService.i18n.t("ban_from_site")}
848 className="btn btn-link btn-animate text-muted"
853 aria-label={I18NextService.i18n.t(
857 {this.state.banLoading ? (
860 I18NextService.i18n.t("unban_from_site")
866 {!isBanned(cv.creator) &&
868 (!this.state.showConfirmAppointAsAdmin ? (
870 className="btn btn-link btn-animate text-muted"
873 this.handleShowConfirmAppointAsAdmin
877 ? I18NextService.i18n.t(
880 : I18NextService.i18n.t(
886 ? I18NextService.i18n.t("remove_as_admin")
887 : I18NextService.i18n.t(
893 <button className="btn btn-link btn-animate text-muted">
894 {I18NextService.i18n.t("are_you_sure")}
897 className="btn btn-link btn-animate text-muted"
902 aria-label={I18NextService.i18n.t("yes")}
904 {this.state.addAdminLoading ? (
907 I18NextService.i18n.t("yes")
911 className="btn btn-link btn-animate text-muted"
914 this.handleCancelConfirmAppointAsAdmin
916 aria-label={I18NextService.i18n.t("no")}
918 {I18NextService.i18n.t("no")}
929 {/* end of button group */}
934 {showMoreChildren && (
936 className={classNames("details ms-1 comment-node py-2", {
937 "border-top border-light": !this.props.noBorder,
939 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
942 className="btn btn-link text-muted"
943 onClick={linkEvent(this, this.handleFetchChildren)}
945 {this.state.fetchChildrenLoading ? (
949 {I18NextService.i18n.t("x_more_replies", {
950 count: node.comment_view.counts.child_count,
951 formattedCount: numToSI(
952 node.comment_view.counts.child_count
961 {/* end of details */}
962 {this.state.showRemoveDialog && (
964 className="form-inline"
965 onSubmit={linkEvent(this, this.handleRemoveComment)}
968 className="visually-hidden"
969 htmlFor={`mod-remove-reason-${cv.comment.id}`}
971 {I18NextService.i18n.t("reason")}
975 id={`mod-remove-reason-${cv.comment.id}`}
976 className="form-control me-2"
977 placeholder={I18NextService.i18n.t("reason")}
978 value={this.state.removeReason}
979 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
983 className="btn btn-secondary"
984 aria-label={I18NextService.i18n.t("remove_comment")}
986 {I18NextService.i18n.t("remove_comment")}
990 {this.state.showReportDialog && (
992 className="form-inline"
993 onSubmit={linkEvent(this, this.handleReportComment)}
996 className="visually-hidden"
997 htmlFor={`report-reason-${cv.comment.id}`}
999 {I18NextService.i18n.t("reason")}
1004 id={`report-reason-${cv.comment.id}`}
1005 className="form-control me-2"
1006 placeholder={I18NextService.i18n.t("reason")}
1007 value={this.state.reportReason}
1008 onInput={linkEvent(this, this.handleReportReasonChange)}
1012 className="btn btn-secondary"
1013 aria-label={I18NextService.i18n.t("create_report")}
1015 {I18NextService.i18n.t("create_report")}
1019 {this.state.showBanDialog && (
1020 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1021 <div className="mb-3 row col-12">
1023 className="col-form-label"
1024 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1026 {I18NextService.i18n.t("reason")}
1030 id={`mod-ban-reason-${cv.comment.id}`}
1031 className="form-control me-2"
1032 placeholder={I18NextService.i18n.t("reason")}
1033 value={this.state.banReason}
1034 onInput={linkEvent(this, this.handleModBanReasonChange)}
1037 className="col-form-label"
1038 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1040 {I18NextService.i18n.t("expires")}
1044 id={`mod-ban-expires-${cv.comment.id}`}
1045 className="form-control me-2"
1046 placeholder={I18NextService.i18n.t("number_of_days")}
1047 value={this.state.banExpireDays}
1048 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1050 <div className="input-group mb-3">
1051 <div className="form-check">
1053 className="form-check-input"
1054 id="mod-ban-remove-data"
1056 checked={this.state.removeData}
1057 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1060 className="form-check-label"
1061 htmlFor="mod-ban-remove-data"
1062 title={I18NextService.i18n.t("remove_content_more")}
1064 {I18NextService.i18n.t("remove_content")}
1069 {/* TODO hold off on expires until later */}
1070 {/* <div class="mb-3 row"> */}
1071 {/* <label class="col-form-label">Expires</label> */}
1072 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1074 <div className="mb-3 row">
1077 className="btn btn-secondary"
1078 aria-label={I18NextService.i18n.t("ban")}
1080 {this.state.banLoading ? (
1084 {I18NextService.i18n.t("ban")} {cv.creator.name}
1092 {this.state.showPurgeDialog && (
1093 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1095 <label className="visually-hidden" htmlFor="purge-reason">
1096 {I18NextService.i18n.t("reason")}
1101 className="form-control my-3"
1102 placeholder={I18NextService.i18n.t("reason")}
1103 value={this.state.purgeReason}
1104 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1106 <div className="mb-3 row col-12">
1107 {this.state.purgeLoading ? (
1112 className="btn btn-secondary"
1113 aria-label={purgeTypeText}
1121 {this.state.showReply && (
1124 onReplyCancel={this.handleReplyCancel}
1125 disabled={this.props.locked}
1126 finished={this.props.finished.get(
1127 this.props.node.comment_view.comment.id
1130 allLanguages={this.props.allLanguages}
1131 siteLanguages={this.props.siteLanguages}
1132 containerClass="comment-comment-container"
1133 onUpsertComment={this.props.onCreateComment}
1136 {!this.state.collapsed && node.children.length > 0 && (
1138 nodes={node.children}
1139 locked={this.props.locked}
1140 moderators={this.props.moderators}
1141 admins={this.props.admins}
1142 enableDownvotes={this.props.enableDownvotes}
1143 viewType={this.props.viewType}
1144 allLanguages={this.props.allLanguages}
1145 siteLanguages={this.props.siteLanguages}
1146 hideImages={this.props.hideImages}
1147 isChild={!this.props.noIndent}
1148 depth={this.props.node.depth + 1}
1149 finished={this.props.finished}
1150 onCommentReplyRead={this.props.onCommentReplyRead}
1151 onPersonMentionRead={this.props.onPersonMentionRead}
1152 onCreateComment={this.props.onCreateComment}
1153 onEditComment={this.props.onEditComment}
1154 onCommentVote={this.props.onCommentVote}
1155 onBlockPerson={this.props.onBlockPerson}
1156 onSaveComment={this.props.onSaveComment}
1157 onDeleteComment={this.props.onDeleteComment}
1158 onRemoveComment={this.props.onRemoveComment}
1159 onDistinguishComment={this.props.onDistinguishComment}
1160 onAddModToCommunity={this.props.onAddModToCommunity}
1161 onAddAdmin={this.props.onAddAdmin}
1162 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1163 onBanPerson={this.props.onBanPerson}
1164 onTransferCommunity={this.props.onTransferCommunity}
1165 onFetchChildren={this.props.onFetchChildren}
1166 onCommentReport={this.props.onCommentReport}
1167 onPurgePerson={this.props.onPurgePerson}
1168 onPurgeComment={this.props.onPurgeComment}
1171 {/* A collapsed clearfix */}
1172 {this.state.collapsed && <div className="row col-12" />}
1177 get commentReplyOrMentionRead(): boolean {
1178 const cv = this.commentView;
1180 if (this.isPersonMentionType(cv)) {
1181 return cv.person_mention.read;
1182 } else if (this.isCommentReplyType(cv)) {
1183 return cv.comment_reply.read;
1189 linkBtn(small = false) {
1190 const cv = this.commentView;
1192 const classnames = classNames("btn btn-link btn-animate text-muted", {
1196 const title = this.props.showContext
1197 ? I18NextService.i18n.t("show_context")
1198 : I18NextService.i18n.t("link");
1200 // The context button should show the parent comment by default
1201 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1206 className={classnames}
1207 to={`/comment/${parentCommentId}`}
1210 <Icon icon="link" classes="icon-inline" />
1213 <a className={classnames} title={title} href={cv.comment.ap_id}>
1214 <Icon icon="fedilink" classes="icon-inline" />
1221 get myComment(): boolean {
1223 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1224 this.commentView.creator.id
1228 get isPostCreator(): boolean {
1229 return this.commentView.creator.id == this.commentView.post.creator_id;
1233 if (this.commentView.my_vote == 1) {
1235 } else if (this.commentView.my_vote == -1) {
1236 return "text-danger";
1238 return "text-muted";
1242 get pointsTippy(): string {
1243 const points = I18NextService.i18n.t("number_of_points", {
1244 count: Number(this.commentView.counts.score),
1245 formattedCount: numToSI(this.commentView.counts.score),
1248 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1249 count: Number(this.commentView.counts.upvotes),
1250 formattedCount: numToSI(this.commentView.counts.upvotes),
1253 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1254 count: Number(this.commentView.counts.downvotes),
1255 formattedCount: numToSI(this.commentView.counts.downvotes),
1258 return `${points} • ${upvotes} • ${downvotes}`;
1261 get expandText(): string {
1262 return this.state.collapsed
1263 ? I18NextService.i18n.t("expand")
1264 : I18NextService.i18n.t("collapse");
1267 get commentUnlessRemoved(): string {
1268 const comment = this.commentView.comment;
1269 return comment.removed
1270 ? `*${I18NextService.i18n.t("removed")}*`
1272 ? `*${I18NextService.i18n.t("deleted")}*`
1276 handleReplyClick(i: CommentNode) {
1277 i.setState({ showReply: true });
1280 handleEditClick(i: CommentNode) {
1281 i.setState({ showEdit: true });
1284 handleReplyCancel() {
1285 this.setState({ showReply: false, showEdit: false });
1288 handleShowReportDialog(i: CommentNode) {
1289 i.setState({ showReportDialog: !i.state.showReportDialog });
1292 handleReportReasonChange(i: CommentNode, event: any) {
1293 i.setState({ reportReason: event.target.value });
1296 handleModRemoveShow(i: CommentNode) {
1298 showRemoveDialog: !i.state.showRemoveDialog,
1299 showBanDialog: false,
1303 handleModRemoveReasonChange(i: CommentNode, event: any) {
1304 i.setState({ removeReason: event.target.value });
1307 handleModRemoveDataChange(i: CommentNode, event: any) {
1308 i.setState({ removeData: event.target.checked });
1311 isPersonMentionType(
1312 item: CommentView | PersonMentionView | CommentReplyView
1313 ): item is PersonMentionView {
1314 return (item as PersonMentionView).person_mention?.id !== undefined;
1318 item: CommentView | PersonMentionView | CommentReplyView
1319 ): item is CommentReplyView {
1320 return (item as CommentReplyView).comment_reply?.id !== undefined;
1323 handleModBanFromCommunityShow(i: CommentNode) {
1325 showBanDialog: true,
1326 banType: BanType.Community,
1327 showRemoveDialog: false,
1331 handleModBanShow(i: CommentNode) {
1333 showBanDialog: true,
1334 banType: BanType.Site,
1335 showRemoveDialog: false,
1339 handleModBanReasonChange(i: CommentNode, event: any) {
1340 i.setState({ banReason: event.target.value });
1343 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1344 i.setState({ banExpireDays: event.target.value });
1347 handlePurgePersonShow(i: CommentNode) {
1349 showPurgeDialog: true,
1350 purgeType: PurgeType.Person,
1351 showRemoveDialog: false,
1355 handlePurgeCommentShow(i: CommentNode) {
1357 showPurgeDialog: true,
1358 purgeType: PurgeType.Comment,
1359 showRemoveDialog: false,
1363 handlePurgeReasonChange(i: CommentNode, event: any) {
1364 i.setState({ purgeReason: event.target.value });
1367 handleShowConfirmAppointAsMod(i: CommentNode) {
1368 i.setState({ showConfirmAppointAsMod: true });
1371 handleCancelConfirmAppointAsMod(i: CommentNode) {
1372 i.setState({ showConfirmAppointAsMod: false });
1375 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1376 i.setState({ showConfirmAppointAsAdmin: true });
1379 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1380 i.setState({ showConfirmAppointAsAdmin: false });
1383 handleShowConfirmTransferCommunity(i: CommentNode) {
1384 i.setState({ showConfirmTransferCommunity: true });
1387 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1388 i.setState({ showConfirmTransferCommunity: false });
1391 handleShowConfirmTransferSite(i: CommentNode) {
1392 i.setState({ showConfirmTransferSite: true });
1395 handleCancelShowConfirmTransferSite(i: CommentNode) {
1396 i.setState({ showConfirmTransferSite: false });
1399 get isCommentNew(): boolean {
1400 const now = subMinutes(new Date(), 10);
1401 const then = parseISO(this.commentView.comment.published);
1402 return isBefore(now, then);
1405 handleCommentCollapse(i: CommentNode) {
1406 i.setState({ collapsed: !i.state.collapsed });
1410 handleViewSource(i: CommentNode) {
1411 i.setState({ viewSource: !i.state.viewSource });
1414 handleShowAdvanced(i: CommentNode) {
1415 i.setState({ showAdvanced: !i.state.showAdvanced });
1419 handleSaveComment(i: CommentNode) {
1420 i.setState({ saveLoading: true });
1422 i.props.onSaveComment({
1423 comment_id: i.commentView.comment.id,
1424 save: !i.commentView.saved,
1425 auth: myAuthRequired(),
1429 handleBlockPerson(i: CommentNode) {
1430 i.setState({ blockPersonLoading: true });
1431 i.props.onBlockPerson({
1432 person_id: i.commentView.creator.id,
1434 auth: myAuthRequired(),
1438 handleMarkAsRead(i: CommentNode) {
1439 i.setState({ readLoading: true });
1440 const cv = i.commentView;
1441 if (i.isPersonMentionType(cv)) {
1442 i.props.onPersonMentionRead({
1443 person_mention_id: cv.person_mention.id,
1444 read: !cv.person_mention.read,
1445 auth: myAuthRequired(),
1447 } else if (i.isCommentReplyType(cv)) {
1448 i.props.onCommentReplyRead({
1449 comment_reply_id: cv.comment_reply.id,
1450 read: !cv.comment_reply.read,
1451 auth: myAuthRequired(),
1456 handleDeleteComment(i: CommentNode) {
1457 i.setState({ deleteLoading: true });
1458 i.props.onDeleteComment({
1459 comment_id: i.commentId,
1460 deleted: !i.commentView.comment.deleted,
1461 auth: myAuthRequired(),
1465 handleRemoveComment(i: CommentNode, event: any) {
1466 event.preventDefault();
1467 i.setState({ removeLoading: true });
1468 i.props.onRemoveComment({
1469 comment_id: i.commentId,
1470 removed: !i.commentView.comment.removed,
1471 auth: myAuthRequired(),
1475 handleDistinguishComment(i: CommentNode) {
1476 i.setState({ distinguishLoading: true });
1477 i.props.onDistinguishComment({
1478 comment_id: i.commentId,
1479 distinguished: !i.commentView.comment.distinguished,
1480 auth: myAuthRequired(),
1484 handleBanPersonFromCommunity(i: CommentNode) {
1485 i.setState({ banLoading: true });
1486 i.props.onBanPersonFromCommunity({
1487 community_id: i.commentView.community.id,
1488 person_id: i.commentView.creator.id,
1489 ban: !i.commentView.creator_banned_from_community,
1490 reason: i.state.banReason,
1491 remove_data: i.state.removeData,
1492 expires: futureDaysToUnixTime(i.state.banExpireDays),
1493 auth: myAuthRequired(),
1497 handleBanPerson(i: CommentNode) {
1498 i.setState({ banLoading: true });
1499 i.props.onBanPerson({
1500 person_id: i.commentView.creator.id,
1501 ban: !i.commentView.creator_banned_from_community,
1502 reason: i.state.banReason,
1503 remove_data: i.state.removeData,
1504 expires: futureDaysToUnixTime(i.state.banExpireDays),
1505 auth: myAuthRequired(),
1509 handleModBanBothSubmit(i: CommentNode, event: any) {
1510 event.preventDefault();
1511 if (i.state.banType == BanType.Community) {
1512 i.handleBanPersonFromCommunity(i);
1514 i.handleBanPerson(i);
1518 handleAddModToCommunity(i: CommentNode) {
1519 i.setState({ addModLoading: true });
1521 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1522 i.props.onAddModToCommunity({
1523 community_id: i.commentView.community.id,
1524 person_id: i.commentView.creator.id,
1526 auth: myAuthRequired(),
1530 handleAddAdmin(i: CommentNode) {
1531 i.setState({ addAdminLoading: true });
1533 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1534 i.props.onAddAdmin({
1535 person_id: i.commentView.creator.id,
1537 auth: myAuthRequired(),
1541 handleTransferCommunity(i: CommentNode) {
1542 i.setState({ transferCommunityLoading: true });
1543 i.props.onTransferCommunity({
1544 community_id: i.commentView.community.id,
1545 person_id: i.commentView.creator.id,
1546 auth: myAuthRequired(),
1550 handleReportComment(i: CommentNode, event: any) {
1551 event.preventDefault();
1552 i.setState({ reportLoading: true });
1553 i.props.onCommentReport({
1554 comment_id: i.commentId,
1555 reason: i.state.reportReason ?? "",
1556 auth: myAuthRequired(),
1560 handlePurgeBothSubmit(i: CommentNode, event: any) {
1561 event.preventDefault();
1562 i.setState({ purgeLoading: true });
1564 if (i.state.purgeType == PurgeType.Person) {
1565 i.props.onPurgePerson({
1566 person_id: i.commentView.creator.id,
1567 reason: i.state.purgeReason,
1568 auth: myAuthRequired(),
1571 i.props.onPurgeComment({
1572 comment_id: i.commentId,
1573 reason: i.state.purgeReason,
1574 auth: myAuthRequired(),
1579 handleFetchChildren(i: CommentNode) {
1580 i.setState({ fetchChildrenLoading: true });
1581 i.props.onFetchChildren?.({
1582 parent_id: i.commentId,
1583 max_depth: commentTreeMaxDepth,