8 import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
16 } from "@utils/roles";
17 import classNames from "classnames";
18 import isBefore from "date-fns/isBefore";
19 import parseISO from "date-fns/parseISO";
20 import subMinutes from "date-fns/subMinutes";
21 import { Component, InfernoNode, linkEvent } from "inferno";
22 import { Link } from "inferno-router";
32 CommunityModeratorView,
41 MarkCommentReplyAsRead,
42 MarkPersonMentionAsRead,
50 } from "lemmy-js-client";
51 import deepEqual from "lodash.isequal";
52 import { commentTreeMaxDepth } from "../../config";
59 } from "../../interfaces";
60 import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
61 import { I18NextService, UserService } from "../../services";
62 import { setupTippy } from "../../tippy";
63 import { Icon, PurgeWarning, Spinner } from "../common/icon";
64 import { MomentTime } from "../common/moment-time";
65 import { UserBadges } from "../common/user-badges";
66 import { VoteButtonsCompact } from "../common/vote-buttons";
67 import { CommunityLink } from "../community/community-link";
68 import { PersonListing } from "../person/person-listing";
69 import { CommentForm } from "./comment-form";
70 import { CommentNodes } from "./comment-nodes";
72 interface CommentNodeState {
75 showRemoveDialog: boolean;
76 removeReason?: string;
77 showBanDialog: boolean;
80 banExpireDays?: number;
82 showPurgeDialog: boolean;
85 showConfirmTransferSite: boolean;
86 showConfirmTransferCommunity: boolean;
87 showConfirmAppointAsMod: boolean;
88 showConfirmAppointAsAdmin: boolean;
91 showAdvanced: boolean;
92 showReportDialog: boolean;
93 reportReason?: string;
94 createOrEditCommentLoading: boolean;
95 upvoteLoading: boolean;
96 downvoteLoading: boolean;
99 blockPersonLoading: boolean;
100 deleteLoading: boolean;
101 removeLoading: boolean;
102 distinguishLoading: boolean;
104 addModLoading: boolean;
105 addAdminLoading: boolean;
106 transferCommunityLoading: boolean;
107 fetchChildrenLoading: boolean;
108 reportLoading: boolean;
109 purgeLoading: boolean;
112 interface CommentNodeProps {
114 moderators?: CommunityModeratorView[];
115 admins?: PersonView[];
121 showContext?: boolean;
122 showCommunity?: boolean;
123 enableDownvotes?: boolean;
124 viewType: CommentViewType;
125 allLanguages: Language[];
126 siteLanguages: number[];
127 hideImages?: boolean;
128 finished: Map<CommentId, boolean | undefined>;
129 onSaveComment(form: SaveComment): void;
130 onCommentReplyRead(form: MarkCommentReplyAsRead): void;
131 onPersonMentionRead(form: MarkPersonMentionAsRead): void;
132 onCreateComment(form: EditComment | CreateComment): void;
133 onEditComment(form: EditComment | CreateComment): void;
134 onCommentVote(form: CreateCommentLike): void;
135 onBlockPerson(form: BlockPerson): void;
136 onDeleteComment(form: DeleteComment): void;
137 onRemoveComment(form: RemoveComment): void;
138 onDistinguishComment(form: DistinguishComment): void;
139 onAddModToCommunity(form: AddModToCommunity): void;
140 onAddAdmin(form: AddAdmin): void;
141 onBanPersonFromCommunity(form: BanFromCommunity): void;
142 onBanPerson(form: BanPerson): void;
143 onTransferCommunity(form: TransferCommunity): void;
144 onFetchChildren?(form: GetComments): void;
145 onCommentReport(form: CreateCommentReport): void;
146 onPurgePerson(form: PurgePerson): void;
147 onPurgeComment(form: PurgeComment): void;
150 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
151 state: CommentNodeState = {
154 showRemoveDialog: false,
155 showBanDialog: false,
157 banType: BanType.Community,
158 showPurgeDialog: false,
159 purgeType: PurgeType.Person,
163 showConfirmTransferSite: false,
164 showConfirmTransferCommunity: false,
165 showConfirmAppointAsMod: false,
166 showConfirmAppointAsAdmin: false,
167 showReportDialog: false,
168 createOrEditCommentLoading: false,
169 upvoteLoading: false,
170 downvoteLoading: false,
173 blockPersonLoading: false,
174 deleteLoading: false,
175 removeLoading: false,
176 distinguishLoading: false,
178 addModLoading: false,
179 addAdminLoading: false,
180 transferCommunityLoading: false,
181 fetchChildrenLoading: false,
182 reportLoading: false,
186 constructor(props: any, context: any) {
187 super(props, context);
189 this.handleReplyCancel = this.handleReplyCancel.bind(this);
192 get commentView(): CommentView {
193 return this.props.node.comment_view;
196 get commentId(): CommentId {
197 return this.commentView.comment.id;
200 componentWillReceiveProps(
201 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
203 if (!deepEqual(this.props, nextProps)) {
207 showRemoveDialog: false,
208 showBanDialog: false,
210 banType: BanType.Community,
211 showPurgeDialog: false,
212 purgeType: PurgeType.Person,
216 showConfirmTransferSite: false,
217 showConfirmTransferCommunity: false,
218 showConfirmAppointAsMod: false,
219 showConfirmAppointAsAdmin: false,
220 showReportDialog: false,
221 createOrEditCommentLoading: false,
222 upvoteLoading: false,
223 downvoteLoading: false,
226 blockPersonLoading: false,
227 deleteLoading: false,
228 removeLoading: false,
229 distinguishLoading: false,
231 addModLoading: false,
232 addAdminLoading: false,
233 transferCommunityLoading: false,
234 fetchChildrenLoading: false,
235 reportLoading: false,
242 const node = this.props.node;
243 const cv = this.commentView;
245 const purgeTypeText =
246 this.state.purgeType == PurgeType.Comment
247 ? I18NextService.i18n.t("purge_comment")
248 : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
250 const canMod_ = canMod(
252 this.props.moderators,
255 const canModOnSelf = canMod(
257 this.props.moderators,
259 UserService.Instance.myUserInfo,
262 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
263 const canAdminOnSelf = canAdmin(
266 UserService.Instance.myUserInfo,
269 const isMod_ = isMod(cv.creator.id, this.props.moderators);
270 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
271 const amCommunityCreator_ = amCommunityCreator(
273 this.props.moderators
276 const moreRepliesBorderColor = this.props.node.depth
277 ? colorList[this.props.node.depth % colorList.length]
280 const showMoreChildren =
281 this.props.viewType == CommentViewType.Tree &&
282 !this.state.collapsed &&
283 node.children.length == 0 &&
284 node.comment_view.counts.child_count > 0;
287 <li className="comment">
289 id={`comment-${cv.comment.id}`}
290 className={classNames(`details comment-node py-2`, {
291 "border-top border-light": !this.props.noBorder,
292 mark: this.isCommentNew || this.commentView.comment.distinguished,
296 className={classNames({
297 "ms-2": !this.props.noIndent,
300 <div className="d-flex flex-wrap align-items-center text-muted small">
302 className="btn btn-sm text-muted me-2"
303 onClick={linkEvent(this, this.handleCommentCollapse)}
304 aria-label={this.expandText}
305 data-tippy-content={this.expandText}
308 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
309 classes="icon-inline"
313 <PersonListing person={cv.creator} />
315 {cv.comment.distinguished && (
316 <Icon icon="shield" inline classes="text-danger ms-1" />
321 isPostCreator={this.isPostCreator}
324 isBot={cv.creator.bot_account}
327 {this.props.showCommunity && (
329 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
330 <CommunityLink community={cv.community} />
331 <span className="mx-2">•</span>
332 <Link className="me-2" to={`/post/${cv.post.id}`}>
338 {this.getLinkButton(true)}
340 {cv.comment.language_id !== 0 && (
341 <span className="badge text-bg-light d-none d-sm-inline me-2">
343 this.props.allLanguages.find(
344 lang => lang.id === cv.comment.language_id
349 {/* This is an expanding spacer for mobile */}
350 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
355 className="me-1 fw-bold"
356 aria-label={I18NextService.i18n.t("number_of_points", {
357 count: Number(this.commentView.counts.score),
358 formattedCount: numToSI(this.commentView.counts.score),
361 {numToSI(this.commentView.counts.score)}
363 <span className="me-1">•</span>
368 published={cv.comment.published}
369 updated={cv.comment.updated}
373 {/* end of user row */}
374 {this.state.showEdit && (
378 onReplyCancel={this.handleReplyCancel}
379 disabled={this.props.locked}
380 finished={this.props.finished.get(
381 this.props.node.comment_view.comment.id
384 allLanguages={this.props.allLanguages}
385 siteLanguages={this.props.siteLanguages}
386 containerClass="comment-comment-container"
387 onUpsertComment={this.props.onEditComment}
390 {!this.state.showEdit && !this.state.collapsed && (
392 {this.state.viewSource ? (
393 <pre>{this.commentUnlessRemoved}</pre>
397 dangerouslySetInnerHTML={
398 this.props.hideImages
399 ? mdToHtmlNoImages(this.commentUnlessRemoved)
400 : mdToHtml(this.commentUnlessRemoved)
404 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold">
405 {this.props.showContext && this.getLinkButton()}
406 {this.props.markable && (
408 className="btn btn-link btn-animate text-muted"
409 onClick={linkEvent(this, this.handleMarkAsRead)}
411 this.commentReplyOrMentionRead
412 ? I18NextService.i18n.t("mark_as_unread")
413 : I18NextService.i18n.t("mark_as_read")
416 this.commentReplyOrMentionRead
417 ? I18NextService.i18n.t("mark_as_unread")
418 : I18NextService.i18n.t("mark_as_read")
421 {this.state.readLoading ? (
426 classes={`icon-inline ${
427 this.commentReplyOrMentionRead && "text-success"
433 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
436 voteContentType={VoteContentType.Comment}
437 id={this.commentView.comment.id}
438 onVote={this.props.onCommentVote}
439 enableDownvotes={this.props.enableDownvotes}
440 counts={this.commentView.counts}
441 my_vote={this.commentView.my_vote}
444 className="btn btn-link btn-animate text-muted"
445 onClick={linkEvent(this, this.handleReplyClick)}
446 data-tippy-content={I18NextService.i18n.t("reply")}
447 aria-label={I18NextService.i18n.t("reply")}
449 <Icon icon="reply1" classes="icon-inline" />
451 {!this.state.showAdvanced ? (
453 className="btn btn-link btn-animate text-muted btn-more"
454 onClick={linkEvent(this, this.handleShowAdvanced)}
455 data-tippy-content={I18NextService.i18n.t("more")}
456 aria-label={I18NextService.i18n.t("more")}
458 <Icon icon="more-vertical" classes="icon-inline" />
462 {!this.myComment && (
465 className="btn btn-link btn-animate text-muted"
466 to={`/create_private_message/${cv.creator.id}`}
467 title={I18NextService.i18n
474 className="btn btn-link btn-animate text-muted"
477 this.handleShowReportDialog
479 data-tippy-content={I18NextService.i18n.t(
482 aria-label={I18NextService.i18n.t(
489 className="btn btn-link btn-animate text-muted"
492 this.handleBlockPerson
494 data-tippy-content={I18NextService.i18n.t(
497 aria-label={I18NextService.i18n.t("block_user")}
499 {this.state.blockPersonLoading ? (
502 <Icon icon="slash" />
508 className="btn btn-link btn-animate text-muted"
509 onClick={linkEvent(this, this.handleSaveComment)}
512 ? I18NextService.i18n.t("unsave")
513 : I18NextService.i18n.t("save")
517 ? I18NextService.i18n.t("unsave")
518 : I18NextService.i18n.t("save")
521 {this.state.saveLoading ? (
526 classes={`icon-inline ${
527 cv.saved && "text-warning"
533 className="btn btn-link btn-animate text-muted"
534 onClick={linkEvent(this, this.handleViewSource)}
535 data-tippy-content={I18NextService.i18n.t(
538 aria-label={I18NextService.i18n.t("view_source")}
542 classes={`icon-inline ${
543 this.state.viewSource && "text-success"
550 className="btn btn-link btn-animate text-muted"
551 onClick={linkEvent(this, this.handleEditClick)}
552 data-tippy-content={I18NextService.i18n.t(
555 aria-label={I18NextService.i18n.t("edit")}
557 <Icon icon="edit" classes="icon-inline" />
560 className="btn btn-link btn-animate text-muted"
563 this.handleDeleteComment
567 ? I18NextService.i18n.t("delete")
568 : I18NextService.i18n.t("restore")
572 ? I18NextService.i18n.t("delete")
573 : I18NextService.i18n.t("restore")
576 {this.state.deleteLoading ? (
581 classes={`icon-inline ${
582 cv.comment.deleted && "text-danger"
588 {(canModOnSelf || canAdminOnSelf) && (
590 className="btn btn-link btn-animate text-muted"
593 this.handleDistinguishComment
596 !cv.comment.distinguished
597 ? I18NextService.i18n.t("distinguish")
598 : I18NextService.i18n.t("undistinguish")
601 !cv.comment.distinguished
602 ? I18NextService.i18n.t("distinguish")
603 : I18NextService.i18n.t("undistinguish")
608 classes={`icon-inline ${
609 cv.comment.distinguished && "text-danger"
616 {/* Admins and mods can remove comments */}
617 {(canMod_ || canAdmin_) && (
619 {!cv.comment.removed ? (
621 className="btn btn-link btn-animate text-muted"
624 this.handleModRemoveShow
626 aria-label={I18NextService.i18n.t("remove")}
628 {I18NextService.i18n.t("remove")}
632 className="btn btn-link btn-animate text-muted"
635 this.handleRemoveComment
637 aria-label={I18NextService.i18n.t("restore")}
639 {this.state.removeLoading ? (
642 I18NextService.i18n.t("restore")
648 {/* Mods can ban from community, and appoint as mods to community */}
652 (!cv.creator_banned_from_community ? (
654 className="btn btn-link btn-animate text-muted"
657 this.handleModBanFromCommunityShow
659 aria-label={I18NextService.i18n.t(
663 {I18NextService.i18n.t(
669 className="btn btn-link btn-animate text-muted"
672 this.handleBanPersonFromCommunity
674 aria-label={I18NextService.i18n.t("unban")}
676 {this.state.banLoading ? (
679 I18NextService.i18n.t("unban")
683 {!cv.creator_banned_from_community &&
684 (!this.state.showConfirmAppointAsMod ? (
686 className="btn btn-link btn-animate text-muted"
689 this.handleShowConfirmAppointAsMod
693 ? I18NextService.i18n.t("remove_as_mod")
694 : I18NextService.i18n.t(
700 ? I18NextService.i18n.t("remove_as_mod")
701 : I18NextService.i18n.t("appoint_as_mod")}
706 className="btn btn-link btn-animate text-muted"
707 aria-label={I18NextService.i18n.t(
711 {I18NextService.i18n.t("are_you_sure")}
714 className="btn btn-link btn-animate text-muted"
717 this.handleAddModToCommunity
719 aria-label={I18NextService.i18n.t("yes")}
721 {this.state.addModLoading ? (
724 I18NextService.i18n.t("yes")
728 className="btn btn-link btn-animate text-muted"
731 this.handleCancelConfirmAppointAsMod
733 aria-label={I18NextService.i18n.t("no")}
735 {I18NextService.i18n.t("no")}
741 {/* Community creators and admins can transfer community to another mod */}
742 {(amCommunityCreator_ || canAdmin_) &&
745 (!this.state.showConfirmTransferCommunity ? (
747 className="btn btn-link btn-animate text-muted"
750 this.handleShowConfirmTransferCommunity
752 aria-label={I18NextService.i18n.t(
756 {I18NextService.i18n.t("transfer_community")}
761 className="btn btn-link btn-animate text-muted"
762 aria-label={I18NextService.i18n.t(
766 {I18NextService.i18n.t("are_you_sure")}
769 className="btn btn-link btn-animate text-muted"
772 this.handleTransferCommunity
774 aria-label={I18NextService.i18n.t("yes")}
776 {this.state.transferCommunityLoading ? (
779 I18NextService.i18n.t("yes")
783 className="btn btn-link btn-animate text-muted"
787 .handleCancelShowConfirmTransferCommunity
789 aria-label={I18NextService.i18n.t("no")}
791 {I18NextService.i18n.t("no")}
795 {/* Admins can ban from all, and appoint other admins */}
801 className="btn btn-link btn-animate text-muted"
804 this.handlePurgePersonShow
806 aria-label={I18NextService.i18n.t(
810 {I18NextService.i18n.t("purge_user")}
813 className="btn btn-link btn-animate text-muted"
816 this.handlePurgeCommentShow
818 aria-label={I18NextService.i18n.t(
822 {I18NextService.i18n.t("purge_comment")}
825 {!isBanned(cv.creator) ? (
827 className="btn btn-link btn-animate text-muted"
830 this.handleModBanShow
832 aria-label={I18NextService.i18n.t(
836 {I18NextService.i18n.t("ban_from_site")}
840 className="btn btn-link btn-animate text-muted"
845 aria-label={I18NextService.i18n.t(
849 {this.state.banLoading ? (
852 I18NextService.i18n.t("unban_from_site")
858 {!isBanned(cv.creator) &&
860 (!this.state.showConfirmAppointAsAdmin ? (
862 className="btn btn-link btn-animate text-muted"
865 this.handleShowConfirmAppointAsAdmin
869 ? I18NextService.i18n.t(
872 : I18NextService.i18n.t(
878 ? I18NextService.i18n.t("remove_as_admin")
879 : I18NextService.i18n.t(
885 <button className="btn btn-link btn-animate text-muted">
886 {I18NextService.i18n.t("are_you_sure")}
889 className="btn btn-link btn-animate text-muted"
894 aria-label={I18NextService.i18n.t("yes")}
896 {this.state.addAdminLoading ? (
899 I18NextService.i18n.t("yes")
903 className="btn btn-link btn-animate text-muted"
906 this.handleCancelConfirmAppointAsAdmin
908 aria-label={I18NextService.i18n.t("no")}
910 {I18NextService.i18n.t("no")}
921 {/* end of button group */}
926 {showMoreChildren && (
928 className={classNames("details ms-1 comment-node py-2", {
929 "border-top border-light": !this.props.noBorder,
931 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
934 className="btn btn-link text-muted"
935 onClick={linkEvent(this, this.handleFetchChildren)}
937 {this.state.fetchChildrenLoading ? (
941 {I18NextService.i18n.t("x_more_replies", {
942 count: node.comment_view.counts.child_count,
943 formattedCount: numToSI(
944 node.comment_view.counts.child_count
953 {/* end of details */}
954 {this.state.showRemoveDialog && (
956 className="form-inline"
957 onSubmit={linkEvent(this, this.handleRemoveComment)}
960 className="visually-hidden"
961 htmlFor={`mod-remove-reason-${cv.comment.id}`}
963 {I18NextService.i18n.t("reason")}
967 id={`mod-remove-reason-${cv.comment.id}`}
968 className="form-control me-2"
969 placeholder={I18NextService.i18n.t("reason")}
970 value={this.state.removeReason}
971 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
975 className="btn btn-secondary"
976 aria-label={I18NextService.i18n.t("remove_comment")}
978 {I18NextService.i18n.t("remove_comment")}
982 {this.state.showReportDialog && (
984 className="form-inline"
985 onSubmit={linkEvent(this, this.handleReportComment)}
988 className="visually-hidden"
989 htmlFor={`report-reason-${cv.comment.id}`}
991 {I18NextService.i18n.t("reason")}
996 id={`report-reason-${cv.comment.id}`}
997 className="form-control me-2"
998 placeholder={I18NextService.i18n.t("reason")}
999 value={this.state.reportReason}
1000 onInput={linkEvent(this, this.handleReportReasonChange)}
1004 className="btn btn-secondary"
1005 aria-label={I18NextService.i18n.t("create_report")}
1007 {I18NextService.i18n.t("create_report")}
1011 {this.state.showBanDialog && (
1012 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1013 <div className="mb-3 row col-12">
1015 className="col-form-label"
1016 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1018 {I18NextService.i18n.t("reason")}
1022 id={`mod-ban-reason-${cv.comment.id}`}
1023 className="form-control me-2"
1024 placeholder={I18NextService.i18n.t("reason")}
1025 value={this.state.banReason}
1026 onInput={linkEvent(this, this.handleModBanReasonChange)}
1029 className="col-form-label"
1030 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1032 {I18NextService.i18n.t("expires")}
1036 id={`mod-ban-expires-${cv.comment.id}`}
1037 className="form-control me-2"
1038 placeholder={I18NextService.i18n.t("number_of_days")}
1039 value={this.state.banExpireDays}
1040 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1042 <div className="input-group mb-3">
1043 <div className="form-check">
1045 className="form-check-input"
1046 id="mod-ban-remove-data"
1048 checked={this.state.removeData}
1049 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1052 className="form-check-label"
1053 htmlFor="mod-ban-remove-data"
1054 title={I18NextService.i18n.t("remove_content_more")}
1056 {I18NextService.i18n.t("remove_content")}
1061 {/* TODO hold off on expires until later */}
1062 {/* <div class="mb-3 row"> */}
1063 {/* <label class="col-form-label">Expires</label> */}
1064 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1066 <div className="mb-3 row">
1069 className="btn btn-secondary"
1070 aria-label={I18NextService.i18n.t("ban")}
1072 {this.state.banLoading ? (
1076 {I18NextService.i18n.t("ban")} {cv.creator.name}
1084 {this.state.showPurgeDialog && (
1085 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1087 <label className="visually-hidden" htmlFor="purge-reason">
1088 {I18NextService.i18n.t("reason")}
1093 className="form-control my-3"
1094 placeholder={I18NextService.i18n.t("reason")}
1095 value={this.state.purgeReason}
1096 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1098 <div className="mb-3 row col-12">
1099 {this.state.purgeLoading ? (
1104 className="btn btn-secondary"
1105 aria-label={purgeTypeText}
1113 {this.state.showReply && (
1116 onReplyCancel={this.handleReplyCancel}
1117 disabled={this.props.locked}
1118 finished={this.props.finished.get(
1119 this.props.node.comment_view.comment.id
1122 allLanguages={this.props.allLanguages}
1123 siteLanguages={this.props.siteLanguages}
1124 containerClass="comment-comment-container"
1125 onUpsertComment={this.props.onCreateComment}
1128 {!this.state.collapsed && node.children.length > 0 && (
1130 nodes={node.children}
1131 locked={this.props.locked}
1132 moderators={this.props.moderators}
1133 admins={this.props.admins}
1134 enableDownvotes={this.props.enableDownvotes}
1135 viewType={this.props.viewType}
1136 allLanguages={this.props.allLanguages}
1137 siteLanguages={this.props.siteLanguages}
1138 hideImages={this.props.hideImages}
1139 isChild={!this.props.noIndent}
1140 depth={this.props.node.depth + 1}
1141 finished={this.props.finished}
1142 onCommentReplyRead={this.props.onCommentReplyRead}
1143 onPersonMentionRead={this.props.onPersonMentionRead}
1144 onCreateComment={this.props.onCreateComment}
1145 onEditComment={this.props.onEditComment}
1146 onCommentVote={this.props.onCommentVote}
1147 onBlockPerson={this.props.onBlockPerson}
1148 onSaveComment={this.props.onSaveComment}
1149 onDeleteComment={this.props.onDeleteComment}
1150 onRemoveComment={this.props.onRemoveComment}
1151 onDistinguishComment={this.props.onDistinguishComment}
1152 onAddModToCommunity={this.props.onAddModToCommunity}
1153 onAddAdmin={this.props.onAddAdmin}
1154 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1155 onBanPerson={this.props.onBanPerson}
1156 onTransferCommunity={this.props.onTransferCommunity}
1157 onFetchChildren={this.props.onFetchChildren}
1158 onCommentReport={this.props.onCommentReport}
1159 onPurgePerson={this.props.onPurgePerson}
1160 onPurgeComment={this.props.onPurgeComment}
1163 {/* A collapsed clearfix */}
1164 {this.state.collapsed && <div className="row col-12" />}
1169 get commentReplyOrMentionRead(): boolean {
1170 const cv = this.commentView;
1172 if (this.isPersonMentionType(cv)) {
1173 return cv.person_mention.read;
1174 } else if (this.isCommentReplyType(cv)) {
1175 return cv.comment_reply.read;
1181 getLinkButton(small = false) {
1182 const cv = this.commentView;
1184 const classnames = classNames("btn btn-link btn-animate text-muted", {
1188 const title = this.props.showContext
1189 ? I18NextService.i18n.t("show_context")
1190 : I18NextService.i18n.t("link");
1192 // The context button should show the parent comment by default
1193 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1198 className={classnames}
1199 to={`/comment/${parentCommentId}`}
1202 <Icon icon="link" classes="icon-inline" />
1205 <a className={classnames} title={title} href={cv.comment.ap_id}>
1206 <Icon icon="fedilink" classes="icon-inline" />
1213 get myComment(): boolean {
1215 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1216 this.commentView.creator.id
1220 get isPostCreator(): boolean {
1221 return this.commentView.creator.id == this.commentView.post.creator_id;
1225 if (this.commentView.my_vote == 1) {
1227 } else if (this.commentView.my_vote == -1) {
1228 return "text-danger";
1230 return "text-muted";
1234 get pointsTippy(): string {
1235 const points = I18NextService.i18n.t("number_of_points", {
1236 count: Number(this.commentView.counts.score),
1237 formattedCount: numToSI(this.commentView.counts.score),
1240 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1241 count: Number(this.commentView.counts.upvotes),
1242 formattedCount: numToSI(this.commentView.counts.upvotes),
1245 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1246 count: Number(this.commentView.counts.downvotes),
1247 formattedCount: numToSI(this.commentView.counts.downvotes),
1250 return `${points} • ${upvotes} • ${downvotes}`;
1253 get expandText(): string {
1254 return this.state.collapsed
1255 ? I18NextService.i18n.t("expand")
1256 : I18NextService.i18n.t("collapse");
1259 get commentUnlessRemoved(): string {
1260 const comment = this.commentView.comment;
1261 return comment.removed
1262 ? `*${I18NextService.i18n.t("removed")}*`
1264 ? `*${I18NextService.i18n.t("deleted")}*`
1268 handleReplyClick(i: CommentNode) {
1269 i.setState({ showReply: true });
1272 handleEditClick(i: CommentNode) {
1273 i.setState({ showEdit: true });
1276 handleReplyCancel() {
1277 this.setState({ showReply: false, showEdit: false });
1280 handleShowReportDialog(i: CommentNode) {
1281 i.setState({ showReportDialog: !i.state.showReportDialog });
1284 handleReportReasonChange(i: CommentNode, event: any) {
1285 i.setState({ reportReason: event.target.value });
1288 handleModRemoveShow(i: CommentNode) {
1290 showRemoveDialog: !i.state.showRemoveDialog,
1291 showBanDialog: false,
1295 handleModRemoveReasonChange(i: CommentNode, event: any) {
1296 i.setState({ removeReason: event.target.value });
1299 handleModRemoveDataChange(i: CommentNode, event: any) {
1300 i.setState({ removeData: event.target.checked });
1303 isPersonMentionType(
1304 item: CommentView | PersonMentionView | CommentReplyView
1305 ): item is PersonMentionView {
1306 return (item as PersonMentionView).person_mention?.id !== undefined;
1310 item: CommentView | PersonMentionView | CommentReplyView
1311 ): item is CommentReplyView {
1312 return (item as CommentReplyView).comment_reply?.id !== undefined;
1315 handleModBanFromCommunityShow(i: CommentNode) {
1317 showBanDialog: true,
1318 banType: BanType.Community,
1319 showRemoveDialog: false,
1323 handleModBanShow(i: CommentNode) {
1325 showBanDialog: true,
1326 banType: BanType.Site,
1327 showRemoveDialog: false,
1331 handleModBanReasonChange(i: CommentNode, event: any) {
1332 i.setState({ banReason: event.target.value });
1335 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1336 i.setState({ banExpireDays: event.target.value });
1339 handlePurgePersonShow(i: CommentNode) {
1341 showPurgeDialog: true,
1342 purgeType: PurgeType.Person,
1343 showRemoveDialog: false,
1347 handlePurgeCommentShow(i: CommentNode) {
1349 showPurgeDialog: true,
1350 purgeType: PurgeType.Comment,
1351 showRemoveDialog: false,
1355 handlePurgeReasonChange(i: CommentNode, event: any) {
1356 i.setState({ purgeReason: event.target.value });
1359 handleShowConfirmAppointAsMod(i: CommentNode) {
1360 i.setState({ showConfirmAppointAsMod: true });
1363 handleCancelConfirmAppointAsMod(i: CommentNode) {
1364 i.setState({ showConfirmAppointAsMod: false });
1367 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1368 i.setState({ showConfirmAppointAsAdmin: true });
1371 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1372 i.setState({ showConfirmAppointAsAdmin: false });
1375 handleShowConfirmTransferCommunity(i: CommentNode) {
1376 i.setState({ showConfirmTransferCommunity: true });
1379 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1380 i.setState({ showConfirmTransferCommunity: false });
1383 handleShowConfirmTransferSite(i: CommentNode) {
1384 i.setState({ showConfirmTransferSite: true });
1387 handleCancelShowConfirmTransferSite(i: CommentNode) {
1388 i.setState({ showConfirmTransferSite: false });
1391 get isCommentNew(): boolean {
1392 const now = subMinutes(new Date(), 10);
1393 const then = parseISO(this.commentView.comment.published);
1394 return isBefore(now, then);
1397 handleCommentCollapse(i: CommentNode) {
1398 i.setState({ collapsed: !i.state.collapsed });
1402 handleViewSource(i: CommentNode) {
1403 i.setState({ viewSource: !i.state.viewSource });
1406 handleShowAdvanced(i: CommentNode) {
1407 i.setState({ showAdvanced: !i.state.showAdvanced });
1411 handleSaveComment(i: CommentNode) {
1412 i.setState({ saveLoading: true });
1414 i.props.onSaveComment({
1415 comment_id: i.commentView.comment.id,
1416 save: !i.commentView.saved,
1417 auth: myAuthRequired(),
1421 handleBlockPerson(i: CommentNode) {
1422 i.setState({ blockPersonLoading: true });
1423 i.props.onBlockPerson({
1424 person_id: i.commentView.creator.id,
1426 auth: myAuthRequired(),
1430 handleMarkAsRead(i: CommentNode) {
1431 i.setState({ readLoading: true });
1432 const cv = i.commentView;
1433 if (i.isPersonMentionType(cv)) {
1434 i.props.onPersonMentionRead({
1435 person_mention_id: cv.person_mention.id,
1436 read: !cv.person_mention.read,
1437 auth: myAuthRequired(),
1439 } else if (i.isCommentReplyType(cv)) {
1440 i.props.onCommentReplyRead({
1441 comment_reply_id: cv.comment_reply.id,
1442 read: !cv.comment_reply.read,
1443 auth: myAuthRequired(),
1448 handleDeleteComment(i: CommentNode) {
1449 i.setState({ deleteLoading: true });
1450 i.props.onDeleteComment({
1451 comment_id: i.commentId,
1452 deleted: !i.commentView.comment.deleted,
1453 auth: myAuthRequired(),
1457 handleRemoveComment(i: CommentNode, event: any) {
1458 event.preventDefault();
1459 i.setState({ removeLoading: true });
1460 i.props.onRemoveComment({
1461 comment_id: i.commentId,
1462 removed: !i.commentView.comment.removed,
1463 auth: myAuthRequired(),
1467 handleDistinguishComment(i: CommentNode) {
1468 i.setState({ distinguishLoading: true });
1469 i.props.onDistinguishComment({
1470 comment_id: i.commentId,
1471 distinguished: !i.commentView.comment.distinguished,
1472 auth: myAuthRequired(),
1476 handleBanPersonFromCommunity(i: CommentNode) {
1477 i.setState({ banLoading: true });
1478 i.props.onBanPersonFromCommunity({
1479 community_id: i.commentView.community.id,
1480 person_id: i.commentView.creator.id,
1481 ban: !i.commentView.creator_banned_from_community,
1482 reason: i.state.banReason,
1483 remove_data: i.state.removeData,
1484 expires: futureDaysToUnixTime(i.state.banExpireDays),
1485 auth: myAuthRequired(),
1489 handleBanPerson(i: CommentNode) {
1490 i.setState({ banLoading: true });
1491 i.props.onBanPerson({
1492 person_id: i.commentView.creator.id,
1493 ban: !i.commentView.creator_banned_from_community,
1494 reason: i.state.banReason,
1495 remove_data: i.state.removeData,
1496 expires: futureDaysToUnixTime(i.state.banExpireDays),
1497 auth: myAuthRequired(),
1501 handleModBanBothSubmit(i: CommentNode, event: any) {
1502 event.preventDefault();
1503 if (i.state.banType == BanType.Community) {
1504 i.handleBanPersonFromCommunity(i);
1506 i.handleBanPerson(i);
1510 handleAddModToCommunity(i: CommentNode) {
1511 i.setState({ addModLoading: true });
1513 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1514 i.props.onAddModToCommunity({
1515 community_id: i.commentView.community.id,
1516 person_id: i.commentView.creator.id,
1518 auth: myAuthRequired(),
1522 handleAddAdmin(i: CommentNode) {
1523 i.setState({ addAdminLoading: true });
1525 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1526 i.props.onAddAdmin({
1527 person_id: i.commentView.creator.id,
1529 auth: myAuthRequired(),
1533 handleTransferCommunity(i: CommentNode) {
1534 i.setState({ transferCommunityLoading: true });
1535 i.props.onTransferCommunity({
1536 community_id: i.commentView.community.id,
1537 person_id: i.commentView.creator.id,
1538 auth: myAuthRequired(),
1542 handleReportComment(i: CommentNode, event: any) {
1543 event.preventDefault();
1544 i.setState({ reportLoading: true });
1545 i.props.onCommentReport({
1546 comment_id: i.commentId,
1547 reason: i.state.reportReason ?? "",
1548 auth: myAuthRequired(),
1552 handlePurgeBothSubmit(i: CommentNode, event: any) {
1553 event.preventDefault();
1554 i.setState({ purgeLoading: true });
1556 if (i.state.purgeType == PurgeType.Person) {
1557 i.props.onPurgePerson({
1558 person_id: i.commentView.creator.id,
1559 reason: i.state.purgeReason,
1560 auth: myAuthRequired(),
1563 i.props.onPurgeComment({
1564 comment_id: i.commentId,
1565 reason: i.state.purgeReason,
1566 auth: myAuthRequired(),
1571 handleFetchChildren(i: CommentNode) {
1572 i.setState({ fetchChildrenLoading: true });
1573 i.props.onFetchChildren?.({
1574 parent_id: i.commentId,
1575 max_depth: commentTreeMaxDepth,