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 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
263 <div className="badge badge-danger mr-2">
267 {this.props.showCommunity && (
269 <span className="mx-1">{i18n.t("to")}</span>
270 <CommunityLink community={cv.community} />
271 <span className="mx-2">•</span>
272 <Link className="mr-2" to={`/post/${cv.post.id}`}>
278 className="btn btn-sm text-muted"
279 onClick={linkEvent(this, this.handleCommentCollapse)}
280 aria-label={this.expandText}
281 data-tippy-content={this.expandText}
283 {this.state.collapsed ? (
284 <Icon icon="plus-square" classes="icon-inline" />
286 <Icon icon="minus-square" classes="icon-inline" />
290 {/* This is an expanding spacer for mobile */}
291 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
295 className={`unselectable pointer ${this.scoreColor}`}
296 onClick={this.handleCommentUpvote}
297 data-tippy-content={this.pointsTippy}
300 className="mr-1 font-weight-bold"
301 aria-label={i18n.t("number_of_points", {
302 count: Number(this.state.score),
303 formattedCount: numToSI(this.state.score),
306 {numToSI(this.state.score)}
309 <span className="mr-1">•</span>
314 published={cv.comment.published}
315 updated={cv.comment.updated}
319 {/* end of user row */}
320 {this.state.showEdit && (
324 onReplyCancel={this.handleReplyCancel}
325 disabled={this.props.locked}
327 allLanguages={this.props.allLanguages}
328 siteLanguages={this.props.siteLanguages}
331 {!this.state.showEdit && !this.state.collapsed && (
333 {this.state.viewSource ? (
334 <pre>{this.commentUnlessRemoved}</pre>
338 dangerouslySetInnerHTML={
339 this.props.hideImages
340 ? mdToHtmlNoImages(this.commentUnlessRemoved)
341 : mdToHtml(this.commentUnlessRemoved)
345 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
346 {this.props.showContext && this.linkBtn()}
347 {this.props.markable && (
349 className="btn btn-link btn-animate text-muted"
350 onClick={linkEvent(this, this.handleMarkRead)}
352 this.commentReplyOrMentionRead
353 ? i18n.t("mark_as_unread")
354 : i18n.t("mark_as_read")
357 this.commentReplyOrMentionRead
358 ? i18n.t("mark_as_unread")
359 : i18n.t("mark_as_read")
362 {this.state.readLoading ? (
367 classes={`icon-inline ${
368 this.commentReplyOrMentionRead && "text-success"
374 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
377 className={`btn btn-link btn-animate ${
378 this.state.my_vote == 1 ? "text-info" : "text-muted"
380 onClick={this.handleCommentUpvote}
381 data-tippy-content={i18n.t("upvote")}
382 aria-label={i18n.t("upvote")}
384 <Icon icon="arrow-up1" classes="icon-inline" />
386 this.state.upvotes !== this.state.score && (
387 <span className="ml-1">
388 {numToSI(this.state.upvotes)}
392 {this.props.enableDownvotes && (
394 className={`btn btn-link btn-animate ${
395 this.state.my_vote == -1
399 onClick={this.handleCommentDownvote}
400 data-tippy-content={i18n.t("downvote")}
401 aria-label={i18n.t("downvote")}
403 <Icon icon="arrow-down1" classes="icon-inline" />
405 this.state.upvotes !== this.state.score && (
406 <span className="ml-1">
407 {numToSI(this.state.downvotes)}
413 className="btn btn-link btn-animate text-muted"
414 onClick={linkEvent(this, this.handleReplyClick)}
415 data-tippy-content={i18n.t("reply")}
416 aria-label={i18n.t("reply")}
418 <Icon icon="reply1" classes="icon-inline" />
420 {!this.state.showAdvanced ? (
422 className="btn btn-link btn-animate text-muted"
423 onClick={linkEvent(this, this.handleShowAdvanced)}
424 data-tippy-content={i18n.t("more")}
425 aria-label={i18n.t("more")}
427 <Icon icon="more-vertical" classes="icon-inline" />
431 {!this.myComment && (
433 <button className="btn btn-link btn-animate">
435 className="text-muted"
436 to={`/create_private_message/${cv.creator.id}`}
437 title={i18n.t("message").toLowerCase()}
443 className="btn btn-link btn-animate text-muted"
446 this.handleShowReportDialog
448 data-tippy-content={i18n.t(
451 aria-label={i18n.t("show_report_dialog")}
456 className="btn btn-link btn-animate text-muted"
459 this.handleBlockUserClick
461 data-tippy-content={i18n.t("block_user")}
462 aria-label={i18n.t("block_user")}
464 <Icon icon="slash" />
469 className="btn btn-link btn-animate text-muted"
472 this.handleSaveCommentClick
475 cv.saved ? i18n.t("unsave") : i18n.t("save")
478 cv.saved ? i18n.t("unsave") : i18n.t("save")
481 {this.state.saveLoading ? (
486 classes={`icon-inline ${
487 cv.saved && "text-warning"
493 className="btn btn-link btn-animate text-muted"
494 onClick={linkEvent(this, this.handleViewSource)}
495 data-tippy-content={i18n.t("view_source")}
496 aria-label={i18n.t("view_source")}
500 classes={`icon-inline ${
501 this.state.viewSource && "text-success"
508 className="btn btn-link btn-animate text-muted"
509 onClick={linkEvent(this, this.handleEditClick)}
510 data-tippy-content={i18n.t("edit")}
511 aria-label={i18n.t("edit")}
513 <Icon icon="edit" classes="icon-inline" />
516 className="btn btn-link btn-animate text-muted"
519 this.handleDeleteClick
534 classes={`icon-inline ${
535 cv.comment.deleted && "text-danger"
540 {(canModOnSelf || canAdminOnSelf) && (
542 className="btn btn-link btn-animate text-muted"
545 this.handleDistinguishClick
548 !cv.comment.distinguished
549 ? i18n.t("distinguish")
550 : i18n.t("undistinguish")
553 !cv.comment.distinguished
554 ? i18n.t("distinguish")
555 : i18n.t("undistinguish")
560 classes={`icon-inline ${
561 cv.comment.distinguished && "text-danger"
568 {/* Admins and mods can remove comments */}
569 {(canMod_ || canAdmin_) && (
571 {!cv.comment.removed ? (
573 className="btn btn-link btn-animate text-muted"
576 this.handleModRemoveShow
578 aria-label={i18n.t("remove")}
584 className="btn btn-link btn-animate text-muted"
587 this.handleModRemoveSubmit
589 aria-label={i18n.t("restore")}
596 {/* Mods can ban from community, and appoint as mods to community */}
600 (!cv.creator_banned_from_community ? (
602 className="btn btn-link btn-animate text-muted"
605 this.handleModBanFromCommunityShow
607 aria-label={i18n.t("ban_from_community")}
609 {i18n.t("ban_from_community")}
613 className="btn btn-link btn-animate text-muted"
616 this.handleModBanFromCommunitySubmit
618 aria-label={i18n.t("unban")}
623 {!cv.creator_banned_from_community &&
624 (!this.state.showConfirmAppointAsMod ? (
626 className="btn btn-link btn-animate text-muted"
629 this.handleShowConfirmAppointAsMod
633 ? i18n.t("remove_as_mod")
634 : i18n.t("appoint_as_mod")
638 ? i18n.t("remove_as_mod")
639 : i18n.t("appoint_as_mod")}
644 className="btn btn-link btn-animate text-muted"
645 aria-label={i18n.t("are_you_sure")}
647 {i18n.t("are_you_sure")}
650 className="btn btn-link btn-animate text-muted"
653 this.handleAddModToCommunity
655 aria-label={i18n.t("yes")}
660 className="btn btn-link btn-animate text-muted"
663 this.handleCancelConfirmAppointAsMod
665 aria-label={i18n.t("no")}
673 {/* Community creators and admins can transfer community to another mod */}
674 {(amCommunityCreator_ || canAdmin_) &&
677 (!this.state.showConfirmTransferCommunity ? (
679 className="btn btn-link btn-animate text-muted"
682 this.handleShowConfirmTransferCommunity
684 aria-label={i18n.t("transfer_community")}
686 {i18n.t("transfer_community")}
691 className="btn btn-link btn-animate text-muted"
692 aria-label={i18n.t("are_you_sure")}
694 {i18n.t("are_you_sure")}
697 className="btn btn-link btn-animate text-muted"
700 this.handleTransferCommunity
702 aria-label={i18n.t("yes")}
707 className="btn btn-link btn-animate text-muted"
711 .handleCancelShowConfirmTransferCommunity
713 aria-label={i18n.t("no")}
719 {/* Admins can ban from all, and appoint other admins */}
725 className="btn btn-link btn-animate text-muted"
728 this.handlePurgePersonShow
730 aria-label={i18n.t("purge_user")}
732 {i18n.t("purge_user")}
735 className="btn btn-link btn-animate text-muted"
738 this.handlePurgeCommentShow
740 aria-label={i18n.t("purge_comment")}
742 {i18n.t("purge_comment")}
745 {!isBanned(cv.creator) ? (
747 className="btn btn-link btn-animate text-muted"
750 this.handleModBanShow
752 aria-label={i18n.t("ban_from_site")}
754 {i18n.t("ban_from_site")}
758 className="btn btn-link btn-animate text-muted"
761 this.handleModBanSubmit
763 aria-label={i18n.t("unban_from_site")}
765 {i18n.t("unban_from_site")}
770 {!isBanned(cv.creator) &&
772 (!this.state.showConfirmAppointAsAdmin ? (
774 className="btn btn-link btn-animate text-muted"
777 this.handleShowConfirmAppointAsAdmin
781 ? i18n.t("remove_as_admin")
782 : i18n.t("appoint_as_admin")
786 ? i18n.t("remove_as_admin")
787 : i18n.t("appoint_as_admin")}
791 <button className="btn btn-link btn-animate text-muted">
792 {i18n.t("are_you_sure")}
795 className="btn btn-link btn-animate text-muted"
800 aria-label={i18n.t("yes")}
805 className="btn btn-link btn-animate text-muted"
808 this.handleCancelConfirmAppointAsAdmin
810 aria-label={i18n.t("no")}
823 {/* end of button group */}
828 {showMoreChildren && (
830 className={`details ml-1 comment-node py-2 ${
831 !this.props.noBorder ? "border-top border-light" : ""
833 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
836 className="btn btn-link text-muted"
837 onClick={linkEvent(this, this.handleFetchChildren)}
839 {i18n.t("x_more_replies", {
840 count: node.comment_view.counts.child_count,
841 formattedCount: numToSI(
842 BigInt(node.comment_view.counts.child_count)
849 {/* end of details */}
850 {this.state.showRemoveDialog && (
852 className="form-inline"
853 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
857 htmlFor={`mod-remove-reason-${cv.comment.id}`}
863 id={`mod-remove-reason-${cv.comment.id}`}
864 className="form-control mr-2"
865 placeholder={i18n.t("reason")}
866 value={this.state.removeReason}
867 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
871 className="btn btn-secondary"
872 aria-label={i18n.t("remove_comment")}
874 {i18n.t("remove_comment")}
878 {this.state.showReportDialog && (
880 className="form-inline"
881 onSubmit={linkEvent(this, this.handleReportSubmit)}
885 htmlFor={`report-reason-${cv.comment.id}`}
892 id={`report-reason-${cv.comment.id}`}
893 className="form-control mr-2"
894 placeholder={i18n.t("reason")}
895 value={this.state.reportReason}
896 onInput={linkEvent(this, this.handleReportReasonChange)}
900 className="btn btn-secondary"
901 aria-label={i18n.t("create_report")}
903 {i18n.t("create_report")}
907 {this.state.showBanDialog && (
908 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
909 <div className="form-group row col-12">
911 className="col-form-label"
912 htmlFor={`mod-ban-reason-${cv.comment.id}`}
918 id={`mod-ban-reason-${cv.comment.id}`}
919 className="form-control mr-2"
920 placeholder={i18n.t("reason")}
921 value={this.state.banReason}
922 onInput={linkEvent(this, this.handleModBanReasonChange)}
925 className="col-form-label"
926 htmlFor={`mod-ban-expires-${cv.comment.id}`}
932 id={`mod-ban-expires-${cv.comment.id}`}
933 className="form-control mr-2"
934 placeholder={i18n.t("number_of_days")}
935 value={this.state.banExpireDays}
936 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
938 <div className="form-group">
939 <div className="form-check">
941 className="form-check-input"
942 id="mod-ban-remove-data"
944 checked={this.state.removeData}
945 onChange={linkEvent(this, this.handleModRemoveDataChange)}
948 className="form-check-label"
949 htmlFor="mod-ban-remove-data"
950 title={i18n.t("remove_content_more")}
952 {i18n.t("remove_content")}
957 {/* TODO hold off on expires until later */}
958 {/* <div class="form-group row"> */}
959 {/* <label class="col-form-label">Expires</label> */}
960 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
962 <div className="form-group row">
965 className="btn btn-secondary"
966 aria-label={i18n.t("ban")}
968 {i18n.t("ban")} {cv.creator.name}
974 {this.state.showPurgeDialog && (
975 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
977 <label className="sr-only" htmlFor="purge-reason">
983 className="form-control my-3"
984 placeholder={i18n.t("reason")}
985 value={this.state.purgeReason}
986 onInput={linkEvent(this, this.handlePurgeReasonChange)}
988 <div className="form-group row col-12">
989 {this.state.purgeLoading ? (
994 className="btn btn-secondary"
995 aria-label={purgeTypeText}
1003 {this.state.showReply && (
1006 onReplyCancel={this.handleReplyCancel}
1007 disabled={this.props.locked}
1009 allLanguages={this.props.allLanguages}
1010 siteLanguages={this.props.siteLanguages}
1013 {!this.state.collapsed && node.children.length > 0 && (
1015 nodes={node.children}
1016 locked={this.props.locked}
1017 moderators={this.props.moderators}
1018 admins={this.props.admins}
1019 enableDownvotes={this.props.enableDownvotes}
1020 viewType={this.props.viewType}
1021 allLanguages={this.props.allLanguages}
1022 siteLanguages={this.props.siteLanguages}
1023 hideImages={this.props.hideImages}
1026 {/* A collapsed clearfix */}
1027 {this.state.collapsed && <div className="row col-12"></div>}
1032 get commentReplyOrMentionRead(): boolean {
1033 let cv = this.props.node.comment_view;
1035 if (this.isPersonMentionType(cv)) {
1036 return cv.person_mention.read;
1037 } else if (this.isCommentReplyType(cv)) {
1038 return cv.comment_reply.read;
1044 linkBtn(small = false) {
1045 let cv = this.props.node.comment_view;
1046 let classnames = classNames("btn btn-link btn-animate text-muted", {
1050 let title = this.props.showContext
1051 ? i18n.t("show_context")
1057 className={classnames}
1058 to={`/comment/${cv.comment.id}`}
1061 <Icon icon="link" classes="icon-inline" />
1064 <a className={classnames} title={title} href={cv.comment.ap_id}>
1065 <Icon icon="fedilink" classes="icon-inline" />
1076 get myComment(): boolean {
1078 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1079 this.props.node.comment_view.creator.id
1083 get isPostCreator(): boolean {
1085 this.props.node.comment_view.creator.id ==
1086 this.props.node.comment_view.post.creator_id
1090 get commentUnlessRemoved(): string {
1091 let comment = this.props.node.comment_view.comment;
1092 return comment.removed
1093 ? `*${i18n.t("removed")}*`
1095 ? `*${i18n.t("deleted")}*`
1099 handleReplyClick(i: CommentNode) {
1100 i.setState({ showReply: true });
1103 handleEditClick(i: CommentNode) {
1104 i.setState({ showEdit: true });
1107 handleBlockUserClick(i: CommentNode) {
1108 let auth = myAuth();
1110 let blockUserForm: BlockPerson = {
1111 person_id: i.props.node.comment_view.creator.id,
1115 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1119 handleDeleteClick(i: CommentNode) {
1120 let comment = i.props.node.comment_view.comment;
1121 let auth = myAuth();
1123 let deleteForm: DeleteComment = {
1124 comment_id: comment.id,
1125 deleted: !comment.deleted,
1128 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1132 handleSaveCommentClick(i: CommentNode) {
1133 let cv = i.props.node.comment_view;
1134 let save = cv.saved == undefined ? true : !cv.saved;
1135 let auth = myAuth();
1137 let form: SaveComment = {
1138 comment_id: cv.comment.id,
1143 WebSocketService.Instance.send(wsClient.saveComment(form));
1145 i.setState({ saveLoading: true });
1149 handleReplyCancel() {
1150 this.setState({ showReply: false, showEdit: false });
1153 handleCommentUpvote(event: any) {
1154 event.preventDefault();
1155 let myVote = this.state.my_vote;
1156 let newVote = myVote == 1 ? 0 : 1;
1160 score: this.state.score - 1n,
1161 upvotes: this.state.upvotes - 1n,
1163 } else if (myVote == -1) {
1165 downvotes: this.state.downvotes - 1n,
1166 upvotes: this.state.upvotes + 1n,
1167 score: this.state.score + 2n,
1171 score: this.state.score + 1n,
1172 upvotes: this.state.upvotes + 1n,
1176 this.setState({ my_vote: newVote });
1178 let auth = myAuth();
1180 let form: CreateCommentLike = {
1181 comment_id: this.props.node.comment_view.comment.id,
1185 WebSocketService.Instance.send(wsClient.likeComment(form));
1190 handleCommentDownvote(event: any) {
1191 event.preventDefault();
1192 let myVote = this.state.my_vote;
1193 let newVote = myVote == -1 ? 0 : -1;
1197 downvotes: this.state.downvotes + 1n,
1198 upvotes: this.state.upvotes - 1n,
1199 score: this.state.score - 2n,
1201 } else if (myVote == -1) {
1203 downvotes: this.state.downvotes - 1n,
1204 score: this.state.score + 1n,
1208 downvotes: this.state.downvotes + 1n,
1209 score: this.state.score - 1n,
1213 this.setState({ my_vote: newVote });
1215 let auth = myAuth();
1217 let form: CreateCommentLike = {
1218 comment_id: this.props.node.comment_view.comment.id,
1223 WebSocketService.Instance.send(wsClient.likeComment(form));
1228 handleShowReportDialog(i: CommentNode) {
1229 i.setState({ showReportDialog: !i.state.showReportDialog });
1232 handleReportReasonChange(i: CommentNode, event: any) {
1233 i.setState({ reportReason: event.target.value });
1236 handleReportSubmit(i: CommentNode) {
1237 let comment = i.props.node.comment_view.comment;
1238 let reason = i.state.reportReason;
1239 let auth = myAuth();
1240 if (reason && auth) {
1241 let form: CreateCommentReport = {
1242 comment_id: comment.id,
1246 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1247 i.setState({ showReportDialog: false });
1251 handleModRemoveShow(i: CommentNode) {
1253 showRemoveDialog: !i.state.showRemoveDialog,
1254 showBanDialog: false,
1258 handleModRemoveReasonChange(i: CommentNode, event: any) {
1259 i.setState({ removeReason: event.target.value });
1262 handleModRemoveDataChange(i: CommentNode, event: any) {
1263 i.setState({ removeData: event.target.checked });
1266 handleModRemoveSubmit(i: CommentNode) {
1267 let comment = i.props.node.comment_view.comment;
1268 let auth = myAuth();
1270 let form: RemoveComment = {
1271 comment_id: comment.id,
1272 removed: !comment.removed,
1273 reason: i.state.removeReason,
1276 WebSocketService.Instance.send(wsClient.removeComment(form));
1278 i.setState({ showRemoveDialog: false });
1282 handleDistinguishClick(i: CommentNode) {
1283 let comment = i.props.node.comment_view.comment;
1284 let auth = myAuth();
1286 let form: DistinguishComment = {
1287 comment_id: comment.id,
1288 distinguished: !comment.distinguished,
1291 WebSocketService.Instance.send(wsClient.editComment(form));
1292 i.setState(i.state);
1296 isPersonMentionType(
1297 item: CommentView | PersonMentionView | CommentReplyView
1298 ): item is PersonMentionView {
1299 return (item as PersonMentionView).person_mention?.id !== undefined;
1303 item: CommentView | PersonMentionView | CommentReplyView
1304 ): item is CommentReplyView {
1305 return (item as CommentReplyView).comment_reply?.id !== undefined;
1308 handleMarkRead(i: CommentNode) {
1309 let auth = myAuth();
1311 if (i.isPersonMentionType(i.props.node.comment_view)) {
1312 let form: MarkPersonMentionAsRead = {
1313 person_mention_id: i.props.node.comment_view.person_mention.id,
1314 read: !i.props.node.comment_view.person_mention.read,
1317 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1318 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1319 let form: MarkCommentReplyAsRead = {
1320 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1321 read: !i.props.node.comment_view.comment_reply.read,
1324 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1327 i.setState({ readLoading: true });
1331 handleModBanFromCommunityShow(i: CommentNode) {
1333 showBanDialog: true,
1334 banType: BanType.Community,
1335 showRemoveDialog: false,
1339 handleModBanShow(i: CommentNode) {
1341 showBanDialog: true,
1342 banType: BanType.Site,
1343 showRemoveDialog: false,
1347 handleModBanReasonChange(i: CommentNode, event: any) {
1348 i.setState({ banReason: event.target.value });
1351 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1352 i.setState({ banExpireDays: event.target.value });
1355 handleModBanFromCommunitySubmit(i: CommentNode) {
1356 i.setState({ banType: BanType.Community });
1357 i.handleModBanBothSubmit(i);
1360 handleModBanSubmit(i: CommentNode) {
1361 i.setState({ banType: BanType.Site });
1362 i.handleModBanBothSubmit(i);
1365 handleModBanBothSubmit(i: CommentNode) {
1366 let cv = i.props.node.comment_view;
1367 let auth = myAuth();
1369 if (i.state.banType == BanType.Community) {
1370 // If its an unban, restore all their data
1371 let ban = !cv.creator_banned_from_community;
1373 i.setState({ removeData: false });
1375 let form: BanFromCommunity = {
1376 person_id: cv.creator.id,
1377 community_id: cv.community.id,
1379 remove_data: i.state.removeData,
1380 reason: i.state.banReason,
1381 expires: futureDaysToUnixTime(i.state.banExpireDays),
1384 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1386 // If its an unban, restore all their data
1387 let ban = !cv.creator.banned;
1389 i.setState({ removeData: false });
1391 let form: BanPerson = {
1392 person_id: cv.creator.id,
1394 remove_data: i.state.removeData,
1395 reason: i.state.banReason,
1396 expires: futureDaysToUnixTime(i.state.banExpireDays),
1399 WebSocketService.Instance.send(wsClient.banPerson(form));
1402 i.setState({ showBanDialog: false });
1406 handlePurgePersonShow(i: CommentNode) {
1408 showPurgeDialog: true,
1409 purgeType: PurgeType.Person,
1410 showRemoveDialog: false,
1414 handlePurgeCommentShow(i: CommentNode) {
1416 showPurgeDialog: true,
1417 purgeType: PurgeType.Comment,
1418 showRemoveDialog: false,
1422 handlePurgeReasonChange(i: CommentNode, event: any) {
1423 i.setState({ purgeReason: event.target.value });
1426 handlePurgeSubmit(i: CommentNode, event: any) {
1427 event.preventDefault();
1428 let auth = myAuth();
1430 if (i.state.purgeType == PurgeType.Person) {
1431 let form: PurgePerson = {
1432 person_id: i.props.node.comment_view.creator.id,
1433 reason: i.state.purgeReason,
1436 WebSocketService.Instance.send(wsClient.purgePerson(form));
1437 } else if (i.state.purgeType == PurgeType.Comment) {
1438 let form: PurgeComment = {
1439 comment_id: i.props.node.comment_view.comment.id,
1440 reason: i.state.purgeReason,
1443 WebSocketService.Instance.send(wsClient.purgeComment(form));
1446 i.setState({ purgeLoading: true });
1450 handleShowConfirmAppointAsMod(i: CommentNode) {
1451 i.setState({ showConfirmAppointAsMod: true });
1454 handleCancelConfirmAppointAsMod(i: CommentNode) {
1455 i.setState({ showConfirmAppointAsMod: false });
1458 handleAddModToCommunity(i: CommentNode) {
1459 let cv = i.props.node.comment_view;
1460 let auth = myAuth();
1462 let form: AddModToCommunity = {
1463 person_id: cv.creator.id,
1464 community_id: cv.community.id,
1465 added: !isMod(cv.creator.id, i.props.moderators),
1468 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1469 i.setState({ showConfirmAppointAsMod: false });
1473 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1474 i.setState({ showConfirmAppointAsAdmin: true });
1477 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1478 i.setState({ showConfirmAppointAsAdmin: false });
1481 handleAddAdmin(i: CommentNode) {
1482 let auth = myAuth();
1484 let creatorId = i.props.node.comment_view.creator.id;
1485 let form: AddAdmin = {
1486 person_id: creatorId,
1487 added: !isAdmin(creatorId, i.props.admins),
1490 WebSocketService.Instance.send(wsClient.addAdmin(form));
1491 i.setState({ showConfirmAppointAsAdmin: false });
1495 handleShowConfirmTransferCommunity(i: CommentNode) {
1496 i.setState({ showConfirmTransferCommunity: true });
1499 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1500 i.setState({ showConfirmTransferCommunity: false });
1503 handleTransferCommunity(i: CommentNode) {
1504 let cv = i.props.node.comment_view;
1505 let auth = myAuth();
1507 let form: TransferCommunity = {
1508 community_id: cv.community.id,
1509 person_id: cv.creator.id,
1512 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1513 i.setState({ showConfirmTransferCommunity: false });
1517 handleShowConfirmTransferSite(i: CommentNode) {
1518 i.setState({ showConfirmTransferSite: true });
1521 handleCancelShowConfirmTransferSite(i: CommentNode) {
1522 i.setState({ showConfirmTransferSite: false });
1525 get isCommentNew(): boolean {
1526 let now = moment.utc().subtract(10, "minutes");
1527 let then = moment.utc(this.props.node.comment_view.comment.published);
1528 return now.isBefore(then);
1531 handleCommentCollapse(i: CommentNode) {
1532 i.setState({ collapsed: !i.state.collapsed });
1536 handleViewSource(i: CommentNode) {
1537 i.setState({ viewSource: !i.state.viewSource });
1540 handleShowAdvanced(i: CommentNode) {
1541 i.setState({ showAdvanced: !i.state.showAdvanced });
1545 handleFetchChildren(i: CommentNode) {
1546 let form: GetComments = {
1547 post_id: i.props.node.comment_view.post.id,
1548 parent_id: i.props.node.comment_view.comment.id,
1549 max_depth: commentTreeMaxDepth,
1550 limit: 999n, // TODO
1553 auth: myAuth(false),
1556 WebSocketService.Instance.send(wsClient.getComments(form));
1560 if (this.state.my_vote == 1) {
1562 } else if (this.state.my_vote == -1) {
1563 return "text-danger";
1565 return "text-muted";
1569 get pointsTippy(): string {
1570 let points = i18n.t("number_of_points", {
1571 count: Number(this.state.score),
1572 formattedCount: numToSI(this.state.score),
1575 let upvotes = i18n.t("number_of_upvotes", {
1576 count: Number(this.state.upvotes),
1577 formattedCount: numToSI(this.state.upvotes),
1580 let downvotes = i18n.t("number_of_downvotes", {
1581 count: Number(this.state.downvotes),
1582 formattedCount: numToSI(this.state.downvotes),
1585 return `${points} • ${upvotes} • ${downvotes}`;
1588 get expandText(): string {
1589 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");