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";
11 CommentNode as CommentNodeI,
14 CommunityModeratorView,
20 MarkCommentReplyAsRead,
21 MarkPersonMentionAsRead,
30 } from "lemmy-js-client";
31 import moment from "moment";
32 import { i18n } from "../../i18next";
33 import { BanType, CommentViewType, PurgeType } from "../../interfaces";
34 import { UserService, WebSocketService } from "../../services";
52 import { Icon, PurgeWarning, Spinner } from "../common/icon";
53 import { MomentTime } from "../common/moment-time";
54 import { CommunityLink } from "../community/community-link";
55 import { PersonListing } from "../person/person-listing";
56 import { CommentForm } from "./comment-form";
57 import { CommentNodes } from "./comment-nodes";
59 interface CommentNodeState {
62 showRemoveDialog: boolean;
63 removeReason: Option<string>;
64 showBanDialog: boolean;
66 banReason: Option<string>;
67 banExpireDays: Option<number>;
69 showPurgeDialog: boolean;
70 purgeReason: Option<string>;
72 purgeLoading: boolean;
73 showConfirmTransferSite: boolean;
74 showConfirmTransferCommunity: boolean;
75 showConfirmAppointAsMod: boolean;
76 showConfirmAppointAsAdmin: boolean;
79 showAdvanced: boolean;
80 showReportDialog: boolean;
82 my_vote: Option<number>;
90 interface CommentNodeProps {
92 moderators: Option<CommunityModeratorView[]>;
93 admins: Option<PersonViewSafe[]>;
99 showContext?: boolean;
100 showCommunity?: boolean;
101 enableDownvotes: boolean;
102 viewType: CommentViewType;
105 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
106 private emptyState: CommentNodeState = {
109 showRemoveDialog: false,
111 showBanDialog: false,
115 banType: BanType.Community,
116 showPurgeDialog: false,
119 purgeType: PurgeType.Person,
123 showConfirmTransferSite: false,
124 showConfirmTransferCommunity: false,
125 showConfirmAppointAsMod: false,
126 showConfirmAppointAsAdmin: false,
127 showReportDialog: false,
129 my_vote: this.props.node.comment_view.my_vote,
130 score: this.props.node.comment_view.counts.score,
131 upvotes: this.props.node.comment_view.counts.upvotes,
132 downvotes: this.props.node.comment_view.counts.downvotes,
137 constructor(props: any, context: any) {
138 super(props, context);
140 this.state = this.emptyState;
141 this.handleReplyCancel = this.handleReplyCancel.bind(this);
142 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
143 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
146 // TODO see if there's a better way to do this, and all willReceiveProps
147 componentWillReceiveProps(nextProps: CommentNodeProps) {
148 let cv = nextProps.node.comment_view;
149 this.state.my_vote = cv.my_vote;
150 this.state.upvotes = cv.counts.upvotes;
151 this.state.downvotes = cv.counts.downvotes;
152 this.state.score = cv.counts.score;
153 this.state.readLoading = false;
154 this.state.saveLoading = false;
155 this.setState(this.state);
159 let node = this.props.node;
160 let cv = this.props.node.comment_view;
162 let purgeTypeText: string;
163 if (this.state.purgeType == PurgeType.Comment) {
164 purgeTypeText = i18n.t("purge_comment");
165 } else if (this.state.purgeType == PurgeType.Person) {
166 purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
169 let canMod_ = canMod(
170 this.props.moderators,
174 let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
175 let isMod_ = isMod(this.props.moderators, cv.creator.id);
176 let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
177 let amCommunityCreator_ = amCommunityCreator(
178 this.props.moderators,
182 let borderColor = this.props.node.depth
183 ? colorList[(this.props.node.depth - 1) % colorList.length]
185 let moreRepliesBorderColor = this.props.node.depth
186 ? colorList[this.props.node.depth % colorList.length]
189 let showMoreChildren =
190 this.props.viewType == CommentViewType.Tree &&
191 !this.state.collapsed &&
192 node.children.length == 0 &&
193 node.comment_view.counts.child_count > 0;
197 className={`comment ${
198 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
202 id={`comment-${cv.comment.id}`}
203 className={`details comment-node py-2 ${
204 !this.props.noBorder ? "border-top border-light" : ""
205 } ${this.isCommentNew ? "mark" : ""}`}
207 !this.props.noIndent &&
208 this.props.node.depth &&
209 `border-left: 2px ${borderColor} solid !important`
213 class={`${!this.props.noIndent && this.props.node.depth && "ml-2"}`}
215 <div class="d-flex flex-wrap align-items-center text-muted small">
217 <PersonListing person={cv.creator} />
221 <div className="badge badge-light d-none d-sm-inline mr-2">
226 <div className="badge badge-light d-none d-sm-inline mr-2">
230 {this.isPostCreator && (
231 <div className="badge badge-light d-none d-sm-inline mr-2">
235 {cv.creator.bot_account && (
236 <div className="badge badge-light d-none d-sm-inline mr-2">
237 {i18n.t("bot_account").toLowerCase()}
240 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
241 <div className="badge badge-danger mr-2">
245 {this.props.showCommunity && (
247 <span class="mx-1">{i18n.t("to")}</span>
248 <CommunityLink community={cv.community} />
249 <span class="mx-2">•</span>
250 <Link className="mr-2" to={`/post/${cv.post.id}`}>
256 class="btn btn-sm text-muted"
257 onClick={linkEvent(this, this.handleCommentCollapse)}
258 aria-label={this.expandText}
259 data-tippy-content={this.expandText}
261 {this.state.collapsed ? (
262 <Icon icon="plus-square" classes="icon-inline" />
264 <Icon icon="minus-square" classes="icon-inline" />
268 {/* This is an expanding spacer for mobile */}
269 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
273 className={`unselectable pointer ${this.scoreColor}`}
274 onClick={this.handleCommentUpvote}
275 data-tippy-content={this.pointsTippy}
278 class="mr-1 font-weight-bold"
279 aria-label={i18n.t("number_of_points", {
280 count: this.state.score,
281 formattedCount: this.state.score,
284 {numToSI(this.state.score)}
287 <span className="mr-1">•</span>
292 published={cv.comment.published}
293 updated={cv.comment.updated}
297 {/* end of user row */}
298 {this.state.showEdit && (
302 onReplyCancel={this.handleReplyCancel}
303 disabled={this.props.locked}
307 {!this.state.showEdit && !this.state.collapsed && (
309 {this.state.viewSource ? (
310 <pre>{this.commentUnlessRemoved}</pre>
314 dangerouslySetInnerHTML={mdToHtml(
315 this.commentUnlessRemoved
319 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
320 {this.props.showContext && this.linkBtn()}
321 {this.props.markable && (
323 class="btn btn-link btn-animate text-muted"
324 onClick={linkEvent(this, this.handleMarkRead)}
326 this.commentReplyOrMentionRead
327 ? i18n.t("mark_as_unread")
328 : i18n.t("mark_as_read")
331 this.commentReplyOrMentionRead
332 ? i18n.t("mark_as_unread")
333 : i18n.t("mark_as_read")
336 {this.state.readLoading ? (
341 classes={`icon-inline ${
342 this.commentReplyOrMentionRead && "text-success"
348 {UserService.Instance.myUserInfo.isSome() &&
349 !this.props.viewOnly && (
352 className={`btn btn-link btn-animate ${
353 this.state.my_vote.unwrapOr(0) == 1
357 onClick={this.handleCommentUpvote}
358 data-tippy-content={i18n.t("upvote")}
359 aria-label={i18n.t("upvote")}
361 <Icon icon="arrow-up1" classes="icon-inline" />
363 this.state.upvotes !== this.state.score && (
365 {numToSI(this.state.upvotes)}
369 {this.props.enableDownvotes && (
371 className={`btn btn-link btn-animate ${
372 this.state.my_vote.unwrapOr(0) == -1
376 onClick={this.handleCommentDownvote}
377 data-tippy-content={i18n.t("downvote")}
378 aria-label={i18n.t("downvote")}
380 <Icon icon="arrow-down1" classes="icon-inline" />
382 this.state.upvotes !== this.state.score && (
384 {numToSI(this.state.downvotes)}
390 class="btn btn-link btn-animate text-muted"
391 onClick={linkEvent(this, this.handleReplyClick)}
392 data-tippy-content={i18n.t("reply")}
393 aria-label={i18n.t("reply")}
395 <Icon icon="reply1" classes="icon-inline" />
397 {!this.state.showAdvanced ? (
399 className="btn btn-link btn-animate text-muted"
400 onClick={linkEvent(this, this.handleShowAdvanced)}
401 data-tippy-content={i18n.t("more")}
402 aria-label={i18n.t("more")}
404 <Icon icon="more-vertical" classes="icon-inline" />
408 {!this.myComment && (
410 <button class="btn btn-link btn-animate">
412 className="text-muted"
413 to={`/create_private_message/recipient/${cv.creator.id}`}
414 title={i18n.t("message").toLowerCase()}
420 class="btn btn-link btn-animate text-muted"
423 this.handleShowReportDialog
425 data-tippy-content={i18n.t(
428 aria-label={i18n.t("show_report_dialog")}
433 class="btn btn-link btn-animate text-muted"
436 this.handleBlockUserClick
438 data-tippy-content={i18n.t("block_user")}
439 aria-label={i18n.t("block_user")}
441 <Icon icon="slash" />
446 class="btn btn-link btn-animate text-muted"
449 this.handleSaveCommentClick
452 cv.saved ? i18n.t("unsave") : i18n.t("save")
455 cv.saved ? i18n.t("unsave") : i18n.t("save")
458 {this.state.saveLoading ? (
463 classes={`icon-inline ${
464 cv.saved && "text-warning"
470 className="btn btn-link btn-animate text-muted"
471 onClick={linkEvent(this, this.handleViewSource)}
472 data-tippy-content={i18n.t("view_source")}
473 aria-label={i18n.t("view_source")}
477 classes={`icon-inline ${
478 this.state.viewSource && "text-success"
485 class="btn btn-link btn-animate text-muted"
490 data-tippy-content={i18n.t("edit")}
491 aria-label={i18n.t("edit")}
493 <Icon icon="edit" classes="icon-inline" />
496 class="btn btn-link btn-animate text-muted"
499 this.handleDeleteClick
514 classes={`icon-inline ${
515 cv.comment.deleted && "text-danger"
521 {/* Admins and mods can remove comments */}
522 {(canMod_ || canAdmin_) && (
524 {!cv.comment.removed ? (
526 class="btn btn-link btn-animate text-muted"
529 this.handleModRemoveShow
531 aria-label={i18n.t("remove")}
537 class="btn btn-link btn-animate text-muted"
540 this.handleModRemoveSubmit
542 aria-label={i18n.t("restore")}
549 {/* Mods can ban from community, and appoint as mods to community */}
553 (!cv.creator_banned_from_community ? (
555 class="btn btn-link btn-animate text-muted"
558 this.handleModBanFromCommunityShow
560 aria-label={i18n.t("ban")}
566 class="btn btn-link btn-animate text-muted"
569 this.handleModBanFromCommunitySubmit
571 aria-label={i18n.t("unban")}
576 {!cv.creator_banned_from_community &&
577 (!this.state.showConfirmAppointAsMod ? (
579 class="btn btn-link btn-animate text-muted"
582 this.handleShowConfirmAppointAsMod
586 ? i18n.t("remove_as_mod")
587 : i18n.t("appoint_as_mod")
591 ? i18n.t("remove_as_mod")
592 : i18n.t("appoint_as_mod")}
597 class="btn btn-link btn-animate text-muted"
598 aria-label={i18n.t("are_you_sure")}
600 {i18n.t("are_you_sure")}
603 class="btn btn-link btn-animate text-muted"
606 this.handleAddModToCommunity
608 aria-label={i18n.t("yes")}
613 class="btn btn-link btn-animate text-muted"
616 this.handleCancelConfirmAppointAsMod
618 aria-label={i18n.t("no")}
626 {/* Community creators and admins can transfer community to another mod */}
627 {(amCommunityCreator_ || canAdmin_) &&
630 (!this.state.showConfirmTransferCommunity ? (
632 class="btn btn-link btn-animate text-muted"
635 this.handleShowConfirmTransferCommunity
637 aria-label={i18n.t("transfer_community")}
639 {i18n.t("transfer_community")}
644 class="btn btn-link btn-animate text-muted"
645 aria-label={i18n.t("are_you_sure")}
647 {i18n.t("are_you_sure")}
650 class="btn btn-link btn-animate text-muted"
653 this.handleTransferCommunity
655 aria-label={i18n.t("yes")}
660 class="btn btn-link btn-animate text-muted"
664 .handleCancelShowConfirmTransferCommunity
666 aria-label={i18n.t("no")}
672 {/* Admins can ban from all, and appoint other admins */}
678 class="btn btn-link btn-animate text-muted"
681 this.handlePurgePersonShow
683 aria-label={i18n.t("purge_user")}
685 {i18n.t("purge_user")}
688 class="btn btn-link btn-animate text-muted"
691 this.handlePurgeCommentShow
693 aria-label={i18n.t("purge_comment")}
695 {i18n.t("purge_comment")}
698 {!isBanned(cv.creator) ? (
700 class="btn btn-link btn-animate text-muted"
703 this.handleModBanShow
705 aria-label={i18n.t("ban_from_site")}
707 {i18n.t("ban_from_site")}
711 class="btn btn-link btn-animate text-muted"
714 this.handleModBanSubmit
716 aria-label={i18n.t("unban_from_site")}
718 {i18n.t("unban_from_site")}
723 {!isBanned(cv.creator) &&
725 (!this.state.showConfirmAppointAsAdmin ? (
727 class="btn btn-link btn-animate text-muted"
730 this.handleShowConfirmAppointAsAdmin
734 ? i18n.t("remove_as_admin")
735 : i18n.t("appoint_as_admin")
739 ? i18n.t("remove_as_admin")
740 : i18n.t("appoint_as_admin")}
744 <button class="btn btn-link btn-animate text-muted">
745 {i18n.t("are_you_sure")}
748 class="btn btn-link btn-animate text-muted"
753 aria-label={i18n.t("yes")}
758 class="btn btn-link btn-animate text-muted"
761 this.handleCancelConfirmAppointAsAdmin
763 aria-label={i18n.t("no")}
776 {/* end of button group */}
781 {showMoreChildren && (
783 className={`details ml-1 comment-node py-2 ${
784 !this.props.noBorder ? "border-top border-light" : ""
786 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
789 class="btn btn-link text-muted"
790 onClick={linkEvent(this, this.handleFetchChildren)}
792 {i18n.t("x_more_replies", {
793 count: node.comment_view.counts.child_count,
794 formattedCount: numToSI(node.comment_view.counts.child_count),
800 {/* end of details */}
801 {this.state.showRemoveDialog && (
804 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
808 htmlFor={`mod-remove-reason-${cv.comment.id}`}
814 id={`mod-remove-reason-${cv.comment.id}`}
815 class="form-control mr-2"
816 placeholder={i18n.t("reason")}
817 value={toUndefined(this.state.removeReason)}
818 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
822 class="btn btn-secondary"
823 aria-label={i18n.t("remove_comment")}
825 {i18n.t("remove_comment")}
829 {this.state.showReportDialog && (
832 onSubmit={linkEvent(this, this.handleReportSubmit)}
834 <label class="sr-only" htmlFor={`report-reason-${cv.comment.id}`}>
840 id={`report-reason-${cv.comment.id}`}
841 class="form-control mr-2"
842 placeholder={i18n.t("reason")}
843 value={this.state.reportReason}
844 onInput={linkEvent(this, this.handleReportReasonChange)}
848 class="btn btn-secondary"
849 aria-label={i18n.t("create_report")}
851 {i18n.t("create_report")}
855 {this.state.showBanDialog && (
856 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
857 <div class="form-group row col-12">
859 class="col-form-label"
860 htmlFor={`mod-ban-reason-${cv.comment.id}`}
866 id={`mod-ban-reason-${cv.comment.id}`}
867 class="form-control mr-2"
868 placeholder={i18n.t("reason")}
869 value={toUndefined(this.state.banReason)}
870 onInput={linkEvent(this, this.handleModBanReasonChange)}
873 class="col-form-label"
874 htmlFor={`mod-ban-expires-${cv.comment.id}`}
880 id={`mod-ban-expires-${cv.comment.id}`}
881 class="form-control mr-2"
882 placeholder={i18n.t("number_of_days")}
883 value={toUndefined(this.state.banExpireDays)}
884 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
886 <div class="form-group">
887 <div class="form-check">
889 class="form-check-input"
890 id="mod-ban-remove-data"
892 checked={this.state.removeData}
893 onChange={linkEvent(this, this.handleModRemoveDataChange)}
896 class="form-check-label"
897 htmlFor="mod-ban-remove-data"
898 title={i18n.t("remove_content_more")}
900 {i18n.t("remove_content")}
905 {/* TODO hold off on expires until later */}
906 {/* <div class="form-group row"> */}
907 {/* <label class="col-form-label">Expires</label> */}
908 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
910 <div class="form-group row">
913 class="btn btn-secondary"
914 aria-label={i18n.t("ban")}
916 {i18n.t("ban")} {cv.creator.name}
922 {this.state.showPurgeDialog && (
923 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
925 <label class="sr-only" htmlFor="purge-reason">
931 class="form-control my-3"
932 placeholder={i18n.t("reason")}
933 value={toUndefined(this.state.purgeReason)}
934 onInput={linkEvent(this, this.handlePurgeReasonChange)}
936 <div class="form-group row col-12">
937 {this.state.purgeLoading ? (
942 class="btn btn-secondary"
943 aria-label={purgeTypeText}
951 {this.state.showReply && (
954 onReplyCancel={this.handleReplyCancel}
955 disabled={this.props.locked}
959 {!this.state.collapsed && node.children.length > 0 && (
961 nodes={node.children}
962 locked={this.props.locked}
963 moderators={this.props.moderators}
964 admins={this.props.admins}
965 maxCommentsShown={None}
966 enableDownvotes={this.props.enableDownvotes}
967 viewType={this.props.viewType}
970 {/* A collapsed clearfix */}
971 {this.state.collapsed && <div class="row col-12"></div>}
976 get commentReplyOrMentionRead(): boolean {
977 let cv = this.props.node.comment_view;
979 if (this.isPersonMentionType(cv)) {
980 return cv.person_mention.read;
981 } else if (this.isCommentReplyType(cv)) {
982 return cv.comment_reply.read;
988 linkBtn(small = false) {
989 let cv = this.props.node.comment_view;
990 let classnames = classNames("btn btn-link btn-animate text-muted", {
994 let title = this.props.showContext
995 ? i18n.t("show_context")
1001 className={classnames}
1002 to={`/comment/${cv.comment.id}`}
1005 <Icon icon="link" classes="icon-inline" />
1008 <a className={classnames} title={title} href={cv.comment.ap_id}>
1009 <Icon icon="fedilink" classes="icon-inline" />
1020 get myComment(): boolean {
1021 return UserService.Instance.myUserInfo
1024 m.local_user_view.person.id == this.props.node.comment_view.creator.id
1029 get isPostCreator(): boolean {
1031 this.props.node.comment_view.creator.id ==
1032 this.props.node.comment_view.post.creator_id
1036 get commentUnlessRemoved(): string {
1037 let comment = this.props.node.comment_view.comment;
1038 return comment.removed
1039 ? `*${i18n.t("removed")}*`
1041 ? `*${i18n.t("deleted")}*`
1045 handleReplyClick(i: CommentNode) {
1046 i.state.showReply = true;
1047 i.setState(i.state);
1050 handleEditClick(i: CommentNode) {
1051 i.state.showEdit = true;
1052 i.setState(i.state);
1055 handleBlockUserClick(i: CommentNode) {
1056 let blockUserForm = new BlockPerson({
1057 person_id: i.props.node.comment_view.creator.id,
1059 auth: auth().unwrap(),
1061 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1064 handleDeleteClick(i: CommentNode) {
1065 let comment = i.props.node.comment_view.comment;
1066 let deleteForm = new DeleteComment({
1067 comment_id: comment.id,
1068 deleted: !comment.deleted,
1069 auth: auth().unwrap(),
1071 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1074 handleSaveCommentClick(i: CommentNode) {
1075 let cv = i.props.node.comment_view;
1076 let save = cv.saved == undefined ? true : !cv.saved;
1077 let form = new SaveComment({
1078 comment_id: cv.comment.id,
1080 auth: auth().unwrap(),
1083 WebSocketService.Instance.send(wsClient.saveComment(form));
1085 i.state.saveLoading = true;
1086 i.setState(this.state);
1089 handleReplyCancel() {
1090 this.state.showReply = false;
1091 this.state.showEdit = false;
1092 this.setState(this.state);
1095 handleCommentUpvote(event: any) {
1096 event.preventDefault();
1097 let myVote = this.state.my_vote.unwrapOr(0);
1098 let newVote = myVote == 1 ? 0 : 1;
1102 this.state.upvotes--;
1103 } else if (myVote == -1) {
1104 this.state.downvotes--;
1105 this.state.upvotes++;
1106 this.state.score += 2;
1108 this.state.upvotes++;
1112 this.state.my_vote = Some(newVote);
1114 let form = new CreateCommentLike({
1115 comment_id: this.props.node.comment_view.comment.id,
1117 auth: auth().unwrap(),
1119 WebSocketService.Instance.send(wsClient.likeComment(form));
1120 this.setState(this.state);
1124 handleCommentDownvote(event: any) {
1125 event.preventDefault();
1126 let myVote = this.state.my_vote.unwrapOr(0);
1127 let newVote = myVote == -1 ? 0 : -1;
1130 this.state.score -= 2;
1131 this.state.upvotes--;
1132 this.state.downvotes++;
1133 } else if (myVote == -1) {
1134 this.state.downvotes--;
1137 this.state.downvotes++;
1141 this.state.my_vote = Some(newVote);
1143 let form = new CreateCommentLike({
1144 comment_id: this.props.node.comment_view.comment.id,
1146 auth: auth().unwrap(),
1149 WebSocketService.Instance.send(wsClient.likeComment(form));
1150 this.setState(this.state);
1154 handleShowReportDialog(i: CommentNode) {
1155 i.state.showReportDialog = !i.state.showReportDialog;
1156 i.setState(i.state);
1159 handleReportReasonChange(i: CommentNode, event: any) {
1160 i.state.reportReason = event.target.value;
1161 i.setState(i.state);
1164 handleReportSubmit(i: CommentNode) {
1165 let comment = i.props.node.comment_view.comment;
1166 let form = new CreateCommentReport({
1167 comment_id: comment.id,
1168 reason: i.state.reportReason,
1169 auth: auth().unwrap(),
1171 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1173 i.state.showReportDialog = false;
1174 i.setState(i.state);
1177 handleModRemoveShow(i: CommentNode) {
1178 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1179 i.state.showBanDialog = false;
1180 i.setState(i.state);
1183 handleModRemoveReasonChange(i: CommentNode, event: any) {
1184 i.state.removeReason = Some(event.target.value);
1185 i.setState(i.state);
1188 handleModRemoveDataChange(i: CommentNode, event: any) {
1189 i.state.removeData = event.target.checked;
1190 i.setState(i.state);
1193 handleModRemoveSubmit(i: CommentNode) {
1194 let comment = i.props.node.comment_view.comment;
1195 let form = new RemoveComment({
1196 comment_id: comment.id,
1197 removed: !comment.removed,
1198 reason: i.state.removeReason,
1199 auth: auth().unwrap(),
1201 WebSocketService.Instance.send(wsClient.removeComment(form));
1203 i.state.showRemoveDialog = false;
1204 i.setState(i.state);
1207 isPersonMentionType(
1208 item: CommentView | PersonMentionView | CommentReplyView
1209 ): item is PersonMentionView {
1210 return (item as PersonMentionView).person_mention?.id !== undefined;
1214 item: CommentView | PersonMentionView | CommentReplyView
1215 ): item is CommentReplyView {
1216 return (item as CommentReplyView).comment_reply?.id !== undefined;
1219 handleMarkRead(i: CommentNode) {
1220 if (i.isPersonMentionType(i.props.node.comment_view)) {
1221 let form = new MarkPersonMentionAsRead({
1222 person_mention_id: i.props.node.comment_view.person_mention.id,
1223 read: !i.props.node.comment_view.person_mention.read,
1224 auth: auth().unwrap(),
1226 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1227 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1228 let form = new MarkCommentReplyAsRead({
1229 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1230 read: !i.props.node.comment_view.comment_reply.read,
1231 auth: auth().unwrap(),
1233 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1236 i.state.readLoading = true;
1237 i.setState(this.state);
1240 handleModBanFromCommunityShow(i: CommentNode) {
1241 i.state.showBanDialog = true;
1242 i.state.banType = BanType.Community;
1243 i.state.showRemoveDialog = false;
1244 i.setState(i.state);
1247 handleModBanShow(i: CommentNode) {
1248 i.state.showBanDialog = true;
1249 i.state.banType = BanType.Site;
1250 i.state.showRemoveDialog = false;
1251 i.setState(i.state);
1254 handleModBanReasonChange(i: CommentNode, event: any) {
1255 i.state.banReason = Some(event.target.value);
1256 i.setState(i.state);
1259 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1260 i.state.banExpireDays = Some(event.target.value);
1261 i.setState(i.state);
1264 handleModBanFromCommunitySubmit(i: CommentNode) {
1265 i.state.banType = BanType.Community;
1266 i.setState(i.state);
1267 i.handleModBanBothSubmit(i);
1270 handleModBanSubmit(i: CommentNode) {
1271 i.state.banType = BanType.Site;
1272 i.setState(i.state);
1273 i.handleModBanBothSubmit(i);
1276 handleModBanBothSubmit(i: CommentNode) {
1277 let cv = i.props.node.comment_view;
1279 if (i.state.banType == BanType.Community) {
1280 // If its an unban, restore all their data
1281 let ban = !cv.creator_banned_from_community;
1283 i.state.removeData = false;
1285 let form = new BanFromCommunity({
1286 person_id: cv.creator.id,
1287 community_id: cv.community.id,
1289 remove_data: Some(i.state.removeData),
1290 reason: i.state.banReason,
1291 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1292 auth: auth().unwrap(),
1294 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1296 // If its an unban, restore all their data
1297 let ban = !cv.creator.banned;
1299 i.state.removeData = false;
1301 let form = new BanPerson({
1302 person_id: cv.creator.id,
1304 remove_data: Some(i.state.removeData),
1305 reason: i.state.banReason,
1306 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1307 auth: auth().unwrap(),
1309 WebSocketService.Instance.send(wsClient.banPerson(form));
1312 i.state.showBanDialog = false;
1313 i.setState(i.state);
1316 handlePurgePersonShow(i: CommentNode) {
1317 i.state.showPurgeDialog = true;
1318 i.state.purgeType = PurgeType.Person;
1319 i.state.showRemoveDialog = false;
1320 i.setState(i.state);
1323 handlePurgeCommentShow(i: CommentNode) {
1324 i.state.showPurgeDialog = true;
1325 i.state.purgeType = PurgeType.Comment;
1326 i.state.showRemoveDialog = false;
1327 i.setState(i.state);
1330 handlePurgeReasonChange(i: CommentNode, event: any) {
1331 i.state.purgeReason = Some(event.target.value);
1332 i.setState(i.state);
1335 handlePurgeSubmit(i: CommentNode, event: any) {
1336 event.preventDefault();
1338 if (i.state.purgeType == PurgeType.Person) {
1339 let form = new PurgePerson({
1340 person_id: i.props.node.comment_view.creator.id,
1341 reason: i.state.purgeReason,
1342 auth: auth().unwrap(),
1344 WebSocketService.Instance.send(wsClient.purgePerson(form));
1345 } else if (i.state.purgeType == PurgeType.Comment) {
1346 let form = new PurgeComment({
1347 comment_id: i.props.node.comment_view.comment.id,
1348 reason: i.state.purgeReason,
1349 auth: auth().unwrap(),
1351 WebSocketService.Instance.send(wsClient.purgeComment(form));
1354 i.state.purgeLoading = true;
1355 i.setState(i.state);
1358 handleShowConfirmAppointAsMod(i: CommentNode) {
1359 i.state.showConfirmAppointAsMod = true;
1360 i.setState(i.state);
1363 handleCancelConfirmAppointAsMod(i: CommentNode) {
1364 i.state.showConfirmAppointAsMod = false;
1365 i.setState(i.state);
1368 handleAddModToCommunity(i: CommentNode) {
1369 let cv = i.props.node.comment_view;
1370 let form = new AddModToCommunity({
1371 person_id: cv.creator.id,
1372 community_id: cv.community.id,
1373 added: !isMod(i.props.moderators, cv.creator.id),
1374 auth: auth().unwrap(),
1376 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1377 i.state.showConfirmAppointAsMod = false;
1378 i.setState(i.state);
1381 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1382 i.state.showConfirmAppointAsAdmin = true;
1383 i.setState(i.state);
1386 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1387 i.state.showConfirmAppointAsAdmin = false;
1388 i.setState(i.state);
1391 handleAddAdmin(i: CommentNode) {
1392 let creatorId = i.props.node.comment_view.creator.id;
1393 let form = new AddAdmin({
1394 person_id: creatorId,
1395 added: !isAdmin(i.props.admins, creatorId),
1396 auth: auth().unwrap(),
1398 WebSocketService.Instance.send(wsClient.addAdmin(form));
1399 i.state.showConfirmAppointAsAdmin = false;
1400 i.setState(i.state);
1403 handleShowConfirmTransferCommunity(i: CommentNode) {
1404 i.state.showConfirmTransferCommunity = true;
1405 i.setState(i.state);
1408 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1409 i.state.showConfirmTransferCommunity = false;
1410 i.setState(i.state);
1413 handleTransferCommunity(i: CommentNode) {
1414 let cv = i.props.node.comment_view;
1415 let form = new TransferCommunity({
1416 community_id: cv.community.id,
1417 person_id: cv.creator.id,
1418 auth: auth().unwrap(),
1420 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1421 i.state.showConfirmTransferCommunity = false;
1422 i.setState(i.state);
1425 handleShowConfirmTransferSite(i: CommentNode) {
1426 i.state.showConfirmTransferSite = true;
1427 i.setState(i.state);
1430 handleCancelShowConfirmTransferSite(i: CommentNode) {
1431 i.state.showConfirmTransferSite = false;
1432 i.setState(i.state);
1435 get isCommentNew(): boolean {
1436 let now = moment.utc().subtract(10, "minutes");
1437 let then = moment.utc(this.props.node.comment_view.comment.published);
1438 return now.isBefore(then);
1441 handleCommentCollapse(i: CommentNode) {
1442 i.state.collapsed = !i.state.collapsed;
1443 i.setState(i.state);
1447 handleViewSource(i: CommentNode) {
1448 i.state.viewSource = !i.state.viewSource;
1449 i.setState(i.state);
1452 handleShowAdvanced(i: CommentNode) {
1453 i.state.showAdvanced = !i.state.showAdvanced;
1454 i.setState(i.state);
1458 handleFetchChildren(i: CommentNode) {
1459 let form = new GetComments({
1460 post_id: Some(i.props.node.comment_view.post.id),
1461 parent_id: Some(i.props.node.comment_view.comment.id),
1462 max_depth: Some(commentTreeMaxDepth),
1466 type_: Some(ListingType.All),
1467 community_name: None,
1469 saved_only: Some(false),
1470 auth: auth(false).ok(),
1473 WebSocketService.Instance.send(wsClient.getComments(form));
1477 if (this.state.my_vote.unwrapOr(0) == 1) {
1479 } else if (this.state.my_vote.unwrapOr(0) == -1) {
1480 return "text-danger";
1482 return "text-muted";
1486 get pointsTippy(): string {
1487 let points = i18n.t("number_of_points", {
1488 count: this.state.score,
1489 formattedCount: this.state.score,
1492 let upvotes = i18n.t("number_of_upvotes", {
1493 count: this.state.upvotes,
1494 formattedCount: this.state.upvotes,
1497 let downvotes = i18n.t("number_of_downvotes", {
1498 count: this.state.downvotes,
1499 formattedCount: this.state.downvotes,
1502 return `${points} • ${upvotes} • ${downvotes}`;
1505 get expandText(): string {
1506 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");