1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
10 CommunityModeratorView,
14 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";
37 import { Icon, Spinner } from "../common/icon";
38 import { MomentTime } from "../common/moment-time";
39 import { CommunityLink } from "../community/community-link";
40 import { PersonListing } from "../person/person-listing";
41 import { CommentForm } from "./comment-form";
42 import { CommentNodes } from "./comment-nodes";
44 interface CommentNodeState {
47 showRemoveDialog: boolean;
49 showBanDialog: boolean;
54 showConfirmTransferSite: boolean;
55 showConfirmTransferCommunity: boolean;
56 showConfirmAppointAsMod: boolean;
57 showConfirmAppointAsAdmin: boolean;
60 showAdvanced: boolean;
70 interface CommentNodeProps {
77 showContext?: boolean;
78 moderators: CommunityModeratorView[];
79 admins: PersonViewSafe[];
80 // TODO is this necessary, can't I get it from the node itself?
81 postCreatorId?: number;
82 showCommunity?: boolean;
83 enableDownvotes: boolean;
86 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
87 private emptyState: CommentNodeState = {
90 showRemoveDialog: false,
96 banType: BanType.Community,
100 showConfirmTransferSite: false,
101 showConfirmTransferCommunity: false,
102 showConfirmAppointAsMod: false,
103 showConfirmAppointAsAdmin: false,
104 my_vote: this.props.node.comment_view.my_vote,
105 score: this.props.node.comment_view.counts.score,
106 upvotes: this.props.node.comment_view.counts.upvotes,
107 downvotes: this.props.node.comment_view.counts.downvotes,
108 borderColor: this.props.node.depth
109 ? colorList[this.props.node.depth % colorList.length]
115 constructor(props: any, context: any) {
116 super(props, context);
118 this.state = this.emptyState;
119 this.handleReplyCancel = this.handleReplyCancel.bind(this);
120 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
121 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
124 // TODO see if there's a better way to do this, and all willReceiveProps
125 componentWillReceiveProps(nextProps: CommentNodeProps) {
126 let cv = nextProps.node.comment_view;
127 this.state.my_vote = cv.my_vote;
128 this.state.upvotes = cv.counts.upvotes;
129 this.state.downvotes = cv.counts.downvotes;
130 this.state.score = cv.counts.score;
131 this.state.readLoading = false;
132 this.state.saveLoading = false;
133 this.setState(this.state);
137 let node = this.props.node;
138 let cv = this.props.node.comment_view;
141 className={`comment ${
142 cv.comment.parent_id && !this.props.noIndent ? "ml-1" : ""
146 id={`comment-${cv.comment.id}`}
147 className={`details comment-node py-2 ${
148 !this.props.noBorder ? "border-top border-light" : ""
149 } ${this.isCommentNew ? "mark" : ""}`}
151 !this.props.noIndent &&
152 cv.comment.parent_id &&
153 `border-left: 2px ${this.state.borderColor} solid !important`
157 class={`${!this.props.noIndent && cv.comment.parent_id && "ml-2"}`}
159 <div class="d-flex flex-wrap align-items-center text-muted small">
161 <PersonListing person={cv.creator} />
165 <div className="badge badge-light d-none d-sm-inline mr-2">
170 <div className="badge badge-light d-none d-sm-inline mr-2">
174 {this.isPostCreator && (
175 <div className="badge badge-light d-none d-sm-inline mr-2">
179 {(cv.creator_banned_from_community || cv.creator.banned) && (
180 <div className="badge badge-danger mr-2">
184 {this.props.showCommunity && (
186 <span class="mx-1">{i18n.t("to")}</span>
187 <CommunityLink community={cv.community} />
188 <span class="mx-2">•</span>
189 <Link className="mr-2" to={`/post/${cv.post.id}`}>
195 class="btn btn-sm text-muted"
196 onClick={linkEvent(this, this.handleCommentCollapse)}
197 aria-label={this.expandText}
198 data-tippy-content={this.expandText}
200 {this.state.collapsed ? (
201 <Icon icon="plus-square" classes="icon-inline" />
203 <Icon icon="minus-square" classes="icon-inline" />
207 {/* This is an expanding spacer for mobile */}
208 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
212 className={`unselectable pointer ${this.scoreColor}`}
213 onClick={linkEvent(node, this.handleCommentUpvote)}
214 data-tippy-content={this.pointsTippy}
217 class="mr-1 font-weight-bold"
218 aria-label={i18n.t("number_of_points", {
219 count: this.state.score,
225 <span className="mr-1">•</span>
229 <MomentTime data={cv.comment} />
232 {/* end of user row */}
233 {this.state.showEdit && (
237 onReplyCancel={this.handleReplyCancel}
238 disabled={this.props.locked}
242 {!this.state.showEdit && !this.state.collapsed && (
244 {this.state.viewSource ? (
245 <pre>{this.commentUnlessRemoved}</pre>
249 dangerouslySetInnerHTML={mdToHtml(
250 this.commentUnlessRemoved
254 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
255 {this.props.showContext && this.linkBtn()}
256 {this.props.markable && (
258 class="btn btn-link btn-animate text-muted"
259 onClick={linkEvent(this, this.handleMarkRead)}
261 this.commentOrMentionRead
262 ? i18n.t("mark_as_unread")
263 : i18n.t("mark_as_read")
266 this.commentOrMentionRead
267 ? i18n.t("mark_as_unread")
268 : i18n.t("mark_as_read")
271 {this.state.readLoading ? (
276 classes={`icon-inline ${
277 this.commentOrMentionRead && "text-success"
283 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
286 className={`btn btn-link btn-animate ${
287 this.state.my_vote == 1 ? "text-info" : "text-muted"
289 onClick={linkEvent(node, this.handleCommentUpvote)}
290 data-tippy-content={i18n.t("upvote")}
291 aria-label={i18n.t("upvote")}
293 <Icon icon="arrow-up1" classes="icon-inline" />
295 this.state.upvotes !== this.state.score && (
296 <span class="ml-1">{this.state.upvotes}</span>
299 {this.props.enableDownvotes && (
301 className={`btn btn-link btn-animate ${
302 this.state.my_vote == -1
306 onClick={linkEvent(node, this.handleCommentDownvote)}
307 data-tippy-content={i18n.t("downvote")}
308 aria-label={i18n.t("downvote")}
310 <Icon icon="arrow-down1" classes="icon-inline" />
312 this.state.upvotes !== this.state.score && (
313 <span class="ml-1">{this.state.downvotes}</span>
318 class="btn btn-link btn-animate text-muted"
319 onClick={linkEvent(this, this.handleReplyClick)}
320 data-tippy-content={i18n.t("reply")}
321 aria-label={i18n.t("reply")}
323 <Icon icon="reply1" classes="icon-inline" />
325 {!this.state.showAdvanced ? (
327 className="btn btn-link btn-animate text-muted"
328 onClick={linkEvent(this, this.handleShowAdvanced)}
329 data-tippy-content={i18n.t("more")}
330 aria-label={i18n.t("more")}
332 <Icon icon="more-vertical" classes="icon-inline" />
336 {!this.myComment && (
338 <button class="btn btn-link btn-animate">
340 className="text-muted"
341 to={`/create_private_message/recipient/${cv.creator.id}`}
342 title={i18n.t("message").toLowerCase()}
348 class="btn btn-link btn-animate text-muted"
351 this.handleBlockUserClick
353 data-tippy-content={i18n.t("block_user")}
354 aria-label={i18n.t("block_user")}
356 <Icon icon="slash" />
361 class="btn btn-link btn-animate text-muted"
364 this.handleSaveCommentClick
367 cv.saved ? i18n.t("unsave") : i18n.t("save")
370 cv.saved ? i18n.t("unsave") : i18n.t("save")
373 {this.state.saveLoading ? (
378 classes={`icon-inline ${
379 cv.saved && "text-warning"
385 className="btn btn-link btn-animate text-muted"
386 onClick={linkEvent(this, this.handleViewSource)}
387 data-tippy-content={i18n.t("view_source")}
388 aria-label={i18n.t("view_source")}
392 classes={`icon-inline ${
393 this.state.viewSource && "text-success"
400 class="btn btn-link btn-animate text-muted"
401 onClick={linkEvent(this, this.handleEditClick)}
402 data-tippy-content={i18n.t("edit")}
403 aria-label={i18n.t("edit")}
405 <Icon icon="edit" classes="icon-inline" />
408 class="btn btn-link btn-animate text-muted"
411 this.handleDeleteClick
426 classes={`icon-inline ${
427 cv.comment.deleted && "text-danger"
433 {/* Admins and mods can remove comments */}
434 {(this.canMod || this.canAdmin) && (
436 {!cv.comment.removed ? (
438 class="btn btn-link btn-animate text-muted"
441 this.handleModRemoveShow
443 aria-label={i18n.t("remove")}
449 class="btn btn-link btn-animate text-muted"
452 this.handleModRemoveSubmit
454 aria-label={i18n.t("restore")}
461 {/* Mods can ban from community, and appoint as mods to community */}
465 (!cv.creator_banned_from_community ? (
467 class="btn btn-link btn-animate text-muted"
470 this.handleModBanFromCommunityShow
472 aria-label={i18n.t("ban")}
478 class="btn btn-link btn-animate text-muted"
481 this.handleModBanFromCommunitySubmit
483 aria-label={i18n.t("unban")}
488 {!cv.creator_banned_from_community &&
489 (!this.state.showConfirmAppointAsMod ? (
491 class="btn btn-link btn-animate text-muted"
494 this.handleShowConfirmAppointAsMod
498 ? i18n.t("remove_as_mod")
499 : i18n.t("appoint_as_mod")
503 ? i18n.t("remove_as_mod")
504 : i18n.t("appoint_as_mod")}
509 class="btn btn-link btn-animate text-muted"
510 aria-label={i18n.t("are_you_sure")}
512 {i18n.t("are_you_sure")}
515 class="btn btn-link btn-animate text-muted"
518 this.handleAddModToCommunity
520 aria-label={i18n.t("yes")}
525 class="btn btn-link btn-animate text-muted"
528 this.handleCancelConfirmAppointAsMod
530 aria-label={i18n.t("no")}
538 {/* Community creators and admins can transfer community to another mod */}
539 {(this.amCommunityCreator || this.canAdmin) &&
542 (!this.state.showConfirmTransferCommunity ? (
544 class="btn btn-link btn-animate text-muted"
547 this.handleShowConfirmTransferCommunity
549 aria-label={i18n.t("transfer_community")}
551 {i18n.t("transfer_community")}
556 class="btn btn-link btn-animate text-muted"
557 aria-label={i18n.t("are_you_sure")}
559 {i18n.t("are_you_sure")}
562 class="btn btn-link btn-animate text-muted"
565 this.handleTransferCommunity
567 aria-label={i18n.t("yes")}
572 class="btn btn-link btn-animate text-muted"
576 .handleCancelShowConfirmTransferCommunity
578 aria-label={i18n.t("no")}
584 {/* Admins can ban from all, and appoint other admins */}
588 (!cv.creator.banned ? (
590 class="btn btn-link btn-animate text-muted"
593 this.handleModBanShow
595 aria-label={i18n.t("ban_from_site")}
597 {i18n.t("ban_from_site")}
601 class="btn btn-link btn-animate text-muted"
604 this.handleModBanSubmit
606 aria-label={i18n.t("unban_from_site")}
608 {i18n.t("unban_from_site")}
611 {!cv.creator.banned &&
613 (!this.state.showConfirmAppointAsAdmin ? (
615 class="btn btn-link btn-animate text-muted"
618 this.handleShowConfirmAppointAsAdmin
622 ? i18n.t("remove_as_admin")
623 : i18n.t("appoint_as_admin")
627 ? i18n.t("remove_as_admin")
628 : i18n.t("appoint_as_admin")}
632 <button class="btn btn-link btn-animate text-muted">
633 {i18n.t("are_you_sure")}
636 class="btn btn-link btn-animate text-muted"
641 aria-label={i18n.t("yes")}
646 class="btn btn-link btn-animate text-muted"
649 this.handleCancelConfirmAppointAsAdmin
651 aria-label={i18n.t("no")}
659 {/* Site Creator can transfer to another admin */}
660 {this.amSiteCreator &&
663 (!this.state.showConfirmTransferSite ? (
665 class="btn btn-link btn-animate text-muted"
668 this.handleShowConfirmTransferSite
670 aria-label={i18n.t("transfer_site")}
672 {i18n.t("transfer_site")}
677 class="btn btn-link btn-animate text-muted"
678 aria-label={i18n.t("are_you_sure")}
680 {i18n.t("are_you_sure")}
683 class="btn btn-link btn-animate text-muted"
686 this.handleTransferSite
688 aria-label={i18n.t("yes")}
693 class="btn btn-link btn-animate text-muted"
696 this.handleCancelShowConfirmTransferSite
698 aria-label={i18n.t("no")}
709 {/* end of button group */}
714 {/* end of details */}
715 {this.state.showRemoveDialog && (
718 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
722 htmlFor={`mod-remove-reason-${cv.comment.id}`}
728 id={`mod-remove-reason-${cv.comment.id}`}
729 class="form-control mr-2"
730 placeholder={i18n.t("reason")}
731 value={this.state.removeReason}
732 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
736 class="btn btn-secondary"
737 aria-label={i18n.t("remove_comment")}
739 {i18n.t("remove_comment")}
743 {this.state.showBanDialog && (
744 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
745 <div class="form-group row">
747 class="col-form-label"
748 htmlFor={`mod-ban-reason-${cv.comment.id}`}
754 id={`mod-ban-reason-${cv.comment.id}`}
755 class="form-control mr-2"
756 placeholder={i18n.t("reason")}
757 value={this.state.banReason}
758 onInput={linkEvent(this, this.handleModBanReasonChange)}
760 <div class="form-group">
761 <div class="form-check">
763 class="form-check-input"
764 id="mod-ban-remove-data"
766 checked={this.state.removeData}
767 onChange={linkEvent(this, this.handleModRemoveDataChange)}
770 class="form-check-label"
771 htmlFor="mod-ban-remove-data"
772 title={i18n.t("remove_content_more")}
774 {i18n.t("remove_content")}
779 {/* TODO hold off on expires until later */}
780 {/* <div class="form-group row"> */}
781 {/* <label class="col-form-label">Expires</label> */}
782 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
784 <div class="form-group row">
787 class="btn btn-secondary"
788 aria-label={i18n.t("ban")}
790 {i18n.t("ban")} {cv.creator.name}
795 {this.state.showReply && (
798 onReplyCancel={this.handleReplyCancel}
799 disabled={this.props.locked}
803 {node.children && !this.state.collapsed && (
805 nodes={node.children}
806 locked={this.props.locked}
807 moderators={this.props.moderators}
808 admins={this.props.admins}
809 postCreatorId={this.props.postCreatorId}
810 enableDownvotes={this.props.enableDownvotes}
813 {/* A collapsed clearfix */}
814 {this.state.collapsed && <div class="row col-12"></div>}
819 get commentOrMentionRead() {
820 let cv = this.props.node.comment_view;
821 return this.isPersonMentionType(cv)
822 ? cv.person_mention.read
826 linkBtn(small = false) {
827 let cv = this.props.node.comment_view;
830 className={`btn ${small && "btn-sm"} btn-link btn-animate text-muted`}
831 to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
832 title={this.props.showContext ? i18n.t("show_context") : i18n.t("link")}
834 <Icon icon="link" classes="icon-inline" />
843 get myComment(): boolean {
845 this.props.node.comment_view.creator.id ==
846 UserService.Instance.myUserInfo?.local_user_view.person.id
850 get isMod(): boolean {
852 this.props.moderators &&
854 this.props.moderators.map(m => m.moderator.id),
855 this.props.node.comment_view.creator.id
860 get isAdmin(): boolean {
864 this.props.admins.map(a => a.person.id),
865 this.props.node.comment_view.creator.id
870 get isPostCreator(): boolean {
871 return this.props.node.comment_view.creator.id == this.props.postCreatorId;
874 get canMod(): boolean {
875 if (this.props.admins && this.props.moderators) {
876 let adminsThenMods = this.props.admins
877 .map(a => a.person.id)
878 .concat(this.props.moderators.map(m => m.moderator.id));
881 UserService.Instance.myUserInfo,
883 this.props.node.comment_view.creator.id
890 get canAdmin(): boolean {
894 UserService.Instance.myUserInfo,
895 this.props.admins.map(a => a.person.id),
896 this.props.node.comment_view.creator.id
901 get amCommunityCreator(): boolean {
903 this.props.moderators &&
904 UserService.Instance.myUserInfo &&
905 this.props.node.comment_view.creator.id !=
906 UserService.Instance.myUserInfo.local_user_view.person.id &&
907 UserService.Instance.myUserInfo.local_user_view.person.id ==
908 this.props.moderators[0].moderator.id
912 get amSiteCreator(): boolean {
915 UserService.Instance.myUserInfo &&
916 this.props.node.comment_view.creator.id !=
917 UserService.Instance.myUserInfo.local_user_view.person.id &&
918 UserService.Instance.myUserInfo.local_user_view.person.id ==
919 this.props.admins[0].person.id
923 get commentUnlessRemoved(): string {
924 let comment = this.props.node.comment_view.comment;
925 return comment.removed
926 ? `*${i18n.t("removed")}*`
928 ? `*${i18n.t("deleted")}*`
932 handleReplyClick(i: CommentNode) {
933 i.state.showReply = true;
937 handleEditClick(i: CommentNode) {
938 i.state.showEdit = true;
942 handleBlockUserClick(i: CommentNode) {
943 let blockUserForm: BlockPerson = {
944 person_id: i.props.node.comment_view.creator.id,
948 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
951 handleDeleteClick(i: CommentNode) {
952 let comment = i.props.node.comment_view.comment;
953 let deleteForm: DeleteComment = {
954 comment_id: comment.id,
955 deleted: !comment.deleted,
958 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
961 handleSaveCommentClick(i: CommentNode) {
962 let cv = i.props.node.comment_view;
963 let save = cv.saved == undefined ? true : !cv.saved;
964 let form: SaveComment = {
965 comment_id: cv.comment.id,
970 WebSocketService.Instance.send(wsClient.saveComment(form));
972 i.state.saveLoading = true;
973 i.setState(this.state);
976 handleReplyCancel() {
977 this.state.showReply = false;
978 this.state.showEdit = false;
979 this.setState(this.state);
982 handleCommentUpvote(i: CommentNodeI, event: any) {
983 event.preventDefault();
984 let new_vote = this.state.my_vote == 1 ? 0 : 1;
986 if (this.state.my_vote == 1) {
988 this.state.upvotes--;
989 } else if (this.state.my_vote == -1) {
990 this.state.downvotes--;
991 this.state.upvotes++;
992 this.state.score += 2;
994 this.state.upvotes++;
998 this.state.my_vote = new_vote;
1000 let form: CreateCommentLike = {
1001 comment_id: i.comment_view.comment.id,
1002 score: this.state.my_vote,
1006 WebSocketService.Instance.send(wsClient.likeComment(form));
1007 this.setState(this.state);
1011 handleCommentDownvote(i: CommentNodeI, event: any) {
1012 event.preventDefault();
1013 let new_vote = this.state.my_vote == -1 ? 0 : -1;
1015 if (this.state.my_vote == 1) {
1016 this.state.score -= 2;
1017 this.state.upvotes--;
1018 this.state.downvotes++;
1019 } else if (this.state.my_vote == -1) {
1020 this.state.downvotes--;
1023 this.state.downvotes++;
1027 this.state.my_vote = new_vote;
1029 let form: CreateCommentLike = {
1030 comment_id: i.comment_view.comment.id,
1031 score: this.state.my_vote,
1035 WebSocketService.Instance.send(wsClient.likeComment(form));
1036 this.setState(this.state);
1040 handleModRemoveShow(i: CommentNode) {
1041 i.state.showRemoveDialog = true;
1042 i.setState(i.state);
1045 handleModRemoveReasonChange(i: CommentNode, event: any) {
1046 i.state.removeReason = event.target.value;
1047 i.setState(i.state);
1050 handleModRemoveDataChange(i: CommentNode, event: any) {
1051 i.state.removeData = event.target.checked;
1052 i.setState(i.state);
1055 handleModRemoveSubmit(i: CommentNode) {
1056 let comment = i.props.node.comment_view.comment;
1057 let form: RemoveComment = {
1058 comment_id: comment.id,
1059 removed: !comment.removed,
1060 reason: i.state.removeReason,
1063 WebSocketService.Instance.send(wsClient.removeComment(form));
1065 i.state.showRemoveDialog = false;
1066 i.setState(i.state);
1069 isPersonMentionType(
1070 item: CommentView | PersonMentionView
1071 ): item is PersonMentionView {
1072 return (item as PersonMentionView).person_mention?.id !== undefined;
1075 handleMarkRead(i: CommentNode) {
1076 if (i.isPersonMentionType(i.props.node.comment_view)) {
1077 let form: MarkPersonMentionAsRead = {
1078 person_mention_id: i.props.node.comment_view.person_mention.id,
1079 read: !i.props.node.comment_view.person_mention.read,
1082 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1084 let form: MarkCommentAsRead = {
1085 comment_id: i.props.node.comment_view.comment.id,
1086 read: !i.props.node.comment_view.comment.read,
1089 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
1092 i.state.readLoading = true;
1093 i.setState(this.state);
1096 handleModBanFromCommunityShow(i: CommentNode) {
1097 i.state.showBanDialog = !i.state.showBanDialog;
1098 i.state.banType = BanType.Community;
1099 i.setState(i.state);
1102 handleModBanShow(i: CommentNode) {
1103 i.state.showBanDialog = !i.state.showBanDialog;
1104 i.state.banType = BanType.Site;
1105 i.setState(i.state);
1108 handleModBanReasonChange(i: CommentNode, event: any) {
1109 i.state.banReason = event.target.value;
1110 i.setState(i.state);
1113 handleModBanExpiresChange(i: CommentNode, event: any) {
1114 i.state.banExpires = event.target.value;
1115 i.setState(i.state);
1118 handleModBanFromCommunitySubmit(i: CommentNode) {
1119 i.state.banType = BanType.Community;
1120 i.setState(i.state);
1121 i.handleModBanBothSubmit(i);
1124 handleModBanSubmit(i: CommentNode) {
1125 i.state.banType = BanType.Site;
1126 i.setState(i.state);
1127 i.handleModBanBothSubmit(i);
1130 handleModBanBothSubmit(i: CommentNode) {
1131 let cv = i.props.node.comment_view;
1133 if (i.state.banType == BanType.Community) {
1134 // If its an unban, restore all their data
1135 let ban = !cv.creator_banned_from_community;
1137 i.state.removeData = false;
1139 let form: BanFromCommunity = {
1140 person_id: cv.creator.id,
1141 community_id: cv.community.id,
1143 remove_data: i.state.removeData,
1144 reason: i.state.banReason,
1145 expires: getUnixTime(i.state.banExpires),
1148 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1150 // If its an unban, restore all their data
1151 let ban = !cv.creator.banned;
1153 i.state.removeData = false;
1155 let form: BanPerson = {
1156 person_id: cv.creator.id,
1158 remove_data: i.state.removeData,
1159 reason: i.state.banReason,
1160 expires: getUnixTime(i.state.banExpires),
1163 WebSocketService.Instance.send(wsClient.banPerson(form));
1166 i.state.showBanDialog = false;
1167 i.setState(i.state);
1170 handleShowConfirmAppointAsMod(i: CommentNode) {
1171 i.state.showConfirmAppointAsMod = true;
1172 i.setState(i.state);
1175 handleCancelConfirmAppointAsMod(i: CommentNode) {
1176 i.state.showConfirmAppointAsMod = false;
1177 i.setState(i.state);
1180 handleAddModToCommunity(i: CommentNode) {
1181 let cv = i.props.node.comment_view;
1182 let form: AddModToCommunity = {
1183 person_id: cv.creator.id,
1184 community_id: cv.community.id,
1188 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1189 i.state.showConfirmAppointAsMod = false;
1190 i.setState(i.state);
1193 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1194 i.state.showConfirmAppointAsAdmin = true;
1195 i.setState(i.state);
1198 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1199 i.state.showConfirmAppointAsAdmin = false;
1200 i.setState(i.state);
1203 handleAddAdmin(i: CommentNode) {
1204 let form: AddAdmin = {
1205 person_id: i.props.node.comment_view.creator.id,
1209 WebSocketService.Instance.send(wsClient.addAdmin(form));
1210 i.state.showConfirmAppointAsAdmin = false;
1211 i.setState(i.state);
1214 handleShowConfirmTransferCommunity(i: CommentNode) {
1215 i.state.showConfirmTransferCommunity = true;
1216 i.setState(i.state);
1219 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1220 i.state.showConfirmTransferCommunity = false;
1221 i.setState(i.state);
1224 handleTransferCommunity(i: CommentNode) {
1225 let cv = i.props.node.comment_view;
1226 let form: TransferCommunity = {
1227 community_id: cv.community.id,
1228 person_id: cv.creator.id,
1231 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1232 i.state.showConfirmTransferCommunity = false;
1233 i.setState(i.state);
1236 handleShowConfirmTransferSite(i: CommentNode) {
1237 i.state.showConfirmTransferSite = true;
1238 i.setState(i.state);
1241 handleCancelShowConfirmTransferSite(i: CommentNode) {
1242 i.state.showConfirmTransferSite = false;
1243 i.setState(i.state);
1246 handleTransferSite(i: CommentNode) {
1247 let form: TransferSite = {
1248 person_id: i.props.node.comment_view.creator.id,
1251 WebSocketService.Instance.send(wsClient.transferSite(form));
1252 i.state.showConfirmTransferSite = false;
1253 i.setState(i.state);
1256 get isCommentNew(): boolean {
1257 let now = moment.utc().subtract(10, "minutes");
1258 let then = moment.utc(this.props.node.comment_view.comment.published);
1259 return now.isBefore(then);
1262 handleCommentCollapse(i: CommentNode) {
1263 i.state.collapsed = !i.state.collapsed;
1264 i.setState(i.state);
1268 handleViewSource(i: CommentNode) {
1269 i.state.viewSource = !i.state.viewSource;
1270 i.setState(i.state);
1273 handleShowAdvanced(i: CommentNode) {
1274 i.state.showAdvanced = !i.state.showAdvanced;
1275 i.setState(i.state);
1280 if (this.state.my_vote == 1) {
1282 } else if (this.state.my_vote == -1) {
1283 return "text-danger";
1285 return "text-muted";
1289 get pointsTippy(): string {
1290 let points = i18n.t("number_of_points", {
1291 count: this.state.score,
1294 let upvotes = i18n.t("number_of_upvotes", {
1295 count: this.state.upvotes,
1298 let downvotes = i18n.t("number_of_downvotes", {
1299 count: this.state.downvotes,
1302 return `${points} • ${upvotes} • ${downvotes}`;
1305 get expandText(): string {
1306 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");