1 import classNames from "classnames";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
12 CommunityModeratorView,
19 MarkCommentReplyAsRead,
20 MarkPersonMentionAsRead,
28 } from "lemmy-js-client";
29 import moment from "moment";
30 import { i18n } from "../../i18next";
36 } from "../../interfaces";
37 import { UserService, WebSocketService } from "../../services";
56 import { Icon, PurgeWarning, Spinner } from "../common/icon";
57 import { MomentTime } from "../common/moment-time";
58 import { CommunityLink } from "../community/community-link";
59 import { PersonListing } from "../person/person-listing";
60 import { CommentForm } from "./comment-form";
61 import { CommentNodes } from "./comment-nodes";
63 interface CommentNodeState {
66 showRemoveDialog: boolean;
67 removeReason?: string;
68 showBanDialog: boolean;
71 banExpireDays?: number;
73 showPurgeDialog: boolean;
76 purgeLoading: boolean;
77 showConfirmTransferSite: boolean;
78 showConfirmTransferCommunity: boolean;
79 showConfirmAppointAsMod: boolean;
80 showConfirmAppointAsAdmin: boolean;
83 showAdvanced: boolean;
84 showReportDialog: boolean;
85 reportReason?: string;
94 interface CommentNodeProps {
96 moderators?: CommunityModeratorView[];
97 admins?: PersonView[];
103 showContext?: boolean;
104 showCommunity?: boolean;
105 enableDownvotes?: boolean;
106 viewType: CommentViewType;
107 allLanguages: Language[];
108 siteLanguages: number[];
109 hideImages?: boolean;
112 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
113 state: CommentNodeState = {
116 showRemoveDialog: false,
117 showBanDialog: false,
119 banType: BanType.Community,
120 showPurgeDialog: false,
122 purgeType: PurgeType.Person,
126 showConfirmTransferSite: false,
127 showConfirmTransferCommunity: false,
128 showConfirmAppointAsMod: false,
129 showConfirmAppointAsAdmin: false,
130 showReportDialog: false,
131 my_vote: this.props.node.comment_view.my_vote,
132 score: this.props.node.comment_view.counts.score,
133 upvotes: this.props.node.comment_view.counts.upvotes,
134 downvotes: this.props.node.comment_view.counts.downvotes,
139 constructor(props: any, context: any) {
140 super(props, context);
142 this.handleReplyCancel = this.handleReplyCancel.bind(this);
143 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
144 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
147 // TODO see if there's a better way to do this, and all willReceiveProps
148 componentWillReceiveProps(nextProps: CommentNodeProps) {
149 let cv = nextProps.node.comment_view;
152 upvotes: cv.counts.upvotes,
153 downvotes: cv.counts.downvotes,
154 score: cv.counts.score,
161 let node = this.props.node;
162 let cv = this.props.node.comment_view;
165 this.state.purgeType == PurgeType.Comment
166 ? i18n.t("purge_comment")
167 : `${i18n.t("purge")} ${cv.creator.name}`;
170 canMod(cv.creator.id, this.props.moderators, this.props.admins) &&
175 this.props.moderators,
177 UserService.Instance.myUserInfo,
179 ) && cv.community.local;
181 canAdmin(cv.creator.id, this.props.admins) && cv.community.local;
186 UserService.Instance.myUserInfo,
188 ) && cv.community.local;
189 let isMod_ = isMod(cv.creator.id, this.props.moderators);
191 isAdmin(cv.creator.id, this.props.admins) && cv.community.local;
192 let amCommunityCreator_ = amCommunityCreator(
194 this.props.moderators
197 let borderColor = this.props.node.depth
198 ? colorList[(this.props.node.depth - 1) % colorList.length]
200 let moreRepliesBorderColor = this.props.node.depth
201 ? colorList[this.props.node.depth % colorList.length]
204 let showMoreChildren =
205 this.props.viewType == CommentViewType.Tree &&
206 !this.state.collapsed &&
207 node.children.length == 0 &&
208 node.comment_view.counts.child_count > 0;
212 className={`comment ${
213 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
217 id={`comment-${cv.comment.id}`}
218 className={classNames(`details comment-node py-2`, {
219 "border-top border-light": !this.props.noBorder,
222 this.props.node.comment_view.comment.distinguished,
225 !this.props.noIndent && this.props.node.depth
226 ? `border-left: 2px ${borderColor} solid !important`
231 className={classNames({
232 "ml-2": !this.props.noIndent && this.props.node.depth,
235 <div className="d-flex flex-wrap align-items-center text-muted small">
236 <span className="mr-2">
237 <PersonListing person={cv.creator} />
239 {cv.comment.distinguished && (
240 <Icon icon="shield" inline classes={`text-danger mr-2`} />
243 <div className="badge badge-light d-none d-sm-inline mr-2">
248 <div className="badge badge-light d-none d-sm-inline mr-2">
252 {this.isPostCreator && (
253 <div className="badge badge-light d-none d-sm-inline mr-2">
257 {cv.creator.bot_account && (
258 <div className="badge badge-light d-none d-sm-inline mr-2">
259 {i18n.t("bot_account").toLowerCase()}
262 {this.props.showCommunity && (
264 <span className="mx-1">{i18n.t("to")}</span>
265 <CommunityLink community={cv.community} />
266 <span className="mx-2">•</span>
267 <Link className="mr-2" to={`/post/${cv.post.id}`}>
273 className="btn btn-sm text-muted"
274 onClick={linkEvent(this, this.handleCommentCollapse)}
275 aria-label={this.expandText}
276 data-tippy-content={this.expandText}
278 {this.state.collapsed ? (
279 <Icon icon="plus-square" classes="icon-inline" />
281 <Icon icon="minus-square" classes="icon-inline" />
285 {/* This is an expanding spacer for mobile */}
286 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
290 className={`unselectable pointer ${this.scoreColor}`}
291 onClick={this.handleCommentUpvote}
292 data-tippy-content={this.pointsTippy}
295 className="mr-1 font-weight-bold"
296 aria-label={i18n.t("number_of_points", {
297 count: Number(this.state.score),
298 formattedCount: numToSI(this.state.score),
301 {numToSI(this.state.score)}
304 <span className="mr-1">•</span>
309 published={cv.comment.published}
310 updated={cv.comment.updated}
314 {/* end of user row */}
315 {this.state.showEdit && (
319 onReplyCancel={this.handleReplyCancel}
320 disabled={this.props.locked}
322 allLanguages={this.props.allLanguages}
323 siteLanguages={this.props.siteLanguages}
326 {!this.state.showEdit && !this.state.collapsed && (
328 {this.state.viewSource ? (
329 <pre>{this.commentUnlessRemoved}</pre>
333 dangerouslySetInnerHTML={
334 this.props.hideImages
335 ? mdToHtmlNoImages(this.commentUnlessRemoved)
336 : mdToHtml(this.commentUnlessRemoved)
340 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
341 {this.props.showContext && this.linkBtn()}
342 {this.props.markable && (
344 className="btn btn-link btn-animate text-muted"
345 onClick={linkEvent(this, this.handleMarkRead)}
347 this.commentReplyOrMentionRead
348 ? i18n.t("mark_as_unread")
349 : i18n.t("mark_as_read")
352 this.commentReplyOrMentionRead
353 ? i18n.t("mark_as_unread")
354 : i18n.t("mark_as_read")
357 {this.state.readLoading ? (
362 classes={`icon-inline ${
363 this.commentReplyOrMentionRead && "text-success"
369 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
372 className={`btn btn-link btn-animate ${
373 this.state.my_vote == 1 ? "text-info" : "text-muted"
375 onClick={this.handleCommentUpvote}
376 data-tippy-content={i18n.t("upvote")}
377 aria-label={i18n.t("upvote")}
379 <Icon icon="arrow-up1" classes="icon-inline" />
381 this.state.upvotes !== this.state.score && (
382 <span className="ml-1">
383 {numToSI(this.state.upvotes)}
387 {this.props.enableDownvotes && (
389 className={`btn btn-link btn-animate ${
390 this.state.my_vote == -1
394 onClick={this.handleCommentDownvote}
395 data-tippy-content={i18n.t("downvote")}
396 aria-label={i18n.t("downvote")}
398 <Icon icon="arrow-down1" classes="icon-inline" />
400 this.state.upvotes !== this.state.score && (
401 <span className="ml-1">
402 {numToSI(this.state.downvotes)}
408 className="btn btn-link btn-animate text-muted"
409 onClick={linkEvent(this, this.handleReplyClick)}
410 data-tippy-content={i18n.t("reply")}
411 aria-label={i18n.t("reply")}
413 <Icon icon="reply1" classes="icon-inline" />
415 {!this.state.showAdvanced ? (
417 className="btn btn-link btn-animate text-muted"
418 onClick={linkEvent(this, this.handleShowAdvanced)}
419 data-tippy-content={i18n.t("more")}
420 aria-label={i18n.t("more")}
422 <Icon icon="more-vertical" classes="icon-inline" />
426 {!this.myComment && (
428 <button className="btn btn-link btn-animate">
430 className="text-muted"
431 to={`/create_private_message/${cv.creator.id}`}
432 title={i18n.t("message").toLowerCase()}
438 className="btn btn-link btn-animate text-muted"
441 this.handleShowReportDialog
443 data-tippy-content={i18n.t(
446 aria-label={i18n.t("show_report_dialog")}
451 className="btn btn-link btn-animate text-muted"
454 this.handleBlockUserClick
456 data-tippy-content={i18n.t("block_user")}
457 aria-label={i18n.t("block_user")}
459 <Icon icon="slash" />
464 className="btn btn-link btn-animate text-muted"
467 this.handleSaveCommentClick
470 cv.saved ? i18n.t("unsave") : i18n.t("save")
473 cv.saved ? i18n.t("unsave") : i18n.t("save")
476 {this.state.saveLoading ? (
481 classes={`icon-inline ${
482 cv.saved && "text-warning"
488 className="btn btn-link btn-animate text-muted"
489 onClick={linkEvent(this, this.handleViewSource)}
490 data-tippy-content={i18n.t("view_source")}
491 aria-label={i18n.t("view_source")}
495 classes={`icon-inline ${
496 this.state.viewSource && "text-success"
503 className="btn btn-link btn-animate text-muted"
504 onClick={linkEvent(this, this.handleEditClick)}
505 data-tippy-content={i18n.t("edit")}
506 aria-label={i18n.t("edit")}
508 <Icon icon="edit" classes="icon-inline" />
511 className="btn btn-link btn-animate text-muted"
514 this.handleDeleteClick
529 classes={`icon-inline ${
530 cv.comment.deleted && "text-danger"
535 {(canModOnSelf || canAdminOnSelf) && (
537 className="btn btn-link btn-animate text-muted"
540 this.handleDistinguishClick
543 !cv.comment.distinguished
544 ? i18n.t("distinguish")
545 : i18n.t("undistinguish")
548 !cv.comment.distinguished
549 ? i18n.t("distinguish")
550 : i18n.t("undistinguish")
555 classes={`icon-inline ${
556 cv.comment.distinguished && "text-danger"
563 {/* Admins and mods can remove comments */}
564 {(canMod_ || canAdmin_) && (
566 {!cv.comment.removed ? (
568 className="btn btn-link btn-animate text-muted"
571 this.handleModRemoveShow
573 aria-label={i18n.t("remove")}
579 className="btn btn-link btn-animate text-muted"
582 this.handleModRemoveSubmit
584 aria-label={i18n.t("restore")}
591 {/* Mods can ban from community, and appoint as mods to community */}
595 (!cv.creator_banned_from_community ? (
597 className="btn btn-link btn-animate text-muted"
600 this.handleModBanFromCommunityShow
602 aria-label={i18n.t("ban_from_community")}
604 {i18n.t("ban_from_community")}
608 className="btn btn-link btn-animate text-muted"
611 this.handleModBanFromCommunitySubmit
613 aria-label={i18n.t("unban")}
618 {!cv.creator_banned_from_community &&
619 (!this.state.showConfirmAppointAsMod ? (
621 className="btn btn-link btn-animate text-muted"
624 this.handleShowConfirmAppointAsMod
628 ? i18n.t("remove_as_mod")
629 : i18n.t("appoint_as_mod")
633 ? i18n.t("remove_as_mod")
634 : i18n.t("appoint_as_mod")}
639 className="btn btn-link btn-animate text-muted"
640 aria-label={i18n.t("are_you_sure")}
642 {i18n.t("are_you_sure")}
645 className="btn btn-link btn-animate text-muted"
648 this.handleAddModToCommunity
650 aria-label={i18n.t("yes")}
655 className="btn btn-link btn-animate text-muted"
658 this.handleCancelConfirmAppointAsMod
660 aria-label={i18n.t("no")}
668 {/* Community creators and admins can transfer community to another mod */}
669 {(amCommunityCreator_ || canAdmin_) &&
672 (!this.state.showConfirmTransferCommunity ? (
674 className="btn btn-link btn-animate text-muted"
677 this.handleShowConfirmTransferCommunity
679 aria-label={i18n.t("transfer_community")}
681 {i18n.t("transfer_community")}
686 className="btn btn-link btn-animate text-muted"
687 aria-label={i18n.t("are_you_sure")}
689 {i18n.t("are_you_sure")}
692 className="btn btn-link btn-animate text-muted"
695 this.handleTransferCommunity
697 aria-label={i18n.t("yes")}
702 className="btn btn-link btn-animate text-muted"
706 .handleCancelShowConfirmTransferCommunity
708 aria-label={i18n.t("no")}
714 {/* Admins can ban from all, and appoint other admins */}
720 className="btn btn-link btn-animate text-muted"
723 this.handlePurgePersonShow
725 aria-label={i18n.t("purge_user")}
727 {i18n.t("purge_user")}
730 className="btn btn-link btn-animate text-muted"
733 this.handlePurgeCommentShow
735 aria-label={i18n.t("purge_comment")}
737 {i18n.t("purge_comment")}
740 {!isBanned(cv.creator) ? (
742 className="btn btn-link btn-animate text-muted"
745 this.handleModBanShow
747 aria-label={i18n.t("ban_from_site")}
749 {i18n.t("ban_from_site")}
753 className="btn btn-link btn-animate text-muted"
756 this.handleModBanSubmit
758 aria-label={i18n.t("unban_from_site")}
760 {i18n.t("unban_from_site")}
765 {!isBanned(cv.creator) &&
767 (!this.state.showConfirmAppointAsAdmin ? (
769 className="btn btn-link btn-animate text-muted"
772 this.handleShowConfirmAppointAsAdmin
776 ? i18n.t("remove_as_admin")
777 : i18n.t("appoint_as_admin")
781 ? i18n.t("remove_as_admin")
782 : i18n.t("appoint_as_admin")}
786 <button className="btn btn-link btn-animate text-muted">
787 {i18n.t("are_you_sure")}
790 className="btn btn-link btn-animate text-muted"
795 aria-label={i18n.t("yes")}
800 className="btn btn-link btn-animate text-muted"
803 this.handleCancelConfirmAppointAsAdmin
805 aria-label={i18n.t("no")}
818 {/* end of button group */}
823 {showMoreChildren && (
825 className={`details ml-1 comment-node py-2 ${
826 !this.props.noBorder ? "border-top border-light" : ""
828 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
831 className="btn btn-link text-muted"
832 onClick={linkEvent(this, this.handleFetchChildren)}
834 {i18n.t("x_more_replies", {
835 count: node.comment_view.counts.child_count,
836 formattedCount: numToSI(node.comment_view.counts.child_count),
842 {/* end of details */}
843 {this.state.showRemoveDialog && (
845 className="form-inline"
846 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
850 htmlFor={`mod-remove-reason-${cv.comment.id}`}
856 id={`mod-remove-reason-${cv.comment.id}`}
857 className="form-control mr-2"
858 placeholder={i18n.t("reason")}
859 value={this.state.removeReason}
860 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
864 className="btn btn-secondary"
865 aria-label={i18n.t("remove_comment")}
867 {i18n.t("remove_comment")}
871 {this.state.showReportDialog && (
873 className="form-inline"
874 onSubmit={linkEvent(this, this.handleReportSubmit)}
878 htmlFor={`report-reason-${cv.comment.id}`}
885 id={`report-reason-${cv.comment.id}`}
886 className="form-control mr-2"
887 placeholder={i18n.t("reason")}
888 value={this.state.reportReason}
889 onInput={linkEvent(this, this.handleReportReasonChange)}
893 className="btn btn-secondary"
894 aria-label={i18n.t("create_report")}
896 {i18n.t("create_report")}
900 {this.state.showBanDialog && (
901 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
902 <div className="form-group row col-12">
904 className="col-form-label"
905 htmlFor={`mod-ban-reason-${cv.comment.id}`}
911 id={`mod-ban-reason-${cv.comment.id}`}
912 className="form-control mr-2"
913 placeholder={i18n.t("reason")}
914 value={this.state.banReason}
915 onInput={linkEvent(this, this.handleModBanReasonChange)}
918 className="col-form-label"
919 htmlFor={`mod-ban-expires-${cv.comment.id}`}
925 id={`mod-ban-expires-${cv.comment.id}`}
926 className="form-control mr-2"
927 placeholder={i18n.t("number_of_days")}
928 value={this.state.banExpireDays}
929 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
931 <div className="form-group">
932 <div className="form-check">
934 className="form-check-input"
935 id="mod-ban-remove-data"
937 checked={this.state.removeData}
938 onChange={linkEvent(this, this.handleModRemoveDataChange)}
941 className="form-check-label"
942 htmlFor="mod-ban-remove-data"
943 title={i18n.t("remove_content_more")}
945 {i18n.t("remove_content")}
950 {/* TODO hold off on expires until later */}
951 {/* <div class="form-group row"> */}
952 {/* <label class="col-form-label">Expires</label> */}
953 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
955 <div className="form-group row">
958 className="btn btn-secondary"
959 aria-label={i18n.t("ban")}
961 {i18n.t("ban")} {cv.creator.name}
967 {this.state.showPurgeDialog && (
968 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
970 <label className="sr-only" htmlFor="purge-reason">
976 className="form-control my-3"
977 placeholder={i18n.t("reason")}
978 value={this.state.purgeReason}
979 onInput={linkEvent(this, this.handlePurgeReasonChange)}
981 <div className="form-group row col-12">
982 {this.state.purgeLoading ? (
987 className="btn btn-secondary"
988 aria-label={purgeTypeText}
996 {this.state.showReply && (
999 onReplyCancel={this.handleReplyCancel}
1000 disabled={this.props.locked}
1002 allLanguages={this.props.allLanguages}
1003 siteLanguages={this.props.siteLanguages}
1006 {!this.state.collapsed && node.children.length > 0 && (
1008 nodes={node.children}
1009 locked={this.props.locked}
1010 moderators={this.props.moderators}
1011 admins={this.props.admins}
1012 enableDownvotes={this.props.enableDownvotes}
1013 viewType={this.props.viewType}
1014 allLanguages={this.props.allLanguages}
1015 siteLanguages={this.props.siteLanguages}
1016 hideImages={this.props.hideImages}
1019 {/* A collapsed clearfix */}
1020 {this.state.collapsed && <div className="row col-12"></div>}
1025 get commentReplyOrMentionRead(): boolean {
1026 let cv = this.props.node.comment_view;
1028 if (this.isPersonMentionType(cv)) {
1029 return cv.person_mention.read;
1030 } else if (this.isCommentReplyType(cv)) {
1031 return cv.comment_reply.read;
1037 linkBtn(small = false) {
1038 let cv = this.props.node.comment_view;
1039 let classnames = classNames("btn btn-link btn-animate text-muted", {
1043 let title = this.props.showContext
1044 ? i18n.t("show_context")
1050 className={classnames}
1051 to={`/comment/${cv.comment.id}`}
1054 <Icon icon="link" classes="icon-inline" />
1057 <a className={classnames} title={title} href={cv.comment.ap_id}>
1058 <Icon icon="fedilink" classes="icon-inline" />
1069 get myComment(): boolean {
1071 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1072 this.props.node.comment_view.creator.id
1076 get isPostCreator(): boolean {
1078 this.props.node.comment_view.creator.id ==
1079 this.props.node.comment_view.post.creator_id
1083 get commentUnlessRemoved(): string {
1084 let comment = this.props.node.comment_view.comment;
1085 return comment.removed
1086 ? `*${i18n.t("removed")}*`
1088 ? `*${i18n.t("deleted")}*`
1092 handleReplyClick(i: CommentNode) {
1093 i.setState({ showReply: true });
1096 handleEditClick(i: CommentNode) {
1097 i.setState({ showEdit: true });
1100 handleBlockUserClick(i: CommentNode) {
1101 let auth = myAuth();
1103 let blockUserForm: BlockPerson = {
1104 person_id: i.props.node.comment_view.creator.id,
1108 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1112 handleDeleteClick(i: CommentNode) {
1113 let comment = i.props.node.comment_view.comment;
1114 let auth = myAuth();
1116 let deleteForm: DeleteComment = {
1117 comment_id: comment.id,
1118 deleted: !comment.deleted,
1121 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1125 handleSaveCommentClick(i: CommentNode) {
1126 let cv = i.props.node.comment_view;
1127 let save = cv.saved == undefined ? true : !cv.saved;
1128 let auth = myAuth();
1130 let form: SaveComment = {
1131 comment_id: cv.comment.id,
1136 WebSocketService.Instance.send(wsClient.saveComment(form));
1138 i.setState({ saveLoading: true });
1142 handleReplyCancel() {
1143 this.setState({ showReply: false, showEdit: false });
1146 handleCommentUpvote(event: any) {
1147 event.preventDefault();
1148 let myVote = this.state.my_vote;
1149 let newVote = myVote == 1 ? 0 : 1;
1153 score: this.state.score - 1,
1154 upvotes: this.state.upvotes - 1,
1156 } else if (myVote == -1) {
1158 downvotes: this.state.downvotes - 1,
1159 upvotes: this.state.upvotes + 1,
1160 score: this.state.score + 2,
1164 score: this.state.score + 1,
1165 upvotes: this.state.upvotes + 1,
1169 this.setState({ my_vote: newVote });
1171 let auth = myAuth();
1173 let form: CreateCommentLike = {
1174 comment_id: this.props.node.comment_view.comment.id,
1178 WebSocketService.Instance.send(wsClient.likeComment(form));
1183 handleCommentDownvote(event: any) {
1184 event.preventDefault();
1185 let myVote = this.state.my_vote;
1186 let newVote = myVote == -1 ? 0 : -1;
1190 downvotes: this.state.downvotes + 1,
1191 upvotes: this.state.upvotes - 1,
1192 score: this.state.score - 2,
1194 } else if (myVote == -1) {
1196 downvotes: this.state.downvotes - 1,
1197 score: this.state.score + 1,
1201 downvotes: this.state.downvotes + 1,
1202 score: this.state.score - 1,
1206 this.setState({ my_vote: newVote });
1208 let auth = myAuth();
1210 let form: CreateCommentLike = {
1211 comment_id: this.props.node.comment_view.comment.id,
1216 WebSocketService.Instance.send(wsClient.likeComment(form));
1221 handleShowReportDialog(i: CommentNode) {
1222 i.setState({ showReportDialog: !i.state.showReportDialog });
1225 handleReportReasonChange(i: CommentNode, event: any) {
1226 i.setState({ reportReason: event.target.value });
1229 handleReportSubmit(i: CommentNode) {
1230 let comment = i.props.node.comment_view.comment;
1231 let reason = i.state.reportReason;
1232 let auth = myAuth();
1233 if (reason && auth) {
1234 let form: CreateCommentReport = {
1235 comment_id: comment.id,
1239 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1240 i.setState({ showReportDialog: false });
1244 handleModRemoveShow(i: CommentNode) {
1246 showRemoveDialog: !i.state.showRemoveDialog,
1247 showBanDialog: false,
1251 handleModRemoveReasonChange(i: CommentNode, event: any) {
1252 i.setState({ removeReason: event.target.value });
1255 handleModRemoveDataChange(i: CommentNode, event: any) {
1256 i.setState({ removeData: event.target.checked });
1259 handleModRemoveSubmit(i: CommentNode) {
1260 let comment = i.props.node.comment_view.comment;
1261 let auth = myAuth();
1263 let form: RemoveComment = {
1264 comment_id: comment.id,
1265 removed: !comment.removed,
1266 reason: i.state.removeReason,
1269 WebSocketService.Instance.send(wsClient.removeComment(form));
1271 i.setState({ showRemoveDialog: false });
1275 handleDistinguishClick(i: CommentNode) {
1276 let comment = i.props.node.comment_view.comment;
1277 let auth = myAuth();
1279 let form: DistinguishComment = {
1280 comment_id: comment.id,
1281 distinguished: !comment.distinguished,
1284 WebSocketService.Instance.send(wsClient.editComment(form));
1285 i.setState(i.state);
1289 isPersonMentionType(
1290 item: CommentView | PersonMentionView | CommentReplyView
1291 ): item is PersonMentionView {
1292 return (item as PersonMentionView).person_mention?.id !== undefined;
1296 item: CommentView | PersonMentionView | CommentReplyView
1297 ): item is CommentReplyView {
1298 return (item as CommentReplyView).comment_reply?.id !== undefined;
1301 handleMarkRead(i: CommentNode) {
1302 let auth = myAuth();
1304 if (i.isPersonMentionType(i.props.node.comment_view)) {
1305 let form: MarkPersonMentionAsRead = {
1306 person_mention_id: i.props.node.comment_view.person_mention.id,
1307 read: !i.props.node.comment_view.person_mention.read,
1310 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1311 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1312 let form: MarkCommentReplyAsRead = {
1313 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1314 read: !i.props.node.comment_view.comment_reply.read,
1317 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1320 i.setState({ readLoading: true });
1324 handleModBanFromCommunityShow(i: CommentNode) {
1326 showBanDialog: true,
1327 banType: BanType.Community,
1328 showRemoveDialog: false,
1332 handleModBanShow(i: CommentNode) {
1334 showBanDialog: true,
1335 banType: BanType.Site,
1336 showRemoveDialog: false,
1340 handleModBanReasonChange(i: CommentNode, event: any) {
1341 i.setState({ banReason: event.target.value });
1344 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1345 i.setState({ banExpireDays: event.target.value });
1348 handleModBanFromCommunitySubmit(i: CommentNode) {
1349 i.setState({ banType: BanType.Community });
1350 i.handleModBanBothSubmit(i);
1353 handleModBanSubmit(i: CommentNode) {
1354 i.setState({ banType: BanType.Site });
1355 i.handleModBanBothSubmit(i);
1358 handleModBanBothSubmit(i: CommentNode) {
1359 let cv = i.props.node.comment_view;
1360 let auth = myAuth();
1362 if (i.state.banType == BanType.Community) {
1363 // If its an unban, restore all their data
1364 let ban = !cv.creator_banned_from_community;
1366 i.setState({ removeData: false });
1368 let form: BanFromCommunity = {
1369 person_id: cv.creator.id,
1370 community_id: cv.community.id,
1372 remove_data: i.state.removeData,
1373 reason: i.state.banReason,
1374 expires: futureDaysToUnixTime(i.state.banExpireDays),
1377 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1379 // If its an unban, restore all their data
1380 let ban = !cv.creator.banned;
1382 i.setState({ removeData: false });
1384 let form: BanPerson = {
1385 person_id: cv.creator.id,
1387 remove_data: i.state.removeData,
1388 reason: i.state.banReason,
1389 expires: futureDaysToUnixTime(i.state.banExpireDays),
1392 WebSocketService.Instance.send(wsClient.banPerson(form));
1395 i.setState({ showBanDialog: false });
1399 handlePurgePersonShow(i: CommentNode) {
1401 showPurgeDialog: true,
1402 purgeType: PurgeType.Person,
1403 showRemoveDialog: false,
1407 handlePurgeCommentShow(i: CommentNode) {
1409 showPurgeDialog: true,
1410 purgeType: PurgeType.Comment,
1411 showRemoveDialog: false,
1415 handlePurgeReasonChange(i: CommentNode, event: any) {
1416 i.setState({ purgeReason: event.target.value });
1419 handlePurgeSubmit(i: CommentNode, event: any) {
1420 event.preventDefault();
1421 let auth = myAuth();
1423 if (i.state.purgeType == PurgeType.Person) {
1424 let form: PurgePerson = {
1425 person_id: i.props.node.comment_view.creator.id,
1426 reason: i.state.purgeReason,
1429 WebSocketService.Instance.send(wsClient.purgePerson(form));
1430 } else if (i.state.purgeType == PurgeType.Comment) {
1431 let form: PurgeComment = {
1432 comment_id: i.props.node.comment_view.comment.id,
1433 reason: i.state.purgeReason,
1436 WebSocketService.Instance.send(wsClient.purgeComment(form));
1439 i.setState({ purgeLoading: true });
1443 handleShowConfirmAppointAsMod(i: CommentNode) {
1444 i.setState({ showConfirmAppointAsMod: true });
1447 handleCancelConfirmAppointAsMod(i: CommentNode) {
1448 i.setState({ showConfirmAppointAsMod: false });
1451 handleAddModToCommunity(i: CommentNode) {
1452 let cv = i.props.node.comment_view;
1453 let auth = myAuth();
1455 let form: AddModToCommunity = {
1456 person_id: cv.creator.id,
1457 community_id: cv.community.id,
1458 added: !isMod(cv.creator.id, i.props.moderators),
1461 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1462 i.setState({ showConfirmAppointAsMod: false });
1466 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1467 i.setState({ showConfirmAppointAsAdmin: true });
1470 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1471 i.setState({ showConfirmAppointAsAdmin: false });
1474 handleAddAdmin(i: CommentNode) {
1475 let auth = myAuth();
1477 let creatorId = i.props.node.comment_view.creator.id;
1478 let form: AddAdmin = {
1479 person_id: creatorId,
1480 added: !isAdmin(creatorId, i.props.admins),
1483 WebSocketService.Instance.send(wsClient.addAdmin(form));
1484 i.setState({ showConfirmAppointAsAdmin: false });
1488 handleShowConfirmTransferCommunity(i: CommentNode) {
1489 i.setState({ showConfirmTransferCommunity: true });
1492 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1493 i.setState({ showConfirmTransferCommunity: false });
1496 handleTransferCommunity(i: CommentNode) {
1497 let cv = i.props.node.comment_view;
1498 let auth = myAuth();
1500 let form: TransferCommunity = {
1501 community_id: cv.community.id,
1502 person_id: cv.creator.id,
1505 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1506 i.setState({ showConfirmTransferCommunity: false });
1510 handleShowConfirmTransferSite(i: CommentNode) {
1511 i.setState({ showConfirmTransferSite: true });
1514 handleCancelShowConfirmTransferSite(i: CommentNode) {
1515 i.setState({ showConfirmTransferSite: false });
1518 get isCommentNew(): boolean {
1519 let now = moment.utc().subtract(10, "minutes");
1520 let then = moment.utc(this.props.node.comment_view.comment.published);
1521 return now.isBefore(then);
1524 handleCommentCollapse(i: CommentNode) {
1525 i.setState({ collapsed: !i.state.collapsed });
1529 handleViewSource(i: CommentNode) {
1530 i.setState({ viewSource: !i.state.viewSource });
1533 handleShowAdvanced(i: CommentNode) {
1534 i.setState({ showAdvanced: !i.state.showAdvanced });
1538 handleFetchChildren(i: CommentNode) {
1539 let form: GetComments = {
1540 post_id: i.props.node.comment_view.post.id,
1541 parent_id: i.props.node.comment_view.comment.id,
1542 max_depth: commentTreeMaxDepth,
1546 auth: myAuth(false),
1549 WebSocketService.Instance.send(wsClient.getComments(form));
1553 if (this.state.my_vote == 1) {
1555 } else if (this.state.my_vote == -1) {
1556 return "text-danger";
1558 return "text-muted";
1562 get pointsTippy(): string {
1563 let points = i18n.t("number_of_points", {
1564 count: Number(this.state.score),
1565 formattedCount: numToSI(this.state.score),
1568 let upvotes = i18n.t("number_of_upvotes", {
1569 count: Number(this.state.upvotes),
1570 formattedCount: numToSI(this.state.upvotes),
1573 let downvotes = i18n.t("number_of_downvotes", {
1574 count: Number(this.state.downvotes),
1575 formattedCount: numToSI(this.state.downvotes),
1578 return `${points} • ${upvotes} • ${downvotes}`;
1581 get expandText(): string {
1582 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");