1 import { Left, None, Option, Some } from "@sniptt/monads";
2 import classNames from "classnames";
3 import { Component, linkEvent } from "inferno";
4 import { Link } from "inferno-router";
12 CommunityModeratorView,
17 MarkPersonMentionAsRead,
26 } from "lemmy-js-client";
27 import moment from "moment";
28 import { i18n } from "../../i18next";
31 CommentNode as CommentNodeI,
33 } from "../../interfaces";
34 import { UserService, WebSocketService } from "../../services";
51 import { Icon, PurgeWarning, Spinner } from "../common/icon";
52 import { MomentTime } from "../common/moment-time";
53 import { CommunityLink } from "../community/community-link";
54 import { PersonListing } from "../person/person-listing";
55 import { CommentForm } from "./comment-form";
56 import { CommentNodes } from "./comment-nodes";
58 interface CommentNodeState {
61 showRemoveDialog: boolean;
62 removeReason: Option<string>;
63 showBanDialog: boolean;
65 banReason: Option<string>;
66 banExpireDays: Option<number>;
68 showPurgeDialog: boolean;
69 purgeReason: Option<string>;
71 purgeLoading: boolean;
72 showConfirmTransferSite: boolean;
73 showConfirmTransferCommunity: boolean;
74 showConfirmAppointAsMod: boolean;
75 showConfirmAppointAsAdmin: boolean;
78 showAdvanced: boolean;
79 showReportDialog: boolean;
81 my_vote: Option<number>;
90 interface CommentNodeProps {
92 moderators: Option<CommunityModeratorView[]>;
93 admins: Option<PersonViewSafe[]>;
99 showContext?: boolean;
100 showCommunity?: boolean;
101 enableDownvotes: boolean;
104 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
105 private emptyState: CommentNodeState = {
108 showRemoveDialog: false,
110 showBanDialog: false,
114 banType: BanType.Community,
115 showPurgeDialog: false,
118 purgeType: PurgeType.Person,
122 showConfirmTransferSite: false,
123 showConfirmTransferCommunity: false,
124 showConfirmAppointAsMod: false,
125 showConfirmAppointAsAdmin: false,
126 showReportDialog: false,
128 my_vote: this.props.node.comment_view.my_vote,
129 score: this.props.node.comment_view.counts.score,
130 upvotes: this.props.node.comment_view.counts.upvotes,
131 downvotes: this.props.node.comment_view.counts.downvotes,
132 borderColor: this.props.node.depth
133 ? colorList[this.props.node.depth % colorList.length]
139 constructor(props: any, context: any) {
140 super(props, context);
142 this.state = this.emptyState;
143 this.handleReplyCancel = this.handleReplyCancel.bind(this);
144 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
145 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
148 // TODO see if there's a better way to do this, and all willReceiveProps
149 componentWillReceiveProps(nextProps: CommentNodeProps) {
150 let cv = nextProps.node.comment_view;
151 this.state.my_vote = cv.my_vote;
152 this.state.upvotes = cv.counts.upvotes;
153 this.state.downvotes = cv.counts.downvotes;
154 this.state.score = cv.counts.score;
155 this.state.readLoading = false;
156 this.state.saveLoading = false;
157 this.setState(this.state);
161 let node = this.props.node;
162 let cv = this.props.node.comment_view;
164 let purgeTypeText: string;
165 if (this.state.purgeType == PurgeType.Comment) {
166 purgeTypeText = i18n.t("purge_comment");
167 } else if (this.state.purgeType == PurgeType.Person) {
168 purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
171 let canMod_ = canMod(
172 this.props.moderators,
176 let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
177 let isMod_ = isMod(this.props.moderators, cv.creator.id);
178 let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
179 let amCommunityCreator_ = amCommunityCreator(
180 this.props.moderators,
186 className={`comment ${
187 cv.comment.parent_id.isSome() && !this.props.noIndent ? "ml-1" : ""
191 id={`comment-${cv.comment.id}`}
192 className={`details comment-node py-2 ${
193 !this.props.noBorder ? "border-top border-light" : ""
194 } ${this.isCommentNew ? "mark" : ""}`}
196 !this.props.noIndent &&
197 cv.comment.parent_id.isSome() &&
198 `border-left: 2px ${this.state.borderColor} solid !important`
203 !this.props.noIndent && cv.comment.parent_id.isSome() && "ml-2"
206 <div class="d-flex flex-wrap align-items-center text-muted small">
208 <PersonListing person={cv.creator} />
212 <div className="badge badge-light d-none d-sm-inline mr-2">
217 <div className="badge badge-light d-none d-sm-inline mr-2">
221 {this.isPostCreator && (
222 <div className="badge badge-light d-none d-sm-inline mr-2">
226 {cv.creator.bot_account && (
227 <div className="badge badge-light d-none d-sm-inline mr-2">
228 {i18n.t("bot_account").toLowerCase()}
231 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
232 <div className="badge badge-danger mr-2">
236 {this.props.showCommunity && (
238 <span class="mx-1">{i18n.t("to")}</span>
239 <CommunityLink community={cv.community} />
240 <span class="mx-2">•</span>
241 <Link className="mr-2" to={`/post/${cv.post.id}`}>
247 class="btn btn-sm text-muted"
248 onClick={linkEvent(this, this.handleCommentCollapse)}
249 aria-label={this.expandText}
250 data-tippy-content={this.expandText}
252 {this.state.collapsed ? (
253 <Icon icon="plus-square" classes="icon-inline" />
255 <Icon icon="minus-square" classes="icon-inline" />
259 {/* This is an expanding spacer for mobile */}
260 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
264 className={`unselectable pointer ${this.scoreColor}`}
265 onClick={linkEvent(node, this.handleCommentUpvote)}
266 data-tippy-content={this.pointsTippy}
269 class="mr-1 font-weight-bold"
270 aria-label={i18n.t("number_of_points", {
271 count: this.state.score,
272 formattedCount: this.state.score,
275 {numToSI(this.state.score)}
278 <span className="mr-1">•</span>
283 published={cv.comment.published}
284 updated={cv.comment.updated}
288 {/* end of user row */}
289 {this.state.showEdit && (
293 onReplyCancel={this.handleReplyCancel}
294 disabled={this.props.locked}
298 {!this.state.showEdit && !this.state.collapsed && (
300 {this.state.viewSource ? (
301 <pre>{this.commentUnlessRemoved}</pre>
305 dangerouslySetInnerHTML={mdToHtml(
306 this.commentUnlessRemoved
310 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
311 {this.props.showContext && this.linkBtn()}
312 {this.props.markable && (
314 class="btn btn-link btn-animate text-muted"
315 onClick={linkEvent(this, this.handleMarkRead)}
317 this.commentOrMentionRead
318 ? i18n.t("mark_as_unread")
319 : i18n.t("mark_as_read")
322 this.commentOrMentionRead
323 ? i18n.t("mark_as_unread")
324 : i18n.t("mark_as_read")
327 {this.state.readLoading ? (
332 classes={`icon-inline ${
333 this.commentOrMentionRead && "text-success"
339 {UserService.Instance.myUserInfo.isSome() &&
340 !this.props.viewOnly && (
343 className={`btn btn-link btn-animate ${
344 this.state.my_vote.unwrapOr(0) == 1
348 onClick={linkEvent(node, this.handleCommentUpvote)}
349 data-tippy-content={i18n.t("upvote")}
350 aria-label={i18n.t("upvote")}
352 <Icon icon="arrow-up1" classes="icon-inline" />
354 this.state.upvotes !== this.state.score && (
356 {numToSI(this.state.upvotes)}
360 {this.props.enableDownvotes && (
362 className={`btn btn-link btn-animate ${
363 this.state.my_vote.unwrapOr(0) == -1
369 this.handleCommentDownvote
371 data-tippy-content={i18n.t("downvote")}
372 aria-label={i18n.t("downvote")}
374 <Icon icon="arrow-down1" classes="icon-inline" />
376 this.state.upvotes !== this.state.score && (
378 {numToSI(this.state.downvotes)}
384 class="btn btn-link btn-animate text-muted"
385 onClick={linkEvent(this, this.handleReplyClick)}
386 data-tippy-content={i18n.t("reply")}
387 aria-label={i18n.t("reply")}
389 <Icon icon="reply1" classes="icon-inline" />
391 {!this.state.showAdvanced ? (
393 className="btn btn-link btn-animate text-muted"
394 onClick={linkEvent(this, this.handleShowAdvanced)}
395 data-tippy-content={i18n.t("more")}
396 aria-label={i18n.t("more")}
398 <Icon icon="more-vertical" classes="icon-inline" />
402 {!this.myComment && (
404 <button class="btn btn-link btn-animate">
406 className="text-muted"
407 to={`/create_private_message/recipient/${cv.creator.id}`}
408 title={i18n.t("message").toLowerCase()}
414 class="btn btn-link btn-animate text-muted"
417 this.handleShowReportDialog
419 data-tippy-content={i18n.t(
422 aria-label={i18n.t("show_report_dialog")}
427 class="btn btn-link btn-animate text-muted"
430 this.handleBlockUserClick
432 data-tippy-content={i18n.t("block_user")}
433 aria-label={i18n.t("block_user")}
435 <Icon icon="slash" />
440 class="btn btn-link btn-animate text-muted"
443 this.handleSaveCommentClick
446 cv.saved ? i18n.t("unsave") : i18n.t("save")
449 cv.saved ? i18n.t("unsave") : i18n.t("save")
452 {this.state.saveLoading ? (
457 classes={`icon-inline ${
458 cv.saved && "text-warning"
464 className="btn btn-link btn-animate text-muted"
465 onClick={linkEvent(this, this.handleViewSource)}
466 data-tippy-content={i18n.t("view_source")}
467 aria-label={i18n.t("view_source")}
471 classes={`icon-inline ${
472 this.state.viewSource && "text-success"
479 class="btn btn-link btn-animate text-muted"
484 data-tippy-content={i18n.t("edit")}
485 aria-label={i18n.t("edit")}
487 <Icon icon="edit" classes="icon-inline" />
490 class="btn btn-link btn-animate text-muted"
493 this.handleDeleteClick
508 classes={`icon-inline ${
509 cv.comment.deleted && "text-danger"
515 {/* Admins and mods can remove comments */}
516 {(canMod_ || canAdmin_) && (
518 {!cv.comment.removed ? (
520 class="btn btn-link btn-animate text-muted"
523 this.handleModRemoveShow
525 aria-label={i18n.t("remove")}
531 class="btn btn-link btn-animate text-muted"
534 this.handleModRemoveSubmit
536 aria-label={i18n.t("restore")}
543 {/* Mods can ban from community, and appoint as mods to community */}
547 (!cv.creator_banned_from_community ? (
549 class="btn btn-link btn-animate text-muted"
552 this.handleModBanFromCommunityShow
554 aria-label={i18n.t("ban")}
560 class="btn btn-link btn-animate text-muted"
563 this.handleModBanFromCommunitySubmit
565 aria-label={i18n.t("unban")}
570 {!cv.creator_banned_from_community &&
571 (!this.state.showConfirmAppointAsMod ? (
573 class="btn btn-link btn-animate text-muted"
576 this.handleShowConfirmAppointAsMod
580 ? i18n.t("remove_as_mod")
581 : i18n.t("appoint_as_mod")
585 ? i18n.t("remove_as_mod")
586 : i18n.t("appoint_as_mod")}
591 class="btn btn-link btn-animate text-muted"
592 aria-label={i18n.t("are_you_sure")}
594 {i18n.t("are_you_sure")}
597 class="btn btn-link btn-animate text-muted"
600 this.handleAddModToCommunity
602 aria-label={i18n.t("yes")}
607 class="btn btn-link btn-animate text-muted"
610 this.handleCancelConfirmAppointAsMod
612 aria-label={i18n.t("no")}
620 {/* Community creators and admins can transfer community to another mod */}
621 {(amCommunityCreator_ || canAdmin_) &&
624 (!this.state.showConfirmTransferCommunity ? (
626 class="btn btn-link btn-animate text-muted"
629 this.handleShowConfirmTransferCommunity
631 aria-label={i18n.t("transfer_community")}
633 {i18n.t("transfer_community")}
638 class="btn btn-link btn-animate text-muted"
639 aria-label={i18n.t("are_you_sure")}
641 {i18n.t("are_you_sure")}
644 class="btn btn-link btn-animate text-muted"
647 this.handleTransferCommunity
649 aria-label={i18n.t("yes")}
654 class="btn btn-link btn-animate text-muted"
658 .handleCancelShowConfirmTransferCommunity
660 aria-label={i18n.t("no")}
666 {/* Admins can ban from all, and appoint other admins */}
672 class="btn btn-link btn-animate text-muted"
675 this.handlePurgePersonShow
677 aria-label={i18n.t("purge_user")}
679 {i18n.t("purge_user")}
682 class="btn btn-link btn-animate text-muted"
685 this.handlePurgeCommentShow
687 aria-label={i18n.t("purge_comment")}
689 {i18n.t("purge_comment")}
692 {!isBanned(cv.creator) ? (
694 class="btn btn-link btn-animate text-muted"
697 this.handleModBanShow
699 aria-label={i18n.t("ban_from_site")}
701 {i18n.t("ban_from_site")}
705 class="btn btn-link btn-animate text-muted"
708 this.handleModBanSubmit
710 aria-label={i18n.t("unban_from_site")}
712 {i18n.t("unban_from_site")}
717 {!isBanned(cv.creator) &&
719 (!this.state.showConfirmAppointAsAdmin ? (
721 class="btn btn-link btn-animate text-muted"
724 this.handleShowConfirmAppointAsAdmin
728 ? i18n.t("remove_as_admin")
729 : i18n.t("appoint_as_admin")
733 ? i18n.t("remove_as_admin")
734 : i18n.t("appoint_as_admin")}
738 <button class="btn btn-link btn-animate text-muted">
739 {i18n.t("are_you_sure")}
742 class="btn btn-link btn-animate text-muted"
747 aria-label={i18n.t("yes")}
752 class="btn btn-link btn-animate text-muted"
755 this.handleCancelConfirmAppointAsAdmin
757 aria-label={i18n.t("no")}
770 {/* end of button group */}
775 {/* end of details */}
776 {this.state.showRemoveDialog && (
779 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
783 htmlFor={`mod-remove-reason-${cv.comment.id}`}
789 id={`mod-remove-reason-${cv.comment.id}`}
790 class="form-control mr-2"
791 placeholder={i18n.t("reason")}
792 value={toUndefined(this.state.removeReason)}
793 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
797 class="btn btn-secondary"
798 aria-label={i18n.t("remove_comment")}
800 {i18n.t("remove_comment")}
804 {this.state.showReportDialog && (
807 onSubmit={linkEvent(this, this.handleReportSubmit)}
809 <label class="sr-only" htmlFor={`report-reason-${cv.comment.id}`}>
815 id={`report-reason-${cv.comment.id}`}
816 class="form-control mr-2"
817 placeholder={i18n.t("reason")}
818 value={this.state.reportReason}
819 onInput={linkEvent(this, this.handleReportReasonChange)}
823 class="btn btn-secondary"
824 aria-label={i18n.t("create_report")}
826 {i18n.t("create_report")}
830 {this.state.showBanDialog && (
831 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
832 <div class="form-group row col-12">
834 class="col-form-label"
835 htmlFor={`mod-ban-reason-${cv.comment.id}`}
841 id={`mod-ban-reason-${cv.comment.id}`}
842 class="form-control mr-2"
843 placeholder={i18n.t("reason")}
844 value={toUndefined(this.state.banReason)}
845 onInput={linkEvent(this, this.handleModBanReasonChange)}
848 class="col-form-label"
849 htmlFor={`mod-ban-expires-${cv.comment.id}`}
855 id={`mod-ban-expires-${cv.comment.id}`}
856 class="form-control mr-2"
857 placeholder={i18n.t("number_of_days")}
858 value={toUndefined(this.state.banExpireDays)}
859 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
861 <div class="form-group">
862 <div class="form-check">
864 class="form-check-input"
865 id="mod-ban-remove-data"
867 checked={this.state.removeData}
868 onChange={linkEvent(this, this.handleModRemoveDataChange)}
871 class="form-check-label"
872 htmlFor="mod-ban-remove-data"
873 title={i18n.t("remove_content_more")}
875 {i18n.t("remove_content")}
880 {/* TODO hold off on expires until later */}
881 {/* <div class="form-group row"> */}
882 {/* <label class="col-form-label">Expires</label> */}
883 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
885 <div class="form-group row">
888 class="btn btn-secondary"
889 aria-label={i18n.t("ban")}
891 {i18n.t("ban")} {cv.creator.name}
897 {this.state.showPurgeDialog && (
898 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
900 <label class="sr-only" htmlFor="purge-reason">
906 class="form-control my-3"
907 placeholder={i18n.t("reason")}
908 value={toUndefined(this.state.purgeReason)}
909 onInput={linkEvent(this, this.handlePurgeReasonChange)}
911 <div class="form-group row col-12">
912 {this.state.purgeLoading ? (
917 class="btn btn-secondary"
918 aria-label={purgeTypeText}
926 {this.state.showReply && (
929 onReplyCancel={this.handleReplyCancel}
930 disabled={this.props.locked}
934 {!this.state.collapsed && node.children && (
936 nodes={node.children}
937 locked={this.props.locked}
938 moderators={this.props.moderators}
939 admins={this.props.admins}
940 maxCommentsShown={None}
941 enableDownvotes={this.props.enableDownvotes}
944 {/* A collapsed clearfix */}
945 {this.state.collapsed && <div class="row col-12"></div>}
950 get commentOrMentionRead() {
951 let cv = this.props.node.comment_view;
952 return this.isPersonMentionType(cv)
953 ? cv.person_mention.read
957 linkBtn(small = false) {
958 let cv = this.props.node.comment_view;
959 let classnames = classNames("btn btn-link btn-animate text-muted", {
963 let title = this.props.showContext
964 ? i18n.t("show_context")
970 className={classnames}
971 to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
974 <Icon icon="link" classes="icon-inline" />
977 <a className={classnames} title={title} href={cv.comment.ap_id}>
978 <Icon icon="fedilink" classes="icon-inline" />
989 get myComment(): boolean {
990 return UserService.Instance.myUserInfo
993 m.local_user_view.person.id == this.props.node.comment_view.creator.id
998 get isPostCreator(): boolean {
1000 this.props.node.comment_view.creator.id ==
1001 this.props.node.comment_view.post.creator_id
1005 get commentUnlessRemoved(): string {
1006 let comment = this.props.node.comment_view.comment;
1007 return comment.removed
1008 ? `*${i18n.t("removed")}*`
1010 ? `*${i18n.t("deleted")}*`
1014 handleReplyClick(i: CommentNode) {
1015 i.state.showReply = true;
1016 i.setState(i.state);
1019 handleEditClick(i: CommentNode) {
1020 i.state.showEdit = true;
1021 i.setState(i.state);
1024 handleBlockUserClick(i: CommentNode) {
1025 let blockUserForm = new BlockPerson({
1026 person_id: i.props.node.comment_view.creator.id,
1028 auth: auth().unwrap(),
1030 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1033 handleDeleteClick(i: CommentNode) {
1034 let comment = i.props.node.comment_view.comment;
1035 let deleteForm = new DeleteComment({
1036 comment_id: comment.id,
1037 deleted: !comment.deleted,
1038 auth: auth().unwrap(),
1040 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1043 handleSaveCommentClick(i: CommentNode) {
1044 let cv = i.props.node.comment_view;
1045 let save = cv.saved == undefined ? true : !cv.saved;
1046 let form = new SaveComment({
1047 comment_id: cv.comment.id,
1049 auth: auth().unwrap(),
1052 WebSocketService.Instance.send(wsClient.saveComment(form));
1054 i.state.saveLoading = true;
1055 i.setState(this.state);
1058 handleReplyCancel() {
1059 this.state.showReply = false;
1060 this.state.showEdit = false;
1061 this.setState(this.state);
1064 handleCommentUpvote(i: CommentNodeI, event: any) {
1065 event.preventDefault();
1066 let myVote = this.state.my_vote.unwrapOr(0);
1067 let newVote = myVote == 1 ? 0 : 1;
1071 this.state.upvotes--;
1072 } else if (myVote == -1) {
1073 this.state.downvotes--;
1074 this.state.upvotes++;
1075 this.state.score += 2;
1077 this.state.upvotes++;
1081 this.state.my_vote = Some(newVote);
1083 let form = new CreateCommentLike({
1084 comment_id: i.comment_view.comment.id,
1086 auth: auth().unwrap(),
1089 WebSocketService.Instance.send(wsClient.likeComment(form));
1090 this.setState(this.state);
1094 handleCommentDownvote(i: CommentNodeI, event: any) {
1095 event.preventDefault();
1096 let myVote = this.state.my_vote.unwrapOr(0);
1097 let newVote = myVote == -1 ? 0 : -1;
1100 this.state.score -= 2;
1101 this.state.upvotes--;
1102 this.state.downvotes++;
1103 } else if (myVote == -1) {
1104 this.state.downvotes--;
1107 this.state.downvotes++;
1111 this.state.my_vote = Some(newVote);
1113 let form = new CreateCommentLike({
1114 comment_id: i.comment_view.comment.id,
1116 auth: auth().unwrap(),
1119 WebSocketService.Instance.send(wsClient.likeComment(form));
1120 this.setState(this.state);
1124 handleShowReportDialog(i: CommentNode) {
1125 i.state.showReportDialog = !i.state.showReportDialog;
1126 i.setState(i.state);
1129 handleReportReasonChange(i: CommentNode, event: any) {
1130 i.state.reportReason = event.target.value;
1131 i.setState(i.state);
1134 handleReportSubmit(i: CommentNode) {
1135 let comment = i.props.node.comment_view.comment;
1136 let form = new CreateCommentReport({
1137 comment_id: comment.id,
1138 reason: i.state.reportReason,
1139 auth: auth().unwrap(),
1141 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1143 i.state.showReportDialog = false;
1144 i.setState(i.state);
1147 handleModRemoveShow(i: CommentNode) {
1148 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1149 i.state.showBanDialog = false;
1150 i.setState(i.state);
1153 handleModRemoveReasonChange(i: CommentNode, event: any) {
1154 i.state.removeReason = Some(event.target.value);
1155 i.setState(i.state);
1158 handleModRemoveDataChange(i: CommentNode, event: any) {
1159 i.state.removeData = event.target.checked;
1160 i.setState(i.state);
1163 handleModRemoveSubmit(i: CommentNode) {
1164 let comment = i.props.node.comment_view.comment;
1165 let form = new RemoveComment({
1166 comment_id: comment.id,
1167 removed: !comment.removed,
1168 reason: i.state.removeReason,
1169 auth: auth().unwrap(),
1171 WebSocketService.Instance.send(wsClient.removeComment(form));
1173 i.state.showRemoveDialog = false;
1174 i.setState(i.state);
1177 isPersonMentionType(
1178 item: CommentView | PersonMentionView
1179 ): item is PersonMentionView {
1180 return (item as PersonMentionView).person_mention?.id !== undefined;
1183 handleMarkRead(i: CommentNode) {
1184 if (i.isPersonMentionType(i.props.node.comment_view)) {
1185 let form = new MarkPersonMentionAsRead({
1186 person_mention_id: i.props.node.comment_view.person_mention.id,
1187 read: !i.props.node.comment_view.person_mention.read,
1188 auth: auth().unwrap(),
1190 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1192 let form = new MarkCommentAsRead({
1193 comment_id: i.props.node.comment_view.comment.id,
1194 read: !i.props.node.comment_view.comment.read,
1195 auth: auth().unwrap(),
1197 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
1200 i.state.readLoading = true;
1201 i.setState(this.state);
1204 handleModBanFromCommunityShow(i: CommentNode) {
1205 i.state.showBanDialog = true;
1206 i.state.banType = BanType.Community;
1207 i.state.showRemoveDialog = false;
1208 i.setState(i.state);
1211 handleModBanShow(i: CommentNode) {
1212 i.state.showBanDialog = true;
1213 i.state.banType = BanType.Site;
1214 i.state.showRemoveDialog = false;
1215 i.setState(i.state);
1218 handleModBanReasonChange(i: CommentNode, event: any) {
1219 i.state.banReason = Some(event.target.value);
1220 i.setState(i.state);
1223 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1224 i.state.banExpireDays = Some(event.target.value);
1225 i.setState(i.state);
1228 handleModBanFromCommunitySubmit(i: CommentNode) {
1229 i.state.banType = BanType.Community;
1230 i.setState(i.state);
1231 i.handleModBanBothSubmit(i);
1234 handleModBanSubmit(i: CommentNode) {
1235 i.state.banType = BanType.Site;
1236 i.setState(i.state);
1237 i.handleModBanBothSubmit(i);
1240 handleModBanBothSubmit(i: CommentNode) {
1241 let cv = i.props.node.comment_view;
1243 if (i.state.banType == BanType.Community) {
1244 // If its an unban, restore all their data
1245 let ban = !cv.creator_banned_from_community;
1247 i.state.removeData = false;
1249 let form = new BanFromCommunity({
1250 person_id: cv.creator.id,
1251 community_id: cv.community.id,
1253 remove_data: Some(i.state.removeData),
1254 reason: i.state.banReason,
1255 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1256 auth: auth().unwrap(),
1258 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1260 // If its an unban, restore all their data
1261 let ban = !cv.creator.banned;
1263 i.state.removeData = false;
1265 let form = new BanPerson({
1266 person_id: cv.creator.id,
1268 remove_data: Some(i.state.removeData),
1269 reason: i.state.banReason,
1270 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1271 auth: auth().unwrap(),
1273 WebSocketService.Instance.send(wsClient.banPerson(form));
1276 i.state.showBanDialog = false;
1277 i.setState(i.state);
1280 handlePurgePersonShow(i: CommentNode) {
1281 i.state.showPurgeDialog = true;
1282 i.state.purgeType = PurgeType.Person;
1283 i.state.showRemoveDialog = false;
1284 i.setState(i.state);
1287 handlePurgeCommentShow(i: CommentNode) {
1288 i.state.showPurgeDialog = true;
1289 i.state.purgeType = PurgeType.Comment;
1290 i.state.showRemoveDialog = false;
1291 i.setState(i.state);
1294 handlePurgeReasonChange(i: CommentNode, event: any) {
1295 i.state.purgeReason = Some(event.target.value);
1296 i.setState(i.state);
1299 handlePurgeSubmit(i: CommentNode, event: any) {
1300 event.preventDefault();
1302 if (i.state.purgeType == PurgeType.Person) {
1303 let form = new PurgePerson({
1304 person_id: i.props.node.comment_view.creator.id,
1305 reason: i.state.purgeReason,
1306 auth: auth().unwrap(),
1308 WebSocketService.Instance.send(wsClient.purgePerson(form));
1309 } else if (i.state.purgeType == PurgeType.Comment) {
1310 let form = new PurgeComment({
1311 comment_id: i.props.node.comment_view.comment.id,
1312 reason: i.state.purgeReason,
1313 auth: auth().unwrap(),
1315 WebSocketService.Instance.send(wsClient.purgeComment(form));
1318 i.state.purgeLoading = true;
1319 i.setState(i.state);
1322 handleShowConfirmAppointAsMod(i: CommentNode) {
1323 i.state.showConfirmAppointAsMod = true;
1324 i.setState(i.state);
1327 handleCancelConfirmAppointAsMod(i: CommentNode) {
1328 i.state.showConfirmAppointAsMod = false;
1329 i.setState(i.state);
1332 handleAddModToCommunity(i: CommentNode) {
1333 let cv = i.props.node.comment_view;
1334 let form = new AddModToCommunity({
1335 person_id: cv.creator.id,
1336 community_id: cv.community.id,
1337 added: !isMod(i.props.moderators, cv.creator.id),
1338 auth: auth().unwrap(),
1340 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1341 i.state.showConfirmAppointAsMod = false;
1342 i.setState(i.state);
1345 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1346 i.state.showConfirmAppointAsAdmin = true;
1347 i.setState(i.state);
1350 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1351 i.state.showConfirmAppointAsAdmin = false;
1352 i.setState(i.state);
1355 handleAddAdmin(i: CommentNode) {
1356 let creatorId = i.props.node.comment_view.creator.id;
1357 let form = new AddAdmin({
1358 person_id: creatorId,
1359 added: !isAdmin(i.props.admins, creatorId),
1360 auth: auth().unwrap(),
1362 WebSocketService.Instance.send(wsClient.addAdmin(form));
1363 i.state.showConfirmAppointAsAdmin = false;
1364 i.setState(i.state);
1367 handleShowConfirmTransferCommunity(i: CommentNode) {
1368 i.state.showConfirmTransferCommunity = true;
1369 i.setState(i.state);
1372 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1373 i.state.showConfirmTransferCommunity = false;
1374 i.setState(i.state);
1377 handleTransferCommunity(i: CommentNode) {
1378 let cv = i.props.node.comment_view;
1379 let form = new TransferCommunity({
1380 community_id: cv.community.id,
1381 person_id: cv.creator.id,
1382 auth: auth().unwrap(),
1384 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1385 i.state.showConfirmTransferCommunity = false;
1386 i.setState(i.state);
1389 handleShowConfirmTransferSite(i: CommentNode) {
1390 i.state.showConfirmTransferSite = true;
1391 i.setState(i.state);
1394 handleCancelShowConfirmTransferSite(i: CommentNode) {
1395 i.state.showConfirmTransferSite = false;
1396 i.setState(i.state);
1399 get isCommentNew(): boolean {
1400 let now = moment.utc().subtract(10, "minutes");
1401 let then = moment.utc(this.props.node.comment_view.comment.published);
1402 return now.isBefore(then);
1405 handleCommentCollapse(i: CommentNode) {
1406 i.state.collapsed = !i.state.collapsed;
1407 i.setState(i.state);
1411 handleViewSource(i: CommentNode) {
1412 i.state.viewSource = !i.state.viewSource;
1413 i.setState(i.state);
1416 handleShowAdvanced(i: CommentNode) {
1417 i.state.showAdvanced = !i.state.showAdvanced;
1418 i.setState(i.state);
1423 if (this.state.my_vote.unwrapOr(0) == 1) {
1425 } else if (this.state.my_vote.unwrapOr(0) == -1) {
1426 return "text-danger";
1428 return "text-muted";
1432 get pointsTippy(): string {
1433 let points = i18n.t("number_of_points", {
1434 count: this.state.score,
1435 formattedCount: this.state.score,
1438 let upvotes = i18n.t("number_of_upvotes", {
1439 count: this.state.upvotes,
1440 formattedCount: this.state.upvotes,
1443 let downvotes = i18n.t("number_of_downvotes", {
1444 count: this.state.downvotes,
1445 formattedCount: this.state.downvotes,
1448 return `${points} • ${upvotes} • ${downvotes}`;
1451 get expandText(): string {
1452 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");