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 get hasBadges(): boolean {
201 const cv = this.commentView;
204 this.isPostCreator ||
205 isMod(cv.creator.id, this.props.moderators) ||
206 isAdmin(cv.creator.id, this.props.admins) ||
207 cv.creator.bot_account
211 componentWillReceiveProps(
212 nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
214 if (!deepEqual(this.props, nextProps)) {
218 showRemoveDialog: false,
219 showBanDialog: false,
221 banType: BanType.Community,
222 showPurgeDialog: false,
223 purgeType: PurgeType.Person,
227 showConfirmTransferSite: false,
228 showConfirmTransferCommunity: false,
229 showConfirmAppointAsMod: false,
230 showConfirmAppointAsAdmin: false,
231 showReportDialog: false,
232 createOrEditCommentLoading: false,
233 upvoteLoading: false,
234 downvoteLoading: false,
237 blockPersonLoading: false,
238 deleteLoading: false,
239 removeLoading: false,
240 distinguishLoading: false,
242 addModLoading: false,
243 addAdminLoading: false,
244 transferCommunityLoading: false,
245 fetchChildrenLoading: false,
246 reportLoading: false,
253 const node = this.props.node;
254 const cv = this.commentView;
256 const purgeTypeText =
257 this.state.purgeType == PurgeType.Comment
258 ? I18NextService.i18n.t("purge_comment")
259 : `${I18NextService.i18n.t("purge")} ${cv.creator.name}`;
261 const canMod_ = canMod(
263 this.props.moderators,
266 const canModOnSelf = canMod(
268 this.props.moderators,
270 UserService.Instance.myUserInfo,
273 const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
274 const canAdminOnSelf = canAdmin(
277 UserService.Instance.myUserInfo,
280 const isMod_ = isMod(cv.creator.id, this.props.moderators);
281 const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
282 const amCommunityCreator_ = amCommunityCreator(
284 this.props.moderators
287 const moreRepliesBorderColor = this.props.node.depth
288 ? colorList[this.props.node.depth % colorList.length]
291 const showMoreChildren =
292 this.props.viewType == CommentViewType.Tree &&
293 !this.state.collapsed &&
294 node.children.length == 0 &&
295 node.comment_view.counts.child_count > 0;
298 <li className="comment">
300 id={`comment-${cv.comment.id}`}
301 className={classNames(`details comment-node py-2`, {
302 "border-top border-light": !this.props.noBorder,
303 mark: this.isCommentNew || this.commentView.comment.distinguished,
307 className={classNames({
308 "ms-2": !this.props.noIndent,
311 <div className="d-flex flex-wrap align-items-center text-muted small">
313 className="btn btn-sm text-muted me-2"
314 onClick={linkEvent(this, this.handleCommentCollapse)}
315 aria-label={this.expandText}
316 data-tippy-content={this.expandText}
319 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
320 classes="icon-inline"
324 <PersonListing person={cv.creator} />
326 {cv.comment.distinguished && (
327 <Icon icon="shield" inline classes="text-danger ms-1" />
332 isPostCreator={this.isPostCreator}
335 isBot={cv.creator.bot_account}
338 {this.props.showCommunity && (
340 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
341 <CommunityLink community={cv.community} />
342 <span className="mx-2">•</span>
343 <Link className="me-2" to={`/post/${cv.post.id}`}>
349 {this.getLinkButton(true)}
351 {cv.comment.language_id !== 0 && (
352 <span className="badge text-bg-light d-none d-sm-inline me-2">
354 this.props.allLanguages.find(
355 lang => lang.id === cv.comment.language_id
360 {/* This is an expanding spacer for mobile */}
361 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
366 className="me-1 fw-bold"
367 aria-label={I18NextService.i18n.t("number_of_points", {
368 count: Number(this.commentView.counts.score),
369 formattedCount: numToSI(this.commentView.counts.score),
372 {numToSI(this.commentView.counts.score)}
374 <span className="me-1">•</span>
379 published={cv.comment.published}
380 updated={cv.comment.updated}
384 {/* end of user row */}
385 {this.state.showEdit && (
389 onReplyCancel={this.handleReplyCancel}
390 disabled={this.props.locked}
391 finished={this.props.finished.get(
392 this.props.node.comment_view.comment.id
395 allLanguages={this.props.allLanguages}
396 siteLanguages={this.props.siteLanguages}
397 containerClass="comment-comment-container"
398 onUpsertComment={this.props.onEditComment}
401 {!this.state.showEdit && !this.state.collapsed && (
403 {this.state.viewSource ? (
404 <pre>{this.commentUnlessRemoved}</pre>
408 dangerouslySetInnerHTML={
409 this.props.hideImages
410 ? mdToHtmlNoImages(this.commentUnlessRemoved)
411 : mdToHtml(this.commentUnlessRemoved)
415 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold">
416 {this.props.showContext && this.getLinkButton()}
417 {this.props.markable && (
419 className="btn btn-link btn-animate text-muted"
420 onClick={linkEvent(this, this.handleMarkAsRead)}
422 this.commentReplyOrMentionRead
423 ? I18NextService.i18n.t("mark_as_unread")
424 : I18NextService.i18n.t("mark_as_read")
427 this.commentReplyOrMentionRead
428 ? I18NextService.i18n.t("mark_as_unread")
429 : I18NextService.i18n.t("mark_as_read")
432 {this.state.readLoading ? (
437 classes={`icon-inline ${
438 this.commentReplyOrMentionRead && "text-success"
444 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
447 voteContentType={VoteContentType.Comment}
448 id={this.commentView.comment.id}
449 onVote={this.props.onCommentVote}
450 enableDownvotes={this.props.enableDownvotes}
451 counts={this.commentView.counts}
452 my_vote={this.commentView.my_vote}
455 className="btn btn-link btn-animate text-muted"
456 onClick={linkEvent(this, this.handleReplyClick)}
457 data-tippy-content={I18NextService.i18n.t("reply")}
458 aria-label={I18NextService.i18n.t("reply")}
460 <Icon icon="reply1" classes="icon-inline" />
462 {!this.state.showAdvanced ? (
464 className="btn btn-link btn-animate text-muted btn-more"
465 onClick={linkEvent(this, this.handleShowAdvanced)}
466 data-tippy-content={I18NextService.i18n.t("more")}
467 aria-label={I18NextService.i18n.t("more")}
469 <Icon icon="more-vertical" classes="icon-inline" />
473 {!this.myComment && (
476 className="btn btn-link btn-animate text-muted"
477 to={`/create_private_message/${cv.creator.id}`}
478 title={I18NextService.i18n
485 className="btn btn-link btn-animate text-muted"
488 this.handleShowReportDialog
490 data-tippy-content={I18NextService.i18n.t(
493 aria-label={I18NextService.i18n.t(
500 className="btn btn-link btn-animate text-muted"
503 this.handleBlockPerson
505 data-tippy-content={I18NextService.i18n.t(
508 aria-label={I18NextService.i18n.t("block_user")}
510 {this.state.blockPersonLoading ? (
513 <Icon icon="slash" />
519 className="btn btn-link btn-animate text-muted"
520 onClick={linkEvent(this, this.handleSaveComment)}
523 ? I18NextService.i18n.t("unsave")
524 : I18NextService.i18n.t("save")
528 ? I18NextService.i18n.t("unsave")
529 : I18NextService.i18n.t("save")
532 {this.state.saveLoading ? (
537 classes={`icon-inline ${
538 cv.saved && "text-warning"
544 className="btn btn-link btn-animate text-muted"
545 onClick={linkEvent(this, this.handleViewSource)}
546 data-tippy-content={I18NextService.i18n.t(
549 aria-label={I18NextService.i18n.t("view_source")}
553 classes={`icon-inline ${
554 this.state.viewSource && "text-success"
561 className="btn btn-link btn-animate text-muted"
562 onClick={linkEvent(this, this.handleEditClick)}
563 data-tippy-content={I18NextService.i18n.t(
566 aria-label={I18NextService.i18n.t("edit")}
568 <Icon icon="edit" classes="icon-inline" />
571 className="btn btn-link btn-animate text-muted"
574 this.handleDeleteComment
578 ? I18NextService.i18n.t("delete")
579 : I18NextService.i18n.t("restore")
583 ? I18NextService.i18n.t("delete")
584 : I18NextService.i18n.t("restore")
587 {this.state.deleteLoading ? (
592 classes={`icon-inline ${
593 cv.comment.deleted && "text-danger"
599 {(canModOnSelf || canAdminOnSelf) && (
601 className="btn btn-link btn-animate text-muted"
604 this.handleDistinguishComment
607 !cv.comment.distinguished
608 ? I18NextService.i18n.t("distinguish")
609 : I18NextService.i18n.t("undistinguish")
612 !cv.comment.distinguished
613 ? I18NextService.i18n.t("distinguish")
614 : I18NextService.i18n.t("undistinguish")
619 classes={`icon-inline ${
620 cv.comment.distinguished && "text-danger"
627 {/* Admins and mods can remove comments */}
628 {(canMod_ || canAdmin_) && (
630 {!cv.comment.removed ? (
632 className="btn btn-link btn-animate text-muted"
635 this.handleModRemoveShow
637 aria-label={I18NextService.i18n.t("remove")}
639 {I18NextService.i18n.t("remove")}
643 className="btn btn-link btn-animate text-muted"
646 this.handleRemoveComment
648 aria-label={I18NextService.i18n.t("restore")}
650 {this.state.removeLoading ? (
653 I18NextService.i18n.t("restore")
659 {/* Mods can ban from community, and appoint as mods to community */}
663 (!cv.creator_banned_from_community ? (
665 className="btn btn-link btn-animate text-muted"
668 this.handleModBanFromCommunityShow
670 aria-label={I18NextService.i18n.t(
674 {I18NextService.i18n.t(
680 className="btn btn-link btn-animate text-muted"
683 this.handleBanPersonFromCommunity
685 aria-label={I18NextService.i18n.t("unban")}
687 {this.state.banLoading ? (
690 I18NextService.i18n.t("unban")
694 {!cv.creator_banned_from_community &&
695 (!this.state.showConfirmAppointAsMod ? (
697 className="btn btn-link btn-animate text-muted"
700 this.handleShowConfirmAppointAsMod
704 ? I18NextService.i18n.t("remove_as_mod")
705 : I18NextService.i18n.t(
711 ? I18NextService.i18n.t("remove_as_mod")
712 : I18NextService.i18n.t("appoint_as_mod")}
717 className="btn btn-link btn-animate text-muted"
718 aria-label={I18NextService.i18n.t(
722 {I18NextService.i18n.t("are_you_sure")}
725 className="btn btn-link btn-animate text-muted"
728 this.handleAddModToCommunity
730 aria-label={I18NextService.i18n.t("yes")}
732 {this.state.addModLoading ? (
735 I18NextService.i18n.t("yes")
739 className="btn btn-link btn-animate text-muted"
742 this.handleCancelConfirmAppointAsMod
744 aria-label={I18NextService.i18n.t("no")}
746 {I18NextService.i18n.t("no")}
752 {/* Community creators and admins can transfer community to another mod */}
753 {(amCommunityCreator_ || canAdmin_) &&
756 (!this.state.showConfirmTransferCommunity ? (
758 className="btn btn-link btn-animate text-muted"
761 this.handleShowConfirmTransferCommunity
763 aria-label={I18NextService.i18n.t(
767 {I18NextService.i18n.t("transfer_community")}
772 className="btn btn-link btn-animate text-muted"
773 aria-label={I18NextService.i18n.t(
777 {I18NextService.i18n.t("are_you_sure")}
780 className="btn btn-link btn-animate text-muted"
783 this.handleTransferCommunity
785 aria-label={I18NextService.i18n.t("yes")}
787 {this.state.transferCommunityLoading ? (
790 I18NextService.i18n.t("yes")
794 className="btn btn-link btn-animate text-muted"
798 .handleCancelShowConfirmTransferCommunity
800 aria-label={I18NextService.i18n.t("no")}
802 {I18NextService.i18n.t("no")}
806 {/* Admins can ban from all, and appoint other admins */}
812 className="btn btn-link btn-animate text-muted"
815 this.handlePurgePersonShow
817 aria-label={I18NextService.i18n.t(
821 {I18NextService.i18n.t("purge_user")}
824 className="btn btn-link btn-animate text-muted"
827 this.handlePurgeCommentShow
829 aria-label={I18NextService.i18n.t(
833 {I18NextService.i18n.t("purge_comment")}
836 {!isBanned(cv.creator) ? (
838 className="btn btn-link btn-animate text-muted"
841 this.handleModBanShow
843 aria-label={I18NextService.i18n.t(
847 {I18NextService.i18n.t("ban_from_site")}
851 className="btn btn-link btn-animate text-muted"
856 aria-label={I18NextService.i18n.t(
860 {this.state.banLoading ? (
863 I18NextService.i18n.t("unban_from_site")
869 {!isBanned(cv.creator) &&
871 (!this.state.showConfirmAppointAsAdmin ? (
873 className="btn btn-link btn-animate text-muted"
876 this.handleShowConfirmAppointAsAdmin
880 ? I18NextService.i18n.t(
883 : I18NextService.i18n.t(
889 ? I18NextService.i18n.t("remove_as_admin")
890 : I18NextService.i18n.t(
896 <button className="btn btn-link btn-animate text-muted">
897 {I18NextService.i18n.t("are_you_sure")}
900 className="btn btn-link btn-animate text-muted"
905 aria-label={I18NextService.i18n.t("yes")}
907 {this.state.addAdminLoading ? (
910 I18NextService.i18n.t("yes")
914 className="btn btn-link btn-animate text-muted"
917 this.handleCancelConfirmAppointAsAdmin
919 aria-label={I18NextService.i18n.t("no")}
921 {I18NextService.i18n.t("no")}
932 {/* end of button group */}
937 {showMoreChildren && (
939 className={classNames("details ms-1 comment-node py-2", {
940 "border-top border-light": !this.props.noBorder,
942 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
945 className="btn btn-link text-muted"
946 onClick={linkEvent(this, this.handleFetchChildren)}
948 {this.state.fetchChildrenLoading ? (
952 {I18NextService.i18n.t("x_more_replies", {
953 count: node.comment_view.counts.child_count,
954 formattedCount: numToSI(
955 node.comment_view.counts.child_count
964 {/* end of details */}
965 {this.state.showRemoveDialog && (
967 className="form-inline"
968 onSubmit={linkEvent(this, this.handleRemoveComment)}
971 className="visually-hidden"
972 htmlFor={`mod-remove-reason-${cv.comment.id}`}
974 {I18NextService.i18n.t("reason")}
978 id={`mod-remove-reason-${cv.comment.id}`}
979 className="form-control me-2"
980 placeholder={I18NextService.i18n.t("reason")}
981 value={this.state.removeReason}
982 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
986 className="btn btn-secondary"
987 aria-label={I18NextService.i18n.t("remove_comment")}
989 {I18NextService.i18n.t("remove_comment")}
993 {this.state.showReportDialog && (
995 className="form-inline"
996 onSubmit={linkEvent(this, this.handleReportComment)}
999 className="visually-hidden"
1000 htmlFor={`report-reason-${cv.comment.id}`}
1002 {I18NextService.i18n.t("reason")}
1007 id={`report-reason-${cv.comment.id}`}
1008 className="form-control me-2"
1009 placeholder={I18NextService.i18n.t("reason")}
1010 value={this.state.reportReason}
1011 onInput={linkEvent(this, this.handleReportReasonChange)}
1015 className="btn btn-secondary"
1016 aria-label={I18NextService.i18n.t("create_report")}
1018 {I18NextService.i18n.t("create_report")}
1022 {this.state.showBanDialog && (
1023 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1024 <div className="mb-3 row col-12">
1026 className="col-form-label"
1027 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1029 {I18NextService.i18n.t("reason")}
1033 id={`mod-ban-reason-${cv.comment.id}`}
1034 className="form-control me-2"
1035 placeholder={I18NextService.i18n.t("reason")}
1036 value={this.state.banReason}
1037 onInput={linkEvent(this, this.handleModBanReasonChange)}
1040 className="col-form-label"
1041 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1043 {I18NextService.i18n.t("expires")}
1047 id={`mod-ban-expires-${cv.comment.id}`}
1048 className="form-control me-2"
1049 placeholder={I18NextService.i18n.t("number_of_days")}
1050 value={this.state.banExpireDays}
1051 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1053 <div className="input-group mb-3">
1054 <div className="form-check">
1056 className="form-check-input"
1057 id="mod-ban-remove-data"
1059 checked={this.state.removeData}
1060 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1063 className="form-check-label"
1064 htmlFor="mod-ban-remove-data"
1065 title={I18NextService.i18n.t("remove_content_more")}
1067 {I18NextService.i18n.t("remove_content")}
1072 {/* TODO hold off on expires until later */}
1073 {/* <div class="mb-3 row"> */}
1074 {/* <label class="col-form-label">Expires</label> */}
1075 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1077 <div className="mb-3 row">
1080 className="btn btn-secondary"
1081 aria-label={I18NextService.i18n.t("ban")}
1083 {this.state.banLoading ? (
1087 {I18NextService.i18n.t("ban")} {cv.creator.name}
1095 {this.state.showPurgeDialog && (
1096 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1098 <label className="visually-hidden" htmlFor="purge-reason">
1099 {I18NextService.i18n.t("reason")}
1104 className="form-control my-3"
1105 placeholder={I18NextService.i18n.t("reason")}
1106 value={this.state.purgeReason}
1107 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1109 <div className="mb-3 row col-12">
1110 {this.state.purgeLoading ? (
1115 className="btn btn-secondary"
1116 aria-label={purgeTypeText}
1124 {this.state.showReply && (
1127 onReplyCancel={this.handleReplyCancel}
1128 disabled={this.props.locked}
1129 finished={this.props.finished.get(
1130 this.props.node.comment_view.comment.id
1133 allLanguages={this.props.allLanguages}
1134 siteLanguages={this.props.siteLanguages}
1135 containerClass="comment-comment-container"
1136 onUpsertComment={this.props.onCreateComment}
1139 {!this.state.collapsed && node.children.length > 0 && (
1141 nodes={node.children}
1142 locked={this.props.locked}
1143 moderators={this.props.moderators}
1144 admins={this.props.admins}
1145 enableDownvotes={this.props.enableDownvotes}
1146 viewType={this.props.viewType}
1147 allLanguages={this.props.allLanguages}
1148 siteLanguages={this.props.siteLanguages}
1149 hideImages={this.props.hideImages}
1150 isChild={!this.props.noIndent}
1151 depth={this.props.node.depth + 1}
1152 finished={this.props.finished}
1153 onCommentReplyRead={this.props.onCommentReplyRead}
1154 onPersonMentionRead={this.props.onPersonMentionRead}
1155 onCreateComment={this.props.onCreateComment}
1156 onEditComment={this.props.onEditComment}
1157 onCommentVote={this.props.onCommentVote}
1158 onBlockPerson={this.props.onBlockPerson}
1159 onSaveComment={this.props.onSaveComment}
1160 onDeleteComment={this.props.onDeleteComment}
1161 onRemoveComment={this.props.onRemoveComment}
1162 onDistinguishComment={this.props.onDistinguishComment}
1163 onAddModToCommunity={this.props.onAddModToCommunity}
1164 onAddAdmin={this.props.onAddAdmin}
1165 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1166 onBanPerson={this.props.onBanPerson}
1167 onTransferCommunity={this.props.onTransferCommunity}
1168 onFetchChildren={this.props.onFetchChildren}
1169 onCommentReport={this.props.onCommentReport}
1170 onPurgePerson={this.props.onPurgePerson}
1171 onPurgeComment={this.props.onPurgeComment}
1174 {/* A collapsed clearfix */}
1175 {this.state.collapsed && <div className="row col-12" />}
1180 get commentReplyOrMentionRead(): boolean {
1181 const cv = this.commentView;
1183 if (this.isPersonMentionType(cv)) {
1184 return cv.person_mention.read;
1185 } else if (this.isCommentReplyType(cv)) {
1186 return cv.comment_reply.read;
1192 getLinkButton(small = false) {
1193 const cv = this.commentView;
1195 const classnames = classNames("btn btn-link btn-animate text-muted", {
1199 const title = this.props.showContext
1200 ? I18NextService.i18n.t("show_context")
1201 : I18NextService.i18n.t("link");
1203 // The context button should show the parent comment by default
1204 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1209 className={classnames}
1210 to={`/comment/${parentCommentId}`}
1213 <Icon icon="link" classes="icon-inline" />
1216 <a className={classnames} title={title} href={cv.comment.ap_id}>
1217 <Icon icon="fedilink" classes="icon-inline" />
1224 get myComment(): boolean {
1226 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1227 this.commentView.creator.id
1231 get isPostCreator(): boolean {
1232 return this.commentView.creator.id == this.commentView.post.creator_id;
1236 if (this.commentView.my_vote == 1) {
1238 } else if (this.commentView.my_vote == -1) {
1239 return "text-danger";
1241 return "text-muted";
1245 get pointsTippy(): string {
1246 const points = I18NextService.i18n.t("number_of_points", {
1247 count: Number(this.commentView.counts.score),
1248 formattedCount: numToSI(this.commentView.counts.score),
1251 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1252 count: Number(this.commentView.counts.upvotes),
1253 formattedCount: numToSI(this.commentView.counts.upvotes),
1256 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1257 count: Number(this.commentView.counts.downvotes),
1258 formattedCount: numToSI(this.commentView.counts.downvotes),
1261 return `${points} • ${upvotes} • ${downvotes}`;
1264 get expandText(): string {
1265 return this.state.collapsed
1266 ? I18NextService.i18n.t("expand")
1267 : I18NextService.i18n.t("collapse");
1270 get commentUnlessRemoved(): string {
1271 const comment = this.commentView.comment;
1272 return comment.removed
1273 ? `*${I18NextService.i18n.t("removed")}*`
1275 ? `*${I18NextService.i18n.t("deleted")}*`
1279 handleReplyClick(i: CommentNode) {
1280 i.setState({ showReply: true });
1283 handleEditClick(i: CommentNode) {
1284 i.setState({ showEdit: true });
1287 handleReplyCancel() {
1288 this.setState({ showReply: false, showEdit: false });
1291 handleShowReportDialog(i: CommentNode) {
1292 i.setState({ showReportDialog: !i.state.showReportDialog });
1295 handleReportReasonChange(i: CommentNode, event: any) {
1296 i.setState({ reportReason: event.target.value });
1299 handleModRemoveShow(i: CommentNode) {
1301 showRemoveDialog: !i.state.showRemoveDialog,
1302 showBanDialog: false,
1306 handleModRemoveReasonChange(i: CommentNode, event: any) {
1307 i.setState({ removeReason: event.target.value });
1310 handleModRemoveDataChange(i: CommentNode, event: any) {
1311 i.setState({ removeData: event.target.checked });
1314 isPersonMentionType(
1315 item: CommentView | PersonMentionView | CommentReplyView
1316 ): item is PersonMentionView {
1317 return (item as PersonMentionView).person_mention?.id !== undefined;
1321 item: CommentView | PersonMentionView | CommentReplyView
1322 ): item is CommentReplyView {
1323 return (item as CommentReplyView).comment_reply?.id !== undefined;
1326 handleModBanFromCommunityShow(i: CommentNode) {
1328 showBanDialog: true,
1329 banType: BanType.Community,
1330 showRemoveDialog: false,
1334 handleModBanShow(i: CommentNode) {
1336 showBanDialog: true,
1337 banType: BanType.Site,
1338 showRemoveDialog: false,
1342 handleModBanReasonChange(i: CommentNode, event: any) {
1343 i.setState({ banReason: event.target.value });
1346 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1347 i.setState({ banExpireDays: event.target.value });
1350 handlePurgePersonShow(i: CommentNode) {
1352 showPurgeDialog: true,
1353 purgeType: PurgeType.Person,
1354 showRemoveDialog: false,
1358 handlePurgeCommentShow(i: CommentNode) {
1360 showPurgeDialog: true,
1361 purgeType: PurgeType.Comment,
1362 showRemoveDialog: false,
1366 handlePurgeReasonChange(i: CommentNode, event: any) {
1367 i.setState({ purgeReason: event.target.value });
1370 handleShowConfirmAppointAsMod(i: CommentNode) {
1371 i.setState({ showConfirmAppointAsMod: true });
1374 handleCancelConfirmAppointAsMod(i: CommentNode) {
1375 i.setState({ showConfirmAppointAsMod: false });
1378 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1379 i.setState({ showConfirmAppointAsAdmin: true });
1382 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1383 i.setState({ showConfirmAppointAsAdmin: false });
1386 handleShowConfirmTransferCommunity(i: CommentNode) {
1387 i.setState({ showConfirmTransferCommunity: true });
1390 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1391 i.setState({ showConfirmTransferCommunity: false });
1394 handleShowConfirmTransferSite(i: CommentNode) {
1395 i.setState({ showConfirmTransferSite: true });
1398 handleCancelShowConfirmTransferSite(i: CommentNode) {
1399 i.setState({ showConfirmTransferSite: false });
1402 get isCommentNew(): boolean {
1403 const now = subMinutes(new Date(), 10);
1404 const then = parseISO(this.commentView.comment.published);
1405 return isBefore(now, then);
1408 handleCommentCollapse(i: CommentNode) {
1409 i.setState({ collapsed: !i.state.collapsed });
1413 handleViewSource(i: CommentNode) {
1414 i.setState({ viewSource: !i.state.viewSource });
1417 handleShowAdvanced(i: CommentNode) {
1418 i.setState({ showAdvanced: !i.state.showAdvanced });
1422 handleSaveComment(i: CommentNode) {
1423 i.setState({ saveLoading: true });
1425 i.props.onSaveComment({
1426 comment_id: i.commentView.comment.id,
1427 save: !i.commentView.saved,
1428 auth: myAuthRequired(),
1432 handleBlockPerson(i: CommentNode) {
1433 i.setState({ blockPersonLoading: true });
1434 i.props.onBlockPerson({
1435 person_id: i.commentView.creator.id,
1437 auth: myAuthRequired(),
1441 handleMarkAsRead(i: CommentNode) {
1442 i.setState({ readLoading: true });
1443 const cv = i.commentView;
1444 if (i.isPersonMentionType(cv)) {
1445 i.props.onPersonMentionRead({
1446 person_mention_id: cv.person_mention.id,
1447 read: !cv.person_mention.read,
1448 auth: myAuthRequired(),
1450 } else if (i.isCommentReplyType(cv)) {
1451 i.props.onCommentReplyRead({
1452 comment_reply_id: cv.comment_reply.id,
1453 read: !cv.comment_reply.read,
1454 auth: myAuthRequired(),
1459 handleDeleteComment(i: CommentNode) {
1460 i.setState({ deleteLoading: true });
1461 i.props.onDeleteComment({
1462 comment_id: i.commentId,
1463 deleted: !i.commentView.comment.deleted,
1464 auth: myAuthRequired(),
1468 handleRemoveComment(i: CommentNode, event: any) {
1469 event.preventDefault();
1470 i.setState({ removeLoading: true });
1471 i.props.onRemoveComment({
1472 comment_id: i.commentId,
1473 removed: !i.commentView.comment.removed,
1474 auth: myAuthRequired(),
1478 handleDistinguishComment(i: CommentNode) {
1479 i.setState({ distinguishLoading: true });
1480 i.props.onDistinguishComment({
1481 comment_id: i.commentId,
1482 distinguished: !i.commentView.comment.distinguished,
1483 auth: myAuthRequired(),
1487 handleBanPersonFromCommunity(i: CommentNode) {
1488 i.setState({ banLoading: true });
1489 i.props.onBanPersonFromCommunity({
1490 community_id: i.commentView.community.id,
1491 person_id: i.commentView.creator.id,
1492 ban: !i.commentView.creator_banned_from_community,
1493 reason: i.state.banReason,
1494 remove_data: i.state.removeData,
1495 expires: futureDaysToUnixTime(i.state.banExpireDays),
1496 auth: myAuthRequired(),
1500 handleBanPerson(i: CommentNode) {
1501 i.setState({ banLoading: true });
1502 i.props.onBanPerson({
1503 person_id: i.commentView.creator.id,
1504 ban: !i.commentView.creator_banned_from_community,
1505 reason: i.state.banReason,
1506 remove_data: i.state.removeData,
1507 expires: futureDaysToUnixTime(i.state.banExpireDays),
1508 auth: myAuthRequired(),
1512 handleModBanBothSubmit(i: CommentNode, event: any) {
1513 event.preventDefault();
1514 if (i.state.banType == BanType.Community) {
1515 i.handleBanPersonFromCommunity(i);
1517 i.handleBanPerson(i);
1521 handleAddModToCommunity(i: CommentNode) {
1522 i.setState({ addModLoading: true });
1524 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1525 i.props.onAddModToCommunity({
1526 community_id: i.commentView.community.id,
1527 person_id: i.commentView.creator.id,
1529 auth: myAuthRequired(),
1533 handleAddAdmin(i: CommentNode) {
1534 i.setState({ addAdminLoading: true });
1536 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1537 i.props.onAddAdmin({
1538 person_id: i.commentView.creator.id,
1540 auth: myAuthRequired(),
1544 handleTransferCommunity(i: CommentNode) {
1545 i.setState({ transferCommunityLoading: true });
1546 i.props.onTransferCommunity({
1547 community_id: i.commentView.community.id,
1548 person_id: i.commentView.creator.id,
1549 auth: myAuthRequired(),
1553 handleReportComment(i: CommentNode, event: any) {
1554 event.preventDefault();
1555 i.setState({ reportLoading: true });
1556 i.props.onCommentReport({
1557 comment_id: i.commentId,
1558 reason: i.state.reportReason ?? "",
1559 auth: myAuthRequired(),
1563 handlePurgeBothSubmit(i: CommentNode, event: any) {
1564 event.preventDefault();
1565 i.setState({ purgeLoading: true });
1567 if (i.state.purgeType == PurgeType.Person) {
1568 i.props.onPurgePerson({
1569 person_id: i.commentView.creator.id,
1570 reason: i.state.purgeReason,
1571 auth: myAuthRequired(),
1574 i.props.onPurgeComment({
1575 comment_id: i.commentId,
1576 reason: i.state.purgeReason,
1577 auth: myAuthRequired(),
1582 handleFetchChildren(i: CommentNode) {
1583 i.setState({ fetchChildrenLoading: true });
1584 i.props.onFetchChildren?.({
1585 parent_id: i.commentId,
1586 max_depth: commentTreeMaxDepth,