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 borderColor = this.props.node.depth
274 ? colorList[(this.props.node.depth - 1) % colorList.length]
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;
288 className={`comment ${
289 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
293 id={`comment-${cv.comment.id}`}
294 className={classNames(`details comment-node py-2`, {
295 "border-top border-light": !this.props.noBorder,
296 mark: this.isCommentNew || this.commentView.comment.distinguished,
299 !this.props.noIndent && this.props.node.depth
300 ? `border-left: 2px ${borderColor} solid !important`
305 className={classNames({
306 "ml-2": !this.props.noIndent && this.props.node.depth,
309 <div className="d-flex flex-wrap align-items-center text-muted small">
311 className="btn btn-sm text-muted mr-2"
312 onClick={linkEvent(this, this.handleCommentCollapse)}
313 aria-label={this.expandText}
314 data-tippy-content={this.expandText}
317 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
318 classes="icon-inline"
321 <span className="mr-2">
322 <PersonListing person={cv.creator} />
324 {cv.comment.distinguished && (
325 <Icon icon="shield" inline classes={`text-danger mr-2`} />
327 {this.isPostCreator && (
328 <div className="badge badge-light d-none d-sm-inline mr-2">
333 <div className="badge d-none d-sm-inline mr-2">
338 <div className="badge d-none d-sm-inline mr-2">
342 {cv.creator.bot_account && (
343 <div className="badge d-none d-sm-inline mr-2">
344 {i18n.t("bot_account").toLowerCase()}
347 {this.props.showCommunity && (
349 <span className="mx-1">{i18n.t("to")}</span>
350 <CommunityLink community={cv.community} />
351 <span className="mx-2">•</span>
352 <Link className="mr-2" to={`/post/${cv.post.id}`}>
358 {cv.comment.language_id !== 0 && (
359 <span className="badge d-none d-sm-inline mr-2">
361 this.props.allLanguages.find(
362 lang => lang.id === cv.comment.language_id
367 {/* This is an expanding spacer for mobile */}
368 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
372 className={`unselectable pointer ${this.scoreColor}`}
373 onClick={linkEvent(this, this.handleUpvote)}
374 data-tippy-content={this.pointsTippy}
376 {this.state.upvoteLoading ? (
380 className="mr-1 font-weight-bold"
381 aria-label={i18n.t("number_of_points", {
382 count: Number(this.commentView.counts.score),
383 formattedCount: numToSI(
384 this.commentView.counts.score
388 {numToSI(this.commentView.counts.score)}
392 <span className="mr-1">•</span>
397 published={cv.comment.published}
398 updated={cv.comment.updated}
402 {/* end of user row */}
403 {this.state.showEdit && (
407 onReplyCancel={this.handleReplyCancel}
408 disabled={this.props.locked}
409 finished={this.props.finished.get(
410 this.props.node.comment_view.comment.id
413 allLanguages={this.props.allLanguages}
414 siteLanguages={this.props.siteLanguages}
415 onUpsertComment={this.props.onEditComment}
418 {!this.state.showEdit && !this.state.collapsed && (
420 {this.state.viewSource ? (
421 <pre>{this.commentUnlessRemoved}</pre>
425 dangerouslySetInnerHTML={
426 this.props.hideImages
427 ? mdToHtmlNoImages(this.commentUnlessRemoved)
428 : mdToHtml(this.commentUnlessRemoved)
432 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
433 {this.props.showContext && this.linkBtn()}
434 {this.props.markable && (
436 className="btn btn-link btn-animate text-muted"
437 onClick={linkEvent(this, this.handleMarkAsRead)}
439 this.commentReplyOrMentionRead
440 ? i18n.t("mark_as_unread")
441 : i18n.t("mark_as_read")
444 this.commentReplyOrMentionRead
445 ? i18n.t("mark_as_unread")
446 : i18n.t("mark_as_read")
449 {this.state.readLoading ? (
454 classes={`icon-inline ${
455 this.commentReplyOrMentionRead && "text-success"
461 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
464 className={`btn btn-link btn-animate ${
465 this.commentView.my_vote === 1
469 onClick={linkEvent(this, this.handleUpvote)}
470 data-tippy-content={i18n.t("upvote")}
471 aria-label={i18n.t("upvote")}
472 aria-pressed={this.commentView.my_vote === 1}
474 {this.state.upvoteLoading ? (
478 <Icon icon="arrow-up1" classes="icon-inline" />
480 this.commentView.counts.upvotes !==
481 this.commentView.counts.score && (
482 <span className="ml-1">
483 {numToSI(this.commentView.counts.upvotes)}
489 {this.props.enableDownvotes && (
491 className={`btn btn-link btn-animate ${
492 this.commentView.my_vote === -1
496 onClick={linkEvent(this, this.handleDownvote)}
497 data-tippy-content={i18n.t("downvote")}
498 aria-label={i18n.t("downvote")}
499 aria-pressed={this.commentView.my_vote === -1}
501 {this.state.downvoteLoading ? (
505 <Icon icon="arrow-down1" classes="icon-inline" />
507 this.commentView.counts.upvotes !==
508 this.commentView.counts.score && (
509 <span className="ml-1">
510 {numToSI(this.commentView.counts.downvotes)}
518 className="btn btn-link btn-animate text-muted"
519 onClick={linkEvent(this, this.handleReplyClick)}
520 data-tippy-content={i18n.t("reply")}
521 aria-label={i18n.t("reply")}
523 <Icon icon="reply1" classes="icon-inline" />
525 {!this.state.showAdvanced ? (
527 className="btn btn-link btn-animate text-muted"
528 onClick={linkEvent(this, this.handleShowAdvanced)}
529 data-tippy-content={i18n.t("more")}
530 aria-label={i18n.t("more")}
532 <Icon icon="more-vertical" classes="icon-inline" />
536 {!this.myComment && (
539 className="btn btn-link btn-animate text-muted"
540 to={`/create_private_message/${cv.creator.id}`}
541 title={i18n.t("message").toLowerCase()}
546 className="btn btn-link btn-animate text-muted"
549 this.handleShowReportDialog
551 data-tippy-content={i18n.t(
554 aria-label={i18n.t("show_report_dialog")}
559 className="btn btn-link btn-animate text-muted"
562 this.handleBlockPerson
564 data-tippy-content={i18n.t("block_user")}
565 aria-label={i18n.t("block_user")}
567 {this.state.blockPersonLoading ? (
570 <Icon icon="slash" />
576 className="btn btn-link btn-animate text-muted"
577 onClick={linkEvent(this, this.handleSaveComment)}
579 cv.saved ? i18n.t("unsave") : i18n.t("save")
582 cv.saved ? i18n.t("unsave") : i18n.t("save")
585 {this.state.saveLoading ? (
590 classes={`icon-inline ${
591 cv.saved && "text-warning"
597 className="btn btn-link btn-animate text-muted"
598 onClick={linkEvent(this, this.handleViewSource)}
599 data-tippy-content={i18n.t("view_source")}
600 aria-label={i18n.t("view_source")}
604 classes={`icon-inline ${
605 this.state.viewSource && "text-success"
612 className="btn btn-link btn-animate text-muted"
613 onClick={linkEvent(this, this.handleEditClick)}
614 data-tippy-content={i18n.t("edit")}
615 aria-label={i18n.t("edit")}
617 <Icon icon="edit" classes="icon-inline" />
620 className="btn btn-link btn-animate text-muted"
623 this.handleDeleteComment
636 {this.state.deleteLoading ? (
641 classes={`icon-inline ${
642 cv.comment.deleted && "text-danger"
648 {(canModOnSelf || canAdminOnSelf) && (
650 className="btn btn-link btn-animate text-muted"
653 this.handleDistinguishComment
656 !cv.comment.distinguished
657 ? i18n.t("distinguish")
658 : i18n.t("undistinguish")
661 !cv.comment.distinguished
662 ? i18n.t("distinguish")
663 : i18n.t("undistinguish")
668 classes={`icon-inline ${
669 cv.comment.distinguished && "text-danger"
676 {/* Admins and mods can remove comments */}
677 {(canMod_ || canAdmin_) && (
679 {!cv.comment.removed ? (
681 className="btn btn-link btn-animate text-muted"
684 this.handleModRemoveShow
686 aria-label={i18n.t("remove")}
692 className="btn btn-link btn-animate text-muted"
695 this.handleRemoveComment
697 aria-label={i18n.t("restore")}
699 {this.state.removeLoading ? (
708 {/* Mods can ban from community, and appoint as mods to community */}
712 (!cv.creator_banned_from_community ? (
714 className="btn btn-link btn-animate text-muted"
717 this.handleModBanFromCommunityShow
719 aria-label={i18n.t("ban_from_community")}
721 {i18n.t("ban_from_community")}
725 className="btn btn-link btn-animate text-muted"
728 this.handleBanPersonFromCommunity
730 aria-label={i18n.t("unban")}
732 {this.state.banLoading ? (
739 {!cv.creator_banned_from_community &&
740 (!this.state.showConfirmAppointAsMod ? (
742 className="btn btn-link btn-animate text-muted"
745 this.handleShowConfirmAppointAsMod
749 ? i18n.t("remove_as_mod")
750 : i18n.t("appoint_as_mod")
754 ? i18n.t("remove_as_mod")
755 : i18n.t("appoint_as_mod")}
760 className="btn btn-link btn-animate text-muted"
761 aria-label={i18n.t("are_you_sure")}
763 {i18n.t("are_you_sure")}
766 className="btn btn-link btn-animate text-muted"
769 this.handleAddModToCommunity
771 aria-label={i18n.t("yes")}
773 {this.state.addModLoading ? (
780 className="btn btn-link btn-animate text-muted"
783 this.handleCancelConfirmAppointAsMod
785 aria-label={i18n.t("no")}
793 {/* Community creators and admins can transfer community to another mod */}
794 {(amCommunityCreator_ || canAdmin_) &&
797 (!this.state.showConfirmTransferCommunity ? (
799 className="btn btn-link btn-animate text-muted"
802 this.handleShowConfirmTransferCommunity
804 aria-label={i18n.t("transfer_community")}
806 {i18n.t("transfer_community")}
811 className="btn btn-link btn-animate text-muted"
812 aria-label={i18n.t("are_you_sure")}
814 {i18n.t("are_you_sure")}
817 className="btn btn-link btn-animate text-muted"
820 this.handleTransferCommunity
822 aria-label={i18n.t("yes")}
824 {this.state.transferCommunityLoading ? (
831 className="btn btn-link btn-animate text-muted"
835 .handleCancelShowConfirmTransferCommunity
837 aria-label={i18n.t("no")}
843 {/* Admins can ban from all, and appoint other admins */}
849 className="btn btn-link btn-animate text-muted"
852 this.handlePurgePersonShow
854 aria-label={i18n.t("purge_user")}
856 {i18n.t("purge_user")}
859 className="btn btn-link btn-animate text-muted"
862 this.handlePurgeCommentShow
864 aria-label={i18n.t("purge_comment")}
866 {i18n.t("purge_comment")}
869 {!isBanned(cv.creator) ? (
871 className="btn btn-link btn-animate text-muted"
874 this.handleModBanShow
876 aria-label={i18n.t("ban_from_site")}
878 {i18n.t("ban_from_site")}
882 className="btn btn-link btn-animate text-muted"
887 aria-label={i18n.t("unban_from_site")}
889 {this.state.banLoading ? (
892 i18n.t("unban_from_site")
898 {!isBanned(cv.creator) &&
900 (!this.state.showConfirmAppointAsAdmin ? (
902 className="btn btn-link btn-animate text-muted"
905 this.handleShowConfirmAppointAsAdmin
909 ? i18n.t("remove_as_admin")
910 : i18n.t("appoint_as_admin")
914 ? i18n.t("remove_as_admin")
915 : i18n.t("appoint_as_admin")}
919 <button className="btn btn-link btn-animate text-muted">
920 {i18n.t("are_you_sure")}
923 className="btn btn-link btn-animate text-muted"
928 aria-label={i18n.t("yes")}
930 {this.state.addAdminLoading ? (
937 className="btn btn-link btn-animate text-muted"
940 this.handleCancelConfirmAppointAsAdmin
942 aria-label={i18n.t("no")}
955 {/* end of button group */}
960 {showMoreChildren && (
962 className={`details ml-1 comment-node py-2 ${
963 !this.props.noBorder ? "border-top border-light" : ""
965 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
968 className="btn btn-link text-muted"
969 onClick={linkEvent(this, this.handleFetchChildren)}
971 {this.state.fetchChildrenLoading ? (
975 {i18n.t("x_more_replies", {
976 count: node.comment_view.counts.child_count,
977 formattedCount: numToSI(
978 node.comment_view.counts.child_count
987 {/* end of details */}
988 {this.state.showRemoveDialog && (
990 className="form-inline"
991 onSubmit={linkEvent(this, this.handleRemoveComment)}
995 htmlFor={`mod-remove-reason-${cv.comment.id}`}
1001 id={`mod-remove-reason-${cv.comment.id}`}
1002 className="form-control mr-2"
1003 placeholder={i18n.t("reason")}
1004 value={this.state.removeReason}
1005 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1009 className="btn btn-secondary"
1010 aria-label={i18n.t("remove_comment")}
1012 {i18n.t("remove_comment")}
1016 {this.state.showReportDialog && (
1018 className="form-inline"
1019 onSubmit={linkEvent(this, this.handleReportComment)}
1023 htmlFor={`report-reason-${cv.comment.id}`}
1030 id={`report-reason-${cv.comment.id}`}
1031 className="form-control mr-2"
1032 placeholder={i18n.t("reason")}
1033 value={this.state.reportReason}
1034 onInput={linkEvent(this, this.handleReportReasonChange)}
1038 className="btn btn-secondary"
1039 aria-label={i18n.t("create_report")}
1041 {i18n.t("create_report")}
1045 {this.state.showBanDialog && (
1046 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1047 <div className="form-group row col-12">
1049 className="col-form-label"
1050 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1056 id={`mod-ban-reason-${cv.comment.id}`}
1057 className="form-control mr-2"
1058 placeholder={i18n.t("reason")}
1059 value={this.state.banReason}
1060 onInput={linkEvent(this, this.handleModBanReasonChange)}
1063 className="col-form-label"
1064 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1070 id={`mod-ban-expires-${cv.comment.id}`}
1071 className="form-control mr-2"
1072 placeholder={i18n.t("number_of_days")}
1073 value={this.state.banExpireDays}
1074 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1076 <div className="form-group">
1077 <div className="form-check">
1079 className="form-check-input"
1080 id="mod-ban-remove-data"
1082 checked={this.state.removeData}
1083 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1086 className="form-check-label"
1087 htmlFor="mod-ban-remove-data"
1088 title={i18n.t("remove_content_more")}
1090 {i18n.t("remove_content")}
1095 {/* TODO hold off on expires until later */}
1096 {/* <div class="form-group row"> */}
1097 {/* <label class="col-form-label">Expires</label> */}
1098 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1100 <div className="form-group row">
1103 className="btn btn-secondary"
1104 aria-label={i18n.t("ban")}
1106 {this.state.banLoading ? (
1110 {i18n.t("ban")} {cv.creator.name}
1118 {this.state.showPurgeDialog && (
1119 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1121 <label className="sr-only" htmlFor="purge-reason">
1127 className="form-control my-3"
1128 placeholder={i18n.t("reason")}
1129 value={this.state.purgeReason}
1130 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1132 <div className="form-group row col-12">
1133 {this.state.purgeLoading ? (
1138 className="btn btn-secondary"
1139 aria-label={purgeTypeText}
1147 {this.state.showReply && (
1150 onReplyCancel={this.handleReplyCancel}
1151 disabled={this.props.locked}
1152 finished={this.props.finished.get(
1153 this.props.node.comment_view.comment.id
1156 allLanguages={this.props.allLanguages}
1157 siteLanguages={this.props.siteLanguages}
1158 onUpsertComment={this.props.onCreateComment}
1161 {!this.state.collapsed && node.children.length > 0 && (
1163 nodes={node.children}
1164 locked={this.props.locked}
1165 moderators={this.props.moderators}
1166 admins={this.props.admins}
1167 enableDownvotes={this.props.enableDownvotes}
1168 viewType={this.props.viewType}
1169 allLanguages={this.props.allLanguages}
1170 siteLanguages={this.props.siteLanguages}
1171 hideImages={this.props.hideImages}
1172 finished={this.props.finished}
1173 onCommentReplyRead={this.props.onCommentReplyRead}
1174 onPersonMentionRead={this.props.onPersonMentionRead}
1175 onCreateComment={this.props.onCreateComment}
1176 onEditComment={this.props.onEditComment}
1177 onCommentVote={this.props.onCommentVote}
1178 onBlockPerson={this.props.onBlockPerson}
1179 onSaveComment={this.props.onSaveComment}
1180 onDeleteComment={this.props.onDeleteComment}
1181 onRemoveComment={this.props.onRemoveComment}
1182 onDistinguishComment={this.props.onDistinguishComment}
1183 onAddModToCommunity={this.props.onAddModToCommunity}
1184 onAddAdmin={this.props.onAddAdmin}
1185 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1186 onBanPerson={this.props.onBanPerson}
1187 onTransferCommunity={this.props.onTransferCommunity}
1188 onFetchChildren={this.props.onFetchChildren}
1189 onCommentReport={this.props.onCommentReport}
1190 onPurgePerson={this.props.onPurgePerson}
1191 onPurgeComment={this.props.onPurgeComment}
1194 {/* A collapsed clearfix */}
1195 {this.state.collapsed && <div className="row col-12"></div>}
1200 get commentReplyOrMentionRead(): boolean {
1201 const cv = this.commentView;
1203 if (this.isPersonMentionType(cv)) {
1204 return cv.person_mention.read;
1205 } else if (this.isCommentReplyType(cv)) {
1206 return cv.comment_reply.read;
1212 linkBtn(small = false) {
1213 const cv = this.commentView;
1214 const classnames = classNames("btn btn-link btn-animate text-muted", {
1218 const title = this.props.showContext
1219 ? i18n.t("show_context")
1222 // The context button should show the parent comment by default
1223 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1228 className={classnames}
1229 to={`/comment/${parentCommentId}`}
1232 <Icon icon="link" classes="icon-inline" />
1235 <a className={classnames} title={title} href={cv.comment.ap_id}>
1236 <Icon icon="fedilink" classes="icon-inline" />
1243 get myComment(): boolean {
1245 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1246 this.commentView.creator.id
1250 get isPostCreator(): boolean {
1251 return this.commentView.creator.id == this.commentView.post.creator_id;
1255 if (this.commentView.my_vote == 1) {
1257 } else if (this.commentView.my_vote == -1) {
1258 return "text-danger";
1260 return "text-muted";
1264 get pointsTippy(): string {
1265 const points = i18n.t("number_of_points", {
1266 count: Number(this.commentView.counts.score),
1267 formattedCount: numToSI(this.commentView.counts.score),
1270 const upvotes = i18n.t("number_of_upvotes", {
1271 count: Number(this.commentView.counts.upvotes),
1272 formattedCount: numToSI(this.commentView.counts.upvotes),
1275 const downvotes = i18n.t("number_of_downvotes", {
1276 count: Number(this.commentView.counts.downvotes),
1277 formattedCount: numToSI(this.commentView.counts.downvotes),
1280 return `${points} • ${upvotes} • ${downvotes}`;
1283 get expandText(): string {
1284 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
1287 get commentUnlessRemoved(): string {
1288 const comment = this.commentView.comment;
1289 return comment.removed
1290 ? `*${i18n.t("removed")}*`
1292 ? `*${i18n.t("deleted")}*`
1296 handleReplyClick(i: CommentNode) {
1297 i.setState({ showReply: true });
1300 handleEditClick(i: CommentNode) {
1301 i.setState({ showEdit: true });
1304 handleReplyCancel() {
1305 this.setState({ showReply: false, showEdit: false });
1308 handleShowReportDialog(i: CommentNode) {
1309 i.setState({ showReportDialog: !i.state.showReportDialog });
1312 handleReportReasonChange(i: CommentNode, event: any) {
1313 i.setState({ reportReason: event.target.value });
1316 handleModRemoveShow(i: CommentNode) {
1318 showRemoveDialog: !i.state.showRemoveDialog,
1319 showBanDialog: false,
1323 handleModRemoveReasonChange(i: CommentNode, event: any) {
1324 i.setState({ removeReason: event.target.value });
1327 handleModRemoveDataChange(i: CommentNode, event: any) {
1328 i.setState({ removeData: event.target.checked });
1331 isPersonMentionType(
1332 item: CommentView | PersonMentionView | CommentReplyView
1333 ): item is PersonMentionView {
1334 return (item as PersonMentionView).person_mention?.id !== undefined;
1338 item: CommentView | PersonMentionView | CommentReplyView
1339 ): item is CommentReplyView {
1340 return (item as CommentReplyView).comment_reply?.id !== undefined;
1343 handleModBanFromCommunityShow(i: CommentNode) {
1345 showBanDialog: true,
1346 banType: BanType.Community,
1347 showRemoveDialog: false,
1351 handleModBanShow(i: CommentNode) {
1353 showBanDialog: true,
1354 banType: BanType.Site,
1355 showRemoveDialog: false,
1359 handleModBanReasonChange(i: CommentNode, event: any) {
1360 i.setState({ banReason: event.target.value });
1363 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1364 i.setState({ banExpireDays: event.target.value });
1367 handlePurgePersonShow(i: CommentNode) {
1369 showPurgeDialog: true,
1370 purgeType: PurgeType.Person,
1371 showRemoveDialog: false,
1375 handlePurgeCommentShow(i: CommentNode) {
1377 showPurgeDialog: true,
1378 purgeType: PurgeType.Comment,
1379 showRemoveDialog: false,
1383 handlePurgeReasonChange(i: CommentNode, event: any) {
1384 i.setState({ purgeReason: event.target.value });
1387 handleShowConfirmAppointAsMod(i: CommentNode) {
1388 i.setState({ showConfirmAppointAsMod: true });
1391 handleCancelConfirmAppointAsMod(i: CommentNode) {
1392 i.setState({ showConfirmAppointAsMod: false });
1395 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1396 i.setState({ showConfirmAppointAsAdmin: true });
1399 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1400 i.setState({ showConfirmAppointAsAdmin: false });
1403 handleShowConfirmTransferCommunity(i: CommentNode) {
1404 i.setState({ showConfirmTransferCommunity: true });
1407 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1408 i.setState({ showConfirmTransferCommunity: false });
1411 handleShowConfirmTransferSite(i: CommentNode) {
1412 i.setState({ showConfirmTransferSite: true });
1415 handleCancelShowConfirmTransferSite(i: CommentNode) {
1416 i.setState({ showConfirmTransferSite: false });
1419 get isCommentNew(): boolean {
1420 const now = moment.utc().subtract(10, "minutes");
1421 const then = moment.utc(this.commentView.comment.published);
1422 return now.isBefore(then);
1425 handleCommentCollapse(i: CommentNode) {
1426 i.setState({ collapsed: !i.state.collapsed });
1430 handleViewSource(i: CommentNode) {
1431 i.setState({ viewSource: !i.state.viewSource });
1434 handleShowAdvanced(i: CommentNode) {
1435 i.setState({ showAdvanced: !i.state.showAdvanced });
1439 handleSaveComment(i: CommentNode) {
1440 i.setState({ saveLoading: true });
1442 i.props.onSaveComment({
1443 comment_id: i.commentView.comment.id,
1444 save: !i.commentView.saved,
1445 auth: myAuthRequired(),
1449 handleUpvote(i: CommentNode) {
1450 i.setState({ upvoteLoading: true });
1451 i.props.onCommentVote({
1452 comment_id: i.commentId,
1453 score: newVote(VoteType.Upvote, i.commentView.my_vote),
1454 auth: myAuthRequired(),
1458 handleDownvote(i: CommentNode) {
1459 i.setState({ downvoteLoading: true });
1460 i.props.onCommentVote({
1461 comment_id: i.commentId,
1462 score: newVote(VoteType.Downvote, i.commentView.my_vote),
1463 auth: myAuthRequired(),
1467 handleBlockPerson(i: CommentNode) {
1468 i.setState({ blockPersonLoading: true });
1469 i.props.onBlockPerson({
1470 person_id: i.commentView.creator.id,
1472 auth: myAuthRequired(),
1476 handleMarkAsRead(i: CommentNode) {
1477 i.setState({ readLoading: true });
1478 const cv = i.commentView;
1479 if (i.isPersonMentionType(cv)) {
1480 i.props.onPersonMentionRead({
1481 person_mention_id: cv.person_mention.id,
1482 read: !cv.person_mention.read,
1483 auth: myAuthRequired(),
1485 } else if (i.isCommentReplyType(cv)) {
1486 i.props.onCommentReplyRead({
1487 comment_reply_id: cv.comment_reply.id,
1488 read: !cv.comment_reply.read,
1489 auth: myAuthRequired(),
1494 handleDeleteComment(i: CommentNode) {
1495 i.setState({ deleteLoading: true });
1496 i.props.onDeleteComment({
1497 comment_id: i.commentId,
1498 deleted: !i.commentView.comment.deleted,
1499 auth: myAuthRequired(),
1503 handleRemoveComment(i: CommentNode, event: any) {
1504 event.preventDefault();
1505 i.setState({ removeLoading: true });
1506 i.props.onRemoveComment({
1507 comment_id: i.commentId,
1508 removed: !i.commentView.comment.removed,
1509 auth: myAuthRequired(),
1513 handleDistinguishComment(i: CommentNode) {
1514 i.setState({ distinguishLoading: true });
1515 i.props.onDistinguishComment({
1516 comment_id: i.commentId,
1517 distinguished: !i.commentView.comment.distinguished,
1518 auth: myAuthRequired(),
1522 handleBanPersonFromCommunity(i: CommentNode) {
1523 i.setState({ banLoading: true });
1524 i.props.onBanPersonFromCommunity({
1525 community_id: i.commentView.community.id,
1526 person_id: i.commentView.creator.id,
1527 ban: !i.commentView.creator_banned_from_community,
1528 reason: i.state.banReason,
1529 remove_data: i.state.removeData,
1530 expires: futureDaysToUnixTime(i.state.banExpireDays),
1531 auth: myAuthRequired(),
1535 handleBanPerson(i: CommentNode) {
1536 i.setState({ banLoading: true });
1537 i.props.onBanPerson({
1538 person_id: i.commentView.creator.id,
1539 ban: !i.commentView.creator_banned_from_community,
1540 reason: i.state.banReason,
1541 remove_data: i.state.removeData,
1542 expires: futureDaysToUnixTime(i.state.banExpireDays),
1543 auth: myAuthRequired(),
1547 handleModBanBothSubmit(i: CommentNode, event: any) {
1548 event.preventDefault();
1549 if (i.state.banType == BanType.Community) {
1550 i.handleBanPersonFromCommunity(i);
1552 i.handleBanPerson(i);
1556 handleAddModToCommunity(i: CommentNode) {
1557 i.setState({ addModLoading: true });
1559 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1560 i.props.onAddModToCommunity({
1561 community_id: i.commentView.community.id,
1562 person_id: i.commentView.creator.id,
1564 auth: myAuthRequired(),
1568 handleAddAdmin(i: CommentNode) {
1569 i.setState({ addAdminLoading: true });
1571 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1572 i.props.onAddAdmin({
1573 person_id: i.commentView.creator.id,
1575 auth: myAuthRequired(),
1579 handleTransferCommunity(i: CommentNode) {
1580 i.setState({ transferCommunityLoading: true });
1581 i.props.onTransferCommunity({
1582 community_id: i.commentView.community.id,
1583 person_id: i.commentView.creator.id,
1584 auth: myAuthRequired(),
1588 handleReportComment(i: CommentNode, event: any) {
1589 event.preventDefault();
1590 i.setState({ reportLoading: true });
1591 i.props.onCommentReport({
1592 comment_id: i.commentId,
1593 reason: i.state.reportReason ?? "",
1594 auth: myAuthRequired(),
1598 handlePurgeBothSubmit(i: CommentNode, event: any) {
1599 event.preventDefault();
1600 i.setState({ purgeLoading: true });
1602 if (i.state.purgeType == PurgeType.Person) {
1603 i.props.onPurgePerson({
1604 person_id: i.commentView.creator.id,
1605 reason: i.state.purgeReason,
1606 auth: myAuthRequired(),
1609 i.props.onPurgeComment({
1610 comment_id: i.commentId,
1611 reason: i.state.purgeReason,
1612 auth: myAuthRequired(),
1617 handleFetchChildren(i: CommentNode) {
1618 i.setState({ fetchChildrenLoading: true });
1619 i.props.onFetchChildren?.({
1620 parent_id: i.commentId,
1621 max_depth: commentTreeMaxDepth,