1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
9 CommunityModeratorView,
13 MarkPersonMentionAsRead,
20 } from "lemmy-js-client";
21 import moment from "moment";
22 import { i18n } from "../../i18next";
23 import { BanType, CommentNode as CommentNodeI } from "../../interfaces";
24 import { UserService, WebSocketService } from "../../services";
36 import { Icon, Spinner } from "../common/icon";
37 import { MomentTime } from "../common/moment-time";
38 import { CommunityLink } from "../community/community-link";
39 import { PersonListing } from "../person/person-listing";
40 import { CommentForm } from "./comment-form";
41 import { CommentNodes } from "./comment-nodes";
43 interface CommentNodeState {
46 showRemoveDialog: boolean;
48 showBanDialog: boolean;
53 showConfirmTransferSite: boolean;
54 showConfirmTransferCommunity: boolean;
55 showConfirmAppointAsMod: boolean;
56 showConfirmAppointAsAdmin: boolean;
59 showAdvanced: boolean;
69 interface CommentNodeProps {
76 showContext?: boolean;
77 moderators: CommunityModeratorView[];
78 admins: PersonViewSafe[];
79 // TODO is this necessary, can't I get it from the node itself?
80 postCreatorId?: number;
81 showCommunity?: boolean;
82 enableDownvotes: boolean;
85 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
86 private emptyState: CommentNodeState = {
89 showRemoveDialog: false,
95 banType: BanType.Community,
99 showConfirmTransferSite: false,
100 showConfirmTransferCommunity: false,
101 showConfirmAppointAsMod: false,
102 showConfirmAppointAsAdmin: false,
103 my_vote: this.props.node.comment_view.my_vote,
104 score: this.props.node.comment_view.counts.score,
105 upvotes: this.props.node.comment_view.counts.upvotes,
106 downvotes: this.props.node.comment_view.counts.downvotes,
107 borderColor: this.props.node.depth
108 ? colorList[this.props.node.depth % colorList.length]
114 constructor(props: any, context: any) {
115 super(props, context);
117 this.state = this.emptyState;
118 this.handleReplyCancel = this.handleReplyCancel.bind(this);
119 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
120 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
123 // TODO see if there's a better way to do this, and all willReceiveProps
124 componentWillReceiveProps(nextProps: CommentNodeProps) {
125 let cv = nextProps.node.comment_view;
126 this.state.my_vote = cv.my_vote;
127 this.state.upvotes = cv.counts.upvotes;
128 this.state.downvotes = cv.counts.downvotes;
129 this.state.score = cv.counts.score;
130 this.state.readLoading = false;
131 this.state.saveLoading = false;
132 this.setState(this.state);
136 let node = this.props.node;
137 let cv = this.props.node.comment_view;
140 className={`comment ${
141 cv.comment.parent_id && !this.props.noIndent ? "ml-1" : ""
145 id={`comment-${cv.comment.id}`}
146 className={`details comment-node py-2 ${
147 !this.props.noBorder ? "border-top border-light" : ""
148 } ${this.isCommentNew ? "mark" : ""}`}
150 !this.props.noIndent &&
151 cv.comment.parent_id &&
152 `border-left: 2px ${this.state.borderColor} solid !important`
156 class={`${!this.props.noIndent && cv.comment.parent_id && "ml-2"}`}
158 <div class="d-flex flex-wrap align-items-center text-muted small">
160 <PersonListing person={cv.creator} />
164 <div className="badge badge-light d-none d-sm-inline mr-2">
169 <div className="badge badge-light d-none d-sm-inline mr-2">
173 {this.isPostCreator && (
174 <div className="badge badge-light d-none d-sm-inline mr-2">
178 {(cv.creator_banned_from_community || cv.creator.banned) && (
179 <div className="badge badge-danger mr-2">
183 {this.props.showCommunity && (
185 <span class="mx-1">{i18n.t("to")}</span>
186 <CommunityLink community={cv.community} />
187 <span class="mx-2">•</span>
188 <Link className="mr-2" to={`/post/${cv.post.id}`}>
194 class="btn btn-sm text-muted"
195 onClick={linkEvent(this, this.handleCommentCollapse)}
196 aria-label={this.expandText}
197 data-tippy-content={this.expandText}
199 {this.state.collapsed ? (
200 <Icon icon="plus-square" classes="icon-inline" />
202 <Icon icon="minus-square" classes="icon-inline" />
206 {/* This is an expanding spacer for mobile */}
207 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
211 className={`unselectable pointer ${this.scoreColor}`}
212 onClick={linkEvent(node, this.handleCommentUpvote)}
213 data-tippy-content={this.pointsTippy}
216 class="mr-1 font-weight-bold"
217 aria-label={i18n.t("number_of_points", {
218 count: this.state.score,
224 <span className="mr-1">•</span>
228 <MomentTime data={cv.comment} />
231 {/* end of user row */}
232 {this.state.showEdit && (
236 onReplyCancel={this.handleReplyCancel}
237 disabled={this.props.locked}
241 {!this.state.showEdit && !this.state.collapsed && (
243 {this.state.viewSource ? (
244 <pre>{this.commentUnlessRemoved}</pre>
248 dangerouslySetInnerHTML={mdToHtml(
249 this.commentUnlessRemoved
253 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
254 {this.props.showContext && this.linkBtn()}
255 {this.props.markable && (
257 class="btn btn-link btn-animate text-muted"
258 onClick={linkEvent(this, this.handleMarkRead)}
260 this.commentOrMentionRead
261 ? i18n.t("mark_as_unread")
262 : i18n.t("mark_as_read")
265 this.commentOrMentionRead
266 ? i18n.t("mark_as_unread")
267 : i18n.t("mark_as_read")
270 {this.state.readLoading ? (
275 classes={`icon-inline ${
276 this.commentOrMentionRead && "text-success"
282 {UserService.Instance.localUserView && !this.props.viewOnly && (
285 className={`btn btn-link btn-animate ${
286 this.state.my_vote == 1 ? "text-info" : "text-muted"
288 onClick={linkEvent(node, this.handleCommentUpvote)}
289 data-tippy-content={i18n.t("upvote")}
290 aria-label={i18n.t("upvote")}
292 <Icon icon="arrow-up1" classes="icon-inline" />
294 this.state.upvotes !== this.state.score && (
295 <span class="ml-1">{this.state.upvotes}</span>
298 {this.props.enableDownvotes && (
300 className={`btn btn-link btn-animate ${
301 this.state.my_vote == -1
305 onClick={linkEvent(node, this.handleCommentDownvote)}
306 data-tippy-content={i18n.t("downvote")}
307 aria-label={i18n.t("downvote")}
309 <Icon icon="arrow-down1" classes="icon-inline" />
311 this.state.upvotes !== this.state.score && (
312 <span class="ml-1">{this.state.downvotes}</span>
317 class="btn btn-link btn-animate text-muted"
318 onClick={linkEvent(this, this.handleReplyClick)}
319 data-tippy-content={i18n.t("reply")}
320 aria-label={i18n.t("reply")}
322 <Icon icon="reply1" classes="icon-inline" />
324 {!this.state.showAdvanced ? (
326 className="btn btn-link btn-animate text-muted"
327 onClick={linkEvent(this, this.handleShowAdvanced)}
328 data-tippy-content={i18n.t("more")}
329 aria-label={i18n.t("more")}
331 <Icon icon="more-vertical" classes="icon-inline" />
335 {!this.myComment && (
336 <button class="btn btn-link btn-animate">
338 className="text-muted"
339 to={`/create_private_message/recipient/${cv.creator.id}`}
340 title={i18n.t("message").toLowerCase()}
347 class="btn btn-link btn-animate text-muted"
350 this.handleSaveCommentClick
353 cv.saved ? i18n.t("unsave") : i18n.t("save")
356 cv.saved ? i18n.t("unsave") : i18n.t("save")
359 {this.state.saveLoading ? (
364 classes={`icon-inline ${
365 cv.saved && "text-warning"
371 className="btn btn-link btn-animate text-muted"
372 onClick={linkEvent(this, this.handleViewSource)}
373 data-tippy-content={i18n.t("view_source")}
374 aria-label={i18n.t("view_source")}
378 classes={`icon-inline ${
379 this.state.viewSource && "text-success"
386 class="btn btn-link btn-animate text-muted"
387 onClick={linkEvent(this, this.handleEditClick)}
388 data-tippy-content={i18n.t("edit")}
389 aria-label={i18n.t("edit")}
391 <Icon icon="edit" classes="icon-inline" />
394 class="btn btn-link btn-animate text-muted"
397 this.handleDeleteClick
412 classes={`icon-inline ${
413 cv.comment.deleted && "text-danger"
419 {/* Admins and mods can remove comments */}
420 {(this.canMod || this.canAdmin) && (
422 {!cv.comment.removed ? (
424 class="btn btn-link btn-animate text-muted"
427 this.handleModRemoveShow
429 aria-label={i18n.t("remove")}
435 class="btn btn-link btn-animate text-muted"
438 this.handleModRemoveSubmit
440 aria-label={i18n.t("restore")}
447 {/* Mods can ban from community, and appoint as mods to community */}
451 (!cv.creator_banned_from_community ? (
453 class="btn btn-link btn-animate text-muted"
456 this.handleModBanFromCommunityShow
458 aria-label={i18n.t("ban")}
464 class="btn btn-link btn-animate text-muted"
467 this.handleModBanFromCommunitySubmit
469 aria-label={i18n.t("unban")}
474 {!cv.creator_banned_from_community &&
475 (!this.state.showConfirmAppointAsMod ? (
477 class="btn btn-link btn-animate text-muted"
480 this.handleShowConfirmAppointAsMod
484 ? i18n.t("remove_as_mod")
485 : i18n.t("appoint_as_mod")
489 ? i18n.t("remove_as_mod")
490 : i18n.t("appoint_as_mod")}
495 class="btn btn-link btn-animate text-muted"
496 aria-label={i18n.t("are_you_sure")}
498 {i18n.t("are_you_sure")}
501 class="btn btn-link btn-animate text-muted"
504 this.handleAddModToCommunity
506 aria-label={i18n.t("yes")}
511 class="btn btn-link btn-animate text-muted"
514 this.handleCancelConfirmAppointAsMod
516 aria-label={i18n.t("no")}
524 {/* Community creators and admins can transfer community to another mod */}
525 {(this.amCommunityCreator || this.canAdmin) &&
528 (!this.state.showConfirmTransferCommunity ? (
530 class="btn btn-link btn-animate text-muted"
533 this.handleShowConfirmTransferCommunity
535 aria-label={i18n.t("transfer_community")}
537 {i18n.t("transfer_community")}
542 class="btn btn-link btn-animate text-muted"
543 aria-label={i18n.t("are_you_sure")}
545 {i18n.t("are_you_sure")}
548 class="btn btn-link btn-animate text-muted"
551 this.handleTransferCommunity
553 aria-label={i18n.t("yes")}
558 class="btn btn-link btn-animate text-muted"
562 .handleCancelShowConfirmTransferCommunity
564 aria-label={i18n.t("no")}
570 {/* Admins can ban from all, and appoint other admins */}
574 (!cv.creator.banned ? (
576 class="btn btn-link btn-animate text-muted"
579 this.handleModBanShow
581 aria-label={i18n.t("ban_from_site")}
583 {i18n.t("ban_from_site")}
587 class="btn btn-link btn-animate text-muted"
590 this.handleModBanSubmit
592 aria-label={i18n.t("unban_from_site")}
594 {i18n.t("unban_from_site")}
597 {!cv.creator.banned &&
599 (!this.state.showConfirmAppointAsAdmin ? (
601 class="btn btn-link btn-animate text-muted"
604 this.handleShowConfirmAppointAsAdmin
608 ? i18n.t("remove_as_admin")
609 : i18n.t("appoint_as_admin")
613 ? i18n.t("remove_as_admin")
614 : i18n.t("appoint_as_admin")}
618 <button class="btn btn-link btn-animate text-muted">
619 {i18n.t("are_you_sure")}
622 class="btn btn-link btn-animate text-muted"
627 aria-label={i18n.t("yes")}
632 class="btn btn-link btn-animate text-muted"
635 this.handleCancelConfirmAppointAsAdmin
637 aria-label={i18n.t("no")}
645 {/* Site Creator can transfer to another admin */}
646 {this.amSiteCreator &&
649 (!this.state.showConfirmTransferSite ? (
651 class="btn btn-link btn-animate text-muted"
654 this.handleShowConfirmTransferSite
656 aria-label={i18n.t("transfer_site")}
658 {i18n.t("transfer_site")}
663 class="btn btn-link btn-animate text-muted"
664 aria-label={i18n.t("are_you_sure")}
666 {i18n.t("are_you_sure")}
669 class="btn btn-link btn-animate text-muted"
672 this.handleTransferSite
674 aria-label={i18n.t("yes")}
679 class="btn btn-link btn-animate text-muted"
682 this.handleCancelShowConfirmTransferSite
684 aria-label={i18n.t("no")}
695 {/* end of button group */}
700 {/* end of details */}
701 {this.state.showRemoveDialog && (
704 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
708 htmlFor={`mod-remove-reason-${cv.comment.id}`}
714 id={`mod-remove-reason-${cv.comment.id}`}
715 class="form-control mr-2"
716 placeholder={i18n.t("reason")}
717 value={this.state.removeReason}
718 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
722 class="btn btn-secondary"
723 aria-label={i18n.t("remove_comment")}
725 {i18n.t("remove_comment")}
729 {this.state.showBanDialog && (
730 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
731 <div class="form-group row">
733 class="col-form-label"
734 htmlFor={`mod-ban-reason-${cv.comment.id}`}
740 id={`mod-ban-reason-${cv.comment.id}`}
741 class="form-control mr-2"
742 placeholder={i18n.t("reason")}
743 value={this.state.banReason}
744 onInput={linkEvent(this, this.handleModBanReasonChange)}
746 <div class="form-group">
747 <div class="form-check">
749 class="form-check-input"
750 id="mod-ban-remove-data"
752 checked={this.state.removeData}
753 onChange={linkEvent(this, this.handleModRemoveDataChange)}
756 class="form-check-label"
757 htmlFor="mod-ban-remove-data"
758 title={i18n.t("remove_content_more")}
760 {i18n.t("remove_content")}
765 {/* TODO hold off on expires until later */}
766 {/* <div class="form-group row"> */}
767 {/* <label class="col-form-label">Expires</label> */}
768 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
770 <div class="form-group row">
773 class="btn btn-secondary"
774 aria-label={i18n.t("ban")}
776 {i18n.t("ban")} {cv.creator.name}
781 {this.state.showReply && (
784 onReplyCancel={this.handleReplyCancel}
785 disabled={this.props.locked}
789 {node.children && !this.state.collapsed && (
791 nodes={node.children}
792 locked={this.props.locked}
793 moderators={this.props.moderators}
794 admins={this.props.admins}
795 postCreatorId={this.props.postCreatorId}
796 enableDownvotes={this.props.enableDownvotes}
799 {/* A collapsed clearfix */}
800 {this.state.collapsed && <div class="row col-12"></div>}
805 get commentOrMentionRead() {
806 let cv = this.props.node.comment_view;
807 return this.isPersonMentionType(cv)
808 ? cv.person_mention.read
812 linkBtn(small = false) {
813 let cv = this.props.node.comment_view;
816 className={`btn ${small && "btn-sm"} btn-link btn-animate text-muted`}
817 to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
818 title={this.props.showContext ? i18n.t("show_context") : i18n.t("link")}
820 <Icon icon="link" classes="icon-inline" />
829 get myComment(): boolean {
831 this.props.node.comment_view.creator.id ==
832 UserService.Instance.localUserView?.person.id
836 get isMod(): boolean {
838 this.props.moderators &&
840 this.props.moderators.map(m => m.moderator.id),
841 this.props.node.comment_view.creator.id
846 get isAdmin(): boolean {
850 this.props.admins.map(a => a.person.id),
851 this.props.node.comment_view.creator.id
856 get isPostCreator(): boolean {
857 return this.props.node.comment_view.creator.id == this.props.postCreatorId;
860 get canMod(): boolean {
861 if (this.props.admins && this.props.moderators) {
862 let adminsThenMods = this.props.admins
863 .map(a => a.person.id)
864 .concat(this.props.moderators.map(m => m.moderator.id));
867 UserService.Instance.localUserView,
869 this.props.node.comment_view.creator.id
876 get canAdmin(): boolean {
880 UserService.Instance.localUserView,
881 this.props.admins.map(a => a.person.id),
882 this.props.node.comment_view.creator.id
887 get amCommunityCreator(): boolean {
889 this.props.moderators &&
890 UserService.Instance.localUserView &&
891 this.props.node.comment_view.creator.id !=
892 UserService.Instance.localUserView.person.id &&
893 UserService.Instance.localUserView.person.id ==
894 this.props.moderators[0].moderator.id
898 get amSiteCreator(): boolean {
901 UserService.Instance.localUserView &&
902 this.props.node.comment_view.creator.id !=
903 UserService.Instance.localUserView.person.id &&
904 UserService.Instance.localUserView.person.id ==
905 this.props.admins[0].person.id
909 get commentUnlessRemoved(): string {
910 let comment = this.props.node.comment_view.comment;
911 return comment.removed
912 ? `*${i18n.t("removed")}*`
914 ? `*${i18n.t("deleted")}*`
918 handleReplyClick(i: CommentNode) {
919 i.state.showReply = true;
923 handleEditClick(i: CommentNode) {
924 i.state.showEdit = true;
928 handleDeleteClick(i: CommentNode) {
929 let comment = i.props.node.comment_view.comment;
930 let deleteForm: DeleteComment = {
931 comment_id: comment.id,
932 deleted: !comment.deleted,
935 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
938 handleSaveCommentClick(i: CommentNode) {
939 let cv = i.props.node.comment_view;
940 let save = cv.saved == undefined ? true : !cv.saved;
941 let form: SaveComment = {
942 comment_id: cv.comment.id,
947 WebSocketService.Instance.send(wsClient.saveComment(form));
949 i.state.saveLoading = true;
950 i.setState(this.state);
953 handleReplyCancel() {
954 this.state.showReply = false;
955 this.state.showEdit = false;
956 this.setState(this.state);
959 handleCommentUpvote(i: CommentNodeI, event: any) {
960 event.preventDefault();
961 let new_vote = this.state.my_vote == 1 ? 0 : 1;
963 if (this.state.my_vote == 1) {
965 this.state.upvotes--;
966 } else if (this.state.my_vote == -1) {
967 this.state.downvotes--;
968 this.state.upvotes++;
969 this.state.score += 2;
971 this.state.upvotes++;
975 this.state.my_vote = new_vote;
977 let form: CreateCommentLike = {
978 comment_id: i.comment_view.comment.id,
979 score: this.state.my_vote,
983 WebSocketService.Instance.send(wsClient.likeComment(form));
984 this.setState(this.state);
988 handleCommentDownvote(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) {
993 this.state.score -= 2;
994 this.state.upvotes--;
995 this.state.downvotes++;
996 } else if (this.state.my_vote == -1) {
997 this.state.downvotes--;
1000 this.state.downvotes++;
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 handleModRemoveShow(i: CommentNode) {
1018 i.state.showRemoveDialog = true;
1019 i.setState(i.state);
1022 handleModRemoveReasonChange(i: CommentNode, event: any) {
1023 i.state.removeReason = event.target.value;
1024 i.setState(i.state);
1027 handleModRemoveDataChange(i: CommentNode, event: any) {
1028 i.state.removeData = event.target.checked;
1029 i.setState(i.state);
1032 handleModRemoveSubmit(i: CommentNode) {
1033 let comment = i.props.node.comment_view.comment;
1034 let form: RemoveComment = {
1035 comment_id: comment.id,
1036 removed: !comment.removed,
1037 reason: i.state.removeReason,
1040 WebSocketService.Instance.send(wsClient.removeComment(form));
1042 i.state.showRemoveDialog = false;
1043 i.setState(i.state);
1046 isPersonMentionType(
1047 item: CommentView | PersonMentionView
1048 ): item is PersonMentionView {
1049 return (item as PersonMentionView).person_mention?.id !== undefined;
1052 handleMarkRead(i: CommentNode) {
1053 if (i.isPersonMentionType(i.props.node.comment_view)) {
1054 let form: MarkPersonMentionAsRead = {
1055 person_mention_id: i.props.node.comment_view.person_mention.id,
1056 read: !i.props.node.comment_view.person_mention.read,
1059 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1061 let form: MarkCommentAsRead = {
1062 comment_id: i.props.node.comment_view.comment.id,
1063 read: !i.props.node.comment_view.comment.read,
1066 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
1069 i.state.readLoading = true;
1070 i.setState(this.state);
1073 handleModBanFromCommunityShow(i: CommentNode) {
1074 i.state.showBanDialog = !i.state.showBanDialog;
1075 i.state.banType = BanType.Community;
1076 i.setState(i.state);
1079 handleModBanShow(i: CommentNode) {
1080 i.state.showBanDialog = !i.state.showBanDialog;
1081 i.state.banType = BanType.Site;
1082 i.setState(i.state);
1085 handleModBanReasonChange(i: CommentNode, event: any) {
1086 i.state.banReason = event.target.value;
1087 i.setState(i.state);
1090 handleModBanExpiresChange(i: CommentNode, event: any) {
1091 i.state.banExpires = event.target.value;
1092 i.setState(i.state);
1095 handleModBanFromCommunitySubmit(i: CommentNode) {
1096 i.state.banType = BanType.Community;
1097 i.setState(i.state);
1098 i.handleModBanBothSubmit(i);
1101 handleModBanSubmit(i: CommentNode) {
1102 i.state.banType = BanType.Site;
1103 i.setState(i.state);
1104 i.handleModBanBothSubmit(i);
1107 handleModBanBothSubmit(i: CommentNode) {
1108 let cv = i.props.node.comment_view;
1110 if (i.state.banType == BanType.Community) {
1111 // If its an unban, restore all their data
1112 let ban = !cv.creator_banned_from_community;
1114 i.state.removeData = false;
1116 let form: BanFromCommunity = {
1117 person_id: cv.creator.id,
1118 community_id: cv.community.id,
1120 remove_data: i.state.removeData,
1121 reason: i.state.banReason,
1122 expires: getUnixTime(i.state.banExpires),
1125 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1127 // If its an unban, restore all their data
1128 let ban = !cv.creator.banned;
1130 i.state.removeData = false;
1132 let form: BanPerson = {
1133 person_id: cv.creator.id,
1135 remove_data: i.state.removeData,
1136 reason: i.state.banReason,
1137 expires: getUnixTime(i.state.banExpires),
1140 WebSocketService.Instance.send(wsClient.banPerson(form));
1143 i.state.showBanDialog = false;
1144 i.setState(i.state);
1147 handleShowConfirmAppointAsMod(i: CommentNode) {
1148 i.state.showConfirmAppointAsMod = true;
1149 i.setState(i.state);
1152 handleCancelConfirmAppointAsMod(i: CommentNode) {
1153 i.state.showConfirmAppointAsMod = false;
1154 i.setState(i.state);
1157 handleAddModToCommunity(i: CommentNode) {
1158 let cv = i.props.node.comment_view;
1159 let form: AddModToCommunity = {
1160 person_id: cv.creator.id,
1161 community_id: cv.community.id,
1165 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1166 i.state.showConfirmAppointAsMod = false;
1167 i.setState(i.state);
1170 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1171 i.state.showConfirmAppointAsAdmin = true;
1172 i.setState(i.state);
1175 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1176 i.state.showConfirmAppointAsAdmin = false;
1177 i.setState(i.state);
1180 handleAddAdmin(i: CommentNode) {
1181 let form: AddAdmin = {
1182 person_id: i.props.node.comment_view.creator.id,
1186 WebSocketService.Instance.send(wsClient.addAdmin(form));
1187 i.state.showConfirmAppointAsAdmin = false;
1188 i.setState(i.state);
1191 handleShowConfirmTransferCommunity(i: CommentNode) {
1192 i.state.showConfirmTransferCommunity = true;
1193 i.setState(i.state);
1196 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1197 i.state.showConfirmTransferCommunity = false;
1198 i.setState(i.state);
1201 handleTransferCommunity(i: CommentNode) {
1202 let cv = i.props.node.comment_view;
1203 let form: TransferCommunity = {
1204 community_id: cv.community.id,
1205 person_id: cv.creator.id,
1208 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1209 i.state.showConfirmTransferCommunity = false;
1210 i.setState(i.state);
1213 handleShowConfirmTransferSite(i: CommentNode) {
1214 i.state.showConfirmTransferSite = true;
1215 i.setState(i.state);
1218 handleCancelShowConfirmTransferSite(i: CommentNode) {
1219 i.state.showConfirmTransferSite = false;
1220 i.setState(i.state);
1223 handleTransferSite(i: CommentNode) {
1224 let form: TransferSite = {
1225 person_id: i.props.node.comment_view.creator.id,
1228 WebSocketService.Instance.send(wsClient.transferSite(form));
1229 i.state.showConfirmTransferSite = false;
1230 i.setState(i.state);
1233 get isCommentNew(): boolean {
1234 let now = moment.utc().subtract(10, "minutes");
1235 let then = moment.utc(this.props.node.comment_view.comment.published);
1236 return now.isBefore(then);
1239 handleCommentCollapse(i: CommentNode) {
1240 i.state.collapsed = !i.state.collapsed;
1241 i.setState(i.state);
1245 handleViewSource(i: CommentNode) {
1246 i.state.viewSource = !i.state.viewSource;
1247 i.setState(i.state);
1250 handleShowAdvanced(i: CommentNode) {
1251 i.state.showAdvanced = !i.state.showAdvanced;
1252 i.setState(i.state);
1257 if (this.state.my_vote == 1) {
1259 } else if (this.state.my_vote == -1) {
1260 return "text-danger";
1262 return "text-muted";
1266 get pointsTippy(): string {
1267 let points = i18n.t("number_of_points", {
1268 count: this.state.score,
1271 let upvotes = i18n.t("number_of_upvotes", {
1272 count: this.state.upvotes,
1275 let downvotes = i18n.t("number_of_downvotes", {
1276 count: this.state.downvotes,
1279 return `${points} • ${upvotes} • ${downvotes}`;
1282 get expandText(): string {
1283 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");