9 import classNames from "classnames";
10 import { Component, InfernoNode, linkEvent } from "inferno";
11 import { Link } from "inferno-router";
21 CommunityModeratorView,
30 MarkCommentReplyAsRead,
31 MarkPersonMentionAsRead,
39 } from "lemmy-js-client";
40 import moment from "moment";
41 import { i18n } from "../../i18next";
48 } from "../../interfaces";
49 import { UserService } from "../../services";
64 import { Icon, PurgeWarning, Spinner } from "../common/icon";
65 import { MomentTime } from "../common/moment-time";
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 (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 ? i18n.t("purge_comment")
247 : `${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" role="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 "ml-2": !this.props.noIndent,
299 <div className="d-flex flex-wrap align-items-center text-muted small">
301 className="btn btn-sm text-muted mr-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="mr-2">
312 <PersonListing person={cv.creator} />
314 {cv.comment.distinguished && (
315 <Icon icon="shield" inline classes={`text-danger mr-2`} />
317 {this.isPostCreator && (
318 <div className="badge badge-light d-none d-sm-inline mr-2">
323 <div className="badge d-none d-sm-inline mr-2">
328 <div className="badge d-none d-sm-inline mr-2">
332 {cv.creator.bot_account && (
333 <div className="badge d-none d-sm-inline mr-2">
334 {i18n.t("bot_account").toLowerCase()}
337 {this.props.showCommunity && (
339 <span className="mx-1">{i18n.t("to")}</span>
340 <CommunityLink community={cv.community} />
341 <span className="mx-2">•</span>
342 <Link className="mr-2" to={`/post/${cv.post.id}`}>
348 {cv.comment.language_id !== 0 && (
349 <span className="badge d-none d-sm-inline mr-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="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
362 className={`unselectable pointer ${this.scoreColor}`}
363 onClick={linkEvent(this, this.handleUpvote)}
364 data-tippy-content={this.pointsTippy}
366 {this.state.upvoteLoading ? (
370 className="mr-1 font-weight-bold"
371 aria-label={i18n.t("number_of_points", {
372 count: Number(this.commentView.counts.score),
373 formattedCount: numToSI(
374 this.commentView.counts.score
378 {numToSI(this.commentView.counts.score)}
382 <span className="mr-1">•</span>
387 published={cv.comment.published}
388 updated={cv.comment.updated}
392 {/* end of user row */}
393 {this.state.showEdit && (
397 onReplyCancel={this.handleReplyCancel}
398 disabled={this.props.locked}
399 finished={this.props.finished.get(
400 this.props.node.comment_view.comment.id
403 allLanguages={this.props.allLanguages}
404 siteLanguages={this.props.siteLanguages}
405 containerClass="comment-comment-container"
406 onUpsertComment={this.props.onEditComment}
409 {!this.state.showEdit && !this.state.collapsed && (
411 {this.state.viewSource ? (
412 <pre>{this.commentUnlessRemoved}</pre>
416 dangerouslySetInnerHTML={
417 this.props.hideImages
418 ? mdToHtmlNoImages(this.commentUnlessRemoved)
419 : mdToHtml(this.commentUnlessRemoved)
423 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
424 {this.props.showContext && this.linkBtn()}
425 {this.props.markable && (
427 className="btn btn-link btn-animate text-muted"
428 onClick={linkEvent(this, this.handleMarkAsRead)}
430 this.commentReplyOrMentionRead
431 ? i18n.t("mark_as_unread")
432 : i18n.t("mark_as_read")
435 this.commentReplyOrMentionRead
436 ? i18n.t("mark_as_unread")
437 : i18n.t("mark_as_read")
440 {this.state.readLoading ? (
445 classes={`icon-inline ${
446 this.commentReplyOrMentionRead && "text-success"
452 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
455 className={`btn btn-link btn-animate ${
456 this.commentView.my_vote === 1
460 onClick={linkEvent(this, this.handleUpvote)}
461 data-tippy-content={i18n.t("upvote")}
462 aria-label={i18n.t("upvote")}
463 aria-pressed={this.commentView.my_vote === 1}
465 {this.state.upvoteLoading ? (
469 <Icon icon="arrow-up1" classes="icon-inline" />
471 this.commentView.counts.upvotes !==
472 this.commentView.counts.score && (
473 <span className="ml-1">
474 {numToSI(this.commentView.counts.upvotes)}
480 {this.props.enableDownvotes && (
482 className={`btn btn-link btn-animate ${
483 this.commentView.my_vote === -1
487 onClick={linkEvent(this, this.handleDownvote)}
488 data-tippy-content={i18n.t("downvote")}
489 aria-label={i18n.t("downvote")}
490 aria-pressed={this.commentView.my_vote === -1}
492 {this.state.downvoteLoading ? (
496 <Icon icon="arrow-down1" classes="icon-inline" />
498 this.commentView.counts.upvotes !==
499 this.commentView.counts.score && (
500 <span className="ml-1">
501 {numToSI(this.commentView.counts.downvotes)}
509 className="btn btn-link btn-animate text-muted"
510 onClick={linkEvent(this, this.handleReplyClick)}
511 data-tippy-content={i18n.t("reply")}
512 aria-label={i18n.t("reply")}
514 <Icon icon="reply1" classes="icon-inline" />
516 {!this.state.showAdvanced ? (
518 className="btn btn-link btn-animate text-muted btn-more"
519 onClick={linkEvent(this, this.handleShowAdvanced)}
520 data-tippy-content={i18n.t("more")}
521 aria-label={i18n.t("more")}
523 <Icon icon="more-vertical" classes="icon-inline" />
527 {!this.myComment && (
530 className="btn btn-link btn-animate text-muted"
531 to={`/create_private_message/${cv.creator.id}`}
532 title={i18n.t("message").toLowerCase()}
537 className="btn btn-link btn-animate text-muted"
540 this.handleShowReportDialog
542 data-tippy-content={i18n.t(
545 aria-label={i18n.t("show_report_dialog")}
550 className="btn btn-link btn-animate text-muted"
553 this.handleBlockPerson
555 data-tippy-content={i18n.t("block_user")}
556 aria-label={i18n.t("block_user")}
558 {this.state.blockPersonLoading ? (
561 <Icon icon="slash" />
567 className="btn btn-link btn-animate text-muted"
568 onClick={linkEvent(this, this.handleSaveComment)}
570 cv.saved ? i18n.t("unsave") : i18n.t("save")
573 cv.saved ? i18n.t("unsave") : i18n.t("save")
576 {this.state.saveLoading ? (
581 classes={`icon-inline ${
582 cv.saved && "text-warning"
588 className="btn btn-link btn-animate text-muted"
589 onClick={linkEvent(this, this.handleViewSource)}
590 data-tippy-content={i18n.t("view_source")}
591 aria-label={i18n.t("view_source")}
595 classes={`icon-inline ${
596 this.state.viewSource && "text-success"
603 className="btn btn-link btn-animate text-muted"
604 onClick={linkEvent(this, this.handleEditClick)}
605 data-tippy-content={i18n.t("edit")}
606 aria-label={i18n.t("edit")}
608 <Icon icon="edit" classes="icon-inline" />
611 className="btn btn-link btn-animate text-muted"
614 this.handleDeleteComment
627 {this.state.deleteLoading ? (
632 classes={`icon-inline ${
633 cv.comment.deleted && "text-danger"
639 {(canModOnSelf || canAdminOnSelf) && (
641 className="btn btn-link btn-animate text-muted"
644 this.handleDistinguishComment
647 !cv.comment.distinguished
648 ? i18n.t("distinguish")
649 : i18n.t("undistinguish")
652 !cv.comment.distinguished
653 ? i18n.t("distinguish")
654 : i18n.t("undistinguish")
659 classes={`icon-inline ${
660 cv.comment.distinguished && "text-danger"
667 {/* Admins and mods can remove comments */}
668 {(canMod_ || canAdmin_) && (
670 {!cv.comment.removed ? (
672 className="btn btn-link btn-animate text-muted"
675 this.handleModRemoveShow
677 aria-label={i18n.t("remove")}
683 className="btn btn-link btn-animate text-muted"
686 this.handleRemoveComment
688 aria-label={i18n.t("restore")}
690 {this.state.removeLoading ? (
699 {/* Mods can ban from community, and appoint as mods to community */}
703 (!cv.creator_banned_from_community ? (
705 className="btn btn-link btn-animate text-muted"
708 this.handleModBanFromCommunityShow
710 aria-label={i18n.t("ban_from_community")}
712 {i18n.t("ban_from_community")}
716 className="btn btn-link btn-animate text-muted"
719 this.handleBanPersonFromCommunity
721 aria-label={i18n.t("unban")}
723 {this.state.banLoading ? (
730 {!cv.creator_banned_from_community &&
731 (!this.state.showConfirmAppointAsMod ? (
733 className="btn btn-link btn-animate text-muted"
736 this.handleShowConfirmAppointAsMod
740 ? i18n.t("remove_as_mod")
741 : i18n.t("appoint_as_mod")
745 ? i18n.t("remove_as_mod")
746 : i18n.t("appoint_as_mod")}
751 className="btn btn-link btn-animate text-muted"
752 aria-label={i18n.t("are_you_sure")}
754 {i18n.t("are_you_sure")}
757 className="btn btn-link btn-animate text-muted"
760 this.handleAddModToCommunity
762 aria-label={i18n.t("yes")}
764 {this.state.addModLoading ? (
771 className="btn btn-link btn-animate text-muted"
774 this.handleCancelConfirmAppointAsMod
776 aria-label={i18n.t("no")}
784 {/* Community creators and admins can transfer community to another mod */}
785 {(amCommunityCreator_ || canAdmin_) &&
788 (!this.state.showConfirmTransferCommunity ? (
790 className="btn btn-link btn-animate text-muted"
793 this.handleShowConfirmTransferCommunity
795 aria-label={i18n.t("transfer_community")}
797 {i18n.t("transfer_community")}
802 className="btn btn-link btn-animate text-muted"
803 aria-label={i18n.t("are_you_sure")}
805 {i18n.t("are_you_sure")}
808 className="btn btn-link btn-animate text-muted"
811 this.handleTransferCommunity
813 aria-label={i18n.t("yes")}
815 {this.state.transferCommunityLoading ? (
822 className="btn btn-link btn-animate text-muted"
826 .handleCancelShowConfirmTransferCommunity
828 aria-label={i18n.t("no")}
834 {/* Admins can ban from all, and appoint other admins */}
840 className="btn btn-link btn-animate text-muted"
843 this.handlePurgePersonShow
845 aria-label={i18n.t("purge_user")}
847 {i18n.t("purge_user")}
850 className="btn btn-link btn-animate text-muted"
853 this.handlePurgeCommentShow
855 aria-label={i18n.t("purge_comment")}
857 {i18n.t("purge_comment")}
860 {!isBanned(cv.creator) ? (
862 className="btn btn-link btn-animate text-muted"
865 this.handleModBanShow
867 aria-label={i18n.t("ban_from_site")}
869 {i18n.t("ban_from_site")}
873 className="btn btn-link btn-animate text-muted"
878 aria-label={i18n.t("unban_from_site")}
880 {this.state.banLoading ? (
883 i18n.t("unban_from_site")
889 {!isBanned(cv.creator) &&
891 (!this.state.showConfirmAppointAsAdmin ? (
893 className="btn btn-link btn-animate text-muted"
896 this.handleShowConfirmAppointAsAdmin
900 ? i18n.t("remove_as_admin")
901 : i18n.t("appoint_as_admin")
905 ? i18n.t("remove_as_admin")
906 : i18n.t("appoint_as_admin")}
910 <button className="btn btn-link btn-animate text-muted">
911 {i18n.t("are_you_sure")}
914 className="btn btn-link btn-animate text-muted"
919 aria-label={i18n.t("yes")}
921 {this.state.addAdminLoading ? (
928 className="btn btn-link btn-animate text-muted"
931 this.handleCancelConfirmAppointAsAdmin
933 aria-label={i18n.t("no")}
946 {/* end of button group */}
951 {showMoreChildren && (
953 className={classNames("details ml-1 comment-node py-2", {
954 "border-top border-light": !this.props.noBorder,
956 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
959 className="btn btn-link text-muted"
960 onClick={linkEvent(this, this.handleFetchChildren)}
962 {this.state.fetchChildrenLoading ? (
966 {i18n.t("x_more_replies", {
967 count: node.comment_view.counts.child_count,
968 formattedCount: numToSI(
969 node.comment_view.counts.child_count
978 {/* end of details */}
979 {this.state.showRemoveDialog && (
981 className="form-inline"
982 onSubmit={linkEvent(this, this.handleRemoveComment)}
986 htmlFor={`mod-remove-reason-${cv.comment.id}`}
992 id={`mod-remove-reason-${cv.comment.id}`}
993 className="form-control mr-2"
994 placeholder={i18n.t("reason")}
995 value={this.state.removeReason}
996 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1000 className="btn btn-secondary"
1001 aria-label={i18n.t("remove_comment")}
1003 {i18n.t("remove_comment")}
1007 {this.state.showReportDialog && (
1009 className="form-inline"
1010 onSubmit={linkEvent(this, this.handleReportComment)}
1014 htmlFor={`report-reason-${cv.comment.id}`}
1021 id={`report-reason-${cv.comment.id}`}
1022 className="form-control mr-2"
1023 placeholder={i18n.t("reason")}
1024 value={this.state.reportReason}
1025 onInput={linkEvent(this, this.handleReportReasonChange)}
1029 className="btn btn-secondary"
1030 aria-label={i18n.t("create_report")}
1032 {i18n.t("create_report")}
1036 {this.state.showBanDialog && (
1037 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1038 <div className="form-group row col-12">
1040 className="col-form-label"
1041 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1047 id={`mod-ban-reason-${cv.comment.id}`}
1048 className="form-control mr-2"
1049 placeholder={i18n.t("reason")}
1050 value={this.state.banReason}
1051 onInput={linkEvent(this, this.handleModBanReasonChange)}
1054 className="col-form-label"
1055 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1061 id={`mod-ban-expires-${cv.comment.id}`}
1062 className="form-control mr-2"
1063 placeholder={i18n.t("number_of_days")}
1064 value={this.state.banExpireDays}
1065 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1067 <div className="form-group">
1068 <div className="form-check">
1070 className="form-check-input"
1071 id="mod-ban-remove-data"
1073 checked={this.state.removeData}
1074 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1077 className="form-check-label"
1078 htmlFor="mod-ban-remove-data"
1079 title={i18n.t("remove_content_more")}
1081 {i18n.t("remove_content")}
1086 {/* TODO hold off on expires until later */}
1087 {/* <div class="form-group row"> */}
1088 {/* <label class="col-form-label">Expires</label> */}
1089 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1091 <div className="form-group row">
1094 className="btn btn-secondary"
1095 aria-label={i18n.t("ban")}
1097 {this.state.banLoading ? (
1101 {i18n.t("ban")} {cv.creator.name}
1109 {this.state.showPurgeDialog && (
1110 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1112 <label className="sr-only" htmlFor="purge-reason">
1118 className="form-control my-3"
1119 placeholder={i18n.t("reason")}
1120 value={this.state.purgeReason}
1121 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1123 <div className="form-group row col-12">
1124 {this.state.purgeLoading ? (
1129 className="btn btn-secondary"
1130 aria-label={purgeTypeText}
1138 {this.state.showReply && (
1141 onReplyCancel={this.handleReplyCancel}
1142 disabled={this.props.locked}
1143 finished={this.props.finished.get(
1144 this.props.node.comment_view.comment.id
1147 allLanguages={this.props.allLanguages}
1148 siteLanguages={this.props.siteLanguages}
1149 containerClass="comment-comment-container"
1150 onUpsertComment={this.props.onCreateComment}
1153 {!this.state.collapsed && node.children.length > 0 && (
1155 nodes={node.children}
1156 locked={this.props.locked}
1157 moderators={this.props.moderators}
1158 admins={this.props.admins}
1159 enableDownvotes={this.props.enableDownvotes}
1160 viewType={this.props.viewType}
1161 allLanguages={this.props.allLanguages}
1162 siteLanguages={this.props.siteLanguages}
1163 hideImages={this.props.hideImages}
1164 isChild={!this.props.noIndent}
1165 depth={this.props.node.depth + 1}
1166 finished={this.props.finished}
1167 onCommentReplyRead={this.props.onCommentReplyRead}
1168 onPersonMentionRead={this.props.onPersonMentionRead}
1169 onCreateComment={this.props.onCreateComment}
1170 onEditComment={this.props.onEditComment}
1171 onCommentVote={this.props.onCommentVote}
1172 onBlockPerson={this.props.onBlockPerson}
1173 onSaveComment={this.props.onSaveComment}
1174 onDeleteComment={this.props.onDeleteComment}
1175 onRemoveComment={this.props.onRemoveComment}
1176 onDistinguishComment={this.props.onDistinguishComment}
1177 onAddModToCommunity={this.props.onAddModToCommunity}
1178 onAddAdmin={this.props.onAddAdmin}
1179 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1180 onBanPerson={this.props.onBanPerson}
1181 onTransferCommunity={this.props.onTransferCommunity}
1182 onFetchChildren={this.props.onFetchChildren}
1183 onCommentReport={this.props.onCommentReport}
1184 onPurgePerson={this.props.onPurgePerson}
1185 onPurgeComment={this.props.onPurgeComment}
1188 {/* A collapsed clearfix */}
1189 {this.state.collapsed && <div className="row col-12" />}
1194 get commentReplyOrMentionRead(): boolean {
1195 const cv = this.commentView;
1197 if (this.isPersonMentionType(cv)) {
1198 return cv.person_mention.read;
1199 } else if (this.isCommentReplyType(cv)) {
1200 return cv.comment_reply.read;
1206 linkBtn(small = false) {
1207 const cv = this.commentView;
1209 const classnames = classNames("btn btn-link btn-animate text-muted", {
1213 const title = this.props.showContext
1214 ? i18n.t("show_context")
1217 // The context button should show the parent comment by default
1218 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1223 className={classnames}
1224 to={`/comment/${parentCommentId}`}
1227 <Icon icon="link" classes="icon-inline" />
1230 <a className={classnames} title={title} href={cv.comment.ap_id}>
1231 <Icon icon="fedilink" classes="icon-inline" />
1238 get myComment(): boolean {
1240 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1241 this.commentView.creator.id
1245 get isPostCreator(): boolean {
1246 return this.commentView.creator.id == this.commentView.post.creator_id;
1250 if (this.commentView.my_vote == 1) {
1252 } else if (this.commentView.my_vote == -1) {
1253 return "text-danger";
1255 return "text-muted";
1259 get pointsTippy(): string {
1260 const points = i18n.t("number_of_points", {
1261 count: Number(this.commentView.counts.score),
1262 formattedCount: numToSI(this.commentView.counts.score),
1265 const upvotes = i18n.t("number_of_upvotes", {
1266 count: Number(this.commentView.counts.upvotes),
1267 formattedCount: numToSI(this.commentView.counts.upvotes),
1270 const downvotes = i18n.t("number_of_downvotes", {
1271 count: Number(this.commentView.counts.downvotes),
1272 formattedCount: numToSI(this.commentView.counts.downvotes),
1275 return `${points} • ${upvotes} • ${downvotes}`;
1278 get expandText(): string {
1279 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
1282 get commentUnlessRemoved(): string {
1283 const comment = this.commentView.comment;
1284 return comment.removed
1285 ? `*${i18n.t("removed")}*`
1287 ? `*${i18n.t("deleted")}*`
1291 handleReplyClick(i: CommentNode) {
1292 i.setState({ showReply: true });
1295 handleEditClick(i: CommentNode) {
1296 i.setState({ showEdit: true });
1299 handleReplyCancel() {
1300 this.setState({ showReply: false, showEdit: false });
1303 handleShowReportDialog(i: CommentNode) {
1304 i.setState({ showReportDialog: !i.state.showReportDialog });
1307 handleReportReasonChange(i: CommentNode, event: any) {
1308 i.setState({ reportReason: event.target.value });
1311 handleModRemoveShow(i: CommentNode) {
1313 showRemoveDialog: !i.state.showRemoveDialog,
1314 showBanDialog: false,
1318 handleModRemoveReasonChange(i: CommentNode, event: any) {
1319 i.setState({ removeReason: event.target.value });
1322 handleModRemoveDataChange(i: CommentNode, event: any) {
1323 i.setState({ removeData: event.target.checked });
1326 isPersonMentionType(
1327 item: CommentView | PersonMentionView | CommentReplyView
1328 ): item is PersonMentionView {
1329 return (item as PersonMentionView).person_mention?.id !== undefined;
1333 item: CommentView | PersonMentionView | CommentReplyView
1334 ): item is CommentReplyView {
1335 return (item as CommentReplyView).comment_reply?.id !== undefined;
1338 handleModBanFromCommunityShow(i: CommentNode) {
1340 showBanDialog: true,
1341 banType: BanType.Community,
1342 showRemoveDialog: false,
1346 handleModBanShow(i: CommentNode) {
1348 showBanDialog: true,
1349 banType: BanType.Site,
1350 showRemoveDialog: false,
1354 handleModBanReasonChange(i: CommentNode, event: any) {
1355 i.setState({ banReason: event.target.value });
1358 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1359 i.setState({ banExpireDays: event.target.value });
1362 handlePurgePersonShow(i: CommentNode) {
1364 showPurgeDialog: true,
1365 purgeType: PurgeType.Person,
1366 showRemoveDialog: false,
1370 handlePurgeCommentShow(i: CommentNode) {
1372 showPurgeDialog: true,
1373 purgeType: PurgeType.Comment,
1374 showRemoveDialog: false,
1378 handlePurgeReasonChange(i: CommentNode, event: any) {
1379 i.setState({ purgeReason: event.target.value });
1382 handleShowConfirmAppointAsMod(i: CommentNode) {
1383 i.setState({ showConfirmAppointAsMod: true });
1386 handleCancelConfirmAppointAsMod(i: CommentNode) {
1387 i.setState({ showConfirmAppointAsMod: false });
1390 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1391 i.setState({ showConfirmAppointAsAdmin: true });
1394 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1395 i.setState({ showConfirmAppointAsAdmin: false });
1398 handleShowConfirmTransferCommunity(i: CommentNode) {
1399 i.setState({ showConfirmTransferCommunity: true });
1402 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1403 i.setState({ showConfirmTransferCommunity: false });
1406 handleShowConfirmTransferSite(i: CommentNode) {
1407 i.setState({ showConfirmTransferSite: true });
1410 handleCancelShowConfirmTransferSite(i: CommentNode) {
1411 i.setState({ showConfirmTransferSite: false });
1414 get isCommentNew(): boolean {
1415 const now = moment.utc().subtract(10, "minutes");
1416 const then = moment.utc(this.commentView.comment.published);
1417 return now.isBefore(then);
1420 handleCommentCollapse(i: CommentNode) {
1421 i.setState({ collapsed: !i.state.collapsed });
1425 handleViewSource(i: CommentNode) {
1426 i.setState({ viewSource: !i.state.viewSource });
1429 handleShowAdvanced(i: CommentNode) {
1430 i.setState({ showAdvanced: !i.state.showAdvanced });
1434 handleSaveComment(i: CommentNode) {
1435 i.setState({ saveLoading: true });
1437 i.props.onSaveComment({
1438 comment_id: i.commentView.comment.id,
1439 save: !i.commentView.saved,
1440 auth: myAuthRequired(),
1444 handleUpvote(i: CommentNode) {
1445 i.setState({ upvoteLoading: true });
1446 i.props.onCommentVote({
1447 comment_id: i.commentId,
1448 score: newVote(VoteType.Upvote, i.commentView.my_vote),
1449 auth: myAuthRequired(),
1453 handleDownvote(i: CommentNode) {
1454 i.setState({ downvoteLoading: true });
1455 i.props.onCommentVote({
1456 comment_id: i.commentId,
1457 score: newVote(VoteType.Downvote, i.commentView.my_vote),
1458 auth: myAuthRequired(),
1462 handleBlockPerson(i: CommentNode) {
1463 i.setState({ blockPersonLoading: true });
1464 i.props.onBlockPerson({
1465 person_id: i.commentView.creator.id,
1467 auth: myAuthRequired(),
1471 handleMarkAsRead(i: CommentNode) {
1472 i.setState({ readLoading: true });
1473 const cv = i.commentView;
1474 if (i.isPersonMentionType(cv)) {
1475 i.props.onPersonMentionRead({
1476 person_mention_id: cv.person_mention.id,
1477 read: !cv.person_mention.read,
1478 auth: myAuthRequired(),
1480 } else if (i.isCommentReplyType(cv)) {
1481 i.props.onCommentReplyRead({
1482 comment_reply_id: cv.comment_reply.id,
1483 read: !cv.comment_reply.read,
1484 auth: myAuthRequired(),
1489 handleDeleteComment(i: CommentNode) {
1490 i.setState({ deleteLoading: true });
1491 i.props.onDeleteComment({
1492 comment_id: i.commentId,
1493 deleted: !i.commentView.comment.deleted,
1494 auth: myAuthRequired(),
1498 handleRemoveComment(i: CommentNode, event: any) {
1499 event.preventDefault();
1500 i.setState({ removeLoading: true });
1501 i.props.onRemoveComment({
1502 comment_id: i.commentId,
1503 removed: !i.commentView.comment.removed,
1504 auth: myAuthRequired(),
1508 handleDistinguishComment(i: CommentNode) {
1509 i.setState({ distinguishLoading: true });
1510 i.props.onDistinguishComment({
1511 comment_id: i.commentId,
1512 distinguished: !i.commentView.comment.distinguished,
1513 auth: myAuthRequired(),
1517 handleBanPersonFromCommunity(i: CommentNode) {
1518 i.setState({ banLoading: true });
1519 i.props.onBanPersonFromCommunity({
1520 community_id: i.commentView.community.id,
1521 person_id: i.commentView.creator.id,
1522 ban: !i.commentView.creator_banned_from_community,
1523 reason: i.state.banReason,
1524 remove_data: i.state.removeData,
1525 expires: futureDaysToUnixTime(i.state.banExpireDays),
1526 auth: myAuthRequired(),
1530 handleBanPerson(i: CommentNode) {
1531 i.setState({ banLoading: true });
1532 i.props.onBanPerson({
1533 person_id: i.commentView.creator.id,
1534 ban: !i.commentView.creator_banned_from_community,
1535 reason: i.state.banReason,
1536 remove_data: i.state.removeData,
1537 expires: futureDaysToUnixTime(i.state.banExpireDays),
1538 auth: myAuthRequired(),
1542 handleModBanBothSubmit(i: CommentNode, event: any) {
1543 event.preventDefault();
1544 if (i.state.banType == BanType.Community) {
1545 i.handleBanPersonFromCommunity(i);
1547 i.handleBanPerson(i);
1551 handleAddModToCommunity(i: CommentNode) {
1552 i.setState({ addModLoading: true });
1554 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1555 i.props.onAddModToCommunity({
1556 community_id: i.commentView.community.id,
1557 person_id: i.commentView.creator.id,
1559 auth: myAuthRequired(),
1563 handleAddAdmin(i: CommentNode) {
1564 i.setState({ addAdminLoading: true });
1566 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1567 i.props.onAddAdmin({
1568 person_id: i.commentView.creator.id,
1570 auth: myAuthRequired(),
1574 handleTransferCommunity(i: CommentNode) {
1575 i.setState({ transferCommunityLoading: true });
1576 i.props.onTransferCommunity({
1577 community_id: i.commentView.community.id,
1578 person_id: i.commentView.creator.id,
1579 auth: myAuthRequired(),
1583 handleReportComment(i: CommentNode, event: any) {
1584 event.preventDefault();
1585 i.setState({ reportLoading: true });
1586 i.props.onCommentReport({
1587 comment_id: i.commentId,
1588 reason: i.state.reportReason ?? "",
1589 auth: myAuthRequired(),
1593 handlePurgeBothSubmit(i: CommentNode, event: any) {
1594 event.preventDefault();
1595 i.setState({ purgeLoading: true });
1597 if (i.state.purgeType == PurgeType.Person) {
1598 i.props.onPurgePerson({
1599 person_id: i.commentView.creator.id,
1600 reason: i.state.purgeReason,
1601 auth: myAuthRequired(),
1604 i.props.onPurgeComment({
1605 comment_id: i.commentId,
1606 reason: i.state.purgeReason,
1607 auth: myAuthRequired(),
1612 handleFetchChildren(i: CommentNode) {
1613 i.setState({ fetchChildrenLoading: true });
1614 i.props.onFetchChildren?.({
1615 parent_id: i.commentId,
1616 max_depth: commentTreeMaxDepth,