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";
38 import { Icon, Spinner } from "../common/icon";
39 import { MomentTime } from "../common/moment-time";
40 import { CommunityLink } from "../community/community-link";
41 import { PersonListing } from "../person/person-listing";
42 import { CommentForm } from "./comment-form";
43 import { CommentNodes } from "./comment-nodes";
45 interface CommentNodeState {
48 showRemoveDialog: boolean;
50 showBanDialog: boolean;
55 showConfirmTransferSite: boolean;
56 showConfirmTransferCommunity: boolean;
57 showConfirmAppointAsMod: boolean;
58 showConfirmAppointAsAdmin: boolean;
61 showAdvanced: boolean;
71 interface CommentNodeProps {
78 showContext?: boolean;
79 moderators: CommunityModeratorView[];
80 admins: PersonViewSafe[];
81 // TODO is this necessary, can't I get it from the node itself?
82 postCreatorId?: number;
83 showCommunity?: boolean;
84 enableDownvotes: boolean;
87 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
88 private emptyState: CommentNodeState = {
91 showRemoveDialog: false,
97 banType: BanType.Community,
101 showConfirmTransferSite: false,
102 showConfirmTransferCommunity: false,
103 showConfirmAppointAsMod: false,
104 showConfirmAppointAsAdmin: false,
105 my_vote: this.props.node.comment_view.my_vote,
106 score: this.props.node.comment_view.counts.score,
107 upvotes: this.props.node.comment_view.counts.upvotes,
108 downvotes: this.props.node.comment_view.counts.downvotes,
109 borderColor: this.props.node.depth
110 ? colorList[this.props.node.depth % colorList.length]
116 constructor(props: any, context: any) {
117 super(props, context);
119 this.state = this.emptyState;
120 this.handleReplyCancel = this.handleReplyCancel.bind(this);
121 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
122 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
125 // TODO see if there's a better way to do this, and all willReceiveProps
126 componentWillReceiveProps(nextProps: CommentNodeProps) {
127 let cv = nextProps.node.comment_view;
128 this.state.my_vote = cv.my_vote;
129 this.state.upvotes = cv.counts.upvotes;
130 this.state.downvotes = cv.counts.downvotes;
131 this.state.score = cv.counts.score;
132 this.state.readLoading = false;
133 this.state.saveLoading = false;
134 this.setState(this.state);
138 let node = this.props.node;
139 let cv = this.props.node.comment_view;
142 className={`comment ${
143 cv.comment.parent_id && !this.props.noIndent ? "ml-1" : ""
147 id={`comment-${cv.comment.id}`}
148 className={`details comment-node py-2 ${
149 !this.props.noBorder ? "border-top border-light" : ""
150 } ${this.isCommentNew ? "mark" : ""}`}
152 !this.props.noIndent &&
153 cv.comment.parent_id &&
154 `border-left: 2px ${this.state.borderColor} solid !important`
158 class={`${!this.props.noIndent && cv.comment.parent_id && "ml-2"}`}
160 <div class="d-flex flex-wrap align-items-center text-muted small">
162 <PersonListing person={cv.creator} />
166 <div className="badge badge-light d-none d-sm-inline mr-2">
171 <div className="badge badge-light d-none d-sm-inline mr-2">
175 {this.isPostCreator && (
176 <div className="badge badge-light d-none d-sm-inline mr-2">
180 {(cv.creator_banned_from_community || cv.creator.banned) && (
181 <div className="badge badge-danger mr-2">
185 {this.props.showCommunity && (
187 <span class="mx-1">{i18n.t("to")}</span>
188 <CommunityLink community={cv.community} />
189 <span class="mx-2">•</span>
190 <Link className="mr-2" to={`/post/${cv.post.id}`}>
196 class="btn btn-sm text-muted"
197 onClick={linkEvent(this, this.handleCommentCollapse)}
198 aria-label={this.expandText}
199 data-tippy-content={this.expandText}
201 {this.state.collapsed ? (
202 <Icon icon="plus-square" classes="icon-inline" />
204 <Icon icon="minus-square" classes="icon-inline" />
208 {/* This is an expanding spacer for mobile */}
209 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
213 className={`unselectable pointer ${this.scoreColor}`}
214 onClick={linkEvent(node, this.handleCommentUpvote)}
215 data-tippy-content={this.pointsTippy}
218 class="mr-1 font-weight-bold"
219 aria-label={i18n.t("number_of_points", {
220 count: this.state.score,
221 formattedCount: this.state.score,
224 {numToSI(this.state.score)}
227 <span className="mr-1">•</span>
231 <MomentTime data={cv.comment} />
234 {/* end of user row */}
235 {this.state.showEdit && (
239 onReplyCancel={this.handleReplyCancel}
240 disabled={this.props.locked}
244 {!this.state.showEdit && !this.state.collapsed && (
246 {this.state.viewSource ? (
247 <pre>{this.commentUnlessRemoved}</pre>
251 dangerouslySetInnerHTML={mdToHtml(
252 this.commentUnlessRemoved
256 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
257 {this.props.showContext && this.linkBtn()}
258 {this.props.markable && (
260 class="btn btn-link btn-animate text-muted"
261 onClick={linkEvent(this, this.handleMarkRead)}
263 this.commentOrMentionRead
264 ? i18n.t("mark_as_unread")
265 : i18n.t("mark_as_read")
268 this.commentOrMentionRead
269 ? i18n.t("mark_as_unread")
270 : i18n.t("mark_as_read")
273 {this.state.readLoading ? (
278 classes={`icon-inline ${
279 this.commentOrMentionRead && "text-success"
285 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
288 className={`btn btn-link btn-animate ${
289 this.state.my_vote == 1 ? "text-info" : "text-muted"
291 onClick={linkEvent(node, this.handleCommentUpvote)}
292 data-tippy-content={i18n.t("upvote")}
293 aria-label={i18n.t("upvote")}
295 <Icon icon="arrow-up1" classes="icon-inline" />
297 this.state.upvotes !== this.state.score && (
299 {numToSI(this.state.upvotes)}
303 {this.props.enableDownvotes && (
305 className={`btn btn-link btn-animate ${
306 this.state.my_vote == -1
310 onClick={linkEvent(node, this.handleCommentDownvote)}
311 data-tippy-content={i18n.t("downvote")}
312 aria-label={i18n.t("downvote")}
314 <Icon icon="arrow-down1" classes="icon-inline" />
316 this.state.upvotes !== this.state.score && (
318 {numToSI(this.state.downvotes)}
324 class="btn btn-link btn-animate text-muted"
325 onClick={linkEvent(this, this.handleReplyClick)}
326 data-tippy-content={i18n.t("reply")}
327 aria-label={i18n.t("reply")}
329 <Icon icon="reply1" classes="icon-inline" />
331 {!this.state.showAdvanced ? (
333 className="btn btn-link btn-animate text-muted"
334 onClick={linkEvent(this, this.handleShowAdvanced)}
335 data-tippy-content={i18n.t("more")}
336 aria-label={i18n.t("more")}
338 <Icon icon="more-vertical" classes="icon-inline" />
342 {!this.myComment && (
344 <button class="btn btn-link btn-animate">
346 className="text-muted"
347 to={`/create_private_message/recipient/${cv.creator.id}`}
348 title={i18n.t("message").toLowerCase()}
354 class="btn btn-link btn-animate text-muted"
357 this.handleBlockUserClick
359 data-tippy-content={i18n.t("block_user")}
360 aria-label={i18n.t("block_user")}
362 <Icon icon="slash" />
367 class="btn btn-link btn-animate text-muted"
370 this.handleSaveCommentClick
373 cv.saved ? i18n.t("unsave") : i18n.t("save")
376 cv.saved ? i18n.t("unsave") : i18n.t("save")
379 {this.state.saveLoading ? (
384 classes={`icon-inline ${
385 cv.saved && "text-warning"
391 className="btn btn-link btn-animate text-muted"
392 onClick={linkEvent(this, this.handleViewSource)}
393 data-tippy-content={i18n.t("view_source")}
394 aria-label={i18n.t("view_source")}
398 classes={`icon-inline ${
399 this.state.viewSource && "text-success"
406 class="btn btn-link btn-animate text-muted"
407 onClick={linkEvent(this, this.handleEditClick)}
408 data-tippy-content={i18n.t("edit")}
409 aria-label={i18n.t("edit")}
411 <Icon icon="edit" classes="icon-inline" />
414 class="btn btn-link btn-animate text-muted"
417 this.handleDeleteClick
432 classes={`icon-inline ${
433 cv.comment.deleted && "text-danger"
439 {/* Admins and mods can remove comments */}
440 {(this.canMod || this.canAdmin) && (
442 {!cv.comment.removed ? (
444 class="btn btn-link btn-animate text-muted"
447 this.handleModRemoveShow
449 aria-label={i18n.t("remove")}
455 class="btn btn-link btn-animate text-muted"
458 this.handleModRemoveSubmit
460 aria-label={i18n.t("restore")}
467 {/* Mods can ban from community, and appoint as mods to community */}
471 (!cv.creator_banned_from_community ? (
473 class="btn btn-link btn-animate text-muted"
476 this.handleModBanFromCommunityShow
478 aria-label={i18n.t("ban")}
484 class="btn btn-link btn-animate text-muted"
487 this.handleModBanFromCommunitySubmit
489 aria-label={i18n.t("unban")}
494 {!cv.creator_banned_from_community &&
495 (!this.state.showConfirmAppointAsMod ? (
497 class="btn btn-link btn-animate text-muted"
500 this.handleShowConfirmAppointAsMod
504 ? i18n.t("remove_as_mod")
505 : i18n.t("appoint_as_mod")
509 ? i18n.t("remove_as_mod")
510 : i18n.t("appoint_as_mod")}
515 class="btn btn-link btn-animate text-muted"
516 aria-label={i18n.t("are_you_sure")}
518 {i18n.t("are_you_sure")}
521 class="btn btn-link btn-animate text-muted"
524 this.handleAddModToCommunity
526 aria-label={i18n.t("yes")}
531 class="btn btn-link btn-animate text-muted"
534 this.handleCancelConfirmAppointAsMod
536 aria-label={i18n.t("no")}
544 {/* Community creators and admins can transfer community to another mod */}
545 {(this.amCommunityCreator || this.canAdmin) &&
548 (!this.state.showConfirmTransferCommunity ? (
550 class="btn btn-link btn-animate text-muted"
553 this.handleShowConfirmTransferCommunity
555 aria-label={i18n.t("transfer_community")}
557 {i18n.t("transfer_community")}
562 class="btn btn-link btn-animate text-muted"
563 aria-label={i18n.t("are_you_sure")}
565 {i18n.t("are_you_sure")}
568 class="btn btn-link btn-animate text-muted"
571 this.handleTransferCommunity
573 aria-label={i18n.t("yes")}
578 class="btn btn-link btn-animate text-muted"
582 .handleCancelShowConfirmTransferCommunity
584 aria-label={i18n.t("no")}
590 {/* Admins can ban from all, and appoint other admins */}
594 (!cv.creator.banned ? (
596 class="btn btn-link btn-animate text-muted"
599 this.handleModBanShow
601 aria-label={i18n.t("ban_from_site")}
603 {i18n.t("ban_from_site")}
607 class="btn btn-link btn-animate text-muted"
610 this.handleModBanSubmit
612 aria-label={i18n.t("unban_from_site")}
614 {i18n.t("unban_from_site")}
617 {!cv.creator.banned &&
619 (!this.state.showConfirmAppointAsAdmin ? (
621 class="btn btn-link btn-animate text-muted"
624 this.handleShowConfirmAppointAsAdmin
628 ? i18n.t("remove_as_admin")
629 : i18n.t("appoint_as_admin")
633 ? i18n.t("remove_as_admin")
634 : i18n.t("appoint_as_admin")}
638 <button class="btn btn-link btn-animate text-muted">
639 {i18n.t("are_you_sure")}
642 class="btn btn-link btn-animate text-muted"
647 aria-label={i18n.t("yes")}
652 class="btn btn-link btn-animate text-muted"
655 this.handleCancelConfirmAppointAsAdmin
657 aria-label={i18n.t("no")}
665 {/* Site Creator can transfer to another admin */}
666 {this.amSiteCreator &&
669 (!this.state.showConfirmTransferSite ? (
671 class="btn btn-link btn-animate text-muted"
674 this.handleShowConfirmTransferSite
676 aria-label={i18n.t("transfer_site")}
678 {i18n.t("transfer_site")}
683 class="btn btn-link btn-animate text-muted"
684 aria-label={i18n.t("are_you_sure")}
686 {i18n.t("are_you_sure")}
689 class="btn btn-link btn-animate text-muted"
692 this.handleTransferSite
694 aria-label={i18n.t("yes")}
699 class="btn btn-link btn-animate text-muted"
702 this.handleCancelShowConfirmTransferSite
704 aria-label={i18n.t("no")}
715 {/* end of button group */}
720 {/* end of details */}
721 {this.state.showRemoveDialog && (
724 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
728 htmlFor={`mod-remove-reason-${cv.comment.id}`}
734 id={`mod-remove-reason-${cv.comment.id}`}
735 class="form-control mr-2"
736 placeholder={i18n.t("reason")}
737 value={this.state.removeReason}
738 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
742 class="btn btn-secondary"
743 aria-label={i18n.t("remove_comment")}
745 {i18n.t("remove_comment")}
749 {this.state.showBanDialog && (
750 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
751 <div class="form-group row">
753 class="col-form-label"
754 htmlFor={`mod-ban-reason-${cv.comment.id}`}
760 id={`mod-ban-reason-${cv.comment.id}`}
761 class="form-control mr-2"
762 placeholder={i18n.t("reason")}
763 value={this.state.banReason}
764 onInput={linkEvent(this, this.handleModBanReasonChange)}
766 <div class="form-group">
767 <div class="form-check">
769 class="form-check-input"
770 id="mod-ban-remove-data"
772 checked={this.state.removeData}
773 onChange={linkEvent(this, this.handleModRemoveDataChange)}
776 class="form-check-label"
777 htmlFor="mod-ban-remove-data"
778 title={i18n.t("remove_content_more")}
780 {i18n.t("remove_content")}
785 {/* TODO hold off on expires until later */}
786 {/* <div class="form-group row"> */}
787 {/* <label class="col-form-label">Expires</label> */}
788 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
790 <div class="form-group row">
793 class="btn btn-secondary"
794 aria-label={i18n.t("ban")}
796 {i18n.t("ban")} {cv.creator.name}
801 {this.state.showReply && (
804 onReplyCancel={this.handleReplyCancel}
805 disabled={this.props.locked}
809 {node.children && !this.state.collapsed && (
811 nodes={node.children}
812 locked={this.props.locked}
813 moderators={this.props.moderators}
814 admins={this.props.admins}
815 postCreatorId={this.props.postCreatorId}
816 enableDownvotes={this.props.enableDownvotes}
819 {/* A collapsed clearfix */}
820 {this.state.collapsed && <div class="row col-12"></div>}
825 get commentOrMentionRead() {
826 let cv = this.props.node.comment_view;
827 return this.isPersonMentionType(cv)
828 ? cv.person_mention.read
832 linkBtn(small = false) {
833 let cv = this.props.node.comment_view;
836 className={`btn ${small && "btn-sm"} btn-link btn-animate text-muted`}
837 to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
838 title={this.props.showContext ? i18n.t("show_context") : i18n.t("link")}
840 <Icon icon="link" classes="icon-inline" />
849 get myComment(): boolean {
851 this.props.node.comment_view.creator.id ==
852 UserService.Instance.myUserInfo?.local_user_view.person.id
856 get isMod(): boolean {
858 this.props.moderators &&
860 this.props.moderators.map(m => m.moderator.id),
861 this.props.node.comment_view.creator.id
866 get isAdmin(): boolean {
870 this.props.admins.map(a => a.person.id),
871 this.props.node.comment_view.creator.id
876 get isPostCreator(): boolean {
877 return this.props.node.comment_view.creator.id == this.props.postCreatorId;
880 get canMod(): boolean {
881 if (this.props.admins && this.props.moderators) {
882 let adminsThenMods = this.props.admins
883 .map(a => a.person.id)
884 .concat(this.props.moderators.map(m => m.moderator.id));
887 UserService.Instance.myUserInfo,
889 this.props.node.comment_view.creator.id
896 get canAdmin(): boolean {
900 UserService.Instance.myUserInfo,
901 this.props.admins.map(a => a.person.id),
902 this.props.node.comment_view.creator.id
907 get amCommunityCreator(): boolean {
909 this.props.moderators &&
910 UserService.Instance.myUserInfo &&
911 this.props.node.comment_view.creator.id !=
912 UserService.Instance.myUserInfo.local_user_view.person.id &&
913 UserService.Instance.myUserInfo.local_user_view.person.id ==
914 this.props.moderators[0].moderator.id
918 get amSiteCreator(): boolean {
921 UserService.Instance.myUserInfo &&
922 this.props.node.comment_view.creator.id !=
923 UserService.Instance.myUserInfo.local_user_view.person.id &&
924 UserService.Instance.myUserInfo.local_user_view.person.id ==
925 this.props.admins[0].person.id
929 get commentUnlessRemoved(): string {
930 let comment = this.props.node.comment_view.comment;
931 return comment.removed
932 ? `*${i18n.t("removed")}*`
934 ? `*${i18n.t("deleted")}*`
938 handleReplyClick(i: CommentNode) {
939 i.state.showReply = true;
943 handleEditClick(i: CommentNode) {
944 i.state.showEdit = true;
948 handleBlockUserClick(i: CommentNode) {
949 let blockUserForm: BlockPerson = {
950 person_id: i.props.node.comment_view.creator.id,
954 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
957 handleDeleteClick(i: CommentNode) {
958 let comment = i.props.node.comment_view.comment;
959 let deleteForm: DeleteComment = {
960 comment_id: comment.id,
961 deleted: !comment.deleted,
964 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
967 handleSaveCommentClick(i: CommentNode) {
968 let cv = i.props.node.comment_view;
969 let save = cv.saved == undefined ? true : !cv.saved;
970 let form: SaveComment = {
971 comment_id: cv.comment.id,
976 WebSocketService.Instance.send(wsClient.saveComment(form));
978 i.state.saveLoading = true;
979 i.setState(this.state);
982 handleReplyCancel() {
983 this.state.showReply = false;
984 this.state.showEdit = false;
985 this.setState(this.state);
988 handleCommentUpvote(i: CommentNodeI, event: any) {
989 event.preventDefault();
990 let new_vote = this.state.my_vote == 1 ? 0 : 1;
992 if (this.state.my_vote == 1) {
994 this.state.upvotes--;
995 } else if (this.state.my_vote == -1) {
996 this.state.downvotes--;
997 this.state.upvotes++;
998 this.state.score += 2;
1000 this.state.upvotes++;
1004 this.state.my_vote = new_vote;
1006 let form: CreateCommentLike = {
1007 comment_id: i.comment_view.comment.id,
1008 score: this.state.my_vote,
1012 WebSocketService.Instance.send(wsClient.likeComment(form));
1013 this.setState(this.state);
1017 handleCommentDownvote(i: CommentNodeI, event: any) {
1018 event.preventDefault();
1019 let new_vote = this.state.my_vote == -1 ? 0 : -1;
1021 if (this.state.my_vote == 1) {
1022 this.state.score -= 2;
1023 this.state.upvotes--;
1024 this.state.downvotes++;
1025 } else if (this.state.my_vote == -1) {
1026 this.state.downvotes--;
1029 this.state.downvotes++;
1033 this.state.my_vote = new_vote;
1035 let form: CreateCommentLike = {
1036 comment_id: i.comment_view.comment.id,
1037 score: this.state.my_vote,
1041 WebSocketService.Instance.send(wsClient.likeComment(form));
1042 this.setState(this.state);
1046 handleModRemoveShow(i: CommentNode) {
1047 i.state.showRemoveDialog = true;
1048 i.setState(i.state);
1051 handleModRemoveReasonChange(i: CommentNode, event: any) {
1052 i.state.removeReason = event.target.value;
1053 i.setState(i.state);
1056 handleModRemoveDataChange(i: CommentNode, event: any) {
1057 i.state.removeData = event.target.checked;
1058 i.setState(i.state);
1061 handleModRemoveSubmit(i: CommentNode) {
1062 let comment = i.props.node.comment_view.comment;
1063 let form: RemoveComment = {
1064 comment_id: comment.id,
1065 removed: !comment.removed,
1066 reason: i.state.removeReason,
1069 WebSocketService.Instance.send(wsClient.removeComment(form));
1071 i.state.showRemoveDialog = false;
1072 i.setState(i.state);
1075 isPersonMentionType(
1076 item: CommentView | PersonMentionView
1077 ): item is PersonMentionView {
1078 return (item as PersonMentionView).person_mention?.id !== undefined;
1081 handleMarkRead(i: CommentNode) {
1082 if (i.isPersonMentionType(i.props.node.comment_view)) {
1083 let form: MarkPersonMentionAsRead = {
1084 person_mention_id: i.props.node.comment_view.person_mention.id,
1085 read: !i.props.node.comment_view.person_mention.read,
1088 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1090 let form: MarkCommentAsRead = {
1091 comment_id: i.props.node.comment_view.comment.id,
1092 read: !i.props.node.comment_view.comment.read,
1095 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
1098 i.state.readLoading = true;
1099 i.setState(this.state);
1102 handleModBanFromCommunityShow(i: CommentNode) {
1103 i.state.showBanDialog = !i.state.showBanDialog;
1104 i.state.banType = BanType.Community;
1105 i.setState(i.state);
1108 handleModBanShow(i: CommentNode) {
1109 i.state.showBanDialog = !i.state.showBanDialog;
1110 i.state.banType = BanType.Site;
1111 i.setState(i.state);
1114 handleModBanReasonChange(i: CommentNode, event: any) {
1115 i.state.banReason = event.target.value;
1116 i.setState(i.state);
1119 handleModBanExpiresChange(i: CommentNode, event: any) {
1120 i.state.banExpires = event.target.value;
1121 i.setState(i.state);
1124 handleModBanFromCommunitySubmit(i: CommentNode) {
1125 i.state.banType = BanType.Community;
1126 i.setState(i.state);
1127 i.handleModBanBothSubmit(i);
1130 handleModBanSubmit(i: CommentNode) {
1131 i.state.banType = BanType.Site;
1132 i.setState(i.state);
1133 i.handleModBanBothSubmit(i);
1136 handleModBanBothSubmit(i: CommentNode) {
1137 let cv = i.props.node.comment_view;
1139 if (i.state.banType == BanType.Community) {
1140 // If its an unban, restore all their data
1141 let ban = !cv.creator_banned_from_community;
1143 i.state.removeData = false;
1145 let form: BanFromCommunity = {
1146 person_id: cv.creator.id,
1147 community_id: cv.community.id,
1149 remove_data: i.state.removeData,
1150 reason: i.state.banReason,
1151 expires: getUnixTime(i.state.banExpires),
1154 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1156 // If its an unban, restore all their data
1157 let ban = !cv.creator.banned;
1159 i.state.removeData = false;
1161 let form: BanPerson = {
1162 person_id: cv.creator.id,
1164 remove_data: i.state.removeData,
1165 reason: i.state.banReason,
1166 expires: getUnixTime(i.state.banExpires),
1169 WebSocketService.Instance.send(wsClient.banPerson(form));
1172 i.state.showBanDialog = false;
1173 i.setState(i.state);
1176 handleShowConfirmAppointAsMod(i: CommentNode) {
1177 i.state.showConfirmAppointAsMod = true;
1178 i.setState(i.state);
1181 handleCancelConfirmAppointAsMod(i: CommentNode) {
1182 i.state.showConfirmAppointAsMod = false;
1183 i.setState(i.state);
1186 handleAddModToCommunity(i: CommentNode) {
1187 let cv = i.props.node.comment_view;
1188 let form: AddModToCommunity = {
1189 person_id: cv.creator.id,
1190 community_id: cv.community.id,
1194 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1195 i.state.showConfirmAppointAsMod = false;
1196 i.setState(i.state);
1199 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1200 i.state.showConfirmAppointAsAdmin = true;
1201 i.setState(i.state);
1204 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1205 i.state.showConfirmAppointAsAdmin = false;
1206 i.setState(i.state);
1209 handleAddAdmin(i: CommentNode) {
1210 let form: AddAdmin = {
1211 person_id: i.props.node.comment_view.creator.id,
1215 WebSocketService.Instance.send(wsClient.addAdmin(form));
1216 i.state.showConfirmAppointAsAdmin = false;
1217 i.setState(i.state);
1220 handleShowConfirmTransferCommunity(i: CommentNode) {
1221 i.state.showConfirmTransferCommunity = true;
1222 i.setState(i.state);
1225 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1226 i.state.showConfirmTransferCommunity = false;
1227 i.setState(i.state);
1230 handleTransferCommunity(i: CommentNode) {
1231 let cv = i.props.node.comment_view;
1232 let form: TransferCommunity = {
1233 community_id: cv.community.id,
1234 person_id: cv.creator.id,
1237 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1238 i.state.showConfirmTransferCommunity = false;
1239 i.setState(i.state);
1242 handleShowConfirmTransferSite(i: CommentNode) {
1243 i.state.showConfirmTransferSite = true;
1244 i.setState(i.state);
1247 handleCancelShowConfirmTransferSite(i: CommentNode) {
1248 i.state.showConfirmTransferSite = false;
1249 i.setState(i.state);
1252 handleTransferSite(i: CommentNode) {
1253 let form: TransferSite = {
1254 person_id: i.props.node.comment_view.creator.id,
1257 WebSocketService.Instance.send(wsClient.transferSite(form));
1258 i.state.showConfirmTransferSite = false;
1259 i.setState(i.state);
1262 get isCommentNew(): boolean {
1263 let now = moment.utc().subtract(10, "minutes");
1264 let then = moment.utc(this.props.node.comment_view.comment.published);
1265 return now.isBefore(then);
1268 handleCommentCollapse(i: CommentNode) {
1269 i.state.collapsed = !i.state.collapsed;
1270 i.setState(i.state);
1274 handleViewSource(i: CommentNode) {
1275 i.state.viewSource = !i.state.viewSource;
1276 i.setState(i.state);
1279 handleShowAdvanced(i: CommentNode) {
1280 i.state.showAdvanced = !i.state.showAdvanced;
1281 i.setState(i.state);
1286 if (this.state.my_vote == 1) {
1288 } else if (this.state.my_vote == -1) {
1289 return "text-danger";
1291 return "text-muted";
1295 get pointsTippy(): string {
1296 let points = i18n.t("number_of_points", {
1297 count: this.state.score,
1298 formattedCount: this.state.score,
1301 let upvotes = i18n.t("number_of_upvotes", {
1302 count: this.state.upvotes,
1303 formattedCount: this.state.upvotes,
1306 let downvotes = i18n.t("number_of_downvotes", {
1307 count: this.state.downvotes,
1308 formattedCount: this.state.downvotes,
1311 return `${points} • ${upvotes} • ${downvotes}`;
1314 get expandText(): string {
1315 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");