1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
10 CommunityModeratorView,
15 MarkPersonMentionAsRead,
21 } from "lemmy-js-client";
22 import moment from "moment";
23 import { i18n } from "../../i18next";
24 import { BanType, CommentNode as CommentNodeI } from "../../interfaces";
25 import { UserService, WebSocketService } from "../../services";
39 import { Icon, Spinner } from "../common/icon";
40 import { MomentTime } from "../common/moment-time";
41 import { CommunityLink } from "../community/community-link";
42 import { PersonListing } from "../person/person-listing";
43 import { CommentForm } from "./comment-form";
44 import { CommentNodes } from "./comment-nodes";
46 interface CommentNodeState {
49 showRemoveDialog: boolean;
51 showBanDialog: boolean;
54 banExpireDays: number;
56 showConfirmTransferSite: boolean;
57 showConfirmTransferCommunity: boolean;
58 showConfirmAppointAsMod: boolean;
59 showConfirmAppointAsAdmin: boolean;
62 showAdvanced: boolean;
63 showReportDialog: boolean;
74 interface CommentNodeProps {
81 showContext?: boolean;
82 moderators: CommunityModeratorView[];
83 admins: PersonViewSafe[];
84 // TODO is this necessary, can't I get it from the node itself?
85 postCreatorId?: number;
86 showCommunity?: boolean;
87 enableDownvotes: boolean;
90 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
91 private emptyState: CommentNodeState = {
94 showRemoveDialog: false,
100 banType: BanType.Community,
104 showConfirmTransferSite: false,
105 showConfirmTransferCommunity: false,
106 showConfirmAppointAsMod: false,
107 showConfirmAppointAsAdmin: false,
108 showReportDialog: false,
110 my_vote: this.props.node.comment_view.my_vote,
111 score: this.props.node.comment_view.counts.score,
112 upvotes: this.props.node.comment_view.counts.upvotes,
113 downvotes: this.props.node.comment_view.counts.downvotes,
114 borderColor: this.props.node.depth
115 ? colorList[this.props.node.depth % colorList.length]
121 constructor(props: any, context: any) {
122 super(props, context);
124 this.state = this.emptyState;
125 this.handleReplyCancel = this.handleReplyCancel.bind(this);
126 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
127 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
130 // TODO see if there's a better way to do this, and all willReceiveProps
131 componentWillReceiveProps(nextProps: CommentNodeProps) {
132 let cv = nextProps.node.comment_view;
133 this.state.my_vote = cv.my_vote;
134 this.state.upvotes = cv.counts.upvotes;
135 this.state.downvotes = cv.counts.downvotes;
136 this.state.score = cv.counts.score;
137 this.state.readLoading = false;
138 this.state.saveLoading = false;
139 this.setState(this.state);
143 let node = this.props.node;
144 let cv = this.props.node.comment_view;
147 className={`comment ${
148 cv.comment.parent_id && !this.props.noIndent ? "ml-1" : ""
152 id={`comment-${cv.comment.id}`}
153 className={`details comment-node py-2 ${
154 !this.props.noBorder ? "border-top border-light" : ""
155 } ${this.isCommentNew ? "mark" : ""}`}
157 !this.props.noIndent &&
158 cv.comment.parent_id &&
159 `border-left: 2px ${this.state.borderColor} solid !important`
163 class={`${!this.props.noIndent && cv.comment.parent_id && "ml-2"}`}
165 <div class="d-flex flex-wrap align-items-center text-muted small">
167 <PersonListing person={cv.creator} />
171 <div className="badge badge-light d-none d-sm-inline mr-2">
176 <div className="badge badge-light d-none d-sm-inline mr-2">
180 {this.isPostCreator && (
181 <div className="badge badge-light d-none d-sm-inline mr-2">
185 {cv.creator.bot_account && (
186 <div className="badge badge-light d-none d-sm-inline mr-2">
187 {i18n.t("bot_account").toLowerCase()}
190 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
191 <div className="badge badge-danger mr-2">
195 {this.props.showCommunity && (
197 <span class="mx-1">{i18n.t("to")}</span>
198 <CommunityLink community={cv.community} />
199 <span class="mx-2">•</span>
200 <Link className="mr-2" to={`/post/${cv.post.id}`}>
206 class="btn btn-sm text-muted"
207 onClick={linkEvent(this, this.handleCommentCollapse)}
208 aria-label={this.expandText}
209 data-tippy-content={this.expandText}
211 {this.state.collapsed ? (
212 <Icon icon="plus-square" classes="icon-inline" />
214 <Icon icon="minus-square" classes="icon-inline" />
218 {/* This is an expanding spacer for mobile */}
219 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
223 className={`unselectable pointer ${this.scoreColor}`}
224 onClick={linkEvent(node, this.handleCommentUpvote)}
225 data-tippy-content={this.pointsTippy}
228 class="mr-1 font-weight-bold"
229 aria-label={i18n.t("number_of_points", {
230 count: this.state.score,
231 formattedCount: this.state.score,
234 {numToSI(this.state.score)}
237 <span className="mr-1">•</span>
241 <MomentTime data={cv.comment} />
244 {/* end of user row */}
245 {this.state.showEdit && (
249 onReplyCancel={this.handleReplyCancel}
250 disabled={this.props.locked}
254 {!this.state.showEdit && !this.state.collapsed && (
256 {this.state.viewSource ? (
257 <pre>{this.commentUnlessRemoved}</pre>
261 dangerouslySetInnerHTML={mdToHtml(
262 this.commentUnlessRemoved
266 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
267 {this.props.showContext && this.linkBtn()}
268 {this.props.markable && (
270 class="btn btn-link btn-animate text-muted"
271 onClick={linkEvent(this, this.handleMarkRead)}
273 this.commentOrMentionRead
274 ? i18n.t("mark_as_unread")
275 : i18n.t("mark_as_read")
278 this.commentOrMentionRead
279 ? i18n.t("mark_as_unread")
280 : i18n.t("mark_as_read")
283 {this.state.readLoading ? (
288 classes={`icon-inline ${
289 this.commentOrMentionRead && "text-success"
295 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
298 className={`btn btn-link btn-animate ${
299 this.state.my_vote == 1 ? "text-info" : "text-muted"
301 onClick={linkEvent(node, this.handleCommentUpvote)}
302 data-tippy-content={i18n.t("upvote")}
303 aria-label={i18n.t("upvote")}
305 <Icon icon="arrow-up1" classes="icon-inline" />
307 this.state.upvotes !== this.state.score && (
309 {numToSI(this.state.upvotes)}
313 {this.props.enableDownvotes && (
315 className={`btn btn-link btn-animate ${
316 this.state.my_vote == -1
320 onClick={linkEvent(node, this.handleCommentDownvote)}
321 data-tippy-content={i18n.t("downvote")}
322 aria-label={i18n.t("downvote")}
324 <Icon icon="arrow-down1" classes="icon-inline" />
326 this.state.upvotes !== this.state.score && (
328 {numToSI(this.state.downvotes)}
334 class="btn btn-link btn-animate text-muted"
335 onClick={linkEvent(this, this.handleReplyClick)}
336 data-tippy-content={i18n.t("reply")}
337 aria-label={i18n.t("reply")}
339 <Icon icon="reply1" classes="icon-inline" />
341 {!this.state.showAdvanced ? (
343 className="btn btn-link btn-animate text-muted"
344 onClick={linkEvent(this, this.handleShowAdvanced)}
345 data-tippy-content={i18n.t("more")}
346 aria-label={i18n.t("more")}
348 <Icon icon="more-vertical" classes="icon-inline" />
352 {!this.myComment && (
354 <button class="btn btn-link btn-animate">
356 className="text-muted"
357 to={`/create_private_message/recipient/${cv.creator.id}`}
358 title={i18n.t("message").toLowerCase()}
364 class="btn btn-link btn-animate text-muted"
367 this.handleShowReportDialog
369 data-tippy-content={i18n.t(
372 aria-label={i18n.t("show_report_dialog")}
377 class="btn btn-link btn-animate text-muted"
380 this.handleBlockUserClick
382 data-tippy-content={i18n.t("block_user")}
383 aria-label={i18n.t("block_user")}
385 <Icon icon="slash" />
390 class="btn btn-link btn-animate text-muted"
393 this.handleSaveCommentClick
396 cv.saved ? i18n.t("unsave") : i18n.t("save")
399 cv.saved ? i18n.t("unsave") : i18n.t("save")
402 {this.state.saveLoading ? (
407 classes={`icon-inline ${
408 cv.saved && "text-warning"
414 className="btn btn-link btn-animate text-muted"
415 onClick={linkEvent(this, this.handleViewSource)}
416 data-tippy-content={i18n.t("view_source")}
417 aria-label={i18n.t("view_source")}
421 classes={`icon-inline ${
422 this.state.viewSource && "text-success"
429 class="btn btn-link btn-animate text-muted"
430 onClick={linkEvent(this, this.handleEditClick)}
431 data-tippy-content={i18n.t("edit")}
432 aria-label={i18n.t("edit")}
434 <Icon icon="edit" classes="icon-inline" />
437 class="btn btn-link btn-animate text-muted"
440 this.handleDeleteClick
455 classes={`icon-inline ${
456 cv.comment.deleted && "text-danger"
462 {/* Admins and mods can remove comments */}
463 {(this.canMod || this.canAdmin) && (
465 {!cv.comment.removed ? (
467 class="btn btn-link btn-animate text-muted"
470 this.handleModRemoveShow
472 aria-label={i18n.t("remove")}
478 class="btn btn-link btn-animate text-muted"
481 this.handleModRemoveSubmit
483 aria-label={i18n.t("restore")}
490 {/* Mods can ban from community, and appoint as mods to community */}
494 (!cv.creator_banned_from_community ? (
496 class="btn btn-link btn-animate text-muted"
499 this.handleModBanFromCommunityShow
501 aria-label={i18n.t("ban")}
507 class="btn btn-link btn-animate text-muted"
510 this.handleModBanFromCommunitySubmit
512 aria-label={i18n.t("unban")}
517 {!cv.creator_banned_from_community &&
518 (!this.state.showConfirmAppointAsMod ? (
520 class="btn btn-link btn-animate text-muted"
523 this.handleShowConfirmAppointAsMod
527 ? i18n.t("remove_as_mod")
528 : i18n.t("appoint_as_mod")
532 ? i18n.t("remove_as_mod")
533 : i18n.t("appoint_as_mod")}
538 class="btn btn-link btn-animate text-muted"
539 aria-label={i18n.t("are_you_sure")}
541 {i18n.t("are_you_sure")}
544 class="btn btn-link btn-animate text-muted"
547 this.handleAddModToCommunity
549 aria-label={i18n.t("yes")}
554 class="btn btn-link btn-animate text-muted"
557 this.handleCancelConfirmAppointAsMod
559 aria-label={i18n.t("no")}
567 {/* Community creators and admins can transfer community to another mod */}
568 {(this.amCommunityCreator || this.canAdmin) &&
571 (!this.state.showConfirmTransferCommunity ? (
573 class="btn btn-link btn-animate text-muted"
576 this.handleShowConfirmTransferCommunity
578 aria-label={i18n.t("transfer_community")}
580 {i18n.t("transfer_community")}
585 class="btn btn-link btn-animate text-muted"
586 aria-label={i18n.t("are_you_sure")}
588 {i18n.t("are_you_sure")}
591 class="btn btn-link btn-animate text-muted"
594 this.handleTransferCommunity
596 aria-label={i18n.t("yes")}
601 class="btn btn-link btn-animate text-muted"
605 .handleCancelShowConfirmTransferCommunity
607 aria-label={i18n.t("no")}
613 {/* Admins can ban from all, and appoint other admins */}
617 (!isBanned(cv.creator) ? (
619 class="btn btn-link btn-animate text-muted"
622 this.handleModBanShow
624 aria-label={i18n.t("ban_from_site")}
626 {i18n.t("ban_from_site")}
630 class="btn btn-link btn-animate text-muted"
633 this.handleModBanSubmit
635 aria-label={i18n.t("unban_from_site")}
637 {i18n.t("unban_from_site")}
640 {!isBanned(cv.creator) &&
642 (!this.state.showConfirmAppointAsAdmin ? (
644 class="btn btn-link btn-animate text-muted"
647 this.handleShowConfirmAppointAsAdmin
651 ? i18n.t("remove_as_admin")
652 : i18n.t("appoint_as_admin")
656 ? i18n.t("remove_as_admin")
657 : i18n.t("appoint_as_admin")}
661 <button class="btn btn-link btn-animate text-muted">
662 {i18n.t("are_you_sure")}
665 class="btn btn-link btn-animate text-muted"
670 aria-label={i18n.t("yes")}
675 class="btn btn-link btn-animate text-muted"
678 this.handleCancelConfirmAppointAsAdmin
680 aria-label={i18n.t("no")}
693 {/* end of button group */}
698 {/* end of details */}
699 {this.state.showRemoveDialog && (
702 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
706 htmlFor={`mod-remove-reason-${cv.comment.id}`}
712 id={`mod-remove-reason-${cv.comment.id}`}
713 class="form-control mr-2"
714 placeholder={i18n.t("reason")}
715 value={this.state.removeReason}
716 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
720 class="btn btn-secondary"
721 aria-label={i18n.t("remove_comment")}
723 {i18n.t("remove_comment")}
727 {this.state.showReportDialog && (
730 onSubmit={linkEvent(this, this.handleReportSubmit)}
732 <label class="sr-only" htmlFor={`report-reason-${cv.comment.id}`}>
738 id={`report-reason-${cv.comment.id}`}
739 class="form-control mr-2"
740 placeholder={i18n.t("reason")}
741 value={this.state.reportReason}
742 onInput={linkEvent(this, this.handleReportReasonChange)}
746 class="btn btn-secondary"
747 aria-label={i18n.t("create_report")}
749 {i18n.t("create_report")}
753 {this.state.showBanDialog && (
754 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
755 <div class="form-group row col-12">
757 class="col-form-label"
758 htmlFor={`mod-ban-reason-${cv.comment.id}`}
764 id={`mod-ban-reason-${cv.comment.id}`}
765 class="form-control mr-2"
766 placeholder={i18n.t("reason")}
767 value={this.state.banReason}
768 onInput={linkEvent(this, this.handleModBanReasonChange)}
771 class="col-form-label"
772 htmlFor={`mod-ban-expires-${cv.comment.id}`}
778 id={`mod-ban-expires-${cv.comment.id}`}
779 class="form-control mr-2"
780 placeholder={i18n.t("number_of_days")}
781 value={this.state.banExpireDays}
782 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
784 <div class="form-group">
785 <div class="form-check">
787 class="form-check-input"
788 id="mod-ban-remove-data"
790 checked={this.state.removeData}
791 onChange={linkEvent(this, this.handleModRemoveDataChange)}
794 class="form-check-label"
795 htmlFor="mod-ban-remove-data"
796 title={i18n.t("remove_content_more")}
798 {i18n.t("remove_content")}
803 {/* TODO hold off on expires until later */}
804 {/* <div class="form-group row"> */}
805 {/* <label class="col-form-label">Expires</label> */}
806 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
808 <div class="form-group row">
811 class="btn btn-secondary"
812 aria-label={i18n.t("ban")}
814 {i18n.t("ban")} {cv.creator.name}
819 {this.state.showReply && (
822 onReplyCancel={this.handleReplyCancel}
823 disabled={this.props.locked}
827 {node.children && !this.state.collapsed && (
829 nodes={node.children}
830 locked={this.props.locked}
831 moderators={this.props.moderators}
832 admins={this.props.admins}
833 postCreatorId={this.props.postCreatorId}
834 enableDownvotes={this.props.enableDownvotes}
837 {/* A collapsed clearfix */}
838 {this.state.collapsed && <div class="row col-12"></div>}
843 get commentOrMentionRead() {
844 let cv = this.props.node.comment_view;
845 return this.isPersonMentionType(cv)
846 ? cv.person_mention.read
850 linkBtn(small = false) {
851 let cv = this.props.node.comment_view;
854 className={`btn ${small && "btn-sm"} btn-link btn-animate text-muted`}
855 to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
856 title={this.props.showContext ? i18n.t("show_context") : i18n.t("link")}
858 <Icon icon="link" classes="icon-inline" />
867 get myComment(): boolean {
869 this.props.node.comment_view.creator.id ==
870 UserService.Instance.myUserInfo?.local_user_view.person.id
874 get isMod(): boolean {
876 this.props.moderators &&
878 this.props.moderators.map(m => m.moderator.id),
879 this.props.node.comment_view.creator.id
884 get isAdmin(): boolean {
888 this.props.admins.map(a => a.person.id),
889 this.props.node.comment_view.creator.id
894 get isPostCreator(): boolean {
895 return this.props.node.comment_view.creator.id == this.props.postCreatorId;
898 get canMod(): boolean {
899 if (this.props.admins && this.props.moderators) {
900 let adminsThenMods = this.props.admins
901 .map(a => a.person.id)
902 .concat(this.props.moderators.map(m => m.moderator.id));
905 UserService.Instance.myUserInfo,
907 this.props.node.comment_view.creator.id
914 get canAdmin(): boolean {
918 UserService.Instance.myUserInfo,
919 this.props.admins.map(a => a.person.id),
920 this.props.node.comment_view.creator.id
925 get amCommunityCreator(): boolean {
927 this.props.moderators &&
928 UserService.Instance.myUserInfo &&
929 this.props.node.comment_view.creator.id !=
930 UserService.Instance.myUserInfo.local_user_view.person.id &&
931 UserService.Instance.myUserInfo.local_user_view.person.id ==
932 this.props.moderators[0].moderator.id
936 get amSiteCreator(): boolean {
939 UserService.Instance.myUserInfo &&
940 this.props.node.comment_view.creator.id !=
941 UserService.Instance.myUserInfo.local_user_view.person.id &&
942 UserService.Instance.myUserInfo.local_user_view.person.id ==
943 this.props.admins[0].person.id
947 get commentUnlessRemoved(): string {
948 let comment = this.props.node.comment_view.comment;
949 return comment.removed
950 ? `*${i18n.t("removed")}*`
952 ? `*${i18n.t("deleted")}*`
956 handleReplyClick(i: CommentNode) {
957 i.state.showReply = true;
961 handleEditClick(i: CommentNode) {
962 i.state.showEdit = true;
966 handleBlockUserClick(i: CommentNode) {
967 let blockUserForm: BlockPerson = {
968 person_id: i.props.node.comment_view.creator.id,
972 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
975 handleDeleteClick(i: CommentNode) {
976 let comment = i.props.node.comment_view.comment;
977 let deleteForm: DeleteComment = {
978 comment_id: comment.id,
979 deleted: !comment.deleted,
982 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
985 handleSaveCommentClick(i: CommentNode) {
986 let cv = i.props.node.comment_view;
987 let save = cv.saved == undefined ? true : !cv.saved;
988 let form: SaveComment = {
989 comment_id: cv.comment.id,
994 WebSocketService.Instance.send(wsClient.saveComment(form));
996 i.state.saveLoading = true;
997 i.setState(this.state);
1000 handleReplyCancel() {
1001 this.state.showReply = false;
1002 this.state.showEdit = false;
1003 this.setState(this.state);
1006 handleCommentUpvote(i: CommentNodeI, event: any) {
1007 event.preventDefault();
1008 let new_vote = this.state.my_vote == 1 ? 0 : 1;
1010 if (this.state.my_vote == 1) {
1012 this.state.upvotes--;
1013 } else if (this.state.my_vote == -1) {
1014 this.state.downvotes--;
1015 this.state.upvotes++;
1016 this.state.score += 2;
1018 this.state.upvotes++;
1022 this.state.my_vote = new_vote;
1024 let form: CreateCommentLike = {
1025 comment_id: i.comment_view.comment.id,
1026 score: this.state.my_vote,
1030 WebSocketService.Instance.send(wsClient.likeComment(form));
1031 this.setState(this.state);
1035 handleCommentDownvote(i: CommentNodeI, event: any) {
1036 event.preventDefault();
1037 let new_vote = this.state.my_vote == -1 ? 0 : -1;
1039 if (this.state.my_vote == 1) {
1040 this.state.score -= 2;
1041 this.state.upvotes--;
1042 this.state.downvotes++;
1043 } else if (this.state.my_vote == -1) {
1044 this.state.downvotes--;
1047 this.state.downvotes++;
1051 this.state.my_vote = new_vote;
1053 let form: CreateCommentLike = {
1054 comment_id: i.comment_view.comment.id,
1055 score: this.state.my_vote,
1059 WebSocketService.Instance.send(wsClient.likeComment(form));
1060 this.setState(this.state);
1064 handleShowReportDialog(i: CommentNode) {
1065 i.state.showReportDialog = !i.state.showReportDialog;
1066 i.setState(i.state);
1069 handleReportReasonChange(i: CommentNode, event: any) {
1070 i.state.reportReason = event.target.value;
1071 i.setState(i.state);
1074 handleReportSubmit(i: CommentNode) {
1075 let comment = i.props.node.comment_view.comment;
1076 let form: CreateCommentReport = {
1077 comment_id: comment.id,
1078 reason: i.state.reportReason,
1081 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1083 i.state.showReportDialog = false;
1084 i.setState(i.state);
1087 handleModRemoveShow(i: CommentNode) {
1088 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1089 i.state.showBanDialog = false;
1090 i.setState(i.state);
1093 handleModRemoveReasonChange(i: CommentNode, event: any) {
1094 i.state.removeReason = event.target.value;
1095 i.setState(i.state);
1098 handleModRemoveDataChange(i: CommentNode, event: any) {
1099 i.state.removeData = event.target.checked;
1100 i.setState(i.state);
1103 handleModRemoveSubmit(i: CommentNode) {
1104 let comment = i.props.node.comment_view.comment;
1105 let form: RemoveComment = {
1106 comment_id: comment.id,
1107 removed: !comment.removed,
1108 reason: i.state.removeReason,
1111 WebSocketService.Instance.send(wsClient.removeComment(form));
1113 i.state.showRemoveDialog = false;
1114 i.setState(i.state);
1117 isPersonMentionType(
1118 item: CommentView | PersonMentionView
1119 ): item is PersonMentionView {
1120 return (item as PersonMentionView).person_mention?.id !== undefined;
1123 handleMarkRead(i: CommentNode) {
1124 if (i.isPersonMentionType(i.props.node.comment_view)) {
1125 let form: MarkPersonMentionAsRead = {
1126 person_mention_id: i.props.node.comment_view.person_mention.id,
1127 read: !i.props.node.comment_view.person_mention.read,
1130 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1132 let form: MarkCommentAsRead = {
1133 comment_id: i.props.node.comment_view.comment.id,
1134 read: !i.props.node.comment_view.comment.read,
1137 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
1140 i.state.readLoading = true;
1141 i.setState(this.state);
1144 handleModBanFromCommunityShow(i: CommentNode) {
1145 i.state.showBanDialog = true;
1146 i.state.banType = BanType.Community;
1147 i.state.showRemoveDialog = false;
1148 i.setState(i.state);
1151 handleModBanShow(i: CommentNode) {
1152 i.state.showBanDialog = true;
1153 i.state.banType = BanType.Site;
1154 i.state.showRemoveDialog = false;
1155 i.setState(i.state);
1158 handleModBanReasonChange(i: CommentNode, event: any) {
1159 i.state.banReason = event.target.value;
1160 i.setState(i.state);
1163 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1164 i.state.banExpireDays = event.target.value;
1165 i.setState(i.state);
1168 handleModBanFromCommunitySubmit(i: CommentNode) {
1169 i.state.banType = BanType.Community;
1170 i.setState(i.state);
1171 i.handleModBanBothSubmit(i);
1174 handleModBanSubmit(i: CommentNode) {
1175 i.state.banType = BanType.Site;
1176 i.setState(i.state);
1177 i.handleModBanBothSubmit(i);
1180 handleModBanBothSubmit(i: CommentNode) {
1181 let cv = i.props.node.comment_view;
1183 if (i.state.banType == BanType.Community) {
1184 // If its an unban, restore all their data
1185 let ban = !cv.creator_banned_from_community;
1187 i.state.removeData = false;
1189 let form: BanFromCommunity = {
1190 person_id: cv.creator.id,
1191 community_id: cv.community.id,
1193 remove_data: i.state.removeData,
1194 reason: i.state.banReason,
1195 expires: futureDaysToUnixTime(i.state.banExpireDays),
1198 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1200 // If its an unban, restore all their data
1201 let ban = !cv.creator.banned;
1203 i.state.removeData = false;
1205 let form: BanPerson = {
1206 person_id: cv.creator.id,
1208 remove_data: i.state.removeData,
1209 reason: i.state.banReason,
1210 expires: futureDaysToUnixTime(i.state.banExpireDays),
1213 WebSocketService.Instance.send(wsClient.banPerson(form));
1216 i.state.showBanDialog = false;
1217 i.setState(i.state);
1220 handleShowConfirmAppointAsMod(i: CommentNode) {
1221 i.state.showConfirmAppointAsMod = true;
1222 i.setState(i.state);
1225 handleCancelConfirmAppointAsMod(i: CommentNode) {
1226 i.state.showConfirmAppointAsMod = false;
1227 i.setState(i.state);
1230 handleAddModToCommunity(i: CommentNode) {
1231 let cv = i.props.node.comment_view;
1232 let form: AddModToCommunity = {
1233 person_id: cv.creator.id,
1234 community_id: cv.community.id,
1238 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1239 i.state.showConfirmAppointAsMod = false;
1240 i.setState(i.state);
1243 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1244 i.state.showConfirmAppointAsAdmin = true;
1245 i.setState(i.state);
1248 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1249 i.state.showConfirmAppointAsAdmin = false;
1250 i.setState(i.state);
1253 handleAddAdmin(i: CommentNode) {
1254 let form: AddAdmin = {
1255 person_id: i.props.node.comment_view.creator.id,
1259 WebSocketService.Instance.send(wsClient.addAdmin(form));
1260 i.state.showConfirmAppointAsAdmin = false;
1261 i.setState(i.state);
1264 handleShowConfirmTransferCommunity(i: CommentNode) {
1265 i.state.showConfirmTransferCommunity = true;
1266 i.setState(i.state);
1269 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1270 i.state.showConfirmTransferCommunity = false;
1271 i.setState(i.state);
1274 handleTransferCommunity(i: CommentNode) {
1275 let cv = i.props.node.comment_view;
1276 let form: TransferCommunity = {
1277 community_id: cv.community.id,
1278 person_id: cv.creator.id,
1281 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1282 i.state.showConfirmTransferCommunity = false;
1283 i.setState(i.state);
1286 handleShowConfirmTransferSite(i: CommentNode) {
1287 i.state.showConfirmTransferSite = true;
1288 i.setState(i.state);
1291 handleCancelShowConfirmTransferSite(i: CommentNode) {
1292 i.state.showConfirmTransferSite = false;
1293 i.setState(i.state);
1296 get isCommentNew(): boolean {
1297 let now = moment.utc().subtract(10, "minutes");
1298 let then = moment.utc(this.props.node.comment_view.comment.published);
1299 return now.isBefore(then);
1302 handleCommentCollapse(i: CommentNode) {
1303 i.state.collapsed = !i.state.collapsed;
1304 i.setState(i.state);
1308 handleViewSource(i: CommentNode) {
1309 i.state.viewSource = !i.state.viewSource;
1310 i.setState(i.state);
1313 handleShowAdvanced(i: CommentNode) {
1314 i.state.showAdvanced = !i.state.showAdvanced;
1315 i.setState(i.state);
1320 if (this.state.my_vote == 1) {
1322 } else if (this.state.my_vote == -1) {
1323 return "text-danger";
1325 return "text-muted";
1329 get pointsTippy(): string {
1330 let points = i18n.t("number_of_points", {
1331 count: this.state.score,
1332 formattedCount: this.state.score,
1335 let upvotes = i18n.t("number_of_upvotes", {
1336 count: this.state.upvotes,
1337 formattedCount: this.state.upvotes,
1340 let downvotes = i18n.t("number_of_downvotes", {
1341 count: this.state.downvotes,
1342 formattedCount: this.state.downvotes,
1345 return `${points} • ${upvotes} • ${downvotes}`;
1348 get expandText(): string {
1349 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");