1 import classNames from "classnames";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
10 CommentNode as CommentNodeI,
13 CommunityModeratorView,
21 MarkCommentReplyAsRead,
22 MarkPersonMentionAsRead,
30 } from "lemmy-js-client";
31 import moment from "moment";
32 import { i18n } from "../../i18next";
33 import { BanType, CommentViewType, PurgeType } from "../../interfaces";
34 import { UserService, WebSocketService } from "../../services";
53 import { Icon, PurgeWarning, Spinner } from "../common/icon";
54 import { MomentTime } from "../common/moment-time";
55 import { CommunityLink } from "../community/community-link";
56 import { PersonListing } from "../person/person-listing";
57 import { CommentForm } from "./comment-form";
58 import { CommentNodes } from "./comment-nodes";
60 interface CommentNodeState {
63 showRemoveDialog: boolean;
64 removeReason?: string;
65 showBanDialog: boolean;
68 banExpireDays?: number;
70 showPurgeDialog: boolean;
73 purgeLoading: boolean;
74 showConfirmTransferSite: boolean;
75 showConfirmTransferCommunity: boolean;
76 showConfirmAppointAsMod: boolean;
77 showConfirmAppointAsAdmin: boolean;
80 showAdvanced: boolean;
81 showReportDialog: boolean;
82 reportReason?: string;
91 interface CommentNodeProps {
93 moderators?: CommunityModeratorView[];
94 admins?: PersonViewSafe[];
100 showContext?: boolean;
101 showCommunity?: boolean;
102 enableDownvotes?: boolean;
103 viewType: CommentViewType;
104 allLanguages: Language[];
105 siteLanguages: number[];
106 hideImages?: boolean;
109 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
110 state: CommentNodeState = {
113 showRemoveDialog: false,
114 showBanDialog: false,
116 banType: BanType.Community,
117 showPurgeDialog: false,
119 purgeType: PurgeType.Person,
123 showConfirmTransferSite: false,
124 showConfirmTransferCommunity: false,
125 showConfirmAppointAsMod: false,
126 showConfirmAppointAsAdmin: false,
127 showReportDialog: false,
128 my_vote: this.props.node.comment_view.my_vote,
129 score: this.props.node.comment_view.counts.score,
130 upvotes: this.props.node.comment_view.counts.upvotes,
131 downvotes: this.props.node.comment_view.counts.downvotes,
136 constructor(props: any, context: any) {
137 super(props, context);
139 this.handleReplyCancel = this.handleReplyCancel.bind(this);
140 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
141 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
144 // TODO see if there's a better way to do this, and all willReceiveProps
145 componentWillReceiveProps(nextProps: CommentNodeProps) {
146 let cv = nextProps.node.comment_view;
149 upvotes: cv.counts.upvotes,
150 downvotes: cv.counts.downvotes,
151 score: cv.counts.score,
158 let node = this.props.node;
159 let cv = this.props.node.comment_view;
162 this.state.purgeType == PurgeType.Comment
163 ? i18n.t("purge_comment")
164 : `${i18n.t("purge")} ${cv.creator.name}`;
167 canMod(cv.creator.id, this.props.moderators, this.props.admins) &&
172 this.props.moderators,
174 UserService.Instance.myUserInfo,
176 ) && cv.community.local;
178 canAdmin(cv.creator.id, this.props.admins) && cv.community.local;
183 UserService.Instance.myUserInfo,
185 ) && cv.community.local;
186 let isMod_ = isMod(cv.creator.id, this.props.moderators);
188 isAdmin(cv.creator.id, this.props.admins) && cv.community.local;
189 let amCommunityCreator_ = amCommunityCreator(
191 this.props.moderators
194 let borderColor = this.props.node.depth
195 ? colorList[(this.props.node.depth - 1) % colorList.length]
197 let moreRepliesBorderColor = this.props.node.depth
198 ? colorList[this.props.node.depth % colorList.length]
201 let showMoreChildren =
202 this.props.viewType == CommentViewType.Tree &&
203 !this.state.collapsed &&
204 node.children.length == 0 &&
205 node.comment_view.counts.child_count > 0;
209 className={`comment ${
210 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
214 id={`comment-${cv.comment.id}`}
215 className={classNames(`details comment-node py-2`, {
216 "border-top border-light": !this.props.noBorder,
219 this.props.node.comment_view.comment.distinguished,
222 !this.props.noIndent && this.props.node.depth
223 ? `border-left: 2px ${borderColor} solid !important`
228 className={classNames({
229 "ml-2": !this.props.noIndent && this.props.node.depth,
232 <div className="d-flex flex-wrap align-items-center text-muted small">
233 <span className="mr-2">
234 <PersonListing person={cv.creator} />
236 {cv.comment.distinguished && (
237 <Icon icon="shield" inline classes={`text-danger mr-2`} />
240 <div className="badge badge-light d-none d-sm-inline mr-2">
245 <div className="badge badge-light d-none d-sm-inline mr-2">
249 {this.isPostCreator && (
250 <div className="badge badge-light d-none d-sm-inline mr-2">
254 {cv.creator.bot_account && (
255 <div className="badge badge-light d-none d-sm-inline mr-2">
256 {i18n.t("bot_account").toLowerCase()}
259 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
260 <div className="badge badge-danger mr-2">
264 {this.props.showCommunity && (
266 <span className="mx-1">{i18n.t("to")}</span>
267 <CommunityLink community={cv.community} />
268 <span className="mx-2">•</span>
269 <Link className="mr-2" to={`/post/${cv.post.id}`}>
275 className="btn btn-sm text-muted"
276 onClick={linkEvent(this, this.handleCommentCollapse)}
277 aria-label={this.expandText}
278 data-tippy-content={this.expandText}
280 {this.state.collapsed ? (
281 <Icon icon="plus-square" classes="icon-inline" />
283 <Icon icon="minus-square" classes="icon-inline" />
287 {/* This is an expanding spacer for mobile */}
288 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
292 className={`unselectable pointer ${this.scoreColor}`}
293 onClick={this.handleCommentUpvote}
294 data-tippy-content={this.pointsTippy}
297 className="mr-1 font-weight-bold"
298 aria-label={i18n.t("number_of_points", {
299 count: this.state.score,
300 formattedCount: this.state.score,
303 {numToSI(this.state.score)}
306 <span className="mr-1">•</span>
311 published={cv.comment.published}
312 updated={cv.comment.updated}
316 {/* end of user row */}
317 {this.state.showEdit && (
321 onReplyCancel={this.handleReplyCancel}
322 disabled={this.props.locked}
324 allLanguages={this.props.allLanguages}
325 siteLanguages={this.props.siteLanguages}
328 {!this.state.showEdit && !this.state.collapsed && (
330 {this.state.viewSource ? (
331 <pre>{this.commentUnlessRemoved}</pre>
335 dangerouslySetInnerHTML={
336 this.props.hideImages
337 ? mdToHtmlNoImages(this.commentUnlessRemoved)
338 : mdToHtml(this.commentUnlessRemoved)
342 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
343 {this.props.showContext && this.linkBtn()}
344 {this.props.markable && (
346 className="btn btn-link btn-animate text-muted"
347 onClick={linkEvent(this, this.handleMarkRead)}
349 this.commentReplyOrMentionRead
350 ? i18n.t("mark_as_unread")
351 : i18n.t("mark_as_read")
354 this.commentReplyOrMentionRead
355 ? i18n.t("mark_as_unread")
356 : i18n.t("mark_as_read")
359 {this.state.readLoading ? (
364 classes={`icon-inline ${
365 this.commentReplyOrMentionRead && "text-success"
371 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
374 className={`btn btn-link btn-animate ${
375 this.state.my_vote == 1 ? "text-info" : "text-muted"
377 onClick={this.handleCommentUpvote}
378 data-tippy-content={i18n.t("upvote")}
379 aria-label={i18n.t("upvote")}
381 <Icon icon="arrow-up1" classes="icon-inline" />
383 this.state.upvotes !== this.state.score && (
384 <span className="ml-1">
385 {numToSI(this.state.upvotes)}
389 {this.props.enableDownvotes && (
391 className={`btn btn-link btn-animate ${
392 this.state.my_vote == -1
396 onClick={this.handleCommentDownvote}
397 data-tippy-content={i18n.t("downvote")}
398 aria-label={i18n.t("downvote")}
400 <Icon icon="arrow-down1" classes="icon-inline" />
402 this.state.upvotes !== this.state.score && (
403 <span className="ml-1">
404 {numToSI(this.state.downvotes)}
410 className="btn btn-link btn-animate text-muted"
411 onClick={linkEvent(this, this.handleReplyClick)}
412 data-tippy-content={i18n.t("reply")}
413 aria-label={i18n.t("reply")}
415 <Icon icon="reply1" classes="icon-inline" />
417 {!this.state.showAdvanced ? (
419 className="btn btn-link btn-animate text-muted"
420 onClick={linkEvent(this, this.handleShowAdvanced)}
421 data-tippy-content={i18n.t("more")}
422 aria-label={i18n.t("more")}
424 <Icon icon="more-vertical" classes="icon-inline" />
428 {!this.myComment && (
430 <button className="btn btn-link btn-animate">
432 className="text-muted"
433 to={`/create_private_message/recipient/${cv.creator.id}`}
434 title={i18n.t("message").toLowerCase()}
440 className="btn btn-link btn-animate text-muted"
443 this.handleShowReportDialog
445 data-tippy-content={i18n.t(
448 aria-label={i18n.t("show_report_dialog")}
453 className="btn btn-link btn-animate text-muted"
456 this.handleBlockUserClick
458 data-tippy-content={i18n.t("block_user")}
459 aria-label={i18n.t("block_user")}
461 <Icon icon="slash" />
466 className="btn btn-link btn-animate text-muted"
469 this.handleSaveCommentClick
472 cv.saved ? i18n.t("unsave") : i18n.t("save")
475 cv.saved ? i18n.t("unsave") : i18n.t("save")
478 {this.state.saveLoading ? (
483 classes={`icon-inline ${
484 cv.saved && "text-warning"
490 className="btn btn-link btn-animate text-muted"
491 onClick={linkEvent(this, this.handleViewSource)}
492 data-tippy-content={i18n.t("view_source")}
493 aria-label={i18n.t("view_source")}
497 classes={`icon-inline ${
498 this.state.viewSource && "text-success"
505 className="btn btn-link btn-animate text-muted"
506 onClick={linkEvent(this, this.handleEditClick)}
507 data-tippy-content={i18n.t("edit")}
508 aria-label={i18n.t("edit")}
510 <Icon icon="edit" classes="icon-inline" />
513 className="btn btn-link btn-animate text-muted"
516 this.handleDeleteClick
531 classes={`icon-inline ${
532 cv.comment.deleted && "text-danger"
537 {(canModOnSelf || canAdminOnSelf) && (
539 className="btn btn-link btn-animate text-muted"
542 this.handleDistinguishClick
545 !cv.comment.distinguished
546 ? i18n.t("distinguish")
547 : i18n.t("undistinguish")
550 !cv.comment.distinguished
551 ? i18n.t("distinguish")
552 : i18n.t("undistinguish")
557 classes={`icon-inline ${
558 cv.comment.distinguished && "text-danger"
565 {/* Admins and mods can remove comments */}
566 {(canMod_ || canAdmin_) && (
568 {!cv.comment.removed ? (
570 className="btn btn-link btn-animate text-muted"
573 this.handleModRemoveShow
575 aria-label={i18n.t("remove")}
581 className="btn btn-link btn-animate text-muted"
584 this.handleModRemoveSubmit
586 aria-label={i18n.t("restore")}
593 {/* Mods can ban from community, and appoint as mods to community */}
597 (!cv.creator_banned_from_community ? (
599 className="btn btn-link btn-animate text-muted"
602 this.handleModBanFromCommunityShow
604 aria-label={i18n.t("ban")}
610 className="btn btn-link btn-animate text-muted"
613 this.handleModBanFromCommunitySubmit
615 aria-label={i18n.t("unban")}
620 {!cv.creator_banned_from_community &&
621 (!this.state.showConfirmAppointAsMod ? (
623 className="btn btn-link btn-animate text-muted"
626 this.handleShowConfirmAppointAsMod
630 ? i18n.t("remove_as_mod")
631 : i18n.t("appoint_as_mod")
635 ? i18n.t("remove_as_mod")
636 : i18n.t("appoint_as_mod")}
641 className="btn btn-link btn-animate text-muted"
642 aria-label={i18n.t("are_you_sure")}
644 {i18n.t("are_you_sure")}
647 className="btn btn-link btn-animate text-muted"
650 this.handleAddModToCommunity
652 aria-label={i18n.t("yes")}
657 className="btn btn-link btn-animate text-muted"
660 this.handleCancelConfirmAppointAsMod
662 aria-label={i18n.t("no")}
670 {/* Community creators and admins can transfer community to another mod */}
671 {(amCommunityCreator_ || canAdmin_) &&
674 (!this.state.showConfirmTransferCommunity ? (
676 className="btn btn-link btn-animate text-muted"
679 this.handleShowConfirmTransferCommunity
681 aria-label={i18n.t("transfer_community")}
683 {i18n.t("transfer_community")}
688 className="btn btn-link btn-animate text-muted"
689 aria-label={i18n.t("are_you_sure")}
691 {i18n.t("are_you_sure")}
694 className="btn btn-link btn-animate text-muted"
697 this.handleTransferCommunity
699 aria-label={i18n.t("yes")}
704 className="btn btn-link btn-animate text-muted"
708 .handleCancelShowConfirmTransferCommunity
710 aria-label={i18n.t("no")}
716 {/* Admins can ban from all, and appoint other admins */}
722 className="btn btn-link btn-animate text-muted"
725 this.handlePurgePersonShow
727 aria-label={i18n.t("purge_user")}
729 {i18n.t("purge_user")}
732 className="btn btn-link btn-animate text-muted"
735 this.handlePurgeCommentShow
737 aria-label={i18n.t("purge_comment")}
739 {i18n.t("purge_comment")}
742 {!isBanned(cv.creator) ? (
744 className="btn btn-link btn-animate text-muted"
747 this.handleModBanShow
749 aria-label={i18n.t("ban_from_site")}
751 {i18n.t("ban_from_site")}
755 className="btn btn-link btn-animate text-muted"
758 this.handleModBanSubmit
760 aria-label={i18n.t("unban_from_site")}
762 {i18n.t("unban_from_site")}
767 {!isBanned(cv.creator) &&
769 (!this.state.showConfirmAppointAsAdmin ? (
771 className="btn btn-link btn-animate text-muted"
774 this.handleShowConfirmAppointAsAdmin
778 ? i18n.t("remove_as_admin")
779 : i18n.t("appoint_as_admin")
783 ? i18n.t("remove_as_admin")
784 : i18n.t("appoint_as_admin")}
788 <button className="btn btn-link btn-animate text-muted">
789 {i18n.t("are_you_sure")}
792 className="btn btn-link btn-animate text-muted"
797 aria-label={i18n.t("yes")}
802 className="btn btn-link btn-animate text-muted"
805 this.handleCancelConfirmAppointAsAdmin
807 aria-label={i18n.t("no")}
820 {/* end of button group */}
825 {showMoreChildren && (
827 className={`details ml-1 comment-node py-2 ${
828 !this.props.noBorder ? "border-top border-light" : ""
830 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
833 className="btn btn-link text-muted"
834 onClick={linkEvent(this, this.handleFetchChildren)}
836 {i18n.t("x_more_replies", {
837 count: node.comment_view.counts.child_count,
838 formattedCount: numToSI(node.comment_view.counts.child_count),
844 {/* end of details */}
845 {this.state.showRemoveDialog && (
847 className="form-inline"
848 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
852 htmlFor={`mod-remove-reason-${cv.comment.id}`}
858 id={`mod-remove-reason-${cv.comment.id}`}
859 className="form-control mr-2"
860 placeholder={i18n.t("reason")}
861 value={this.state.removeReason}
862 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
866 className="btn btn-secondary"
867 aria-label={i18n.t("remove_comment")}
869 {i18n.t("remove_comment")}
873 {this.state.showReportDialog && (
875 className="form-inline"
876 onSubmit={linkEvent(this, this.handleReportSubmit)}
880 htmlFor={`report-reason-${cv.comment.id}`}
887 id={`report-reason-${cv.comment.id}`}
888 className="form-control mr-2"
889 placeholder={i18n.t("reason")}
890 value={this.state.reportReason}
891 onInput={linkEvent(this, this.handleReportReasonChange)}
895 className="btn btn-secondary"
896 aria-label={i18n.t("create_report")}
898 {i18n.t("create_report")}
902 {this.state.showBanDialog && (
903 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
904 <div className="form-group row col-12">
906 className="col-form-label"
907 htmlFor={`mod-ban-reason-${cv.comment.id}`}
913 id={`mod-ban-reason-${cv.comment.id}`}
914 className="form-control mr-2"
915 placeholder={i18n.t("reason")}
916 value={this.state.banReason}
917 onInput={linkEvent(this, this.handleModBanReasonChange)}
920 className="col-form-label"
921 htmlFor={`mod-ban-expires-${cv.comment.id}`}
927 id={`mod-ban-expires-${cv.comment.id}`}
928 className="form-control mr-2"
929 placeholder={i18n.t("number_of_days")}
930 value={this.state.banExpireDays}
931 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
933 <div className="form-group">
934 <div className="form-check">
936 className="form-check-input"
937 id="mod-ban-remove-data"
939 checked={this.state.removeData}
940 onChange={linkEvent(this, this.handleModRemoveDataChange)}
943 className="form-check-label"
944 htmlFor="mod-ban-remove-data"
945 title={i18n.t("remove_content_more")}
947 {i18n.t("remove_content")}
952 {/* TODO hold off on expires until later */}
953 {/* <div class="form-group row"> */}
954 {/* <label class="col-form-label">Expires</label> */}
955 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
957 <div className="form-group row">
960 className="btn btn-secondary"
961 aria-label={i18n.t("ban")}
963 {i18n.t("ban")} {cv.creator.name}
969 {this.state.showPurgeDialog && (
970 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
972 <label className="sr-only" htmlFor="purge-reason">
978 className="form-control my-3"
979 placeholder={i18n.t("reason")}
980 value={this.state.purgeReason}
981 onInput={linkEvent(this, this.handlePurgeReasonChange)}
983 <div className="form-group row col-12">
984 {this.state.purgeLoading ? (
989 className="btn btn-secondary"
990 aria-label={purgeTypeText}
998 {this.state.showReply && (
1001 onReplyCancel={this.handleReplyCancel}
1002 disabled={this.props.locked}
1004 allLanguages={this.props.allLanguages}
1005 siteLanguages={this.props.siteLanguages}
1008 {!this.state.collapsed && node.children.length > 0 && (
1010 nodes={node.children}
1011 locked={this.props.locked}
1012 moderators={this.props.moderators}
1013 admins={this.props.admins}
1014 enableDownvotes={this.props.enableDownvotes}
1015 viewType={this.props.viewType}
1016 allLanguages={this.props.allLanguages}
1017 siteLanguages={this.props.siteLanguages}
1018 hideImages={this.props.hideImages}
1021 {/* A collapsed clearfix */}
1022 {this.state.collapsed && <div className="row col-12"></div>}
1027 get commentReplyOrMentionRead(): boolean {
1028 let cv = this.props.node.comment_view;
1030 if (this.isPersonMentionType(cv)) {
1031 return cv.person_mention.read;
1032 } else if (this.isCommentReplyType(cv)) {
1033 return cv.comment_reply.read;
1039 linkBtn(small = false) {
1040 let cv = this.props.node.comment_view;
1041 let classnames = classNames("btn btn-link btn-animate text-muted", {
1045 let title = this.props.showContext
1046 ? i18n.t("show_context")
1052 className={classnames}
1053 to={`/comment/${cv.comment.id}`}
1056 <Icon icon="link" classes="icon-inline" />
1059 <a className={classnames} title={title} href={cv.comment.ap_id}>
1060 <Icon icon="fedilink" classes="icon-inline" />
1071 get myComment(): boolean {
1073 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1074 this.props.node.comment_view.creator.id
1078 get isPostCreator(): boolean {
1080 this.props.node.comment_view.creator.id ==
1081 this.props.node.comment_view.post.creator_id
1085 get commentUnlessRemoved(): string {
1086 let comment = this.props.node.comment_view.comment;
1087 return comment.removed
1088 ? `*${i18n.t("removed")}*`
1090 ? `*${i18n.t("deleted")}*`
1094 handleReplyClick(i: CommentNode) {
1095 i.setState({ showReply: true });
1098 handleEditClick(i: CommentNode) {
1099 i.setState({ showEdit: true });
1102 handleBlockUserClick(i: CommentNode) {
1103 let auth = myAuth();
1105 let blockUserForm: BlockPerson = {
1106 person_id: i.props.node.comment_view.creator.id,
1110 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1114 handleDeleteClick(i: CommentNode) {
1115 let comment = i.props.node.comment_view.comment;
1116 let auth = myAuth();
1118 let deleteForm: DeleteComment = {
1119 comment_id: comment.id,
1120 deleted: !comment.deleted,
1123 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1127 handleSaveCommentClick(i: CommentNode) {
1128 let cv = i.props.node.comment_view;
1129 let save = cv.saved == undefined ? true : !cv.saved;
1130 let auth = myAuth();
1132 let form: SaveComment = {
1133 comment_id: cv.comment.id,
1138 WebSocketService.Instance.send(wsClient.saveComment(form));
1140 i.setState({ saveLoading: true });
1144 handleReplyCancel() {
1145 this.setState({ showReply: false, showEdit: false });
1148 handleCommentUpvote(event: any) {
1149 event.preventDefault();
1150 let myVote = this.state.my_vote;
1151 let newVote = myVote == 1 ? 0 : 1;
1155 score: this.state.score - 1,
1156 upvotes: this.state.upvotes - 1,
1158 } else if (myVote == -1) {
1160 downvotes: this.state.downvotes - 1,
1161 upvotes: this.state.upvotes + 1,
1162 score: this.state.score + 2,
1166 score: this.state.score + 1,
1167 upvotes: this.state.upvotes + 1,
1171 this.setState({ my_vote: newVote });
1173 let auth = myAuth();
1175 let form: CreateCommentLike = {
1176 comment_id: this.props.node.comment_view.comment.id,
1180 WebSocketService.Instance.send(wsClient.likeComment(form));
1185 handleCommentDownvote(event: any) {
1186 event.preventDefault();
1187 let myVote = this.state.my_vote;
1188 let newVote = myVote == -1 ? 0 : -1;
1192 downvotes: this.state.downvotes + 1,
1193 upvotes: this.state.upvotes - 1,
1194 score: this.state.score - 2,
1196 } else if (myVote == -1) {
1198 downvotes: this.state.downvotes - 1,
1199 score: this.state.score + 1,
1203 downvotes: this.state.downvotes + 1,
1204 score: this.state.score - 1,
1208 this.setState({ my_vote: newVote });
1210 let auth = myAuth();
1212 let form: CreateCommentLike = {
1213 comment_id: this.props.node.comment_view.comment.id,
1218 WebSocketService.Instance.send(wsClient.likeComment(form));
1223 handleShowReportDialog(i: CommentNode) {
1224 i.setState({ showReportDialog: !i.state.showReportDialog });
1227 handleReportReasonChange(i: CommentNode, event: any) {
1228 i.setState({ reportReason: event.target.value });
1231 handleReportSubmit(i: CommentNode) {
1232 let comment = i.props.node.comment_view.comment;
1233 let reason = i.state.reportReason;
1234 let auth = myAuth();
1235 if (reason && auth) {
1236 let form: CreateCommentReport = {
1237 comment_id: comment.id,
1241 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1242 i.setState({ showReportDialog: false });
1246 handleModRemoveShow(i: CommentNode) {
1248 showRemoveDialog: !i.state.showRemoveDialog,
1249 showBanDialog: false,
1253 handleModRemoveReasonChange(i: CommentNode, event: any) {
1254 i.setState({ removeReason: event.target.value });
1257 handleModRemoveDataChange(i: CommentNode, event: any) {
1258 i.setState({ removeData: event.target.checked });
1261 handleModRemoveSubmit(i: CommentNode) {
1262 let comment = i.props.node.comment_view.comment;
1263 let auth = myAuth();
1265 let form: RemoveComment = {
1266 comment_id: comment.id,
1267 removed: !comment.removed,
1268 reason: i.state.removeReason,
1271 WebSocketService.Instance.send(wsClient.removeComment(form));
1273 i.setState({ showRemoveDialog: false });
1277 handleDistinguishClick(i: CommentNode) {
1278 let comment = i.props.node.comment_view.comment;
1279 let auth = myAuth();
1281 let form: EditComment = {
1282 comment_id: comment.id,
1283 distinguished: !comment.distinguished,
1286 WebSocketService.Instance.send(wsClient.editComment(form));
1287 i.setState(i.state);
1291 isPersonMentionType(
1292 item: CommentView | PersonMentionView | CommentReplyView
1293 ): item is PersonMentionView {
1294 return (item as PersonMentionView).person_mention?.id !== undefined;
1298 item: CommentView | PersonMentionView | CommentReplyView
1299 ): item is CommentReplyView {
1300 return (item as CommentReplyView).comment_reply?.id !== undefined;
1303 handleMarkRead(i: CommentNode) {
1304 let auth = myAuth();
1306 if (i.isPersonMentionType(i.props.node.comment_view)) {
1307 let form: MarkPersonMentionAsRead = {
1308 person_mention_id: i.props.node.comment_view.person_mention.id,
1309 read: !i.props.node.comment_view.person_mention.read,
1312 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1313 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1314 let form: MarkCommentReplyAsRead = {
1315 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1316 read: !i.props.node.comment_view.comment_reply.read,
1319 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1322 i.setState({ readLoading: true });
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 handleModBanFromCommunitySubmit(i: CommentNode) {
1351 i.setState({ banType: BanType.Community });
1352 i.handleModBanBothSubmit(i);
1355 handleModBanSubmit(i: CommentNode) {
1356 i.setState({ banType: BanType.Site });
1357 i.handleModBanBothSubmit(i);
1360 handleModBanBothSubmit(i: CommentNode) {
1361 let cv = i.props.node.comment_view;
1362 let auth = myAuth();
1364 if (i.state.banType == BanType.Community) {
1365 // If its an unban, restore all their data
1366 let ban = !cv.creator_banned_from_community;
1368 i.setState({ removeData: false });
1370 let form: BanFromCommunity = {
1371 person_id: cv.creator.id,
1372 community_id: cv.community.id,
1374 remove_data: i.state.removeData,
1375 reason: i.state.banReason,
1376 expires: futureDaysToUnixTime(i.state.banExpireDays),
1379 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1381 // If its an unban, restore all their data
1382 let ban = !cv.creator.banned;
1384 i.setState({ removeData: false });
1386 let form: BanPerson = {
1387 person_id: cv.creator.id,
1389 remove_data: i.state.removeData,
1390 reason: i.state.banReason,
1391 expires: futureDaysToUnixTime(i.state.banExpireDays),
1394 WebSocketService.Instance.send(wsClient.banPerson(form));
1397 i.setState({ showBanDialog: false });
1401 handlePurgePersonShow(i: CommentNode) {
1403 showPurgeDialog: true,
1404 purgeType: PurgeType.Person,
1405 showRemoveDialog: false,
1409 handlePurgeCommentShow(i: CommentNode) {
1411 showPurgeDialog: true,
1412 purgeType: PurgeType.Comment,
1413 showRemoveDialog: false,
1417 handlePurgeReasonChange(i: CommentNode, event: any) {
1418 i.setState({ purgeReason: event.target.value });
1421 handlePurgeSubmit(i: CommentNode, event: any) {
1422 event.preventDefault();
1423 let auth = myAuth();
1425 if (i.state.purgeType == PurgeType.Person) {
1426 let form: PurgePerson = {
1427 person_id: i.props.node.comment_view.creator.id,
1428 reason: i.state.purgeReason,
1431 WebSocketService.Instance.send(wsClient.purgePerson(form));
1432 } else if (i.state.purgeType == PurgeType.Comment) {
1433 let form: PurgeComment = {
1434 comment_id: i.props.node.comment_view.comment.id,
1435 reason: i.state.purgeReason,
1438 WebSocketService.Instance.send(wsClient.purgeComment(form));
1441 i.setState({ purgeLoading: true });
1445 handleShowConfirmAppointAsMod(i: CommentNode) {
1446 i.setState({ showConfirmAppointAsMod: true });
1449 handleCancelConfirmAppointAsMod(i: CommentNode) {
1450 i.setState({ showConfirmAppointAsMod: false });
1453 handleAddModToCommunity(i: CommentNode) {
1454 let cv = i.props.node.comment_view;
1455 let auth = myAuth();
1457 let form: AddModToCommunity = {
1458 person_id: cv.creator.id,
1459 community_id: cv.community.id,
1460 added: !isMod(cv.creator.id, i.props.moderators),
1463 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1464 i.setState({ showConfirmAppointAsMod: false });
1468 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1469 i.setState({ showConfirmAppointAsAdmin: true });
1472 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1473 i.setState({ showConfirmAppointAsAdmin: false });
1476 handleAddAdmin(i: CommentNode) {
1477 let auth = myAuth();
1479 let creatorId = i.props.node.comment_view.creator.id;
1480 let form: AddAdmin = {
1481 person_id: creatorId,
1482 added: !isAdmin(creatorId, i.props.admins),
1485 WebSocketService.Instance.send(wsClient.addAdmin(form));
1486 i.setState({ showConfirmAppointAsAdmin: false });
1490 handleShowConfirmTransferCommunity(i: CommentNode) {
1491 i.setState({ showConfirmTransferCommunity: true });
1494 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1495 i.setState({ showConfirmTransferCommunity: false });
1498 handleTransferCommunity(i: CommentNode) {
1499 let cv = i.props.node.comment_view;
1500 let auth = myAuth();
1502 let form: TransferCommunity = {
1503 community_id: cv.community.id,
1504 person_id: cv.creator.id,
1507 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1508 i.setState({ showConfirmTransferCommunity: false });
1512 handleShowConfirmTransferSite(i: CommentNode) {
1513 i.setState({ showConfirmTransferSite: true });
1516 handleCancelShowConfirmTransferSite(i: CommentNode) {
1517 i.setState({ showConfirmTransferSite: false });
1520 get isCommentNew(): boolean {
1521 let now = moment.utc().subtract(10, "minutes");
1522 let then = moment.utc(this.props.node.comment_view.comment.published);
1523 return now.isBefore(then);
1526 handleCommentCollapse(i: CommentNode) {
1527 i.setState({ collapsed: !i.state.collapsed });
1531 handleViewSource(i: CommentNode) {
1532 i.setState({ viewSource: !i.state.viewSource });
1535 handleShowAdvanced(i: CommentNode) {
1536 i.setState({ showAdvanced: !i.state.showAdvanced });
1540 handleFetchChildren(i: CommentNode) {
1541 let form: GetComments = {
1542 post_id: i.props.node.comment_view.post.id,
1543 parent_id: i.props.node.comment_view.comment.id,
1544 max_depth: commentTreeMaxDepth,
1546 type_: ListingType.All,
1548 auth: myAuth(false),
1551 WebSocketService.Instance.send(wsClient.getComments(form));
1555 if (this.state.my_vote == 1) {
1557 } else if (this.state.my_vote == -1) {
1558 return "text-danger";
1560 return "text-muted";
1564 get pointsTippy(): string {
1565 let points = i18n.t("number_of_points", {
1566 count: this.state.score,
1567 formattedCount: this.state.score,
1570 let upvotes = i18n.t("number_of_upvotes", {
1571 count: this.state.upvotes,
1572 formattedCount: this.state.upvotes,
1575 let downvotes = i18n.t("number_of_downvotes", {
1576 count: this.state.downvotes,
1577 formattedCount: this.state.downvotes,
1580 return `${points} • ${upvotes} • ${downvotes}`;
1583 get expandText(): string {
1584 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");