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 { UserBadges } from "../common/user-badges";
66 import { VoteButtonsCompact } from "../common/vote-buttons";
67 import { CommunityLink } from "../community/community-link";
68 import { PersonListing } from "../person/person-listing";
69 import { CommentForm } from "./comment-form";
70 import { CommentNodes } from "./comment-nodes";
72 interface CommentNodeState {
75 showRemoveDialog: boolean;
76 removeReason?: string;
77 showBanDialog: boolean;
80 banExpireDays?: number;
82 showPurgeDialog: boolean;
85 showConfirmTransferSite: boolean;
86 showConfirmTransferCommunity: boolean;
87 showConfirmAppointAsMod: boolean;
88 showConfirmAppointAsAdmin: boolean;
91 showAdvanced: boolean;
92 showReportDialog: boolean;
93 reportReason?: string;
94 createOrEditCommentLoading: boolean;
95 upvoteLoading: boolean;
96 downvoteLoading: boolean;
99 blockPersonLoading: boolean;
100 deleteLoading: boolean;
101 removeLoading: boolean;
102 distinguishLoading: boolean;
104 addModLoading: boolean;
105 addAdminLoading: boolean;
106 transferCommunityLoading: boolean;
107 fetchChildrenLoading: boolean;
108 reportLoading: boolean;
109 purgeLoading: boolean;
112 interface CommentNodeProps {
114 moderators?: CommunityModeratorView[];
115 admins?: PersonView[];
117 isTopLevel?: boolean;
121 showContext?: boolean;
122 showCommunity?: boolean;
123 enableDownvotes?: boolean;
124 viewType: CommentViewType;
125 allLanguages: Language[];
126 siteLanguages: number[];
127 hideImages?: boolean;
128 finished: Map<CommentId, boolean | undefined>;
129 onSaveComment(form: SaveComment): void;
130 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
131 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
132 onCreateComment(form: EditComment | CreateComment): void;
133 onEditComment(form: EditComment | CreateComment): void;
134 onCommentVote(form: CreateCommentLike): void;
135 onBlockPerson(form: BlockPerson): void;
136 onDeleteComment(form: DeleteComment): void;
137 onRemoveComment(form: RemoveComment): void;
138 onDistinguishComment(form: DistinguishComment): void;
139 onAddModToCommunity(form: AddModToCommunity): void;
140 onAddAdmin(form: AddAdmin): void;
141 onBanPersonFromCommunity(form: BanFromCommunity): void;
142 onBanPerson(form: BanPerson): void;
143 onTransferCommunity(form: TransferCommunity): void;
144 onFetchChildren?(form: GetComments): void;
145 onCommentReport(form: CreateCommentReport): void;
146 onPurgePerson(form: PurgePerson): void;
147 onPurgeComment(form: PurgeComment): void;
150 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
151 state: CommentNodeState = {
154 showRemoveDialog: false,
155 showBanDialog: false,
157 banType: BanType.Community,
158 showPurgeDialog: false,
159 purgeType: PurgeType.Person,
163 showConfirmTransferSite: false,
164 showConfirmTransferCommunity: false,
165 showConfirmAppointAsMod: false,
166 showConfirmAppointAsAdmin: false,
167 showReportDialog: false,
168 createOrEditCommentLoading: false,
169 upvoteLoading: false,
170 downvoteLoading: false,
173 blockPersonLoading: false,
174 deleteLoading: false,
175 removeLoading: false,
176 distinguishLoading: false,
178 addModLoading: false,
179 addAdminLoading: false,
180 transferCommunityLoading: false,
181 fetchChildrenLoading: false,
182 reportLoading: false,
186 constructor(props: any, context: any) {
187 super(props, context);
189 this.handleReplyCancel = this.handleReplyCancel.bind(this);
192 get commentView(): CommentView {
193 return this.props.node.comment_view;
196 get commentId(): CommentId {
197 return this.commentView.comment.id;
200 componentWillReceiveProps(
201 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>,
203 if (!deepEqual(this.props, nextProps)) {
207 showRemoveDialog: false,
208 showBanDialog: false,
210 banType: BanType.Community,
211 showPurgeDialog: false,
212 purgeType: PurgeType.Person,
216 showConfirmTransferSite: false,
217 showConfirmTransferCommunity: false,
218 showConfirmAppointAsMod: false,
219 showConfirmAppointAsAdmin: false,
220 showReportDialog: false,
221 createOrEditCommentLoading: false,
222 upvoteLoading: false,
223 downvoteLoading: false,
226 blockPersonLoading: false,
227 deleteLoading: false,
228 removeLoading: false,
229 distinguishLoading: false,
231 addModLoading: false,
232 addAdminLoading: false,
233 transferCommunityLoading: false,
234 fetchChildrenLoading: false,
235 reportLoading: false,
242 const node = this.props.node;
243 const cv = this.commentView;
245 const purgeTypeText =
246 this.state.purgeType === PurgeType.Comment
247 ? I18NextService.i18n.t("purge_comment")
248 : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
250 const canMod_ = canMod(
252 this.props.moderators,
255 const canModOnSelf = canMod(
257 this.props.moderators,
259 UserService.Instance.myUserInfo,
262 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
263 const canAdminOnSelf = canAdmin(
266 UserService.Instance.myUserInfo,
269 const isMod_ = isMod(cv.creator.id, this.props.moderators);
270 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
271 const amCommunityCreator_ = amCommunityCreator(
273 this.props.moderators,
276 const moreRepliesBorderColor = this.props.node.depth
277 ? colorList[this.props.node.depth % colorList.length]
280 const showMoreChildren =
281 this.props.viewType === CommentViewType.Tree &&
282 !this.state.collapsed &&
283 node.children.length === 0 &&
284 node.comment_view.counts.child_count > 0;
287 <li className="comment list-unstyled">
289 id={`comment-${cv.comment.id}`}
290 className={classNames(`details comment-node py-2`, {
291 "border-top border-light": !this.props.noBorder,
292 mark: this.isCommentNew || this.commentView.comment.distinguished,
295 <div className="ms-2">
296 <div className="d-flex flex-wrap align-items-center text-muted small">
298 className="btn btn-sm btn-link 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"
309 <PersonListing person={cv.creator} />
311 {cv.comment.distinguished && (
312 <Icon icon="shield" inline classes="text-danger ms-1" />
317 isPostCreator={this.isPostCreator}
320 isBot={cv.creator.bot_account}
323 {this.props.showCommunity && (
325 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
326 <CommunityLink community={cv.community} />
327 <span className="mx-2">•</span>
328 <Link className="me-2" to={`/post/${cv.post.id}`}>
334 {this.getLinkButton(true)}
336 {cv.comment.language_id !== 0 && (
337 <span className="badge text-bg-light d-none d-sm-inline me-2">
339 this.props.allLanguages.find(
340 lang => lang.id === cv.comment.language_id,
345 {/* This is an expanding spacer for mobile */}
346 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
351 className={`me-1 fw-bold ${this.scoreColor}`}
352 aria-label={I18NextService.i18n.t("number_of_points", {
353 count: Number(this.commentView.counts.score),
354 formattedCount: numToSI(this.commentView.counts.score),
357 {numToSI(this.commentView.counts.score)}
359 <span className="me-1">•</span>
364 published={cv.comment.published}
365 updated={cv.comment.updated}
369 {/* end of user row */}
370 {this.state.showEdit && (
374 onReplyCancel={this.handleReplyCancel}
375 disabled={this.props.locked}
376 finished={this.props.finished.get(
377 this.props.node.comment_view.comment.id,
380 allLanguages={this.props.allLanguages}
381 siteLanguages={this.props.siteLanguages}
382 containerClass="comment-comment-container"
383 onUpsertComment={this.props.onEditComment}
386 {!this.state.showEdit && !this.state.collapsed && (
388 <div className="comment-content">
389 {this.state.viewSource ? (
390 <pre>{this.commentUnlessRemoved}</pre>
394 dangerouslySetInnerHTML={
395 this.props.hideImages
396 ? mdToHtmlNoImages(this.commentUnlessRemoved)
397 : mdToHtml(this.commentUnlessRemoved)
402 <div className="comment-bottom-btns d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold">
403 {this.props.showContext && this.getLinkButton()}
404 {this.props.markable && (
406 className="btn btn-link btn-animate text-muted"
407 onClick={linkEvent(this, this.handleMarkAsRead)}
409 this.commentReplyOrMentionRead
410 ? I18NextService.i18n.t("mark_as_unread")
411 : I18NextService.i18n.t("mark_as_read")
414 this.commentReplyOrMentionRead
415 ? I18NextService.i18n.t("mark_as_unread")
416 : I18NextService.i18n.t("mark_as_read")
419 {this.state.readLoading ? (
424 classes={`icon-inline ${
425 this.commentReplyOrMentionRead && "text-success"
431 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
434 voteContentType={VoteContentType.Comment}
435 id={this.commentView.comment.id}
436 onVote={this.props.onCommentVote}
437 enableDownvotes={this.props.enableDownvotes}
438 counts={this.commentView.counts}
439 my_vote={this.commentView.my_vote}
442 className="btn btn-link btn-animate text-muted"
443 onClick={linkEvent(this, this.handleReplyClick)}
444 data-tippy-content={I18NextService.i18n.t("reply")}
445 aria-label={I18NextService.i18n.t("reply")}
447 <Icon icon="reply1" classes="icon-inline" />
449 {!this.state.showAdvanced ? (
451 className="btn btn-link btn-animate text-muted btn-more"
452 onClick={linkEvent(this, this.handleShowAdvanced)}
453 data-tippy-content={I18NextService.i18n.t("more")}
454 aria-label={I18NextService.i18n.t("more")}
456 <Icon icon="more-vertical" classes="icon-inline" />
460 {!this.myComment && (
463 className="btn btn-link btn-animate text-muted"
464 to={`/create_private_message/${cv.creator.id}`}
465 title={I18NextService.i18n
472 className="btn btn-link btn-animate text-muted"
475 this.handleShowReportDialog,
477 data-tippy-content={I18NextService.i18n.t(
478 "show_report_dialog",
480 aria-label={I18NextService.i18n.t(
481 "show_report_dialog",
487 className="btn btn-link btn-animate text-muted"
490 this.handleBlockPerson,
492 data-tippy-content={I18NextService.i18n.t(
495 aria-label={I18NextService.i18n.t("block_user")}
497 {this.state.blockPersonLoading ? (
500 <Icon icon="slash" />
506 className="btn btn-link btn-animate text-muted"
507 onClick={linkEvent(this, this.handleSaveComment)}
510 ? I18NextService.i18n.t("unsave")
511 : I18NextService.i18n.t("save")
515 ? I18NextService.i18n.t("unsave")
516 : I18NextService.i18n.t("save")
519 {this.state.saveLoading ? (
524 classes={`icon-inline ${
525 cv.saved && "text-warning"
531 className="btn btn-link btn-animate text-muted"
532 onClick={linkEvent(this, this.handleViewSource)}
533 data-tippy-content={I18NextService.i18n.t(
536 aria-label={I18NextService.i18n.t("view_source")}
540 classes={`icon-inline ${
541 this.state.viewSource && "text-success"
548 className="btn btn-link btn-animate text-muted"
549 onClick={linkEvent(this, this.handleEditClick)}
550 data-tippy-content={I18NextService.i18n.t(
553 aria-label={I18NextService.i18n.t("edit")}
555 <Icon icon="edit" classes="icon-inline" />
558 className="btn btn-link btn-animate text-muted"
561 this.handleDeleteComment,
565 ? I18NextService.i18n.t("delete")
566 : I18NextService.i18n.t("restore")
570 ? I18NextService.i18n.t("delete")
571 : I18NextService.i18n.t("restore")
574 {this.state.deleteLoading ? (
579 classes={`icon-inline ${
580 cv.comment.deleted && "text-danger"
586 {(canModOnSelf || canAdminOnSelf) && (
588 className="btn btn-link btn-animate text-muted"
591 this.handleDistinguishComment,
594 !cv.comment.distinguished
595 ? I18NextService.i18n.t("distinguish")
596 : I18NextService.i18n.t("undistinguish")
599 !cv.comment.distinguished
600 ? I18NextService.i18n.t("distinguish")
601 : I18NextService.i18n.t("undistinguish")
606 classes={`icon-inline ${
607 cv.comment.distinguished && "text-danger"
614 {/* Admins and mods can remove comments */}
615 {(canMod_ || canAdmin_) && (
617 {!cv.comment.removed ? (
619 className="btn btn-link btn-animate text-muted"
622 this.handleModRemoveShow,
624 aria-label={I18NextService.i18n.t("remove")}
626 {I18NextService.i18n.t("remove")}
630 className="btn btn-link btn-animate text-muted"
633 this.handleRemoveComment,
635 aria-label={I18NextService.i18n.t("restore")}
637 {this.state.removeLoading ? (
640 I18NextService.i18n.t("restore")
646 {/* Mods can ban from community, and appoint as mods to community */}
650 (!cv.creator_banned_from_community ? (
652 className="btn btn-link btn-animate text-muted"
655 this.handleModBanFromCommunityShow,
657 aria-label={I18NextService.i18n.t(
658 "ban_from_community",
661 {I18NextService.i18n.t(
662 "ban_from_community",
667 className="btn btn-link btn-animate text-muted"
670 this.handleBanPersonFromCommunity,
672 aria-label={I18NextService.i18n.t("unban")}
674 {this.state.banLoading ? (
677 I18NextService.i18n.t("unban")
681 {!cv.creator_banned_from_community &&
682 (!this.state.showConfirmAppointAsMod ? (
684 className="btn btn-link btn-animate text-muted"
687 this.handleShowConfirmAppointAsMod,
691 ? I18NextService.i18n.t("remove_as_mod")
692 : I18NextService.i18n.t(
698 ? I18NextService.i18n.t("remove_as_mod")
699 : I18NextService.i18n.t("appoint_as_mod")}
704 className="btn btn-link btn-animate text-muted"
705 aria-label={I18NextService.i18n.t(
709 {I18NextService.i18n.t("are_you_sure")}
712 className="btn btn-link btn-animate text-muted"
715 this.handleAddModToCommunity,
717 aria-label={I18NextService.i18n.t("yes")}
719 {this.state.addModLoading ? (
722 I18NextService.i18n.t("yes")
726 className="btn btn-link btn-animate text-muted"
729 this.handleCancelConfirmAppointAsMod,
731 aria-label={I18NextService.i18n.t("no")}
733 {I18NextService.i18n.t("no")}
739 {/* Community creators and admins can transfer community to another mod */}
740 {(amCommunityCreator_ || canAdmin_) &&
743 (!this.state.showConfirmTransferCommunity ? (
745 className="btn btn-link btn-animate text-muted"
748 this.handleShowConfirmTransferCommunity,
750 aria-label={I18NextService.i18n.t(
751 "transfer_community",
754 {I18NextService.i18n.t("transfer_community")}
759 className="btn btn-link btn-animate text-muted"
760 aria-label={I18NextService.i18n.t(
764 {I18NextService.i18n.t("are_you_sure")}
767 className="btn btn-link btn-animate text-muted"
770 this.handleTransferCommunity,
772 aria-label={I18NextService.i18n.t("yes")}
774 {this.state.transferCommunityLoading ? (
777 I18NextService.i18n.t("yes")
781 className="btn btn-link btn-animate text-muted"
785 .handleCancelShowConfirmTransferCommunity,
787 aria-label={I18NextService.i18n.t("no")}
789 {I18NextService.i18n.t("no")}
793 {/* Admins can ban from all, and appoint other admins */}
799 className="btn btn-link btn-animate text-muted"
802 this.handlePurgePersonShow,
804 aria-label={I18NextService.i18n.t(
808 {I18NextService.i18n.t("purge_user")}
811 className="btn btn-link btn-animate text-muted"
814 this.handlePurgeCommentShow,
816 aria-label={I18NextService.i18n.t(
820 {I18NextService.i18n.t("purge_comment")}
823 {!isBanned(cv.creator) ? (
825 className="btn btn-link btn-animate text-muted"
828 this.handleModBanShow,
830 aria-label={I18NextService.i18n.t(
834 {I18NextService.i18n.t("ban_from_site")}
838 className="btn btn-link btn-animate text-muted"
841 this.handleBanPerson,
843 aria-label={I18NextService.i18n.t(
847 {this.state.banLoading ? (
850 I18NextService.i18n.t("unban_from_site")
856 {!isBanned(cv.creator) &&
858 (!this.state.showConfirmAppointAsAdmin ? (
860 className="btn btn-link btn-animate text-muted"
863 this.handleShowConfirmAppointAsAdmin,
867 ? I18NextService.i18n.t(
870 : I18NextService.i18n.t(
876 ? I18NextService.i18n.t("remove_as_admin")
877 : I18NextService.i18n.t(
883 <button className="btn btn-link btn-animate text-muted">
884 {I18NextService.i18n.t("are_you_sure")}
887 className="btn btn-link btn-animate text-muted"
892 aria-label={I18NextService.i18n.t("yes")}
894 {this.state.addAdminLoading ? (
897 I18NextService.i18n.t("yes")
901 className="btn btn-link btn-animate text-muted"
904 this.handleCancelConfirmAppointAsAdmin,
906 aria-label={I18NextService.i18n.t("no")}
908 {I18NextService.i18n.t("no")}
919 {/* end of button group */}
924 {showMoreChildren && (
926 className={classNames("details ms-1 comment-node py-2", {
927 "border-top border-light": !this.props.noBorder,
929 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
932 className="btn btn-link text-muted"
933 onClick={linkEvent(this, this.handleFetchChildren)}
935 {this.state.fetchChildrenLoading ? (
939 {I18NextService.i18n.t("x_more_replies", {
940 count: node.comment_view.counts.child_count,
941 formattedCount: numToSI(
942 node.comment_view.counts.child_count,
951 {/* end of details */}
952 {this.state.showRemoveDialog && (
954 className="form-inline"
955 onSubmit={linkEvent(this, this.handleRemoveComment)}
958 className="visually-hidden"
959 htmlFor={`mod-remove-reason-${cv.comment.id}`}
961 {I18NextService.i18n.t("reason")}
965 id={`mod-remove-reason-${cv.comment.id}`}
966 className="form-control me-2"
967 placeholder={I18NextService.i18n.t("reason")}
968 value={this.state.removeReason}
969 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
973 className="btn btn-secondary"
974 aria-label={I18NextService.i18n.t("remove_comment")}
976 {I18NextService.i18n.t("remove_comment")}
980 {this.state.showReportDialog && (
982 className="form-inline"
983 onSubmit={linkEvent(this, this.handleReportComment)}
986 className="visually-hidden"
987 htmlFor={`report-reason-${cv.comment.id}`}
989 {I18NextService.i18n.t("reason")}
994 id={`report-reason-${cv.comment.id}`}
995 className="form-control me-2"
996 placeholder={I18NextService.i18n.t("reason")}
997 value={this.state.reportReason}
998 onInput={linkEvent(this, this.handleReportReasonChange)}
1002 className="btn btn-secondary"
1003 aria-label={I18NextService.i18n.t("create_report")}
1005 {I18NextService.i18n.t("create_report")}
1009 {this.state.showBanDialog && (
1010 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1011 <div className="mb-3 row col-12">
1013 className="col-form-label"
1014 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1016 {I18NextService.i18n.t("reason")}
1020 id={`mod-ban-reason-${cv.comment.id}`}
1021 className="form-control me-2"
1022 placeholder={I18NextService.i18n.t("reason")}
1023 value={this.state.banReason}
1024 onInput={linkEvent(this, this.handleModBanReasonChange)}
1027 className="col-form-label"
1028 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1030 {I18NextService.i18n.t("expires")}
1034 id={`mod-ban-expires-${cv.comment.id}`}
1035 className="form-control me-2"
1036 placeholder={I18NextService.i18n.t("number_of_days")}
1037 value={this.state.banExpireDays}
1038 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1040 <div className="input-group mb-3">
1041 <div className="form-check">
1043 className="form-check-input"
1044 id="mod-ban-remove-data"
1046 checked={this.state.removeData}
1047 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1050 className="form-check-label"
1051 htmlFor="mod-ban-remove-data"
1052 title={I18NextService.i18n.t("remove_content_more")}
1054 {I18NextService.i18n.t("remove_content")}
1059 {/* TODO hold off on expires until later */}
1060 {/* <div class="mb-3 row"> */}
1061 {/* <label class="col-form-label">Expires</label> */}
1062 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1064 <div className="mb-3 row">
1067 className="btn btn-secondary"
1068 aria-label={I18NextService.i18n.t("ban")}
1070 {this.state.banLoading ? (
1074 {I18NextService.i18n.t("ban")} {cv.creator.name}
1082 {this.state.showPurgeDialog && (
1083 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1085 <label className="visually-hidden" htmlFor="purge-reason">
1086 {I18NextService.i18n.t("reason")}
1091 className="form-control my-3"
1092 placeholder={I18NextService.i18n.t("reason")}
1093 value={this.state.purgeReason}
1094 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1096 <div className="mb-3 row col-12">
1097 {this.state.purgeLoading ? (
1102 className="btn btn-secondary"
1103 aria-label={purgeTypeText}
1111 {this.state.showReply && (
1114 onReplyCancel={this.handleReplyCancel}
1115 disabled={this.props.locked}
1116 finished={this.props.finished.get(
1117 this.props.node.comment_view.comment.id,
1120 allLanguages={this.props.allLanguages}
1121 siteLanguages={this.props.siteLanguages}
1122 containerClass="comment-comment-container"
1123 onUpsertComment={this.props.onCreateComment}
1126 {!this.state.collapsed && node.children.length > 0 && (
1128 nodes={node.children}
1129 locked={this.props.locked}
1130 moderators={this.props.moderators}
1131 admins={this.props.admins}
1132 enableDownvotes={this.props.enableDownvotes}
1133 viewType={this.props.viewType}
1134 allLanguages={this.props.allLanguages}
1135 siteLanguages={this.props.siteLanguages}
1136 hideImages={this.props.hideImages}
1137 isChild={!this.props.isTopLevel}
1138 depth={this.props.node.depth + 1}
1139 finished={this.props.finished}
1140 onCommentReplyRead={this.props.onCommentReplyRead}
1141 onPersonMentionRead={this.props.onPersonMentionRead}
1142 onCreateComment={this.props.onCreateComment}
1143 onEditComment={this.props.onEditComment}
1144 onCommentVote={this.props.onCommentVote}
1145 onBlockPerson={this.props.onBlockPerson}
1146 onSaveComment={this.props.onSaveComment}
1147 onDeleteComment={this.props.onDeleteComment}
1148 onRemoveComment={this.props.onRemoveComment}
1149 onDistinguishComment={this.props.onDistinguishComment}
1150 onAddModToCommunity={this.props.onAddModToCommunity}
1151 onAddAdmin={this.props.onAddAdmin}
1152 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1153 onBanPerson={this.props.onBanPerson}
1154 onTransferCommunity={this.props.onTransferCommunity}
1155 onFetchChildren={this.props.onFetchChildren}
1156 onCommentReport={this.props.onCommentReport}
1157 onPurgePerson={this.props.onPurgePerson}
1158 onPurgeComment={this.props.onPurgeComment}
1161 {/* A collapsed clearfix */}
1162 {this.state.collapsed && <div className="row col-12" />}
1167 get commentReplyOrMentionRead(): boolean {
1168 const cv = this.commentView;
1170 if (this.isPersonMentionType(cv)) {
1171 return cv.person_mention.read;
1172 } else if (this.isCommentReplyType(cv)) {
1173 return cv.comment_reply.read;
1179 getLinkButton(small = false) {
1180 const cv = this.commentView;
1182 const classnames = classNames("btn btn-link btn-animate text-muted", {
1186 const title = this.props.showContext
1187 ? I18NextService.i18n.t("show_context")
1188 : I18NextService.i18n.t("link");
1190 // The context button should show the parent comment by default
1191 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1196 className={classnames}
1197 to={`/comment/${parentCommentId}`}
1200 <Icon icon="link" classes="icon-inline" />
1203 <a className={classnames} title={title} href={cv.comment.ap_id}>
1204 <Icon icon="fedilink" classes="icon-inline" />
1211 get myComment(): boolean {
1213 UserService.Instance.myUserInfo?.local_user_view.person.id ===
1214 this.commentView.creator.id
1218 get isPostCreator(): boolean {
1219 return this.commentView.creator.id === this.commentView.post.creator_id;
1223 if (this.commentView.my_vote === 1) {
1225 } else if (this.commentView.my_vote === -1) {
1226 return "text-danger";
1228 return "text-muted";
1232 get pointsTippy(): string {
1233 const points = I18NextService.i18n.t("number_of_points", {
1234 count: Number(this.commentView.counts.score),
1235 formattedCount: numToSI(this.commentView.counts.score),
1238 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1239 count: Number(this.commentView.counts.upvotes),
1240 formattedCount: numToSI(this.commentView.counts.upvotes),
1243 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1244 count: Number(this.commentView.counts.downvotes),
1245 formattedCount: numToSI(this.commentView.counts.downvotes),
1248 return `${points} • ${upvotes} • ${downvotes}`;
1251 get expandText(): string {
1252 return this.state.collapsed
1253 ? I18NextService.i18n.t("expand")
1254 : I18NextService.i18n.t("collapse");
1257 get commentUnlessRemoved(): string {
1258 const comment = this.commentView.comment;
1259 return comment.removed
1260 ? `*${I18NextService.i18n.t("removed")}*`
1262 ? `*${I18NextService.i18n.t("deleted")}*`
1266 handleReplyClick(i: CommentNode) {
1267 i.setState({ showReply: true });
1270 handleEditClick(i: CommentNode) {
1271 i.setState({ showEdit: true });
1274 handleReplyCancel() {
1275 this.setState({ showReply: false, showEdit: false });
1278 handleShowReportDialog(i: CommentNode) {
1279 i.setState({ showReportDialog: !i.state.showReportDialog });
1282 handleReportReasonChange(i: CommentNode, event: any) {
1283 i.setState({ reportReason: event.target.value });
1286 handleModRemoveShow(i: CommentNode) {
1288 showRemoveDialog: !i.state.showRemoveDialog,
1289 showBanDialog: false,
1293 handleModRemoveReasonChange(i: CommentNode, event: any) {
1294 i.setState({ removeReason: event.target.value });
1297 handleModRemoveDataChange(i: CommentNode, event: any) {
1298 i.setState({ removeData: event.target.checked });
1301 isPersonMentionType(
1302 item: CommentView | PersonMentionView | CommentReplyView,
1303 ): item is PersonMentionView {
1304 return (item as PersonMentionView).person_mention?.id !== undefined;
1308 item: CommentView | PersonMentionView | CommentReplyView,
1309 ): item is CommentReplyView {
1310 return (item as CommentReplyView).comment_reply?.id !== undefined;
1313 handleModBanFromCommunityShow(i: CommentNode) {
1315 showBanDialog: true,
1316 banType: BanType.Community,
1317 showRemoveDialog: false,
1321 handleModBanShow(i: CommentNode) {
1323 showBanDialog: true,
1324 banType: BanType.Site,
1325 showRemoveDialog: false,
1329 handleModBanReasonChange(i: CommentNode, event: any) {
1330 i.setState({ banReason: event.target.value });
1333 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1334 i.setState({ banExpireDays: event.target.value });
1337 handlePurgePersonShow(i: CommentNode) {
1339 showPurgeDialog: true,
1340 purgeType: PurgeType.Person,
1341 showRemoveDialog: false,
1345 handlePurgeCommentShow(i: CommentNode) {
1347 showPurgeDialog: true,
1348 purgeType: PurgeType.Comment,
1349 showRemoveDialog: false,
1353 handlePurgeReasonChange(i: CommentNode, event: any) {
1354 i.setState({ purgeReason: event.target.value });
1357 handleShowConfirmAppointAsMod(i: CommentNode) {
1358 i.setState({ showConfirmAppointAsMod: true });
1361 handleCancelConfirmAppointAsMod(i: CommentNode) {
1362 i.setState({ showConfirmAppointAsMod: false });
1365 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1366 i.setState({ showConfirmAppointAsAdmin: true });
1369 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1370 i.setState({ showConfirmAppointAsAdmin: false });
1373 handleShowConfirmTransferCommunity(i: CommentNode) {
1374 i.setState({ showConfirmTransferCommunity: true });
1377 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1378 i.setState({ showConfirmTransferCommunity: false });
1381 handleShowConfirmTransferSite(i: CommentNode) {
1382 i.setState({ showConfirmTransferSite: true });
1385 handleCancelShowConfirmTransferSite(i: CommentNode) {
1386 i.setState({ showConfirmTransferSite: false });
1389 get isCommentNew(): boolean {
1390 const now = subMinutes(new Date(), 10);
1391 const then = parseISO(this.commentView.comment.published);
1392 return isBefore(now, then);
1395 handleCommentCollapse(i: CommentNode) {
1396 i.setState({ collapsed: !i.state.collapsed });
1400 handleViewSource(i: CommentNode) {
1401 i.setState({ viewSource: !i.state.viewSource });
1404 handleShowAdvanced(i: CommentNode) {
1405 i.setState({ showAdvanced: !i.state.showAdvanced });
1409 handleSaveComment(i: CommentNode) {
1410 i.setState({ saveLoading: true });
1412 i.props.onSaveComment({
1413 comment_id: i.commentView.comment.id,
1414 save: !i.commentView.saved,
1415 auth: myAuthRequired(),
1419 handleBlockPerson(i: CommentNode) {
1420 i.setState({ blockPersonLoading: true });
1421 i.props.onBlockPerson({
1422 person_id: i.commentView.creator.id,
1424 auth: myAuthRequired(),
1428 handleMarkAsRead(i: CommentNode) {
1429 i.setState({ readLoading: true });
1430 const cv = i.commentView;
1431 if (i.isPersonMentionType(cv)) {
1432 i.props.onPersonMentionRead({
1433 person_mention_id: cv.person_mention.id,
1434 read: !cv.person_mention.read,
1435 auth: myAuthRequired(),
1437 } else if (i.isCommentReplyType(cv)) {
1438 i.props.onCommentReplyRead({
1439 comment_reply_id: cv.comment_reply.id,
1440 read: !cv.comment_reply.read,
1441 auth: myAuthRequired(),
1446 handleDeleteComment(i: CommentNode) {
1447 i.setState({ deleteLoading: true });
1448 i.props.onDeleteComment({
1449 comment_id: i.commentId,
1450 deleted: !i.commentView.comment.deleted,
1451 auth: myAuthRequired(),
1455 handleRemoveComment(i: CommentNode, event: any) {
1456 event.preventDefault();
1457 i.setState({ removeLoading: true });
1458 i.props.onRemoveComment({
1459 comment_id: i.commentId,
1460 removed: !i.commentView.comment.removed,
1461 auth: myAuthRequired(),
1462 reason: i.state.removeReason,
1466 handleDistinguishComment(i: CommentNode) {
1467 i.setState({ distinguishLoading: true });
1468 i.props.onDistinguishComment({
1469 comment_id: i.commentId,
1470 distinguished: !i.commentView.comment.distinguished,
1471 auth: myAuthRequired(),
1475 handleBanPersonFromCommunity(i: CommentNode) {
1476 i.setState({ banLoading: true });
1477 i.props.onBanPersonFromCommunity({
1478 community_id: i.commentView.community.id,
1479 person_id: i.commentView.creator.id,
1480 ban: !i.commentView.creator_banned_from_community,
1481 reason: i.state.banReason,
1482 remove_data: i.state.removeData,
1483 expires: futureDaysToUnixTime(i.state.banExpireDays),
1484 auth: myAuthRequired(),
1488 handleBanPerson(i: CommentNode) {
1489 i.setState({ banLoading: true });
1490 i.props.onBanPerson({
1491 person_id: i.commentView.creator.id,
1492 ban: !i.commentView.creator_banned_from_community,
1493 reason: i.state.banReason,
1494 remove_data: i.state.removeData,
1495 expires: futureDaysToUnixTime(i.state.banExpireDays),
1496 auth: myAuthRequired(),
1500 handleModBanBothSubmit(i: CommentNode, event: any) {
1501 event.preventDefault();
1502 if (i.state.banType === BanType.Community) {
1503 i.handleBanPersonFromCommunity(i);
1505 i.handleBanPerson(i);
1509 handleAddModToCommunity(i: CommentNode) {
1510 i.setState({ addModLoading: true });
1512 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1513 i.props.onAddModToCommunity({
1514 community_id: i.commentView.community.id,
1515 person_id: i.commentView.creator.id,
1517 auth: myAuthRequired(),
1521 handleAddAdmin(i: CommentNode) {
1522 i.setState({ addAdminLoading: true });
1524 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1525 i.props.onAddAdmin({
1526 person_id: i.commentView.creator.id,
1528 auth: myAuthRequired(),
1532 handleTransferCommunity(i: CommentNode) {
1533 i.setState({ transferCommunityLoading: true });
1534 i.props.onTransferCommunity({
1535 community_id: i.commentView.community.id,
1536 person_id: i.commentView.creator.id,
1537 auth: myAuthRequired(),
1541 handleReportComment(i: CommentNode, event: any) {
1542 event.preventDefault();
1543 i.setState({ reportLoading: true });
1544 i.props.onCommentReport({
1545 comment_id: i.commentId,
1546 reason: i.state.reportReason ?? "",
1547 auth: myAuthRequired(),
1551 handlePurgeBothSubmit(i: CommentNode, event: any) {
1552 event.preventDefault();
1553 i.setState({ purgeLoading: true });
1555 if (i.state.purgeType === PurgeType.Person) {
1556 i.props.onPurgePerson({
1557 person_id: i.commentView.creator.id,
1558 reason: i.state.purgeReason,
1559 auth: myAuthRequired(),
1562 i.props.onPurgeComment({
1563 comment_id: i.commentId,
1564 reason: i.state.purgeReason,
1565 auth: myAuthRequired(),
1570 handleFetchChildren(i: CommentNode) {
1571 i.setState({ fetchChildrenLoading: true });
1572 i.props.onFetchChildren?.({
1573 parent_id: i.commentId,
1574 max_depth: commentTreeMaxDepth,