9 import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
17 } from "@utils/roles";
18 import classNames from "classnames";
19 import isBefore from "date-fns/isBefore";
20 import parseISO from "date-fns/parseISO";
21 import subMinutes from "date-fns/subMinutes";
22 import { Component, InfernoNode, linkEvent } from "inferno";
23 import { Link } from "inferno-router";
33 CommunityModeratorView,
42 MarkCommentReplyAsRead,
43 MarkPersonMentionAsRead,
51 } from "lemmy-js-client";
52 import deepEqual from "lodash.isequal";
53 import { commentTreeMaxDepth } from "../../config";
60 } from "../../interfaces";
61 import { mdToHtml, mdToHtmlNoImages } from "../../markdown";
62 import { I18NextService, UserService } from "../../services";
63 import { setupTippy } from "../../tippy";
64 import { Icon, PurgeWarning, Spinner } from "../common/icon";
65 import { MomentTime } from "../common/moment-time";
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 <span className="me-2">
314 <PersonListing person={cv.creator} />
317 {cv.comment.distinguished && (
318 <Icon icon="shield" inline classes="text-danger me-2" />
321 {this.isPostCreator &&
323 label: I18NextService.i18n.t("op").toUpperCase(),
324 tooltip: I18NextService.i18n.t("creator"),
325 classes: "text-bg-info",
331 label: I18NextService.i18n.t("mod"),
332 tooltip: I18NextService.i18n.t("mod"),
333 classes: "text-bg-primary",
338 label: I18NextService.i18n.t("admin"),
339 tooltip: I18NextService.i18n.t("admin"),
340 classes: "text-bg-danger",
343 {cv.creator.bot_account &&
345 label: I18NextService.i18n.t("bot_account").toLowerCase(),
346 tooltip: I18NextService.i18n.t("bot_account"),
349 {this.props.showCommunity && (
351 <span className="mx-1">{I18NextService.i18n.t("to")}</span>
352 <CommunityLink community={cv.community} />
353 <span className="mx-2">•</span>
354 <Link className="me-2" to={`/post/${cv.post.id}`}>
360 {this.getLinkButton(true)}
362 {cv.comment.language_id !== 0 && (
363 <span className="badge text-bg-light d-none d-sm-inline me-2">
365 this.props.allLanguages.find(
366 lang => lang.id === cv.comment.language_id
371 {/* This is an expanding spacer for mobile */}
372 <div className="me-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
377 className="me-1 fw-bold"
378 aria-label={I18NextService.i18n.t("number_of_points", {
379 count: Number(this.commentView.counts.score),
380 formattedCount: numToSI(this.commentView.counts.score),
383 {numToSI(this.commentView.counts.score)}
385 <span className="me-1">•</span>
390 published={cv.comment.published}
391 updated={cv.comment.updated}
395 {/* end of user row */}
396 {this.state.showEdit && (
400 onReplyCancel={this.handleReplyCancel}
401 disabled={this.props.locked}
402 finished={this.props.finished.get(
403 this.props.node.comment_view.comment.id
406 allLanguages={this.props.allLanguages}
407 siteLanguages={this.props.siteLanguages}
408 containerClass="comment-comment-container"
409 onUpsertComment={this.props.onEditComment}
412 {!this.state.showEdit && !this.state.collapsed && (
414 {this.state.viewSource ? (
415 <pre>{this.commentUnlessRemoved}</pre>
419 dangerouslySetInnerHTML={
420 this.props.hideImages
421 ? mdToHtmlNoImages(this.commentUnlessRemoved)
422 : mdToHtml(this.commentUnlessRemoved)
426 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted fw-bold">
427 {this.props.showContext && this.getLinkButton()}
428 {this.props.markable && (
430 className="btn btn-link btn-animate text-muted"
431 onClick={linkEvent(this, this.handleMarkAsRead)}
433 this.commentReplyOrMentionRead
434 ? I18NextService.i18n.t("mark_as_unread")
435 : I18NextService.i18n.t("mark_as_read")
438 this.commentReplyOrMentionRead
439 ? I18NextService.i18n.t("mark_as_unread")
440 : I18NextService.i18n.t("mark_as_read")
443 {this.state.readLoading ? (
448 classes={`icon-inline ${
449 this.commentReplyOrMentionRead && "text-success"
455 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
458 voteContentType={VoteContentType.Comment}
459 id={this.commentView.comment.id}
460 onVote={this.props.onCommentVote}
461 enableDownvotes={this.props.enableDownvotes}
462 counts={this.commentView.counts}
463 my_vote={this.commentView.my_vote}
466 className="btn btn-link btn-animate text-muted"
467 onClick={linkEvent(this, this.handleReplyClick)}
468 data-tippy-content={I18NextService.i18n.t("reply")}
469 aria-label={I18NextService.i18n.t("reply")}
471 <Icon icon="reply1" classes="icon-inline" />
473 {!this.state.showAdvanced ? (
475 className="btn btn-link btn-animate text-muted btn-more"
476 onClick={linkEvent(this, this.handleShowAdvanced)}
477 data-tippy-content={I18NextService.i18n.t("more")}
478 aria-label={I18NextService.i18n.t("more")}
480 <Icon icon="more-vertical" classes="icon-inline" />
484 {!this.myComment && (
487 className="btn btn-link btn-animate text-muted"
488 to={`/create_private_message/${cv.creator.id}`}
489 title={I18NextService.i18n
496 className="btn btn-link btn-animate text-muted"
499 this.handleShowReportDialog
501 data-tippy-content={I18NextService.i18n.t(
504 aria-label={I18NextService.i18n.t(
511 className="btn btn-link btn-animate text-muted"
514 this.handleBlockPerson
516 data-tippy-content={I18NextService.i18n.t(
519 aria-label={I18NextService.i18n.t("block_user")}
521 {this.state.blockPersonLoading ? (
524 <Icon icon="slash" />
530 className="btn btn-link btn-animate text-muted"
531 onClick={linkEvent(this, this.handleSaveComment)}
534 ? I18NextService.i18n.t("unsave")
535 : I18NextService.i18n.t("save")
539 ? I18NextService.i18n.t("unsave")
540 : I18NextService.i18n.t("save")
543 {this.state.saveLoading ? (
548 classes={`icon-inline ${
549 cv.saved && "text-warning"
555 className="btn btn-link btn-animate text-muted"
556 onClick={linkEvent(this, this.handleViewSource)}
557 data-tippy-content={I18NextService.i18n.t(
560 aria-label={I18NextService.i18n.t("view_source")}
564 classes={`icon-inline ${
565 this.state.viewSource && "text-success"
572 className="btn btn-link btn-animate text-muted"
573 onClick={linkEvent(this, this.handleEditClick)}
574 data-tippy-content={I18NextService.i18n.t(
577 aria-label={I18NextService.i18n.t("edit")}
579 <Icon icon="edit" classes="icon-inline" />
582 className="btn btn-link btn-animate text-muted"
585 this.handleDeleteComment
589 ? I18NextService.i18n.t("delete")
590 : I18NextService.i18n.t("restore")
594 ? I18NextService.i18n.t("delete")
595 : I18NextService.i18n.t("restore")
598 {this.state.deleteLoading ? (
603 classes={`icon-inline ${
604 cv.comment.deleted && "text-danger"
610 {(canModOnSelf || canAdminOnSelf) && (
612 className="btn btn-link btn-animate text-muted"
615 this.handleDistinguishComment
618 !cv.comment.distinguished
619 ? I18NextService.i18n.t("distinguish")
620 : I18NextService.i18n.t("undistinguish")
623 !cv.comment.distinguished
624 ? I18NextService.i18n.t("distinguish")
625 : I18NextService.i18n.t("undistinguish")
630 classes={`icon-inline ${
631 cv.comment.distinguished && "text-danger"
638 {/* Admins and mods can remove comments */}
639 {(canMod_ || canAdmin_) && (
641 {!cv.comment.removed ? (
643 className="btn btn-link btn-animate text-muted"
646 this.handleModRemoveShow
648 aria-label={I18NextService.i18n.t("remove")}
650 {I18NextService.i18n.t("remove")}
654 className="btn btn-link btn-animate text-muted"
657 this.handleRemoveComment
659 aria-label={I18NextService.i18n.t("restore")}
661 {this.state.removeLoading ? (
664 I18NextService.i18n.t("restore")
670 {/* Mods can ban from community, and appoint as mods to community */}
674 (!cv.creator_banned_from_community ? (
676 className="btn btn-link btn-animate text-muted"
679 this.handleModBanFromCommunityShow
681 aria-label={I18NextService.i18n.t(
685 {I18NextService.i18n.t(
691 className="btn btn-link btn-animate text-muted"
694 this.handleBanPersonFromCommunity
696 aria-label={I18NextService.i18n.t("unban")}
698 {this.state.banLoading ? (
701 I18NextService.i18n.t("unban")
705 {!cv.creator_banned_from_community &&
706 (!this.state.showConfirmAppointAsMod ? (
708 className="btn btn-link btn-animate text-muted"
711 this.handleShowConfirmAppointAsMod
715 ? I18NextService.i18n.t("remove_as_mod")
716 : I18NextService.i18n.t(
722 ? I18NextService.i18n.t("remove_as_mod")
723 : I18NextService.i18n.t("appoint_as_mod")}
728 className="btn btn-link btn-animate text-muted"
729 aria-label={I18NextService.i18n.t(
733 {I18NextService.i18n.t("are_you_sure")}
736 className="btn btn-link btn-animate text-muted"
739 this.handleAddModToCommunity
741 aria-label={I18NextService.i18n.t("yes")}
743 {this.state.addModLoading ? (
746 I18NextService.i18n.t("yes")
750 className="btn btn-link btn-animate text-muted"
753 this.handleCancelConfirmAppointAsMod
755 aria-label={I18NextService.i18n.t("no")}
757 {I18NextService.i18n.t("no")}
763 {/* Community creators and admins can transfer community to another mod */}
764 {(amCommunityCreator_ || canAdmin_) &&
767 (!this.state.showConfirmTransferCommunity ? (
769 className="btn btn-link btn-animate text-muted"
772 this.handleShowConfirmTransferCommunity
774 aria-label={I18NextService.i18n.t(
778 {I18NextService.i18n.t("transfer_community")}
783 className="btn btn-link btn-animate text-muted"
784 aria-label={I18NextService.i18n.t(
788 {I18NextService.i18n.t("are_you_sure")}
791 className="btn btn-link btn-animate text-muted"
794 this.handleTransferCommunity
796 aria-label={I18NextService.i18n.t("yes")}
798 {this.state.transferCommunityLoading ? (
801 I18NextService.i18n.t("yes")
805 className="btn btn-link btn-animate text-muted"
809 .handleCancelShowConfirmTransferCommunity
811 aria-label={I18NextService.i18n.t("no")}
813 {I18NextService.i18n.t("no")}
817 {/* Admins can ban from all, and appoint other admins */}
823 className="btn btn-link btn-animate text-muted"
826 this.handlePurgePersonShow
828 aria-label={I18NextService.i18n.t(
832 {I18NextService.i18n.t("purge_user")}
835 className="btn btn-link btn-animate text-muted"
838 this.handlePurgeCommentShow
840 aria-label={I18NextService.i18n.t(
844 {I18NextService.i18n.t("purge_comment")}
847 {!isBanned(cv.creator) ? (
849 className="btn btn-link btn-animate text-muted"
852 this.handleModBanShow
854 aria-label={I18NextService.i18n.t(
858 {I18NextService.i18n.t("ban_from_site")}
862 className="btn btn-link btn-animate text-muted"
867 aria-label={I18NextService.i18n.t(
871 {this.state.banLoading ? (
874 I18NextService.i18n.t("unban_from_site")
880 {!isBanned(cv.creator) &&
882 (!this.state.showConfirmAppointAsAdmin ? (
884 className="btn btn-link btn-animate text-muted"
887 this.handleShowConfirmAppointAsAdmin
891 ? I18NextService.i18n.t(
894 : I18NextService.i18n.t(
900 ? I18NextService.i18n.t("remove_as_admin")
901 : I18NextService.i18n.t(
907 <button className="btn btn-link btn-animate text-muted">
908 {I18NextService.i18n.t("are_you_sure")}
911 className="btn btn-link btn-animate text-muted"
916 aria-label={I18NextService.i18n.t("yes")}
918 {this.state.addAdminLoading ? (
921 I18NextService.i18n.t("yes")
925 className="btn btn-link btn-animate text-muted"
928 this.handleCancelConfirmAppointAsAdmin
930 aria-label={I18NextService.i18n.t("no")}
932 {I18NextService.i18n.t("no")}
943 {/* end of button group */}
948 {showMoreChildren && (
950 className={classNames("details ms-1 comment-node py-2", {
951 "border-top border-light": !this.props.noBorder,
953 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
956 className="btn btn-link text-muted"
957 onClick={linkEvent(this, this.handleFetchChildren)}
959 {this.state.fetchChildrenLoading ? (
963 {I18NextService.i18n.t("x_more_replies", {
964 count: node.comment_view.counts.child_count,
965 formattedCount: numToSI(
966 node.comment_view.counts.child_count
975 {/* end of details */}
976 {this.state.showRemoveDialog && (
978 className="form-inline"
979 onSubmit={linkEvent(this, this.handleRemoveComment)}
982 className="visually-hidden"
983 htmlFor={`mod-remove-reason-${cv.comment.id}`}
985 {I18NextService.i18n.t("reason")}
989 id={`mod-remove-reason-${cv.comment.id}`}
990 className="form-control me-2"
991 placeholder={I18NextService.i18n.t("reason")}
992 value={this.state.removeReason}
993 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
997 className="btn btn-secondary"
998 aria-label={I18NextService.i18n.t("remove_comment")}
1000 {I18NextService.i18n.t("remove_comment")}
1004 {this.state.showReportDialog && (
1006 className="form-inline"
1007 onSubmit={linkEvent(this, this.handleReportComment)}
1010 className="visually-hidden"
1011 htmlFor={`report-reason-${cv.comment.id}`}
1013 {I18NextService.i18n.t("reason")}
1018 id={`report-reason-${cv.comment.id}`}
1019 className="form-control me-2"
1020 placeholder={I18NextService.i18n.t("reason")}
1021 value={this.state.reportReason}
1022 onInput={linkEvent(this, this.handleReportReasonChange)}
1026 className="btn btn-secondary"
1027 aria-label={I18NextService.i18n.t("create_report")}
1029 {I18NextService.i18n.t("create_report")}
1033 {this.state.showBanDialog && (
1034 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1035 <div className="mb-3 row col-12">
1037 className="col-form-label"
1038 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1040 {I18NextService.i18n.t("reason")}
1044 id={`mod-ban-reason-${cv.comment.id}`}
1045 className="form-control me-2"
1046 placeholder={I18NextService.i18n.t("reason")}
1047 value={this.state.banReason}
1048 onInput={linkEvent(this, this.handleModBanReasonChange)}
1051 className="col-form-label"
1052 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1054 {I18NextService.i18n.t("expires")}
1058 id={`mod-ban-expires-${cv.comment.id}`}
1059 className="form-control me-2"
1060 placeholder={I18NextService.i18n.t("number_of_days")}
1061 value={this.state.banExpireDays}
1062 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1064 <div className="input-group mb-3">
1065 <div className="form-check">
1067 className="form-check-input"
1068 id="mod-ban-remove-data"
1070 checked={this.state.removeData}
1071 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1074 className="form-check-label"
1075 htmlFor="mod-ban-remove-data"
1076 title={I18NextService.i18n.t("remove_content_more")}
1078 {I18NextService.i18n.t("remove_content")}
1083 {/* TODO hold off on expires until later */}
1084 {/* <div class="mb-3 row"> */}
1085 {/* <label class="col-form-label">Expires</label> */}
1086 {/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1088 <div className="mb-3 row">
1091 className="btn btn-secondary"
1092 aria-label={I18NextService.i18n.t("ban")}
1094 {this.state.banLoading ? (
1098 {I18NextService.i18n.t("ban")} {cv.creator.name}
1106 {this.state.showPurgeDialog && (
1107 <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1109 <label className="visually-hidden" htmlFor="purge-reason">
1110 {I18NextService.i18n.t("reason")}
1115 className="form-control my-3"
1116 placeholder={I18NextService.i18n.t("reason")}
1117 value={this.state.purgeReason}
1118 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1120 <div className="mb-3 row col-12">
1121 {this.state.purgeLoading ? (
1126 className="btn btn-secondary"
1127 aria-label={purgeTypeText}
1135 {this.state.showReply && (
1138 onReplyCancel={this.handleReplyCancel}
1139 disabled={this.props.locked}
1140 finished={this.props.finished.get(
1141 this.props.node.comment_view.comment.id
1144 allLanguages={this.props.allLanguages}
1145 siteLanguages={this.props.siteLanguages}
1146 containerClass="comment-comment-container"
1147 onUpsertComment={this.props.onCreateComment}
1150 {!this.state.collapsed && node.children.length > 0 && (
1152 nodes={node.children}
1153 locked={this.props.locked}
1154 moderators={this.props.moderators}
1155 admins={this.props.admins}
1156 enableDownvotes={this.props.enableDownvotes}
1157 viewType={this.props.viewType}
1158 allLanguages={this.props.allLanguages}
1159 siteLanguages={this.props.siteLanguages}
1160 hideImages={this.props.hideImages}
1161 isChild={!this.props.noIndent}
1162 depth={this.props.node.depth + 1}
1163 finished={this.props.finished}
1164 onCommentReplyRead={this.props.onCommentReplyRead}
1165 onPersonMentionRead={this.props.onPersonMentionRead}
1166 onCreateComment={this.props.onCreateComment}
1167 onEditComment={this.props.onEditComment}
1168 onCommentVote={this.props.onCommentVote}
1169 onBlockPerson={this.props.onBlockPerson}
1170 onSaveComment={this.props.onSaveComment}
1171 onDeleteComment={this.props.onDeleteComment}
1172 onRemoveComment={this.props.onRemoveComment}
1173 onDistinguishComment={this.props.onDistinguishComment}
1174 onAddModToCommunity={this.props.onAddModToCommunity}
1175 onAddAdmin={this.props.onAddAdmin}
1176 onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1177 onBanPerson={this.props.onBanPerson}
1178 onTransferCommunity={this.props.onTransferCommunity}
1179 onFetchChildren={this.props.onFetchChildren}
1180 onCommentReport={this.props.onCommentReport}
1181 onPurgePerson={this.props.onPurgePerson}
1182 onPurgeComment={this.props.onPurgeComment}
1185 {/* A collapsed clearfix */}
1186 {this.state.collapsed && <div className="row col-12" />}
1191 get commentReplyOrMentionRead(): boolean {
1192 const cv = this.commentView;
1194 if (this.isPersonMentionType(cv)) {
1195 return cv.person_mention.read;
1196 } else if (this.isCommentReplyType(cv)) {
1197 return cv.comment_reply.read;
1203 getLinkButton(small = false) {
1204 const cv = this.commentView;
1206 const classnames = classNames("btn btn-link btn-animate text-muted", {
1210 const title = this.props.showContext
1211 ? I18NextService.i18n.t("show_context")
1212 : I18NextService.i18n.t("link");
1214 // The context button should show the parent comment by default
1215 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1220 className={classnames}
1221 to={`/comment/${parentCommentId}`}
1224 <Icon icon="link" classes="icon-inline" />
1227 <a className={classnames} title={title} href={cv.comment.ap_id}>
1228 <Icon icon="fedilink" classes="icon-inline" />
1235 get myComment(): boolean {
1237 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1238 this.commentView.creator.id
1242 get isPostCreator(): boolean {
1243 return this.commentView.creator.id == this.commentView.post.creator_id;
1247 if (this.commentView.my_vote == 1) {
1249 } else if (this.commentView.my_vote == -1) {
1250 return "text-danger";
1252 return "text-muted";
1256 get pointsTippy(): string {
1257 const points = I18NextService.i18n.t("number_of_points", {
1258 count: Number(this.commentView.counts.score),
1259 formattedCount: numToSI(this.commentView.counts.score),
1262 const upvotes = I18NextService.i18n.t("number_of_upvotes", {
1263 count: Number(this.commentView.counts.upvotes),
1264 formattedCount: numToSI(this.commentView.counts.upvotes),
1267 const downvotes = I18NextService.i18n.t("number_of_downvotes", {
1268 count: Number(this.commentView.counts.downvotes),
1269 formattedCount: numToSI(this.commentView.counts.downvotes),
1272 return `${points} • ${upvotes} • ${downvotes}`;
1275 get expandText(): string {
1276 return this.state.collapsed
1277 ? I18NextService.i18n.t("expand")
1278 : I18NextService.i18n.t("collapse");
1281 get commentUnlessRemoved(): string {
1282 const comment = this.commentView.comment;
1283 return comment.removed
1284 ? `*${I18NextService.i18n.t("removed")}*`
1286 ? `*${I18NextService.i18n.t("deleted")}*`
1290 handleReplyClick(i: CommentNode) {
1291 i.setState({ showReply: true });
1294 handleEditClick(i: CommentNode) {
1295 i.setState({ showEdit: true });
1298 handleReplyCancel() {
1299 this.setState({ showReply: false, showEdit: false });
1302 handleShowReportDialog(i: CommentNode) {
1303 i.setState({ showReportDialog: !i.state.showReportDialog });
1306 handleReportReasonChange(i: CommentNode, event: any) {
1307 i.setState({ reportReason: event.target.value });
1310 handleModRemoveShow(i: CommentNode) {
1312 showRemoveDialog: !i.state.showRemoveDialog,
1313 showBanDialog: false,
1317 handleModRemoveReasonChange(i: CommentNode, event: any) {
1318 i.setState({ removeReason: event.target.value });
1321 handleModRemoveDataChange(i: CommentNode, event: any) {
1322 i.setState({ removeData: event.target.checked });
1325 isPersonMentionType(
1326 item: CommentView | PersonMentionView | CommentReplyView
1327 ): item is PersonMentionView {
1328 return (item as PersonMentionView).person_mention?.id !== undefined;
1332 item: CommentView | PersonMentionView | CommentReplyView
1333 ): item is CommentReplyView {
1334 return (item as CommentReplyView).comment_reply?.id !== undefined;
1337 handleModBanFromCommunityShow(i: CommentNode) {
1339 showBanDialog: true,
1340 banType: BanType.Community,
1341 showRemoveDialog: false,
1345 handleModBanShow(i: CommentNode) {
1347 showBanDialog: true,
1348 banType: BanType.Site,
1349 showRemoveDialog: false,
1353 handleModBanReasonChange(i: CommentNode, event: any) {
1354 i.setState({ banReason: event.target.value });
1357 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1358 i.setState({ banExpireDays: event.target.value });
1361 handlePurgePersonShow(i: CommentNode) {
1363 showPurgeDialog: true,
1364 purgeType: PurgeType.Person,
1365 showRemoveDialog: false,
1369 handlePurgeCommentShow(i: CommentNode) {
1371 showPurgeDialog: true,
1372 purgeType: PurgeType.Comment,
1373 showRemoveDialog: false,
1377 handlePurgeReasonChange(i: CommentNode, event: any) {
1378 i.setState({ purgeReason: event.target.value });
1381 handleShowConfirmAppointAsMod(i: CommentNode) {
1382 i.setState({ showConfirmAppointAsMod: true });
1385 handleCancelConfirmAppointAsMod(i: CommentNode) {
1386 i.setState({ showConfirmAppointAsMod: false });
1389 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1390 i.setState({ showConfirmAppointAsAdmin: true });
1393 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1394 i.setState({ showConfirmAppointAsAdmin: false });
1397 handleShowConfirmTransferCommunity(i: CommentNode) {
1398 i.setState({ showConfirmTransferCommunity: true });
1401 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1402 i.setState({ showConfirmTransferCommunity: false });
1405 handleShowConfirmTransferSite(i: CommentNode) {
1406 i.setState({ showConfirmTransferSite: true });
1409 handleCancelShowConfirmTransferSite(i: CommentNode) {
1410 i.setState({ showConfirmTransferSite: false });
1413 get isCommentNew(): boolean {
1414 const now = subMinutes(new Date(), 10);
1415 const then = parseISO(this.commentView.comment.published);
1416 return isBefore(now, then);
1419 handleCommentCollapse(i: CommentNode) {
1420 i.setState({ collapsed: !i.state.collapsed });
1424 handleViewSource(i: CommentNode) {
1425 i.setState({ viewSource: !i.state.viewSource });
1428 handleShowAdvanced(i: CommentNode) {
1429 i.setState({ showAdvanced: !i.state.showAdvanced });
1433 handleSaveComment(i: CommentNode) {
1434 i.setState({ saveLoading: true });
1436 i.props.onSaveComment({
1437 comment_id: i.commentView.comment.id,
1438 save: !i.commentView.saved,
1439 auth: myAuthRequired(),
1443 handleBlockPerson(i: CommentNode) {
1444 i.setState({ blockPersonLoading: true });
1445 i.props.onBlockPerson({
1446 person_id: i.commentView.creator.id,
1448 auth: myAuthRequired(),
1452 handleMarkAsRead(i: CommentNode) {
1453 i.setState({ readLoading: true });
1454 const cv = i.commentView;
1455 if (i.isPersonMentionType(cv)) {
1456 i.props.onPersonMentionRead({
1457 person_mention_id: cv.person_mention.id,
1458 read: !cv.person_mention.read,
1459 auth: myAuthRequired(),
1461 } else if (i.isCommentReplyType(cv)) {
1462 i.props.onCommentReplyRead({
1463 comment_reply_id: cv.comment_reply.id,
1464 read: !cv.comment_reply.read,
1465 auth: myAuthRequired(),
1470 handleDeleteComment(i: CommentNode) {
1471 i.setState({ deleteLoading: true });
1472 i.props.onDeleteComment({
1473 comment_id: i.commentId,
1474 deleted: !i.commentView.comment.deleted,
1475 auth: myAuthRequired(),
1479 handleRemoveComment(i: CommentNode, event: any) {
1480 event.preventDefault();
1481 i.setState({ removeLoading: true });
1482 i.props.onRemoveComment({
1483 comment_id: i.commentId,
1484 removed: !i.commentView.comment.removed,
1485 auth: myAuthRequired(),
1489 handleDistinguishComment(i: CommentNode) {
1490 i.setState({ distinguishLoading: true });
1491 i.props.onDistinguishComment({
1492 comment_id: i.commentId,
1493 distinguished: !i.commentView.comment.distinguished,
1494 auth: myAuthRequired(),
1498 handleBanPersonFromCommunity(i: CommentNode) {
1499 i.setState({ banLoading: true });
1500 i.props.onBanPersonFromCommunity({
1501 community_id: i.commentView.community.id,
1502 person_id: i.commentView.creator.id,
1503 ban: !i.commentView.creator_banned_from_community,
1504 reason: i.state.banReason,
1505 remove_data: i.state.removeData,
1506 expires: futureDaysToUnixTime(i.state.banExpireDays),
1507 auth: myAuthRequired(),
1511 handleBanPerson(i: CommentNode) {
1512 i.setState({ banLoading: true });
1513 i.props.onBanPerson({
1514 person_id: i.commentView.creator.id,
1515 ban: !i.commentView.creator_banned_from_community,
1516 reason: i.state.banReason,
1517 remove_data: i.state.removeData,
1518 expires: futureDaysToUnixTime(i.state.banExpireDays),
1519 auth: myAuthRequired(),
1523 handleModBanBothSubmit(i: CommentNode, event: any) {
1524 event.preventDefault();
1525 if (i.state.banType == BanType.Community) {
1526 i.handleBanPersonFromCommunity(i);
1528 i.handleBanPerson(i);
1532 handleAddModToCommunity(i: CommentNode) {
1533 i.setState({ addModLoading: true });
1535 const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1536 i.props.onAddModToCommunity({
1537 community_id: i.commentView.community.id,
1538 person_id: i.commentView.creator.id,
1540 auth: myAuthRequired(),
1544 handleAddAdmin(i: CommentNode) {
1545 i.setState({ addAdminLoading: true });
1547 const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1548 i.props.onAddAdmin({
1549 person_id: i.commentView.creator.id,
1551 auth: myAuthRequired(),
1555 handleTransferCommunity(i: CommentNode) {
1556 i.setState({ transferCommunityLoading: true });
1557 i.props.onTransferCommunity({
1558 community_id: i.commentView.community.id,
1559 person_id: i.commentView.creator.id,
1560 auth: myAuthRequired(),
1564 handleReportComment(i: CommentNode, event: any) {
1565 event.preventDefault();
1566 i.setState({ reportLoading: true });
1567 i.props.onCommentReport({
1568 comment_id: i.commentId,
1569 reason: i.state.reportReason ?? "",
1570 auth: myAuthRequired(),
1574 handlePurgeBothSubmit(i: CommentNode, event: any) {
1575 event.preventDefault();
1576 i.setState({ purgeLoading: true });
1578 if (i.state.purgeType == PurgeType.Person) {
1579 i.props.onPurgePerson({
1580 person_id: i.commentView.creator.id,
1581 reason: i.state.purgeReason,
1582 auth: myAuthRequired(),
1585 i.props.onPurgeComment({
1586 comment_id: i.commentId,
1587 reason: i.state.purgeReason,
1588 auth: myAuthRequired(),
1593 handleFetchChildren(i: CommentNode) {
1594 i.setState({ fetchChildrenLoading: true });
1595 i.props.onFetchChildren?.({
1596 parent_id: i.commentId,
1597 max_depth: commentTreeMaxDepth,