1 import classNames from "classnames";
2 import { Component, InfernoNode, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
13 CommunityModeratorView,
22 MarkCommentReplyAsRead,
23 MarkPersonMentionAsRead,
31 } from "lemmy-js-client";
32 import moment from "moment";
33 import { i18n } from "../../i18next";
40 } from "../../interfaces";
41 import { UserService } from "../../services";
62 import { Icon, PurgeWarning, Spinner } from "../common/icon";
63 import { MomentTime } from "../common/moment-time";
64 import { CommunityLink } from "../community/community-link";
65 import { PersonListing } from "../person/person-listing";
66 import { CommentForm } from "./comment-form";
67 import { CommentNodes } from "./comment-nodes";
69 interface CommentNodeState {
72 showRemoveDialog: boolean;
73 removeReason?: string;
74 showBanDialog: boolean;
77 banExpireDays?: number;
79 showPurgeDialog: boolean;
82 showConfirmTransferSite: boolean;
83 showConfirmTransferCommunity: boolean;
84 showConfirmAppointAsMod: boolean;
85 showConfirmAppointAsAdmin: boolean;
88 showAdvanced: boolean;
89 showReportDialog: boolean;
90 reportReason?: string;
91 createOrEditCommentLoading: boolean;
92 upvoteLoading: boolean;
93 downvoteLoading: boolean;
96 blockPersonLoading: boolean;
97 deleteLoading: boolean;
98 removeLoading: boolean;
99 distinguishLoading: boolean;
101 addModLoading: boolean;
102 addAdminLoading: boolean;
103 transferCommunityLoading: boolean;
104 fetchChildrenLoading: boolean;
105 reportLoading: boolean;
106 purgeLoading: boolean;
109 interface CommentNodeProps {
111 moderators?: CommunityModeratorView[];
112 admins?: PersonView[];
118 showContext?: boolean;
119 showCommunity?: boolean;
120 enableDownvotes?: boolean;
121 viewType: CommentViewType;
122 allLanguages: Language[];
123 siteLanguages: number[];
124 hideImages?: boolean;
125 finished: Map<CommentId, boolean | undefined>;
126 onSaveComment(form: SaveComment): void;
127 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
128 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
129 onCreateComment(form: EditComment | CreateComment): void;
130 onEditComment(form: EditComment | CreateComment): void;
131 onCommentVote(form: CreateCommentLike): void;
132 onBlockPerson(form: BlockPerson): void;
133 onDeleteComment(form: DeleteComment): void;
134 onRemoveComment(form: RemoveComment): void;
135 onDistinguishComment(form: DistinguishComment): void;
136 onAddModToCommunity(form: AddModToCommunity): void;
137 onAddAdmin(form: AddAdmin): void;
138 onBanPersonFromCommunity(form: BanFromCommunity): void;
139 onBanPerson(form: BanPerson): void;
140 onTransferCommunity(form: TransferCommunity): void;
141 onFetchChildren?(form: GetComments): void;
142 onCommentReport(form: CreateCommentReport): void;
143 onPurgePerson(form: PurgePerson): void;
144 onPurgeComment(form: PurgeComment): void;
147 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
148 state: CommentNodeState = {
151 showRemoveDialog: false,
152 showBanDialog: false,
154 banType: BanType.Community,
155 showPurgeDialog: false,
156 purgeType: PurgeType.Person,
160 showConfirmTransferSite: false,
161 showConfirmTransferCommunity: false,
162 showConfirmAppointAsMod: false,
163 showConfirmAppointAsAdmin: false,
164 showReportDialog: false,
165 createOrEditCommentLoading: false,
166 upvoteLoading: false,
167 downvoteLoading: false,
170 blockPersonLoading: false,
171 deleteLoading: false,
172 removeLoading: false,
173 distinguishLoading: false,
175 addModLoading: false,
176 addAdminLoading: false,
177 transferCommunityLoading: false,
178 fetchChildrenLoading: false,
179 reportLoading: false,
183 constructor(props: any, context: any) {
184 super(props, context);
186 this.handleReplyCancel = this.handleReplyCancel.bind(this);
189 get commentView(): CommentView {
190 return this.props.node.comment_view;
193 get commentId(): CommentId {
194 return this.commentView.comment.id;
197 componentWillReceiveProps(
198 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
200 if (this.props != nextProps) {
204 showRemoveDialog: false,
205 showBanDialog: false,
207 banType: BanType.Community,
208 showPurgeDialog: false,
209 purgeType: PurgeType.Person,
213 showConfirmTransferSite: false,
214 showConfirmTransferCommunity: false,
215 showConfirmAppointAsMod: false,
216 showConfirmAppointAsAdmin: false,
217 showReportDialog: false,
218 createOrEditCommentLoading: false,
219 upvoteLoading: false,
220 downvoteLoading: false,
223 blockPersonLoading: false,
224 deleteLoading: false,
225 removeLoading: false,
226 distinguishLoading: false,
228 addModLoading: false,
229 addAdminLoading: false,
230 transferCommunityLoading: false,
231 fetchChildrenLoading: false,
232 reportLoading: false,
239 const node = this.props.node;
240 const cv = this.commentView;
242 const purgeTypeText =
243 this.state.purgeType == PurgeType.Comment
244 ? i18n.t("purge_comment")
245 : `${i18n.t("purge")} ${cv.creator.name}`;
247 const canMod_ = canMod(
249 this.props.moderators,
252 const canModOnSelf = canMod(
254 this.props.moderators,
256 UserService.Instance.myUserInfo,
259 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
260 const canAdminOnSelf = canAdmin(
263 UserService.Instance.myUserInfo,
266 const isMod_ = isMod(cv.creator.id, this.props.moderators);
267 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
268 const amCommunityCreator_ = amCommunityCreator(
270 this.props.moderators
273 const moreRepliesBorderColor = this.props.node.depth
274 ? colorList[this.props.node.depth % colorList.length]
277 const showMoreChildren =
278 this.props.viewType == CommentViewType.Tree &&
279 !this.state.collapsed &&
280 node.children.length == 0 &&
281 node.comment_view.counts.child_count > 0;
284 <li className="comment" role="comment">
286 id={`comment-${cv.comment.id}`}
287 className={classNames(`details comment-node py-2`, {
288 "border-top border-light": !this.props.noBorder,
289 mark: this.isCommentNew || this.commentView.comment.distinguished,
293 className={classNames({
294 "ml-2": !this.props.noIndent,
297 <div className="d-flex flex-wrap align-items-center text-muted small">
299 className="btn btn-sm text-muted mr-2"
300 onClick={linkEvent(this, this.handleCommentCollapse)}
301 aria-label={this.expandText}
302 data-tippy-content={this.expandText}
305 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
306 classes="icon-inline"
309 <span className="mr-2">
310 <PersonListing person={cv.creator} />
312 {cv.comment.distinguished && (
313 <Icon icon="shield" inline classes={`text-danger mr-2`} />
315 {this.isPostCreator && (
316 <div className="badge badge-light d-none d-sm-inline mr-2">
321 <div className="badge d-none d-sm-inline mr-2">
326 <div className="badge d-none d-sm-inline mr-2">
330 {cv.creator.bot_account && (
331 <div className="badge d-none d-sm-inline mr-2">
332 {i18n.t("bot_account").toLowerCase()}
335 {this.props.showCommunity && (
337 <span className="mx-1">{i18n.t("to")}</span>
338 <CommunityLink community={cv.community} />
339 <span className="mx-2">•</span>
340 <Link className="mr-2" to={`/post/${cv.post.id}`}>
346 {cv.comment.language_id !== 0 && (
347 <span className="badge d-none d-sm-inline mr-2">
349 this.props.allLanguages.find(
350 lang => lang.id === cv.comment.language_id
355 {/* This is an expanding spacer for mobile */}
356 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
360 className={`unselectable pointer ${this.scoreColor}`}
361 onClick={linkEvent(this, this.handleUpvote)}
362 data-tippy-content={this.pointsTippy}
364 {this.state.upvoteLoading ? (
368 className="mr-1 font-weight-bold"
369 aria-label={i18n.t("number_of_points", {
370 count: Number(this.commentView.counts.score),
371 formattedCount: numToSI(
372 this.commentView.counts.score
376 {numToSI(this.commentView.counts.score)}
380 <span className="mr-1">•</span>
385 published={cv.comment.published}
386 updated={cv.comment.updated}
390 {/* end of user row */}
391 {this.state.showEdit && (
395 onReplyCancel={this.handleReplyCancel}
396 disabled={this.props.locked}
397 finished={this.props.finished.get(
398 this.props.node.comment_view.comment.id
401 allLanguages={this.props.allLanguages}
402 siteLanguages={this.props.siteLanguages}
403 containerClass="comment-comment-container"
404 onUpsertComment={this.props.onEditComment}
407 {!this.state.showEdit && !this.state.collapsed && (
409 {this.state.viewSource ? (
410 <pre>{this.commentUnlessRemoved}</pre>
414 dangerouslySetInnerHTML={
415 this.props.hideImages
416 ? mdToHtmlNoImages(this.commentUnlessRemoved)
417 : mdToHtml(this.commentUnlessRemoved)
421 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
422 {this.props.showContext && this.linkBtn()}
423 {this.props.markable && (
425 className="btn btn-link btn-animate text-muted"
426 onClick={linkEvent(this, this.handleMarkAsRead)}
428 this.commentReplyOrMentionRead
429 ? i18n.t("mark_as_unread")
430 : i18n.t("mark_as_read")
433 this.commentReplyOrMentionRead
434 ? i18n.t("mark_as_unread")
435 : i18n.t("mark_as_read")
438 {this.state.readLoading ? (
443 classes={`icon-inline ${
444 this.commentReplyOrMentionRead && "text-success"
450 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
453 className={`btn btn-link btn-animate ${
454 this.commentView.my_vote === 1
458 onClick={linkEvent(this, this.handleUpvote)}
459 data-tippy-content={i18n.t("upvote")}
460 aria-label={i18n.t("upvote")}
461 aria-pressed={this.commentView.my_vote === 1}
463 {this.state.upvoteLoading ? (
467 <Icon icon="arrow-up1" classes="icon-inline" />
469 this.commentView.counts.upvotes !==
470 this.commentView.counts.score && (
471 <span className="ml-1">
472 {numToSI(this.commentView.counts.upvotes)}
478 {this.props.enableDownvotes && (
480 className={`btn btn-link btn-animate ${
481 this.commentView.my_vote === -1
485 onClick={linkEvent(this, this.handleDownvote)}
486 data-tippy-content={i18n.t("downvote")}
487 aria-label={i18n.t("downvote")}
488 aria-pressed={this.commentView.my_vote === -1}
490 {this.state.downvoteLoading ? (
494 <Icon icon="arrow-down1" classes="icon-inline" />
496 this.commentView.counts.upvotes !==
497 this.commentView.counts.score && (
498 <span className="ml-1">
499 {numToSI(this.commentView.counts.downvotes)}
507 className="btn btn-link btn-animate text-muted"
508 onClick={linkEvent(this, this.handleReplyClick)}
509 data-tippy-content={i18n.t("reply")}
510 aria-label={i18n.t("reply")}
512 <Icon icon="reply1" classes="icon-inline" />
514 {!this.state.showAdvanced ? (
516 className="btn btn-link btn-animate text-muted"
517 onClick={linkEvent(this, this.handleShowAdvanced)}
518 data-tippy-content={i18n.t("more")}
519 aria-label={i18n.t("more")}
521 <Icon icon="more-vertical" classes="icon-inline" />
525 {!this.myComment && (
528 className="btn btn-link btn-animate text-muted"
529 to={`/create_private_message/${cv.creator.id}`}
530 title={i18n.t("message").toLowerCase()}
535 className="btn btn-link btn-animate text-muted"
538 this.handleShowReportDialog
540 data-tippy-content={i18n.t(
543 aria-label={i18n.t("show_report_dialog")}
548 className="btn btn-link btn-animate text-muted"
551 this.handleBlockPerson
553 data-tippy-content={i18n.t("block_user")}
554 aria-label={i18n.t("block_user")}
556 {this.state.blockPersonLoading ? (
559 <Icon icon="slash" />
565 className="btn btn-link btn-animate text-muted"
566 onClick={linkEvent(this, this.handleSaveComment)}
568 cv.saved ? i18n.t("unsave") : i18n.t("save")
571 cv.saved ? i18n.t("unsave") : i18n.t("save")
574 {this.state.saveLoading ? (
579 classes={`icon-inline ${
580 cv.saved && "text-warning"
586 className="btn btn-link btn-animate text-muted"
587 onClick={linkEvent(this, this.handleViewSource)}
588 data-tippy-content={i18n.t("view_source")}
589 aria-label={i18n.t("view_source")}
593 classes={`icon-inline ${
594 this.state.viewSource && "text-success"
601 className="btn btn-link btn-animate text-muted"
602 onClick={linkEvent(this, this.handleEditClick)}
603 data-tippy-content={i18n.t("edit")}
604 aria-label={i18n.t("edit")}
606 <Icon icon="edit" classes="icon-inline" />
609 className="btn btn-link btn-animate text-muted"
612 this.handleDeleteComment
625 {this.state.deleteLoading ? (
630 classes={`icon-inline ${
631 cv.comment.deleted && "text-danger"
637 {(canModOnSelf || canAdminOnSelf) && (
639 className="btn btn-link btn-animate text-muted"
642 this.handleDistinguishComment
645 !cv.comment.distinguished
646 ? i18n.t("distinguish")
647 : i18n.t("undistinguish")
650 !cv.comment.distinguished
651 ? i18n.t("distinguish")
652 : i18n.t("undistinguish")
657 classes={`icon-inline ${
658 cv.comment.distinguished && "text-danger"
665 {/* Admins and mods can remove comments */}
666 {(canMod_ || canAdmin_) && (
668 {!cv.comment.removed ? (
670 className="btn btn-link btn-animate text-muted"
673 this.handleModRemoveShow
675 aria-label={i18n.t("remove")}
681 className="btn btn-link btn-animate text-muted"
684 this.handleRemoveComment
686 aria-label={i18n.t("restore")}
688 {this.state.removeLoading ? (
697 {/* Mods can ban from community, and appoint as mods to community */}
701 (!cv.creator_banned_from_community ? (
703 className="btn btn-link btn-animate text-muted"
706 this.handleModBanFromCommunityShow
708 aria-label={i18n.t("ban_from_community")}
710 {i18n.t("ban_from_community")}
714 className="btn btn-link btn-animate text-muted"
717 this.handleBanPersonFromCommunity
719 aria-label={i18n.t("unban")}
721 {this.state.banLoading ? (
728 {!cv.creator_banned_from_community &&
729 (!this.state.showConfirmAppointAsMod ? (
731 className="btn btn-link btn-animate text-muted"
734 this.handleShowConfirmAppointAsMod
738 ? i18n.t("remove_as_mod")
739 : i18n.t("appoint_as_mod")
743 ? i18n.t("remove_as_mod")
744 : i18n.t("appoint_as_mod")}
749 className="btn btn-link btn-animate text-muted"
750 aria-label={i18n.t("are_you_sure")}
752 {i18n.t("are_you_sure")}
755 className="btn btn-link btn-animate text-muted"
758 this.handleAddModToCommunity
760 aria-label={i18n.t("yes")}
762 {this.state.addModLoading ? (
769 className="btn btn-link btn-animate text-muted"
772 this.handleCancelConfirmAppointAsMod
774 aria-label={i18n.t("no")}
782 {/* Community creators and admins can transfer community to another mod */}
783 {(amCommunityCreator_ || canAdmin_) &&
786 (!this.state.showConfirmTransferCommunity ? (
788 className="btn btn-link btn-animate text-muted"
791 this.handleShowConfirmTransferCommunity
793 aria-label={i18n.t("transfer_community")}
795 {i18n.t("transfer_community")}
800 className="btn btn-link btn-animate text-muted"
801 aria-label={i18n.t("are_you_sure")}
803 {i18n.t("are_you_sure")}
806 className="btn btn-link btn-animate text-muted"
809 this.handleTransferCommunity
811 aria-label={i18n.t("yes")}
813 {this.state.transferCommunityLoading ? (
820 className="btn btn-link btn-animate text-muted"
824 .handleCancelShowConfirmTransferCommunity
826 aria-label={i18n.t("no")}
832 {/* Admins can ban from all, and appoint other admins */}
838 className="btn btn-link btn-animate text-muted"
841 this.handlePurgePersonShow
843 aria-label={i18n.t("purge_user")}
845 {i18n.t("purge_user")}
848 className="btn btn-link btn-animate text-muted"
851 this.handlePurgeCommentShow
853 aria-label={i18n.t("purge_comment")}
855 {i18n.t("purge_comment")}
858 {!isBanned(cv.creator) ? (
860 className="btn btn-link btn-animate text-muted"
863 this.handleModBanShow
865 aria-label={i18n.t("ban_from_site")}
867 {i18n.t("ban_from_site")}
871 className="btn btn-link btn-animate text-muted"
876 aria-label={i18n.t("unban_from_site")}
878 {this.state.banLoading ? (
881 i18n.t("unban_from_site")
887 {!isBanned(cv.creator) &&
889 (!this.state.showConfirmAppointAsAdmin ? (
891 className="btn btn-link btn-animate text-muted"
894 this.handleShowConfirmAppointAsAdmin
898 ? i18n.t("remove_as_admin")
899 : i18n.t("appoint_as_admin")
903 ? i18n.t("remove_as_admin")
904 : i18n.t("appoint_as_admin")}
908 <button className="btn btn-link btn-animate text-muted">
909 {i18n.t("are_you_sure")}
912 className="btn btn-link btn-animate text-muted"
917 aria-label={i18n.t("yes")}
919 {this.state.addAdminLoading ? (
926 className="btn btn-link btn-animate text-muted"
929 this.handleCancelConfirmAppointAsAdmin
931 aria-label={i18n.t("no")}
944 {/* end of button group */}
949 {showMoreChildren && (
951 className={classNames("details ml-1 comment-node py-2", {
952 "border-top border-light": !this.props.noBorder,
954 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
957 className="btn btn-link text-muted"
958 onClick={linkEvent(this, this.handleFetchChildren)}
960 {this.state.fetchChildrenLoading ? (
964 {i18n.t("x_more_replies", {
965 count: node.comment_view.counts.child_count,
966 formattedCount: numToSI(
967 node.comment_view.counts.child_count
976 {/* end of details */}
977 {this.state.showRemoveDialog && (
979 className="form-inline"
980 onSubmit={linkEvent(this, this.handleRemoveComment)}
984 htmlFor={`mod-remove-reason-${cv.comment.id}`}
990 id={`mod-remove-reason-${cv.comment.id}`}
991 className="form-control mr-2"
992 placeholder={i18n.t("reason")}
993 value={this.state.removeReason}
994 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
998 className="btn btn-secondary"
999 aria-label={i18n.t("remove_comment")}
1001 {i18n.t("remove_comment")}
1005 {this.state.showReportDialog && (
1007 className="form-inline"
1008 onSubmit={linkEvent(this, this.handleReportComment)}
1012 htmlFor={`report-reason-${cv.comment.id}`}
1019 id={`report-reason-${cv.comment.id}`}
1020 className="form-control mr-2"
1021 placeholder={i18n.t("reason")}
1022 value={this.state.reportReason}
1023 onInput={linkEvent(this, this.handleReportReasonChange)}
1027 className="btn btn-secondary"
1028 aria-label={i18n.t("create_report")}
1030 {i18n.t("create_report")}
1034 {this.state.showBanDialog && (
1035 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1036 <div className="form-group row col-12">
1038 className="col-form-label"
1039 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1045 id={`mod-ban-reason-${cv.comment.id}`}
1046 className="form-control mr-2"
1047 placeholder={i18n.t("reason")}
1048 value={this.state.banReason}
1049 onInput={linkEvent(this, this.handleModBanReasonChange)}
1052 className="col-form-label"
1053 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1059 id={`mod-ban-expires-${cv.comment.id}`}
1060 className="form-control mr-2"
1061 placeholder={i18n.t("number_of_days")}
1062 value={this.state.banExpireDays}
1063 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1065 <div className="form-group">
1066 <div className="form-check">
1068 className="form-check-input"
1069 id="mod-ban-remove-data"
1071 checked={this.state.removeData}
1072 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1075 className="form-check-label"
1076 htmlFor="mod-ban-remove-data"
1077 title={i18n.t("remove_content_more")}
1079 {i18n.t("remove_content")}
1084 {/* TODO hold off on expires until later */}
1085 {/* <div class="form-group row"> */}
1086 {/* <label class="col-form-label">Expires</label> */}
1087 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1089 <div className="form-group row">
1092 className="btn btn-secondary"
1093 aria-label={i18n.t("ban")}
1095 {this.state.banLoading ? (
1099 {i18n.t("ban")} {cv.creator.name}
1107 {this.state.showPurgeDialog && (
1108 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1110 <label className="sr-only" htmlFor="purge-reason">
1116 className="form-control my-3"
1117 placeholder={i18n.t("reason")}
1118 value={this.state.purgeReason}
1119 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1121 <div className="form-group row col-12">
1122 {this.state.purgeLoading ? (
1127 className="btn btn-secondary"
1128 aria-label={purgeTypeText}
1136 {this.state.showReply && (
1139 onReplyCancel={this.handleReplyCancel}
1140 disabled={this.props.locked}
1141 finished={this.props.finished.get(
1142 this.props.node.comment_view.comment.id
1145 allLanguages={this.props.allLanguages}
1146 siteLanguages={this.props.siteLanguages}
1147 containerClass="comment-comment-container"
1148 onUpsertComment={this.props.onCreateComment}
1151 {!this.state.collapsed && node.children.length > 0 && (
1153 nodes={node.children}
1154 locked={this.props.locked}
1155 moderators={this.props.moderators}
1156 admins={this.props.admins}
1157 enableDownvotes={this.props.enableDownvotes}
1158 viewType={this.props.viewType}
1159 allLanguages={this.props.allLanguages}
1160 siteLanguages={this.props.siteLanguages}
1161 hideImages={this.props.hideImages}
1162 isChild={!this.props.noIndent}
1163 depth={this.props.node.depth + 1}
1164 finished={this.props.finished}
1165 onCommentReplyRead={this.props.onCommentReplyRead}
1166 onPersonMentionRead={this.props.onPersonMentionRead}
1167 onCreateComment={this.props.onCreateComment}
1168 onEditComment={this.props.onEditComment}
1169 onCommentVote={this.props.onCommentVote}
1170 onBlockPerson={this.props.onBlockPerson}
1171 onSaveComment={this.props.onSaveComment}
1172 onDeleteComment={this.props.onDeleteComment}
1173 onRemoveComment={this.props.onRemoveComment}
1174 onDistinguishComment={this.props.onDistinguishComment}
1175 onAddModToCommunity={this.props.onAddModToCommunity}
1176 onAddAdmin={this.props.onAddAdmin}
1177 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1178 onBanPerson={this.props.onBanPerson}
1179 onTransferCommunity={this.props.onTransferCommunity}
1180 onFetchChildren={this.props.onFetchChildren}
1181 onCommentReport={this.props.onCommentReport}
1182 onPurgePerson={this.props.onPurgePerson}
1183 onPurgeComment={this.props.onPurgeComment}
1186 {/* A collapsed clearfix */}
1187 {this.state.collapsed && <div className="row col-12" />}
1192 get commentReplyOrMentionRead(): boolean {
1193 const cv = this.commentView;
1195 if (this.isPersonMentionType(cv)) {
1196 return cv.person_mention.read;
1197 } else if (this.isCommentReplyType(cv)) {
1198 return cv.comment_reply.read;
1204 linkBtn(small = false) {
1205 const cv = this.commentView;
1207 const classnames = classNames("btn btn-link btn-animate text-muted", {
1211 const title = this.props.showContext
1212 ? i18n.t("show_context")
1215 // The context button should show the parent comment by default
1216 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1221 className={classnames}
1222 to={`/comment/${parentCommentId}`}
1225 <Icon icon="link" classes="icon-inline" />
1228 <a className={classnames} title={title} href={cv.comment.ap_id}>
1229 <Icon icon="fedilink" classes="icon-inline" />
1236 get myComment(): boolean {
1238 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1239 this.commentView.creator.id
1243 get isPostCreator(): boolean {
1244 return this.commentView.creator.id == this.commentView.post.creator_id;
1248 if (this.commentView.my_vote == 1) {
1250 } else if (this.commentView.my_vote == -1) {
1251 return "text-danger";
1253 return "text-muted";
1257 get pointsTippy(): string {
1258 const points = i18n.t("number_of_points", {
1259 count: Number(this.commentView.counts.score),
1260 formattedCount: numToSI(this.commentView.counts.score),
1263 const upvotes = i18n.t("number_of_upvotes", {
1264 count: Number(this.commentView.counts.upvotes),
1265 formattedCount: numToSI(this.commentView.counts.upvotes),
1268 const downvotes = i18n.t("number_of_downvotes", {
1269 count: Number(this.commentView.counts.downvotes),
1270 formattedCount: numToSI(this.commentView.counts.downvotes),
1273 return `${points} • ${upvotes} • ${downvotes}`;
1276 get expandText(): string {
1277 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
1280 get commentUnlessRemoved(): string {
1281 const comment = this.commentView.comment;
1282 return comment.removed
1283 ? `*${i18n.t("removed")}*`
1285 ? `*${i18n.t("deleted")}*`
1289 handleReplyClick(i: CommentNode) {
1290 i.setState({ showReply: true });
1293 handleEditClick(i: CommentNode) {
1294 i.setState({ showEdit: true });
1297 handleReplyCancel() {
1298 this.setState({ showReply: false, showEdit: false });
1301 handleShowReportDialog(i: CommentNode) {
1302 i.setState({ showReportDialog: !i.state.showReportDialog });
1305 handleReportReasonChange(i: CommentNode, event: any) {
1306 i.setState({ reportReason: event.target.value });
1309 handleModRemoveShow(i: CommentNode) {
1311 showRemoveDialog: !i.state.showRemoveDialog,
1312 showBanDialog: false,
1316 handleModRemoveReasonChange(i: CommentNode, event: any) {
1317 i.setState({ removeReason: event.target.value });
1320 handleModRemoveDataChange(i: CommentNode, event: any) {
1321 i.setState({ removeData: event.target.checked });
1324 isPersonMentionType(
1325 item: CommentView | PersonMentionView | CommentReplyView
1326 ): item is PersonMentionView {
1327 return (item as PersonMentionView).person_mention?.id !== undefined;
1331 item: CommentView | PersonMentionView | CommentReplyView
1332 ): item is CommentReplyView {
1333 return (item as CommentReplyView).comment_reply?.id !== undefined;
1336 handleModBanFromCommunityShow(i: CommentNode) {
1338 showBanDialog: true,
1339 banType: BanType.Community,
1340 showRemoveDialog: false,
1344 handleModBanShow(i: CommentNode) {
1346 showBanDialog: true,
1347 banType: BanType.Site,
1348 showRemoveDialog: false,
1352 handleModBanReasonChange(i: CommentNode, event: any) {
1353 i.setState({ banReason: event.target.value });
1356 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1357 i.setState({ banExpireDays: event.target.value });
1360 handlePurgePersonShow(i: CommentNode) {
1362 showPurgeDialog: true,
1363 purgeType: PurgeType.Person,
1364 showRemoveDialog: false,
1368 handlePurgeCommentShow(i: CommentNode) {
1370 showPurgeDialog: true,
1371 purgeType: PurgeType.Comment,
1372 showRemoveDialog: false,
1376 handlePurgeReasonChange(i: CommentNode, event: any) {
1377 i.setState({ purgeReason: event.target.value });
1380 handleShowConfirmAppointAsMod(i: CommentNode) {
1381 i.setState({ showConfirmAppointAsMod: true });
1384 handleCancelConfirmAppointAsMod(i: CommentNode) {
1385 i.setState({ showConfirmAppointAsMod: false });
1388 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1389 i.setState({ showConfirmAppointAsAdmin: true });
1392 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1393 i.setState({ showConfirmAppointAsAdmin: false });
1396 handleShowConfirmTransferCommunity(i: CommentNode) {
1397 i.setState({ showConfirmTransferCommunity: true });
1400 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1401 i.setState({ showConfirmTransferCommunity: false });
1404 handleShowConfirmTransferSite(i: CommentNode) {
1405 i.setState({ showConfirmTransferSite: true });
1408 handleCancelShowConfirmTransferSite(i: CommentNode) {
1409 i.setState({ showConfirmTransferSite: false });
1412 get isCommentNew(): boolean {
1413 const now = moment.utc().subtract(10, "minutes");
1414 const then = moment.utc(this.commentView.comment.published);
1415 return now.isBefore(then);
1418 handleCommentCollapse(i: CommentNode) {
1419 i.setState({ collapsed: !i.state.collapsed });
1423 handleViewSource(i: CommentNode) {
1424 i.setState({ viewSource: !i.state.viewSource });
1427 handleShowAdvanced(i: CommentNode) {
1428 i.setState({ showAdvanced: !i.state.showAdvanced });
1432 handleSaveComment(i: CommentNode) {
1433 i.setState({ saveLoading: true });
1435 i.props.onSaveComment({
1436 comment_id: i.commentView.comment.id,
1437 save: !i.commentView.saved,
1438 auth: myAuthRequired(),
1442 handleUpvote(i: CommentNode) {
1443 i.setState({ upvoteLoading: true });
1444 i.props.onCommentVote({
1445 comment_id: i.commentId,
1446 score: newVote(VoteType.Upvote, i.commentView.my_vote),
1447 auth: myAuthRequired(),
1451 handleDownvote(i: CommentNode) {
1452 i.setState({ downvoteLoading: true });
1453 i.props.onCommentVote({
1454 comment_id: i.commentId,
1455 score: newVote(VoteType.Downvote, i.commentView.my_vote),
1456 auth: myAuthRequired(),
1460 handleBlockPerson(i: CommentNode) {
1461 i.setState({ blockPersonLoading: true });
1462 i.props.onBlockPerson({
1463 person_id: i.commentView.creator.id,
1465 auth: myAuthRequired(),
1469 handleMarkAsRead(i: CommentNode) {
1470 i.setState({ readLoading: true });
1471 const cv = i.commentView;
1472 if (i.isPersonMentionType(cv)) {
1473 i.props.onPersonMentionRead({
1474 person_mention_id: cv.person_mention.id,
1475 read: !cv.person_mention.read,
1476 auth: myAuthRequired(),
1478 } else if (i.isCommentReplyType(cv)) {
1479 i.props.onCommentReplyRead({
1480 comment_reply_id: cv.comment_reply.id,
1481 read: !cv.comment_reply.read,
1482 auth: myAuthRequired(),
1487 handleDeleteComment(i: CommentNode) {
1488 i.setState({ deleteLoading: true });
1489 i.props.onDeleteComment({
1490 comment_id: i.commentId,
1491 deleted: !i.commentView.comment.deleted,
1492 auth: myAuthRequired(),
1496 handleRemoveComment(i: CommentNode, event: any) {
1497 event.preventDefault();
1498 i.setState({ removeLoading: true });
1499 i.props.onRemoveComment({
1500 comment_id: i.commentId,
1501 removed: !i.commentView.comment.removed,
1502 auth: myAuthRequired(),
1506 handleDistinguishComment(i: CommentNode) {
1507 i.setState({ distinguishLoading: true });
1508 i.props.onDistinguishComment({
1509 comment_id: i.commentId,
1510 distinguished: !i.commentView.comment.distinguished,
1511 auth: myAuthRequired(),
1515 handleBanPersonFromCommunity(i: CommentNode) {
1516 i.setState({ banLoading: true });
1517 i.props.onBanPersonFromCommunity({
1518 community_id: i.commentView.community.id,
1519 person_id: i.commentView.creator.id,
1520 ban: !i.commentView.creator_banned_from_community,
1521 reason: i.state.banReason,
1522 remove_data: i.state.removeData,
1523 expires: futureDaysToUnixTime(i.state.banExpireDays),
1524 auth: myAuthRequired(),
1528 handleBanPerson(i: CommentNode) {
1529 i.setState({ banLoading: true });
1530 i.props.onBanPerson({
1531 person_id: i.commentView.creator.id,
1532 ban: !i.commentView.creator_banned_from_community,
1533 reason: i.state.banReason,
1534 remove_data: i.state.removeData,
1535 expires: futureDaysToUnixTime(i.state.banExpireDays),
1536 auth: myAuthRequired(),
1540 handleModBanBothSubmit(i: CommentNode, event: any) {
1541 event.preventDefault();
1542 if (i.state.banType == BanType.Community) {
1543 i.handleBanPersonFromCommunity(i);
1545 i.handleBanPerson(i);
1549 handleAddModToCommunity(i: CommentNode) {
1550 i.setState({ addModLoading: true });
1552 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1553 i.props.onAddModToCommunity({
1554 community_id: i.commentView.community.id,
1555 person_id: i.commentView.creator.id,
1557 auth: myAuthRequired(),
1561 handleAddAdmin(i: CommentNode) {
1562 i.setState({ addAdminLoading: true });
1564 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1565 i.props.onAddAdmin({
1566 person_id: i.commentView.creator.id,
1568 auth: myAuthRequired(),
1572 handleTransferCommunity(i: CommentNode) {
1573 i.setState({ transferCommunityLoading: true });
1574 i.props.onTransferCommunity({
1575 community_id: i.commentView.community.id,
1576 person_id: i.commentView.creator.id,
1577 auth: myAuthRequired(),
1581 handleReportComment(i: CommentNode, event: any) {
1582 event.preventDefault();
1583 i.setState({ reportLoading: true });
1584 i.props.onCommentReport({
1585 comment_id: i.commentId,
1586 reason: i.state.reportReason ?? "",
1587 auth: myAuthRequired(),
1591 handlePurgeBothSubmit(i: CommentNode, event: any) {
1592 event.preventDefault();
1593 i.setState({ purgeLoading: true });
1595 if (i.state.purgeType == PurgeType.Person) {
1596 i.props.onPurgePerson({
1597 person_id: i.commentView.creator.id,
1598 reason: i.state.purgeReason,
1599 auth: myAuthRequired(),
1602 i.props.onPurgeComment({
1603 comment_id: i.commentId,
1604 reason: i.state.purgeReason,
1605 auth: myAuthRequired(),
1610 handleFetchChildren(i: CommentNode) {
1611 i.setState({ fetchChildrenLoading: true });
1612 i.props.onFetchChildren?.({
1613 parent_id: i.commentId,
1614 max_depth: commentTreeMaxDepth,