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";
57 import { Icon, PurgeWarning, Spinner } from "../common/icon";
58 import { MomentTime } from "../common/moment-time";
59 import { CommunityLink } from "../community/community-link";
60 import { PersonListing } from "../person/person-listing";
61 import { CommentForm } from "./comment-form";
62 import { CommentNodes } from "./comment-nodes";
64 interface CommentNodeState {
67 showRemoveDialog: boolean;
68 removeReason?: string;
69 showBanDialog: boolean;
72 banExpireDays?: number;
74 showPurgeDialog: boolean;
77 purgeLoading: boolean;
78 showConfirmTransferSite: boolean;
79 showConfirmTransferCommunity: boolean;
80 showConfirmAppointAsMod: boolean;
81 showConfirmAppointAsAdmin: boolean;
84 showAdvanced: boolean;
85 showReportDialog: boolean;
86 reportReason?: string;
95 interface CommentNodeProps {
97 moderators?: CommunityModeratorView[];
98 admins?: PersonView[];
104 showContext?: boolean;
105 showCommunity?: boolean;
106 enableDownvotes?: boolean;
107 viewType: CommentViewType;
108 allLanguages: Language[];
109 siteLanguages: number[];
110 hideImages?: boolean;
113 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
114 state: CommentNodeState = {
117 showRemoveDialog: false,
118 showBanDialog: false,
120 banType: BanType.Community,
121 showPurgeDialog: false,
123 purgeType: PurgeType.Person,
127 showConfirmTransferSite: false,
128 showConfirmTransferCommunity: false,
129 showConfirmAppointAsMod: false,
130 showConfirmAppointAsAdmin: false,
131 showReportDialog: false,
132 my_vote: this.props.node.comment_view.my_vote,
133 score: this.props.node.comment_view.counts.score,
134 upvotes: this.props.node.comment_view.counts.upvotes,
135 downvotes: this.props.node.comment_view.counts.downvotes,
140 constructor(props: any, context: any) {
141 super(props, context);
143 this.handleReplyCancel = this.handleReplyCancel.bind(this);
144 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
145 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
148 // TODO see if there's a better way to do this, and all willReceiveProps
149 componentWillReceiveProps(nextProps: CommentNodeProps) {
150 let cv = nextProps.node.comment_view;
153 upvotes: cv.counts.upvotes,
154 downvotes: cv.counts.downvotes,
155 score: cv.counts.score,
162 let node = this.props.node;
163 let cv = this.props.node.comment_view;
166 this.state.purgeType == PurgeType.Comment
167 ? i18n.t("purge_comment")
168 : `${i18n.t("purge")} ${cv.creator.name}`;
171 canMod(cv.creator.id, this.props.moderators, this.props.admins) &&
176 this.props.moderators,
178 UserService.Instance.myUserInfo,
180 ) && cv.community.local;
182 canAdmin(cv.creator.id, this.props.admins) && cv.community.local;
187 UserService.Instance.myUserInfo,
189 ) && cv.community.local;
190 let isMod_ = isMod(cv.creator.id, this.props.moderators);
192 isAdmin(cv.creator.id, this.props.admins) && cv.community.local;
193 let amCommunityCreator_ = amCommunityCreator(
195 this.props.moderators
198 let borderColor = this.props.node.depth
199 ? colorList[(this.props.node.depth - 1) % colorList.length]
201 let moreRepliesBorderColor = this.props.node.depth
202 ? colorList[this.props.node.depth % colorList.length]
205 let showMoreChildren =
206 this.props.viewType == CommentViewType.Tree &&
207 !this.state.collapsed &&
208 node.children.length == 0 &&
209 node.comment_view.counts.child_count > 0;
213 className={`comment ${
214 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
218 id={`comment-${cv.comment.id}`}
219 className={classNames(`details comment-node py-2`, {
220 "border-top border-light": !this.props.noBorder,
223 this.props.node.comment_view.comment.distinguished,
226 !this.props.noIndent && this.props.node.depth
227 ? `border-left: 2px ${borderColor} solid !important`
232 className={classNames({
233 "ml-2": !this.props.noIndent && this.props.node.depth,
236 <div className="d-flex flex-wrap align-items-center text-muted small">
237 <span className="mr-2">
238 <PersonListing person={cv.creator} />
240 {cv.comment.distinguished && (
241 <Icon icon="shield" inline classes={`text-danger mr-2`} />
243 {this.isPostCreator && (
244 <div className="badge badge-light d-none d-sm-inline mr-2">
249 <div className="badge d-none d-sm-inline mr-2">
254 <div className="badge d-none d-sm-inline mr-2">
258 {cv.creator.bot_account && (
259 <div className="badge d-none d-sm-inline mr-2">
260 {i18n.t("bot_account").toLowerCase()}
263 {this.props.showCommunity && (
265 <span className="mx-1">{i18n.t("to")}</span>
266 <CommunityLink community={cv.community} />
267 <span className="mx-2">•</span>
268 <Link className="mr-2" to={`/post/${cv.post.id}`}>
274 className="btn btn-sm text-muted"
275 onClick={linkEvent(this, this.handleCommentCollapse)}
276 aria-label={this.expandText}
277 data-tippy-content={this.expandText}
279 {this.state.collapsed ? (
280 <Icon icon="plus-square" classes="icon-inline" />
282 <Icon icon="minus-square" classes="icon-inline" />
286 {cv.comment.language_id !== 0 && (
287 <span className="badge d-none d-sm-inline mr-2">
289 this.props.allLanguages.find(
290 lang => lang.id === cv.comment.language_id
295 {/* This is an expanding spacer for mobile */}
296 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
300 className={`unselectable pointer ${this.scoreColor}`}
301 onClick={this.handleCommentUpvote}
302 data-tippy-content={this.pointsTippy}
305 className="mr-1 font-weight-bold"
306 aria-label={i18n.t("number_of_points", {
307 count: Number(this.state.score),
308 formattedCount: numToSI(this.state.score),
311 {numToSI(this.state.score)}
314 <span className="mr-1">•</span>
319 published={cv.comment.published}
320 updated={cv.comment.updated}
324 {/* end of user row */}
325 {this.state.showEdit && (
329 onReplyCancel={this.handleReplyCancel}
330 disabled={this.props.locked}
332 allLanguages={this.props.allLanguages}
333 siteLanguages={this.props.siteLanguages}
336 {!this.state.showEdit && !this.state.collapsed && (
338 {this.state.viewSource ? (
339 <pre>{this.commentUnlessRemoved}</pre>
343 dangerouslySetInnerHTML={
344 this.props.hideImages
345 ? mdToHtmlNoImages(this.commentUnlessRemoved)
346 : mdToHtml(this.commentUnlessRemoved)
350 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
351 {this.props.showContext && this.linkBtn()}
352 {this.props.markable && (
354 className="btn btn-link btn-animate text-muted"
355 onClick={linkEvent(this, this.handleMarkRead)}
357 this.commentReplyOrMentionRead
358 ? i18n.t("mark_as_unread")
359 : i18n.t("mark_as_read")
362 this.commentReplyOrMentionRead
363 ? i18n.t("mark_as_unread")
364 : i18n.t("mark_as_read")
367 {this.state.readLoading ? (
372 classes={`icon-inline ${
373 this.commentReplyOrMentionRead && "text-success"
379 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
382 className={`btn btn-link btn-animate ${
383 this.state.my_vote === 1 ? "text-info" : "text-muted"
385 onClick={this.handleCommentUpvote}
386 data-tippy-content={i18n.t("upvote")}
387 aria-label={i18n.t("upvote")}
388 aria-pressed={this.state.my_vote === 1}
390 <Icon icon="arrow-up1" classes="icon-inline" />
392 this.state.upvotes !== this.state.score && (
393 <span className="ml-1">
394 {numToSI(this.state.upvotes)}
398 {this.props.enableDownvotes && (
400 className={`btn btn-link btn-animate ${
401 this.state.my_vote === -1
405 onClick={this.handleCommentDownvote}
406 data-tippy-content={i18n.t("downvote")}
407 aria-label={i18n.t("downvote")}
408 aria-pressed={this.state.my_vote === -1}
410 <Icon icon="arrow-down1" classes="icon-inline" />
412 this.state.upvotes !== this.state.score && (
413 <span className="ml-1">
414 {numToSI(this.state.downvotes)}
420 className="btn btn-link btn-animate text-muted"
421 onClick={linkEvent(this, this.handleReplyClick)}
422 data-tippy-content={i18n.t("reply")}
423 aria-label={i18n.t("reply")}
425 <Icon icon="reply1" classes="icon-inline" />
427 {!this.state.showAdvanced ? (
429 className="btn btn-link btn-animate text-muted"
430 onClick={linkEvent(this, this.handleShowAdvanced)}
431 data-tippy-content={i18n.t("more")}
432 aria-label={i18n.t("more")}
434 <Icon icon="more-vertical" classes="icon-inline" />
438 {!this.myComment && (
440 <button className="btn btn-link btn-animate">
442 className="text-muted"
443 to={`/create_private_message/${cv.creator.id}`}
444 title={i18n.t("message").toLowerCase()}
450 className="btn btn-link btn-animate text-muted"
453 this.handleShowReportDialog
455 data-tippy-content={i18n.t(
458 aria-label={i18n.t("show_report_dialog")}
463 className="btn btn-link btn-animate text-muted"
466 this.handleBlockUserClick
468 data-tippy-content={i18n.t("block_user")}
469 aria-label={i18n.t("block_user")}
471 <Icon icon="slash" />
476 className="btn btn-link btn-animate text-muted"
479 this.handleSaveCommentClick
482 cv.saved ? i18n.t("unsave") : i18n.t("save")
485 cv.saved ? i18n.t("unsave") : i18n.t("save")
488 {this.state.saveLoading ? (
493 classes={`icon-inline ${
494 cv.saved && "text-warning"
500 className="btn btn-link btn-animate text-muted"
501 onClick={linkEvent(this, this.handleViewSource)}
502 data-tippy-content={i18n.t("view_source")}
503 aria-label={i18n.t("view_source")}
507 classes={`icon-inline ${
508 this.state.viewSource && "text-success"
515 className="btn btn-link btn-animate text-muted"
516 onClick={linkEvent(this, this.handleEditClick)}
517 data-tippy-content={i18n.t("edit")}
518 aria-label={i18n.t("edit")}
520 <Icon icon="edit" classes="icon-inline" />
523 className="btn btn-link btn-animate text-muted"
526 this.handleDeleteClick
541 classes={`icon-inline ${
542 cv.comment.deleted && "text-danger"
547 {(canModOnSelf || canAdminOnSelf) && (
549 className="btn btn-link btn-animate text-muted"
552 this.handleDistinguishClick
555 !cv.comment.distinguished
556 ? i18n.t("distinguish")
557 : i18n.t("undistinguish")
560 !cv.comment.distinguished
561 ? i18n.t("distinguish")
562 : i18n.t("undistinguish")
567 classes={`icon-inline ${
568 cv.comment.distinguished && "text-danger"
575 {/* Admins and mods can remove comments */}
576 {(canMod_ || canAdmin_) && (
578 {!cv.comment.removed ? (
580 className="btn btn-link btn-animate text-muted"
583 this.handleModRemoveShow
585 aria-label={i18n.t("remove")}
591 className="btn btn-link btn-animate text-muted"
594 this.handleModRemoveSubmit
596 aria-label={i18n.t("restore")}
603 {/* Mods can ban from community, and appoint as mods to community */}
607 (!cv.creator_banned_from_community ? (
609 className="btn btn-link btn-animate text-muted"
612 this.handleModBanFromCommunityShow
614 aria-label={i18n.t("ban_from_community")}
616 {i18n.t("ban_from_community")}
620 className="btn btn-link btn-animate text-muted"
623 this.handleModBanFromCommunitySubmit
625 aria-label={i18n.t("unban")}
630 {!cv.creator_banned_from_community &&
631 (!this.state.showConfirmAppointAsMod ? (
633 className="btn btn-link btn-animate text-muted"
636 this.handleShowConfirmAppointAsMod
640 ? i18n.t("remove_as_mod")
641 : i18n.t("appoint_as_mod")
645 ? i18n.t("remove_as_mod")
646 : i18n.t("appoint_as_mod")}
651 className="btn btn-link btn-animate text-muted"
652 aria-label={i18n.t("are_you_sure")}
654 {i18n.t("are_you_sure")}
657 className="btn btn-link btn-animate text-muted"
660 this.handleAddModToCommunity
662 aria-label={i18n.t("yes")}
667 className="btn btn-link btn-animate text-muted"
670 this.handleCancelConfirmAppointAsMod
672 aria-label={i18n.t("no")}
680 {/* Community creators and admins can transfer community to another mod */}
681 {(amCommunityCreator_ || canAdmin_) &&
684 (!this.state.showConfirmTransferCommunity ? (
686 className="btn btn-link btn-animate text-muted"
689 this.handleShowConfirmTransferCommunity
691 aria-label={i18n.t("transfer_community")}
693 {i18n.t("transfer_community")}
698 className="btn btn-link btn-animate text-muted"
699 aria-label={i18n.t("are_you_sure")}
701 {i18n.t("are_you_sure")}
704 className="btn btn-link btn-animate text-muted"
707 this.handleTransferCommunity
709 aria-label={i18n.t("yes")}
714 className="btn btn-link btn-animate text-muted"
718 .handleCancelShowConfirmTransferCommunity
720 aria-label={i18n.t("no")}
726 {/* Admins can ban from all, and appoint other admins */}
732 className="btn btn-link btn-animate text-muted"
735 this.handlePurgePersonShow
737 aria-label={i18n.t("purge_user")}
739 {i18n.t("purge_user")}
742 className="btn btn-link btn-animate text-muted"
745 this.handlePurgeCommentShow
747 aria-label={i18n.t("purge_comment")}
749 {i18n.t("purge_comment")}
752 {!isBanned(cv.creator) ? (
754 className="btn btn-link btn-animate text-muted"
757 this.handleModBanShow
759 aria-label={i18n.t("ban_from_site")}
761 {i18n.t("ban_from_site")}
765 className="btn btn-link btn-animate text-muted"
768 this.handleModBanSubmit
770 aria-label={i18n.t("unban_from_site")}
772 {i18n.t("unban_from_site")}
777 {!isBanned(cv.creator) &&
779 (!this.state.showConfirmAppointAsAdmin ? (
781 className="btn btn-link btn-animate text-muted"
784 this.handleShowConfirmAppointAsAdmin
788 ? i18n.t("remove_as_admin")
789 : i18n.t("appoint_as_admin")
793 ? i18n.t("remove_as_admin")
794 : i18n.t("appoint_as_admin")}
798 <button className="btn btn-link btn-animate text-muted">
799 {i18n.t("are_you_sure")}
802 className="btn btn-link btn-animate text-muted"
807 aria-label={i18n.t("yes")}
812 className="btn btn-link btn-animate text-muted"
815 this.handleCancelConfirmAppointAsAdmin
817 aria-label={i18n.t("no")}
830 {/* end of button group */}
835 {showMoreChildren && (
837 className={`details ml-1 comment-node py-2 ${
838 !this.props.noBorder ? "border-top border-light" : ""
840 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
843 className="btn btn-link text-muted"
844 onClick={linkEvent(this, this.handleFetchChildren)}
846 {i18n.t("x_more_replies", {
847 count: node.comment_view.counts.child_count,
848 formattedCount: numToSI(node.comment_view.counts.child_count),
854 {/* end of details */}
855 {this.state.showRemoveDialog && (
857 className="form-inline"
858 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
862 htmlFor={`mod-remove-reason-${cv.comment.id}`}
868 id={`mod-remove-reason-${cv.comment.id}`}
869 className="form-control mr-2"
870 placeholder={i18n.t("reason")}
871 value={this.state.removeReason}
872 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
876 className="btn btn-secondary"
877 aria-label={i18n.t("remove_comment")}
879 {i18n.t("remove_comment")}
883 {this.state.showReportDialog && (
885 className="form-inline"
886 onSubmit={linkEvent(this, this.handleReportSubmit)}
890 htmlFor={`report-reason-${cv.comment.id}`}
897 id={`report-reason-${cv.comment.id}`}
898 className="form-control mr-2"
899 placeholder={i18n.t("reason")}
900 value={this.state.reportReason}
901 onInput={linkEvent(this, this.handleReportReasonChange)}
905 className="btn btn-secondary"
906 aria-label={i18n.t("create_report")}
908 {i18n.t("create_report")}
912 {this.state.showBanDialog && (
913 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
914 <div className="form-group row col-12">
916 className="col-form-label"
917 htmlFor={`mod-ban-reason-${cv.comment.id}`}
923 id={`mod-ban-reason-${cv.comment.id}`}
924 className="form-control mr-2"
925 placeholder={i18n.t("reason")}
926 value={this.state.banReason}
927 onInput={linkEvent(this, this.handleModBanReasonChange)}
930 className="col-form-label"
931 htmlFor={`mod-ban-expires-${cv.comment.id}`}
937 id={`mod-ban-expires-${cv.comment.id}`}
938 className="form-control mr-2"
939 placeholder={i18n.t("number_of_days")}
940 value={this.state.banExpireDays}
941 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
943 <div className="form-group">
944 <div className="form-check">
946 className="form-check-input"
947 id="mod-ban-remove-data"
949 checked={this.state.removeData}
950 onChange={linkEvent(this, this.handleModRemoveDataChange)}
953 className="form-check-label"
954 htmlFor="mod-ban-remove-data"
955 title={i18n.t("remove_content_more")}
957 {i18n.t("remove_content")}
962 {/* TODO hold off on expires until later */}
963 {/* <div class="form-group row"> */}
964 {/* <label class="col-form-label">Expires</label> */}
965 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
967 <div className="form-group row">
970 className="btn btn-secondary"
971 aria-label={i18n.t("ban")}
973 {i18n.t("ban")} {cv.creator.name}
979 {this.state.showPurgeDialog && (
980 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
982 <label className="sr-only" htmlFor="purge-reason">
988 className="form-control my-3"
989 placeholder={i18n.t("reason")}
990 value={this.state.purgeReason}
991 onInput={linkEvent(this, this.handlePurgeReasonChange)}
993 <div className="form-group row col-12">
994 {this.state.purgeLoading ? (
999 className="btn btn-secondary"
1000 aria-label={purgeTypeText}
1008 {this.state.showReply && (
1011 onReplyCancel={this.handleReplyCancel}
1012 disabled={this.props.locked}
1014 allLanguages={this.props.allLanguages}
1015 siteLanguages={this.props.siteLanguages}
1018 {!this.state.collapsed && node.children.length > 0 && (
1020 nodes={node.children}
1021 locked={this.props.locked}
1022 moderators={this.props.moderators}
1023 admins={this.props.admins}
1024 enableDownvotes={this.props.enableDownvotes}
1025 viewType={this.props.viewType}
1026 allLanguages={this.props.allLanguages}
1027 siteLanguages={this.props.siteLanguages}
1028 hideImages={this.props.hideImages}
1031 {/* A collapsed clearfix */}
1032 {this.state.collapsed && <div className="row col-12"></div>}
1037 get commentReplyOrMentionRead(): boolean {
1038 let cv = this.props.node.comment_view;
1040 if (this.isPersonMentionType(cv)) {
1041 return cv.person_mention.read;
1042 } else if (this.isCommentReplyType(cv)) {
1043 return cv.comment_reply.read;
1049 linkBtn(small = false) {
1050 let cv = this.props.node.comment_view;
1051 let classnames = classNames("btn btn-link btn-animate text-muted", {
1055 let title = this.props.showContext
1056 ? i18n.t("show_context")
1059 // The context button should show the parent comment by default
1060 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1065 className={classnames}
1066 to={`/comment/${parentCommentId}`}
1069 <Icon icon="link" classes="icon-inline" />
1072 <a className={classnames} title={title} href={cv.comment.ap_id}>
1073 <Icon icon="fedilink" classes="icon-inline" />
1084 get myComment(): boolean {
1086 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1087 this.props.node.comment_view.creator.id
1091 get isPostCreator(): boolean {
1093 this.props.node.comment_view.creator.id ==
1094 this.props.node.comment_view.post.creator_id
1098 get commentUnlessRemoved(): string {
1099 let comment = this.props.node.comment_view.comment;
1100 return comment.removed
1101 ? `*${i18n.t("removed")}*`
1103 ? `*${i18n.t("deleted")}*`
1107 handleReplyClick(i: CommentNode) {
1108 i.setState({ showReply: true });
1111 handleEditClick(i: CommentNode) {
1112 i.setState({ showEdit: true });
1115 handleBlockUserClick(i: CommentNode) {
1116 let auth = myAuth();
1118 let blockUserForm: BlockPerson = {
1119 person_id: i.props.node.comment_view.creator.id,
1123 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1127 handleDeleteClick(i: CommentNode) {
1128 let comment = i.props.node.comment_view.comment;
1129 let auth = myAuth();
1131 let deleteForm: DeleteComment = {
1132 comment_id: comment.id,
1133 deleted: !comment.deleted,
1136 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1140 handleSaveCommentClick(i: CommentNode) {
1141 let cv = i.props.node.comment_view;
1142 let save = cv.saved == undefined ? true : !cv.saved;
1143 let auth = myAuth();
1145 let form: SaveComment = {
1146 comment_id: cv.comment.id,
1151 WebSocketService.Instance.send(wsClient.saveComment(form));
1153 i.setState({ saveLoading: true });
1157 handleReplyCancel() {
1158 this.setState({ showReply: false, showEdit: false });
1161 handleCommentUpvote(event: any) {
1162 event.preventDefault();
1163 let myVote = this.state.my_vote;
1164 let newVote = myVote == 1 ? 0 : 1;
1168 score: this.state.score - 1,
1169 upvotes: this.state.upvotes - 1,
1171 } else if (myVote == -1) {
1173 downvotes: this.state.downvotes - 1,
1174 upvotes: this.state.upvotes + 1,
1175 score: this.state.score + 2,
1179 score: this.state.score + 1,
1180 upvotes: this.state.upvotes + 1,
1184 this.setState({ my_vote: newVote });
1186 let auth = myAuth();
1188 let form: CreateCommentLike = {
1189 comment_id: this.props.node.comment_view.comment.id,
1193 WebSocketService.Instance.send(wsClient.likeComment(form));
1198 handleCommentDownvote(event: any) {
1199 event.preventDefault();
1200 let myVote = this.state.my_vote;
1201 let newVote = myVote == -1 ? 0 : -1;
1205 downvotes: this.state.downvotes + 1,
1206 upvotes: this.state.upvotes - 1,
1207 score: this.state.score - 2,
1209 } else if (myVote == -1) {
1211 downvotes: this.state.downvotes - 1,
1212 score: this.state.score + 1,
1216 downvotes: this.state.downvotes + 1,
1217 score: this.state.score - 1,
1221 this.setState({ my_vote: newVote });
1223 let auth = myAuth();
1225 let form: CreateCommentLike = {
1226 comment_id: this.props.node.comment_view.comment.id,
1231 WebSocketService.Instance.send(wsClient.likeComment(form));
1236 handleShowReportDialog(i: CommentNode) {
1237 i.setState({ showReportDialog: !i.state.showReportDialog });
1240 handleReportReasonChange(i: CommentNode, event: any) {
1241 i.setState({ reportReason: event.target.value });
1244 handleReportSubmit(i: CommentNode) {
1245 let comment = i.props.node.comment_view.comment;
1246 let reason = i.state.reportReason;
1247 let auth = myAuth();
1248 if (reason && auth) {
1249 let form: CreateCommentReport = {
1250 comment_id: comment.id,
1254 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1255 i.setState({ showReportDialog: false });
1259 handleModRemoveShow(i: CommentNode) {
1261 showRemoveDialog: !i.state.showRemoveDialog,
1262 showBanDialog: false,
1266 handleModRemoveReasonChange(i: CommentNode, event: any) {
1267 i.setState({ removeReason: event.target.value });
1270 handleModRemoveDataChange(i: CommentNode, event: any) {
1271 i.setState({ removeData: event.target.checked });
1274 handleModRemoveSubmit(i: CommentNode) {
1275 let comment = i.props.node.comment_view.comment;
1276 let auth = myAuth();
1278 let form: RemoveComment = {
1279 comment_id: comment.id,
1280 removed: !comment.removed,
1281 reason: i.state.removeReason,
1284 WebSocketService.Instance.send(wsClient.removeComment(form));
1286 i.setState({ showRemoveDialog: false });
1290 handleDistinguishClick(i: CommentNode) {
1291 let comment = i.props.node.comment_view.comment;
1292 let auth = myAuth();
1294 let form: DistinguishComment = {
1295 comment_id: comment.id,
1296 distinguished: !comment.distinguished,
1299 WebSocketService.Instance.send(wsClient.editComment(form));
1300 i.setState(i.state);
1304 isPersonMentionType(
1305 item: CommentView | PersonMentionView | CommentReplyView
1306 ): item is PersonMentionView {
1307 return (item as PersonMentionView).person_mention?.id !== undefined;
1311 item: CommentView | PersonMentionView | CommentReplyView
1312 ): item is CommentReplyView {
1313 return (item as CommentReplyView).comment_reply?.id !== undefined;
1316 handleMarkRead(i: CommentNode) {
1317 let auth = myAuth();
1319 if (i.isPersonMentionType(i.props.node.comment_view)) {
1320 let form: MarkPersonMentionAsRead = {
1321 person_mention_id: i.props.node.comment_view.person_mention.id,
1322 read: !i.props.node.comment_view.person_mention.read,
1325 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1326 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1327 let form: MarkCommentReplyAsRead = {
1328 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1329 read: !i.props.node.comment_view.comment_reply.read,
1332 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1335 i.setState({ readLoading: true });
1339 handleModBanFromCommunityShow(i: CommentNode) {
1341 showBanDialog: true,
1342 banType: BanType.Community,
1343 showRemoveDialog: false,
1347 handleModBanShow(i: CommentNode) {
1349 showBanDialog: true,
1350 banType: BanType.Site,
1351 showRemoveDialog: false,
1355 handleModBanReasonChange(i: CommentNode, event: any) {
1356 i.setState({ banReason: event.target.value });
1359 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1360 i.setState({ banExpireDays: event.target.value });
1363 handleModBanFromCommunitySubmit(i: CommentNode) {
1364 i.setState({ banType: BanType.Community });
1365 i.handleModBanBothSubmit(i);
1368 handleModBanSubmit(i: CommentNode) {
1369 i.setState({ banType: BanType.Site });
1370 i.handleModBanBothSubmit(i);
1373 handleModBanBothSubmit(i: CommentNode) {
1374 let cv = i.props.node.comment_view;
1375 let auth = myAuth();
1377 if (i.state.banType == BanType.Community) {
1378 // If its an unban, restore all their data
1379 let ban = !cv.creator_banned_from_community;
1381 i.setState({ removeData: false });
1383 let form: BanFromCommunity = {
1384 person_id: cv.creator.id,
1385 community_id: cv.community.id,
1387 remove_data: i.state.removeData,
1388 reason: i.state.banReason,
1389 expires: futureDaysToUnixTime(i.state.banExpireDays),
1392 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1394 // If its an unban, restore all their data
1395 let ban = !cv.creator.banned;
1397 i.setState({ removeData: false });
1399 let form: BanPerson = {
1400 person_id: cv.creator.id,
1402 remove_data: i.state.removeData,
1403 reason: i.state.banReason,
1404 expires: futureDaysToUnixTime(i.state.banExpireDays),
1407 WebSocketService.Instance.send(wsClient.banPerson(form));
1410 i.setState({ showBanDialog: false });
1414 handlePurgePersonShow(i: CommentNode) {
1416 showPurgeDialog: true,
1417 purgeType: PurgeType.Person,
1418 showRemoveDialog: false,
1422 handlePurgeCommentShow(i: CommentNode) {
1424 showPurgeDialog: true,
1425 purgeType: PurgeType.Comment,
1426 showRemoveDialog: false,
1430 handlePurgeReasonChange(i: CommentNode, event: any) {
1431 i.setState({ purgeReason: event.target.value });
1434 handlePurgeSubmit(i: CommentNode, event: any) {
1435 event.preventDefault();
1436 let auth = myAuth();
1438 if (i.state.purgeType == PurgeType.Person) {
1439 let form: PurgePerson = {
1440 person_id: i.props.node.comment_view.creator.id,
1441 reason: i.state.purgeReason,
1444 WebSocketService.Instance.send(wsClient.purgePerson(form));
1445 } else if (i.state.purgeType == PurgeType.Comment) {
1446 let form: PurgeComment = {
1447 comment_id: i.props.node.comment_view.comment.id,
1448 reason: i.state.purgeReason,
1451 WebSocketService.Instance.send(wsClient.purgeComment(form));
1454 i.setState({ purgeLoading: true });
1458 handleShowConfirmAppointAsMod(i: CommentNode) {
1459 i.setState({ showConfirmAppointAsMod: true });
1462 handleCancelConfirmAppointAsMod(i: CommentNode) {
1463 i.setState({ showConfirmAppointAsMod: false });
1466 handleAddModToCommunity(i: CommentNode) {
1467 let cv = i.props.node.comment_view;
1468 let auth = myAuth();
1470 let form: AddModToCommunity = {
1471 person_id: cv.creator.id,
1472 community_id: cv.community.id,
1473 added: !isMod(cv.creator.id, i.props.moderators),
1476 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1477 i.setState({ showConfirmAppointAsMod: false });
1481 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1482 i.setState({ showConfirmAppointAsAdmin: true });
1485 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1486 i.setState({ showConfirmAppointAsAdmin: false });
1489 handleAddAdmin(i: CommentNode) {
1490 let auth = myAuth();
1492 let creatorId = i.props.node.comment_view.creator.id;
1493 let form: AddAdmin = {
1494 person_id: creatorId,
1495 added: !isAdmin(creatorId, i.props.admins),
1498 WebSocketService.Instance.send(wsClient.addAdmin(form));
1499 i.setState({ showConfirmAppointAsAdmin: false });
1503 handleShowConfirmTransferCommunity(i: CommentNode) {
1504 i.setState({ showConfirmTransferCommunity: true });
1507 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1508 i.setState({ showConfirmTransferCommunity: false });
1511 handleTransferCommunity(i: CommentNode) {
1512 let cv = i.props.node.comment_view;
1513 let auth = myAuth();
1515 let form: TransferCommunity = {
1516 community_id: cv.community.id,
1517 person_id: cv.creator.id,
1520 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1521 i.setState({ showConfirmTransferCommunity: false });
1525 handleShowConfirmTransferSite(i: CommentNode) {
1526 i.setState({ showConfirmTransferSite: true });
1529 handleCancelShowConfirmTransferSite(i: CommentNode) {
1530 i.setState({ showConfirmTransferSite: false });
1533 get isCommentNew(): boolean {
1534 let now = moment.utc().subtract(10, "minutes");
1535 let then = moment.utc(this.props.node.comment_view.comment.published);
1536 return now.isBefore(then);
1539 handleCommentCollapse(i: CommentNode) {
1540 i.setState({ collapsed: !i.state.collapsed });
1544 handleViewSource(i: CommentNode) {
1545 i.setState({ viewSource: !i.state.viewSource });
1548 handleShowAdvanced(i: CommentNode) {
1549 i.setState({ showAdvanced: !i.state.showAdvanced });
1553 handleFetchChildren(i: CommentNode) {
1554 let form: GetComments = {
1555 post_id: i.props.node.comment_view.post.id,
1556 parent_id: i.props.node.comment_view.comment.id,
1557 max_depth: commentTreeMaxDepth,
1561 auth: myAuth(false),
1564 WebSocketService.Instance.send(wsClient.getComments(form));
1568 if (this.state.my_vote == 1) {
1570 } else if (this.state.my_vote == -1) {
1571 return "text-danger";
1573 return "text-muted";
1577 get pointsTippy(): string {
1578 let points = i18n.t("number_of_points", {
1579 count: Number(this.state.score),
1580 formattedCount: numToSI(this.state.score),
1583 let upvotes = i18n.t("number_of_upvotes", {
1584 count: Number(this.state.upvotes),
1585 formattedCount: numToSI(this.state.upvotes),
1588 let downvotes = i18n.t("number_of_downvotes", {
1589 count: Number(this.state.downvotes),
1590 formattedCount: numToSI(this.state.downvotes),
1593 return `${points} • ${upvotes} • ${downvotes}`;
1596 get expandText(): string {
1597 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");