8 import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
16 } from "@utils/roles";
17 import classNames from "classnames";
18 import { Component, InfernoNode, linkEvent } from "inferno";
19 import { Link } from "inferno-router";
29 CommunityModeratorView,
38 MarkCommentReplyAsRead,
39 MarkPersonMentionAsRead,
47 } from "lemmy-js-client";
48 import moment from "moment";
49 import { commentTreeMaxDepth } from "../../config";
56 } from "../../interfaces";
57 import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
58 import { I18NextService, UserService } from "../../services";
59 import { setupTippy } from "../../tippy";
60 import { Icon, PurgeWarning, Spinner } from "../common/icon";
61 import { MomentTime } from "../common/moment-time";
62 import { VoteButtonsCompact } from "../common/vote-buttons";
63 import { CommunityLink } from "../community/community-link";
64 import { PersonListing } from "../person/person-listing";
65 import { CommentForm } from "./comment-form";
66 import { CommentNodes } from "./comment-nodes";
68 interface CommentNodeState {
71 showRemoveDialog: boolean;
72 removeReason?: string;
73 showBanDialog: boolean;
76 banExpireDays?: number;
78 showPurgeDialog: boolean;
81 showConfirmTransferSite: boolean;
82 showConfirmTransferCommunity: boolean;
83 showConfirmAppointAsMod: boolean;
84 showConfirmAppointAsAdmin: boolean;
87 showAdvanced: boolean;
88 showReportDialog: boolean;
89 reportReason?: string;
90 createOrEditCommentLoading: boolean;
91 upvoteLoading: boolean;
92 downvoteLoading: boolean;
95 blockPersonLoading: boolean;
96 deleteLoading: boolean;
97 removeLoading: boolean;
98 distinguishLoading: boolean;
100 addModLoading: boolean;
101 addAdminLoading: boolean;
102 transferCommunityLoading: boolean;
103 fetchChildrenLoading: boolean;
104 reportLoading: boolean;
105 purgeLoading: boolean;
108 interface CommentNodeProps {
110 moderators?: CommunityModeratorView[];
111 admins?: PersonView[];
117 showContext?: boolean;
118 showCommunity?: boolean;
119 enableDownvotes?: boolean;
120 viewType: CommentViewType;
121 allLanguages: Language[];
122 siteLanguages: number[];
123 hideImages?: boolean;
124 finished: Map<CommentId, boolean | undefined>;
125 onSaveComment(form: SaveComment): void;
126 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
127 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
128 onCreateComment(form: EditComment | CreateComment): void;
129 onEditComment(form: EditComment | CreateComment): void;
130 onCommentVote(form: CreateCommentLike): void;
131 onBlockPerson(form: BlockPerson): void;
132 onDeleteComment(form: DeleteComment): void;
133 onRemoveComment(form: RemoveComment): void;
134 onDistinguishComment(form: DistinguishComment): void;
135 onAddModToCommunity(form: AddModToCommunity): void;
136 onAddAdmin(form: AddAdmin): void;
137 onBanPersonFromCommunity(form: BanFromCommunity): void;
138 onBanPerson(form: BanPerson): void;
139 onTransferCommunity(form: TransferCommunity): void;
140 onFetchChildren?(form: GetComments): void;
141 onCommentReport(form: CreateCommentReport): void;
142 onPurgePerson(form: PurgePerson): void;
143 onPurgeComment(form: PurgeComment): void;
146 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
147 state: CommentNodeState = {
150 showRemoveDialog: false,
151 showBanDialog: false,
153 banType: BanType.Community,
154 showPurgeDialog: false,
155 purgeType: PurgeType.Person,
159 showConfirmTransferSite: false,
160 showConfirmTransferCommunity: false,
161 showConfirmAppointAsMod: false,
162 showConfirmAppointAsAdmin: false,
163 showReportDialog: false,
164 createOrEditCommentLoading: false,
165 upvoteLoading: false,
166 downvoteLoading: false,
169 blockPersonLoading: false,
170 deleteLoading: false,
171 removeLoading: false,
172 distinguishLoading: false,
174 addModLoading: false,
175 addAdminLoading: false,
176 transferCommunityLoading: false,
177 fetchChildrenLoading: false,
178 reportLoading: false,
182 constructor(props: any, context: any) {
183 super(props, context);
185 this.handleReplyCancel = this.handleReplyCancel.bind(this);
188 get commentView(): CommentView {
189 return this.props.node.comment_view;
192 get commentId(): CommentId {
193 return this.commentView.comment.id;
196 componentWillReceiveProps(
197 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
199 if (this.props != nextProps) {
203 showRemoveDialog: false,
204 showBanDialog: false,
206 banType: BanType.Community,
207 showPurgeDialog: false,
208 purgeType: PurgeType.Person,
212 showConfirmTransferSite: false,
213 showConfirmTransferCommunity: false,
214 showConfirmAppointAsMod: false,
215 showConfirmAppointAsAdmin: false,
216 showReportDialog: false,
217 createOrEditCommentLoading: false,
218 upvoteLoading: false,
219 downvoteLoading: false,
222 blockPersonLoading: false,
223 deleteLoading: false,
224 removeLoading: false,
225 distinguishLoading: false,
227 addModLoading: false,
228 addAdminLoading: false,
229 transferCommunityLoading: false,
230 fetchChildrenLoading: false,
231 reportLoading: false,
238 const node = this.props.node;
239 const cv = this.commentView;
241 const purgeTypeText =
242 this.state.purgeType == PurgeType.Comment
243 ? I18NextService.i18n.t("purge_comment")
244 : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
246 const canMod_ = canMod(
248 this.props.moderators,
251 const canModOnSelf = canMod(
253 this.props.moderators,
255 UserService.Instance.myUserInfo,
258 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
259 const canAdminOnSelf = canAdmin(
262 UserService.Instance.myUserInfo,
265 const isMod_ = isMod(cv.creator.id, this.props.moderators);
266 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
267 const amCommunityCreator_ = amCommunityCreator(
269 this.props.moderators
272 const moreRepliesBorderColor = this.props.node.depth
273 ? colorList[this.props.node.depth % colorList.length]
276 const showMoreChildren =
277 this.props.viewType == CommentViewType.Tree &&
278 !this.state.collapsed &&
279 node.children.length == 0 &&
280 node.comment_view.counts.child_count > 0;
283 <li className="comment">
285 id={`comment-${cv.comment.id}`}
286 className={classNames(`details comment-node py-2`, {
287 "border-top border-light": !this.props.noBorder,
288 mark: this.isCommentNew || this.commentView.comment.distinguished,
292 className={classNames({
293 "ms-2": !this.props.noIndent,
296 <div className="d-flex flex-wrap align-items-center text-muted small">
298 className="btn btn-sm text-muted me-2"
299 onClick={linkEvent(this, this.handleCommentCollapse)}
300 aria-label={this.expandText}
301 data-tippy-content={this.expandText}
304 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
305 classes="icon-inline"
308 <span className="me-2">
309 <PersonListing person={cv.creator} />
311 {cv.comment.distinguished && (
312 <Icon icon="shield" inline classes={`text-danger me-2`} />
314 {this.isPostCreator && (
315 <div className="badge text-bg-light d-none d-sm-inline me-2">
316 {I18NextService.i18n.t("creator")}
320 <div className="badge text-bg-light d-none d-sm-inline me-2">
321 {I18NextService.i18n.t("mod")}
325 <div className="badge text-bg-light d-none d-sm-inline me-2">
326 {I18NextService.i18n.t("admin")}
329 {cv.creator.bot_account && (
330 <div className="badge text-bg-light d-none d-sm-inline me-2">
331 {I18NextService.i18n.t("bot_account").toLowerCase()}
334 {this.props.showCommunity && (
336 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
337 <CommunityLink community={cv.community} />
338 <span className="mx-2">•</span>
339 <Link className="me-2" to={`/post/${cv.post.id}`}>
345 {cv.comment.language_id !== 0 && (
346 <span className="badge text-bg-light d-none d-sm-inline me-2">
348 this.props.allLanguages.find(
349 lang => lang.id === cv.comment.language_id
354 {/* This is an expanding spacer for mobile */}
355 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
360 className="me-1 font-weight-bold"
361 aria-label={I18NextService.i18n.t("number_of_points", {
362 count: Number(this.commentView.counts.score),
363 formattedCount: numToSI(this.commentView.counts.score),
366 {numToSI(this.commentView.counts.score)}
368 <span className="me-1">•</span>
373 published={cv.comment.published}
374 updated={cv.comment.updated}
378 {/* end of user row */}
379 {this.state.showEdit && (
383 onReplyCancel={this.handleReplyCancel}
384 disabled={this.props.locked}
385 finished={this.props.finished.get(
386 this.props.node.comment_view.comment.id
389 allLanguages={this.props.allLanguages}
390 siteLanguages={this.props.siteLanguages}
391 containerClass="comment-comment-container"
392 onUpsertComment={this.props.onEditComment}
395 {!this.state.showEdit && !this.state.collapsed && (
397 {this.state.viewSource ? (
398 <pre>{this.commentUnlessRemoved}</pre>
402 dangerouslySetInnerHTML={
403 this.props.hideImages
404 ? mdToHtmlNoImages(this.commentUnlessRemoved)
405 : mdToHtml(this.commentUnlessRemoved)
409 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
410 {this.props.showContext && this.linkBtn()}
411 {this.props.markable && (
413 className="btn btn-link btn-animate text-muted"
414 onClick={linkEvent(this, this.handleMarkAsRead)}
416 this.commentReplyOrMentionRead
417 ? I18NextService.i18n.t("mark_as_unread")
418 : I18NextService.i18n.t("mark_as_read")
421 this.commentReplyOrMentionRead
422 ? I18NextService.i18n.t("mark_as_unread")
423 : I18NextService.i18n.t("mark_as_read")
426 {this.state.readLoading ? (
431 classes={`icon-inline ${
432 this.commentReplyOrMentionRead && "text-success"
438 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
441 voteContentType={VoteContentType.Comment}
442 id={this.commentView.comment.id}
443 onVote={this.props.onCommentVote}
444 enableDownvotes={this.props.enableDownvotes}
445 counts={this.commentView.counts}
446 my_vote={this.commentView.my_vote}
449 className="btn btn-link btn-animate text-muted"
450 onClick={linkEvent(this, this.handleReplyClick)}
451 data-tippy-content={I18NextService.i18n.t("reply")}
452 aria-label={I18NextService.i18n.t("reply")}
454 <Icon icon="reply1" classes="icon-inline" />
456 {!this.state.showAdvanced ? (
458 className="btn btn-link btn-animate text-muted btn-more"
459 onClick={linkEvent(this, this.handleShowAdvanced)}
460 data-tippy-content={I18NextService.i18n.t("more")}
461 aria-label={I18NextService.i18n.t("more")}
463 <Icon icon="more-vertical" classes="icon-inline" />
467 {!this.myComment && (
470 className="btn btn-link btn-animate text-muted"
471 to={`/create_private_message/${cv.creator.id}`}
472 title={I18NextService.i18n
479 className="btn btn-link btn-animate text-muted"
482 this.handleShowReportDialog
484 data-tippy-content={I18NextService.i18n.t(
487 aria-label={I18NextService.i18n.t(
494 className="btn btn-link btn-animate text-muted"
497 this.handleBlockPerson
499 data-tippy-content={I18NextService.i18n.t(
502 aria-label={I18NextService.i18n.t("block_user")}
504 {this.state.blockPersonLoading ? (
507 <Icon icon="slash" />
513 className="btn btn-link btn-animate text-muted"
514 onClick={linkEvent(this, this.handleSaveComment)}
517 ? I18NextService.i18n.t("unsave")
518 : I18NextService.i18n.t("save")
522 ? I18NextService.i18n.t("unsave")
523 : I18NextService.i18n.t("save")
526 {this.state.saveLoading ? (
531 classes={`icon-inline ${
532 cv.saved && "text-warning"
538 className="btn btn-link btn-animate text-muted"
539 onClick={linkEvent(this, this.handleViewSource)}
540 data-tippy-content={I18NextService.i18n.t(
543 aria-label={I18NextService.i18n.t("view_source")}
547 classes={`icon-inline ${
548 this.state.viewSource && "text-success"
555 className="btn btn-link btn-animate text-muted"
556 onClick={linkEvent(this, this.handleEditClick)}
557 data-tippy-content={I18NextService.i18n.t(
560 aria-label={I18NextService.i18n.t("edit")}
562 <Icon icon="edit" classes="icon-inline" />
565 className="btn btn-link btn-animate text-muted"
568 this.handleDeleteComment
572 ? I18NextService.i18n.t("delete")
573 : I18NextService.i18n.t("restore")
577 ? I18NextService.i18n.t("delete")
578 : I18NextService.i18n.t("restore")
581 {this.state.deleteLoading ? (
586 classes={`icon-inline ${
587 cv.comment.deleted && "text-danger"
593 {(canModOnSelf || canAdminOnSelf) && (
595 className="btn btn-link btn-animate text-muted"
598 this.handleDistinguishComment
601 !cv.comment.distinguished
602 ? I18NextService.i18n.t("distinguish")
603 : I18NextService.i18n.t("undistinguish")
606 !cv.comment.distinguished
607 ? I18NextService.i18n.t("distinguish")
608 : I18NextService.i18n.t("undistinguish")
613 classes={`icon-inline ${
614 cv.comment.distinguished && "text-danger"
621 {/* Admins and mods can remove comments */}
622 {(canMod_ || canAdmin_) && (
624 {!cv.comment.removed ? (
626 className="btn btn-link btn-animate text-muted"
629 this.handleModRemoveShow
631 aria-label={I18NextService.i18n.t("remove")}
633 {I18NextService.i18n.t("remove")}
637 className="btn btn-link btn-animate text-muted"
640 this.handleRemoveComment
642 aria-label={I18NextService.i18n.t("restore")}
644 {this.state.removeLoading ? (
647 I18NextService.i18n.t("restore")
653 {/* Mods can ban from community, and appoint as mods to community */}
657 (!cv.creator_banned_from_community ? (
659 className="btn btn-link btn-animate text-muted"
662 this.handleModBanFromCommunityShow
664 aria-label={I18NextService.i18n.t(
668 {I18NextService.i18n.t(
674 className="btn btn-link btn-animate text-muted"
677 this.handleBanPersonFromCommunity
679 aria-label={I18NextService.i18n.t("unban")}
681 {this.state.banLoading ? (
684 I18NextService.i18n.t("unban")
688 {!cv.creator_banned_from_community &&
689 (!this.state.showConfirmAppointAsMod ? (
691 className="btn btn-link btn-animate text-muted"
694 this.handleShowConfirmAppointAsMod
698 ? I18NextService.i18n.t("remove_as_mod")
699 : I18NextService.i18n.t(
705 ? I18NextService.i18n.t("remove_as_mod")
706 : I18NextService.i18n.t("appoint_as_mod")}
711 className="btn btn-link btn-animate text-muted"
712 aria-label={I18NextService.i18n.t(
716 {I18NextService.i18n.t("are_you_sure")}
719 className="btn btn-link btn-animate text-muted"
722 this.handleAddModToCommunity
724 aria-label={I18NextService.i18n.t("yes")}
726 {this.state.addModLoading ? (
729 I18NextService.i18n.t("yes")
733 className="btn btn-link btn-animate text-muted"
736 this.handleCancelConfirmAppointAsMod
738 aria-label={I18NextService.i18n.t("no")}
740 {I18NextService.i18n.t("no")}
746 {/* Community creators and admins can transfer community to another mod */}
747 {(amCommunityCreator_ || canAdmin_) &&
750 (!this.state.showConfirmTransferCommunity ? (
752 className="btn btn-link btn-animate text-muted"
755 this.handleShowConfirmTransferCommunity
757 aria-label={I18NextService.i18n.t(
761 {I18NextService.i18n.t("transfer_community")}
766 className="btn btn-link btn-animate text-muted"
767 aria-label={I18NextService.i18n.t(
771 {I18NextService.i18n.t("are_you_sure")}
774 className="btn btn-link btn-animate text-muted"
777 this.handleTransferCommunity
779 aria-label={I18NextService.i18n.t("yes")}
781 {this.state.transferCommunityLoading ? (
784 I18NextService.i18n.t("yes")
788 className="btn btn-link btn-animate text-muted"
792 .handleCancelShowConfirmTransferCommunity
794 aria-label={I18NextService.i18n.t("no")}
796 {I18NextService.i18n.t("no")}
800 {/* Admins can ban from all, and appoint other admins */}
806 className="btn btn-link btn-animate text-muted"
809 this.handlePurgePersonShow
811 aria-label={I18NextService.i18n.t(
815 {I18NextService.i18n.t("purge_user")}
818 className="btn btn-link btn-animate text-muted"
821 this.handlePurgeCommentShow
823 aria-label={I18NextService.i18n.t(
827 {I18NextService.i18n.t("purge_comment")}
830 {!isBanned(cv.creator) ? (
832 className="btn btn-link btn-animate text-muted"
835 this.handleModBanShow
837 aria-label={I18NextService.i18n.t(
841 {I18NextService.i18n.t("ban_from_site")}
845 className="btn btn-link btn-animate text-muted"
850 aria-label={I18NextService.i18n.t(
854 {this.state.banLoading ? (
857 I18NextService.i18n.t("unban_from_site")
863 {!isBanned(cv.creator) &&
865 (!this.state.showConfirmAppointAsAdmin ? (
867 className="btn btn-link btn-animate text-muted"
870 this.handleShowConfirmAppointAsAdmin
874 ? I18NextService.i18n.t(
877 : I18NextService.i18n.t(
883 ? I18NextService.i18n.t("remove_as_admin")
884 : I18NextService.i18n.t(
890 <button className="btn btn-link btn-animate text-muted">
891 {I18NextService.i18n.t("are_you_sure")}
894 className="btn btn-link btn-animate text-muted"
899 aria-label={I18NextService.i18n.t("yes")}
901 {this.state.addAdminLoading ? (
904 I18NextService.i18n.t("yes")
908 className="btn btn-link btn-animate text-muted"
911 this.handleCancelConfirmAppointAsAdmin
913 aria-label={I18NextService.i18n.t("no")}
915 {I18NextService.i18n.t("no")}
926 {/* end of button group */}
931 {showMoreChildren && (
933 className={classNames("details ms-1 comment-node py-2", {
934 "border-top border-light": !this.props.noBorder,
936 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
939 className="btn btn-link text-muted"
940 onClick={linkEvent(this, this.handleFetchChildren)}
942 {this.state.fetchChildrenLoading ? (
946 {I18NextService.i18n.t("x_more_replies", {
947 count: node.comment_view.counts.child_count,
948 formattedCount: numToSI(
949 node.comment_view.counts.child_count
958 {/* end of details */}
959 {this.state.showRemoveDialog && (
961 className="form-inline"
962 onSubmit={linkEvent(this, this.handleRemoveComment)}
965 className="visually-hidden"
966 htmlFor={`mod-remove-reason-${cv.comment.id}`}
968 {I18NextService.i18n.t("reason")}
972 id={`mod-remove-reason-${cv.comment.id}`}
973 className="form-control me-2"
974 placeholder={I18NextService.i18n.t("reason")}
975 value={this.state.removeReason}
976 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
980 className="btn btn-secondary"
981 aria-label={I18NextService.i18n.t("remove_comment")}
983 {I18NextService.i18n.t("remove_comment")}
987 {this.state.showReportDialog && (
989 className="form-inline"
990 onSubmit={linkEvent(this, this.handleReportComment)}
993 className="visually-hidden"
994 htmlFor={`report-reason-${cv.comment.id}`}
996 {I18NextService.i18n.t("reason")}
1001 id={`report-reason-${cv.comment.id}`}
1002 className="form-control me-2"
1003 placeholder={I18NextService.i18n.t("reason")}
1004 value={this.state.reportReason}
1005 onInput={linkEvent(this, this.handleReportReasonChange)}
1009 className="btn btn-secondary"
1010 aria-label={I18NextService.i18n.t("create_report")}
1012 {I18NextService.i18n.t("create_report")}
1016 {this.state.showBanDialog && (
1017 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1018 <div className="mb-3 row col-12">
1020 className="col-form-label"
1021 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1023 {I18NextService.i18n.t("reason")}
1027 id={`mod-ban-reason-${cv.comment.id}`}
1028 className="form-control me-2"
1029 placeholder={I18NextService.i18n.t("reason")}
1030 value={this.state.banReason}
1031 onInput={linkEvent(this, this.handleModBanReasonChange)}
1034 className="col-form-label"
1035 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1037 {I18NextService.i18n.t("expires")}
1041 id={`mod-ban-expires-${cv.comment.id}`}
1042 className="form-control me-2"
1043 placeholder={I18NextService.i18n.t("number_of_days")}
1044 value={this.state.banExpireDays}
1045 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1047 <div className="input-group mb-3">
1048 <div className="form-check">
1050 className="form-check-input"
1051 id="mod-ban-remove-data"
1053 checked={this.state.removeData}
1054 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1057 className="form-check-label"
1058 htmlFor="mod-ban-remove-data"
1059 title={I18NextService.i18n.t("remove_content_more")}
1061 {I18NextService.i18n.t("remove_content")}
1066 {/* TODO hold off on expires until later */}
1067 {/* <div class="mb-3 row"> */}
1068 {/* <label class="col-form-label">Expires</label> */}
1069 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1071 <div className="mb-3 row">
1074 className="btn btn-secondary"
1075 aria-label={I18NextService.i18n.t("ban")}
1077 {this.state.banLoading ? (
1081 {I18NextService.i18n.t("ban")} {cv.creator.name}
1089 {this.state.showPurgeDialog && (
1090 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1092 <label className="visually-hidden" htmlFor="purge-reason">
1093 {I18NextService.i18n.t("reason")}
1098 className="form-control my-3"
1099 placeholder={I18NextService.i18n.t("reason")}
1100 value={this.state.purgeReason}
1101 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1103 <div className="mb-3 row col-12">
1104 {this.state.purgeLoading ? (
1109 className="btn btn-secondary"
1110 aria-label={purgeTypeText}
1118 {this.state.showReply && (
1121 onReplyCancel={this.handleReplyCancel}
1122 disabled={this.props.locked}
1123 finished={this.props.finished.get(
1124 this.props.node.comment_view.comment.id
1127 allLanguages={this.props.allLanguages}
1128 siteLanguages={this.props.siteLanguages}
1129 containerClass="comment-comment-container"
1130 onUpsertComment={this.props.onCreateComment}
1133 {!this.state.collapsed && node.children.length > 0 && (
1135 nodes={node.children}
1136 locked={this.props.locked}
1137 moderators={this.props.moderators}
1138 admins={this.props.admins}
1139 enableDownvotes={this.props.enableDownvotes}
1140 viewType={this.props.viewType}
1141 allLanguages={this.props.allLanguages}
1142 siteLanguages={this.props.siteLanguages}
1143 hideImages={this.props.hideImages}
1144 isChild={!this.props.noIndent}
1145 depth={this.props.node.depth + 1}
1146 finished={this.props.finished}
1147 onCommentReplyRead={this.props.onCommentReplyRead}
1148 onPersonMentionRead={this.props.onPersonMentionRead}
1149 onCreateComment={this.props.onCreateComment}
1150 onEditComment={this.props.onEditComment}
1151 onCommentVote={this.props.onCommentVote}
1152 onBlockPerson={this.props.onBlockPerson}
1153 onSaveComment={this.props.onSaveComment}
1154 onDeleteComment={this.props.onDeleteComment}
1155 onRemoveComment={this.props.onRemoveComment}
1156 onDistinguishComment={this.props.onDistinguishComment}
1157 onAddModToCommunity={this.props.onAddModToCommunity}
1158 onAddAdmin={this.props.onAddAdmin}
1159 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1160 onBanPerson={this.props.onBanPerson}
1161 onTransferCommunity={this.props.onTransferCommunity}
1162 onFetchChildren={this.props.onFetchChildren}
1163 onCommentReport={this.props.onCommentReport}
1164 onPurgePerson={this.props.onPurgePerson}
1165 onPurgeComment={this.props.onPurgeComment}
1168 {/* A collapsed clearfix */}
1169 {this.state.collapsed && <div className="row col-12" />}
1174 get commentReplyOrMentionRead(): boolean {
1175 const cv = this.commentView;
1177 if (this.isPersonMentionType(cv)) {
1178 return cv.person_mention.read;
1179 } else if (this.isCommentReplyType(cv)) {
1180 return cv.comment_reply.read;
1186 linkBtn(small = false) {
1187 const cv = this.commentView;
1189 const classnames = classNames("btn btn-link btn-animate text-muted", {
1193 const title = this.props.showContext
1194 ? I18NextService.i18n.t("show_context")
1195 : I18NextService.i18n.t("link");
1197 // The context button should show the parent comment by default
1198 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1203 className={classnames}
1204 to={`/comment/${parentCommentId}`}
1207 <Icon icon="link" classes="icon-inline" />
1210 <a className={classnames} title={title} href={cv.comment.ap_id}>
1211 <Icon icon="fedilink" classes="icon-inline" />
1218 get myComment(): boolean {
1220 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1221 this.commentView.creator.id
1225 get isPostCreator(): boolean {
1226 return this.commentView.creator.id == this.commentView.post.creator_id;
1230 if (this.commentView.my_vote == 1) {
1232 } else if (this.commentView.my_vote == -1) {
1233 return "text-danger";
1235 return "text-muted";
1239 get pointsTippy(): string {
1240 const points = I18NextService.i18n.t("number_of_points", {
1241 count: Number(this.commentView.counts.score),
1242 formattedCount: numToSI(this.commentView.counts.score),
1245 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1246 count: Number(this.commentView.counts.upvotes),
1247 formattedCount: numToSI(this.commentView.counts.upvotes),
1250 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1251 count: Number(this.commentView.counts.downvotes),
1252 formattedCount: numToSI(this.commentView.counts.downvotes),
1255 return `${points} • ${upvotes} • ${downvotes}`;
1258 get expandText(): string {
1259 return this.state.collapsed
1260 ? I18NextService.i18n.t("expand")
1261 : I18NextService.i18n.t("collapse");
1264 get commentUnlessRemoved(): string {
1265 const comment = this.commentView.comment;
1266 return comment.removed
1267 ? `*${I18NextService.i18n.t("removed")}*`
1269 ? `*${I18NextService.i18n.t("deleted")}*`
1273 handleReplyClick(i: CommentNode) {
1274 i.setState({ showReply: true });
1277 handleEditClick(i: CommentNode) {
1278 i.setState({ showEdit: true });
1281 handleReplyCancel() {
1282 this.setState({ showReply: false, showEdit: false });
1285 handleShowReportDialog(i: CommentNode) {
1286 i.setState({ showReportDialog: !i.state.showReportDialog });
1289 handleReportReasonChange(i: CommentNode, event: any) {
1290 i.setState({ reportReason: event.target.value });
1293 handleModRemoveShow(i: CommentNode) {
1295 showRemoveDialog: !i.state.showRemoveDialog,
1296 showBanDialog: false,
1300 handleModRemoveReasonChange(i: CommentNode, event: any) {
1301 i.setState({ removeReason: event.target.value });
1304 handleModRemoveDataChange(i: CommentNode, event: any) {
1305 i.setState({ removeData: event.target.checked });
1308 isPersonMentionType(
1309 item: CommentView | PersonMentionView | CommentReplyView
1310 ): item is PersonMentionView {
1311 return (item as PersonMentionView).person_mention?.id !== undefined;
1315 item: CommentView | PersonMentionView | CommentReplyView
1316 ): item is CommentReplyView {
1317 return (item as CommentReplyView).comment_reply?.id !== undefined;
1320 handleModBanFromCommunityShow(i: CommentNode) {
1322 showBanDialog: true,
1323 banType: BanType.Community,
1324 showRemoveDialog: false,
1328 handleModBanShow(i: CommentNode) {
1330 showBanDialog: true,
1331 banType: BanType.Site,
1332 showRemoveDialog: false,
1336 handleModBanReasonChange(i: CommentNode, event: any) {
1337 i.setState({ banReason: event.target.value });
1340 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1341 i.setState({ banExpireDays: event.target.value });
1344 handlePurgePersonShow(i: CommentNode) {
1346 showPurgeDialog: true,
1347 purgeType: PurgeType.Person,
1348 showRemoveDialog: false,
1352 handlePurgeCommentShow(i: CommentNode) {
1354 showPurgeDialog: true,
1355 purgeType: PurgeType.Comment,
1356 showRemoveDialog: false,
1360 handlePurgeReasonChange(i: CommentNode, event: any) {
1361 i.setState({ purgeReason: event.target.value });
1364 handleShowConfirmAppointAsMod(i: CommentNode) {
1365 i.setState({ showConfirmAppointAsMod: true });
1368 handleCancelConfirmAppointAsMod(i: CommentNode) {
1369 i.setState({ showConfirmAppointAsMod: false });
1372 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1373 i.setState({ showConfirmAppointAsAdmin: true });
1376 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1377 i.setState({ showConfirmAppointAsAdmin: false });
1380 handleShowConfirmTransferCommunity(i: CommentNode) {
1381 i.setState({ showConfirmTransferCommunity: true });
1384 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1385 i.setState({ showConfirmTransferCommunity: false });
1388 handleShowConfirmTransferSite(i: CommentNode) {
1389 i.setState({ showConfirmTransferSite: true });
1392 handleCancelShowConfirmTransferSite(i: CommentNode) {
1393 i.setState({ showConfirmTransferSite: false });
1396 get isCommentNew(): boolean {
1397 const now = moment.utc().subtract(10, "minutes");
1398 const then = moment.utc(this.commentView.comment.published);
1399 return now.isBefore(then);
1402 handleCommentCollapse(i: CommentNode) {
1403 i.setState({ collapsed: !i.state.collapsed });
1407 handleViewSource(i: CommentNode) {
1408 i.setState({ viewSource: !i.state.viewSource });
1411 handleShowAdvanced(i: CommentNode) {
1412 i.setState({ showAdvanced: !i.state.showAdvanced });
1416 handleSaveComment(i: CommentNode) {
1417 i.setState({ saveLoading: true });
1419 i.props.onSaveComment({
1420 comment_id: i.commentView.comment.id,
1421 save: !i.commentView.saved,
1422 auth: myAuthRequired(),
1426 handleBlockPerson(i: CommentNode) {
1427 i.setState({ blockPersonLoading: true });
1428 i.props.onBlockPerson({
1429 person_id: i.commentView.creator.id,
1431 auth: myAuthRequired(),
1435 handleMarkAsRead(i: CommentNode) {
1436 i.setState({ readLoading: true });
1437 const cv = i.commentView;
1438 if (i.isPersonMentionType(cv)) {
1439 i.props.onPersonMentionRead({
1440 person_mention_id: cv.person_mention.id,
1441 read: !cv.person_mention.read,
1442 auth: myAuthRequired(),
1444 } else if (i.isCommentReplyType(cv)) {
1445 i.props.onCommentReplyRead({
1446 comment_reply_id: cv.comment_reply.id,
1447 read: !cv.comment_reply.read,
1448 auth: myAuthRequired(),
1453 handleDeleteComment(i: CommentNode) {
1454 i.setState({ deleteLoading: true });
1455 i.props.onDeleteComment({
1456 comment_id: i.commentId,
1457 deleted: !i.commentView.comment.deleted,
1458 auth: myAuthRequired(),
1462 handleRemoveComment(i: CommentNode, event: any) {
1463 event.preventDefault();
1464 i.setState({ removeLoading: true });
1465 i.props.onRemoveComment({
1466 comment_id: i.commentId,
1467 removed: !i.commentView.comment.removed,
1468 auth: myAuthRequired(),
1472 handleDistinguishComment(i: CommentNode) {
1473 i.setState({ distinguishLoading: true });
1474 i.props.onDistinguishComment({
1475 comment_id: i.commentId,
1476 distinguished: !i.commentView.comment.distinguished,
1477 auth: myAuthRequired(),
1481 handleBanPersonFromCommunity(i: CommentNode) {
1482 i.setState({ banLoading: true });
1483 i.props.onBanPersonFromCommunity({
1484 community_id: i.commentView.community.id,
1485 person_id: i.commentView.creator.id,
1486 ban: !i.commentView.creator_banned_from_community,
1487 reason: i.state.banReason,
1488 remove_data: i.state.removeData,
1489 expires: futureDaysToUnixTime(i.state.banExpireDays),
1490 auth: myAuthRequired(),
1494 handleBanPerson(i: CommentNode) {
1495 i.setState({ banLoading: true });
1496 i.props.onBanPerson({
1497 person_id: i.commentView.creator.id,
1498 ban: !i.commentView.creator_banned_from_community,
1499 reason: i.state.banReason,
1500 remove_data: i.state.removeData,
1501 expires: futureDaysToUnixTime(i.state.banExpireDays),
1502 auth: myAuthRequired(),
1506 handleModBanBothSubmit(i: CommentNode, event: any) {
1507 event.preventDefault();
1508 if (i.state.banType == BanType.Community) {
1509 i.handleBanPersonFromCommunity(i);
1511 i.handleBanPerson(i);
1515 handleAddModToCommunity(i: CommentNode) {
1516 i.setState({ addModLoading: true });
1518 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1519 i.props.onAddModToCommunity({
1520 community_id: i.commentView.community.id,
1521 person_id: i.commentView.creator.id,
1523 auth: myAuthRequired(),
1527 handleAddAdmin(i: CommentNode) {
1528 i.setState({ addAdminLoading: true });
1530 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1531 i.props.onAddAdmin({
1532 person_id: i.commentView.creator.id,
1534 auth: myAuthRequired(),
1538 handleTransferCommunity(i: CommentNode) {
1539 i.setState({ transferCommunityLoading: true });
1540 i.props.onTransferCommunity({
1541 community_id: i.commentView.community.id,
1542 person_id: i.commentView.creator.id,
1543 auth: myAuthRequired(),
1547 handleReportComment(i: CommentNode, event: any) {
1548 event.preventDefault();
1549 i.setState({ reportLoading: true });
1550 i.props.onCommentReport({
1551 comment_id: i.commentId,
1552 reason: i.state.reportReason ?? "",
1553 auth: myAuthRequired(),
1557 handlePurgeBothSubmit(i: CommentNode, event: any) {
1558 event.preventDefault();
1559 i.setState({ purgeLoading: true });
1561 if (i.state.purgeType == PurgeType.Person) {
1562 i.props.onPurgePerson({
1563 person_id: i.commentView.creator.id,
1564 reason: i.state.purgeReason,
1565 auth: myAuthRequired(),
1568 i.props.onPurgeComment({
1569 comment_id: i.commentId,
1570 reason: i.state.purgeReason,
1571 auth: myAuthRequired(),
1576 handleFetchChildren(i: CommentNode) {
1577 i.setState({ fetchChildrenLoading: true });
1578 i.props.onFetchChildren?.({
1579 parent_id: i.commentId,
1580 max_depth: commentTreeMaxDepth,