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,
21 MarkCommentReplyAsRead,
22 MarkPersonMentionAsRead,
31 } from "lemmy-js-client";
32 import moment from "moment";
33 import { i18n } from "../../i18next";
34 import { BanType, CommentViewType, PurgeType } from "../../interfaces";
35 import { UserService, WebSocketService } from "../../services";
53 import { Icon, PurgeWarning, Spinner } from "../common/icon";
54 import { MomentTime } from "../common/moment-time";
55 import { CommunityLink } from "../community/community-link";
56 import { PersonListing } from "../person/person-listing";
57 import { CommentForm } from "./comment-form";
58 import { CommentNodes } from "./comment-nodes";
60 interface CommentNodeState {
63 showRemoveDialog: boolean;
64 removeReason: Option<string>;
65 showBanDialog: boolean;
67 banReason: Option<string>;
68 banExpireDays: Option<number>;
70 showPurgeDialog: boolean;
71 purgeReason: Option<string>;
73 purgeLoading: boolean;
74 showConfirmTransferSite: boolean;
75 showConfirmTransferCommunity: boolean;
76 showConfirmAppointAsMod: boolean;
77 showConfirmAppointAsAdmin: boolean;
80 showAdvanced: boolean;
81 showReportDialog: boolean;
83 my_vote: Option<number>;
91 interface CommentNodeProps {
93 moderators: Option<CommunityModeratorView[]>;
94 admins: Option<PersonViewSafe[]>;
100 showContext?: boolean;
101 showCommunity?: boolean;
102 enableDownvotes: boolean;
103 viewType: CommentViewType;
106 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
107 private emptyState: CommentNodeState = {
110 showRemoveDialog: false,
112 showBanDialog: false,
116 banType: BanType.Community,
117 showPurgeDialog: false,
120 purgeType: PurgeType.Person,
124 showConfirmTransferSite: false,
125 showConfirmTransferCommunity: false,
126 showConfirmAppointAsMod: false,
127 showConfirmAppointAsAdmin: false,
128 showReportDialog: false,
130 my_vote: this.props.node.comment_view.my_vote,
131 score: this.props.node.comment_view.counts.score,
132 upvotes: this.props.node.comment_view.counts.upvotes,
133 downvotes: this.props.node.comment_view.counts.downvotes,
138 constructor(props: any, context: any) {
139 super(props, context);
141 this.state = this.emptyState;
142 this.handleReplyCancel = this.handleReplyCancel.bind(this);
143 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
144 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
147 // TODO see if there's a better way to do this, and all willReceiveProps
148 componentWillReceiveProps(nextProps: CommentNodeProps) {
149 let cv = nextProps.node.comment_view;
150 this.state.my_vote = cv.my_vote;
151 this.state.upvotes = cv.counts.upvotes;
152 this.state.downvotes = cv.counts.downvotes;
153 this.state.score = cv.counts.score;
154 this.state.readLoading = false;
155 this.state.saveLoading = false;
156 this.setState(this.state);
160 let node = this.props.node;
161 let cv = this.props.node.comment_view;
163 let purgeTypeText: string;
164 if (this.state.purgeType == PurgeType.Comment) {
165 purgeTypeText = i18n.t("purge_comment");
166 } else if (this.state.purgeType == PurgeType.Person) {
167 purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
170 let canMod_ = canMod(
171 this.props.moderators,
175 let canModOnSelf = canMod(
176 this.props.moderators,
179 UserService.Instance.myUserInfo,
182 let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
183 let canAdminOnSelf = canAdmin(
186 UserService.Instance.myUserInfo,
189 let isMod_ = isMod(this.props.moderators, cv.creator.id);
190 let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
191 let amCommunityCreator_ = amCommunityCreator(
192 this.props.moderators,
196 let borderColor = this.props.node.depth
197 ? colorList[(this.props.node.depth - 1) % colorList.length]
199 let moreRepliesBorderColor = this.props.node.depth
200 ? colorList[this.props.node.depth % colorList.length]
203 let showMoreChildren =
204 this.props.viewType == CommentViewType.Tree &&
205 !this.state.collapsed &&
206 node.children.length == 0 &&
207 node.comment_view.counts.child_count > 0;
211 className={`comment ${
212 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
216 id={`comment-${cv.comment.id}`}
217 className={classNames(`details comment-node py-2`, {
218 "border-top border-light": !this.props.noBorder,
221 this.props.node.comment_view.comment.distinguished,
224 !this.props.noIndent &&
225 this.props.node.depth &&
226 `border-left: 2px ${borderColor} solid !important`
230 class={`${!this.props.noIndent && this.props.node.depth && "ml-2"}`}
232 <div class="d-flex flex-wrap align-items-center text-muted small">
234 <PersonListing person={cv.creator} />
236 {cv.comment.distinguished && (
237 <Icon icon="shield" inline classes={`text-danger mr-2`} />
240 <div className="badge badge-light d-none d-sm-inline mr-2">
245 <div className="badge badge-light d-none d-sm-inline mr-2">
249 {this.isPostCreator && (
250 <div className="badge badge-light d-none d-sm-inline mr-2">
254 {cv.creator.bot_account && (
255 <div className="badge badge-light d-none d-sm-inline mr-2">
256 {i18n.t("bot_account").toLowerCase()}
259 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
260 <div className="badge badge-danger mr-2">
264 {this.props.showCommunity && (
266 <span class="mx-1">{i18n.t("to")}</span>
267 <CommunityLink community={cv.community} />
268 <span class="mx-2">•</span>
269 <Link className="mr-2" to={`/post/${cv.post.id}`}>
275 class="btn btn-sm text-muted"
276 onClick={linkEvent(this, this.handleCommentCollapse)}
277 aria-label={this.expandText}
278 data-tippy-content={this.expandText}
280 {this.state.collapsed ? (
281 <Icon icon="plus-square" classes="icon-inline" />
283 <Icon icon="minus-square" classes="icon-inline" />
287 {/* This is an expanding spacer for mobile */}
288 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
292 className={`unselectable pointer ${this.scoreColor}`}
293 onClick={this.handleCommentUpvote}
294 data-tippy-content={this.pointsTippy}
297 class="mr-1 font-weight-bold"
298 aria-label={i18n.t("number_of_points", {
299 count: this.state.score,
300 formattedCount: this.state.score,
303 {numToSI(this.state.score)}
306 <span className="mr-1">•</span>
311 published={cv.comment.published}
312 updated={cv.comment.updated}
316 {/* end of user row */}
317 {this.state.showEdit && (
321 onReplyCancel={this.handleReplyCancel}
322 disabled={this.props.locked}
326 {!this.state.showEdit && !this.state.collapsed && (
328 {this.state.viewSource ? (
329 <pre>{this.commentUnlessRemoved}</pre>
333 dangerouslySetInnerHTML={mdToHtml(
334 this.commentUnlessRemoved
338 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
339 {this.props.showContext && this.linkBtn()}
340 {this.props.markable && (
342 class="btn btn-link btn-animate text-muted"
343 onClick={linkEvent(this, this.handleMarkRead)}
345 this.commentReplyOrMentionRead
346 ? i18n.t("mark_as_unread")
347 : i18n.t("mark_as_read")
350 this.commentReplyOrMentionRead
351 ? i18n.t("mark_as_unread")
352 : i18n.t("mark_as_read")
355 {this.state.readLoading ? (
360 classes={`icon-inline ${
361 this.commentReplyOrMentionRead && "text-success"
367 {UserService.Instance.myUserInfo.isSome() &&
368 !this.props.viewOnly && (
371 className={`btn btn-link btn-animate ${
372 this.state.my_vote.unwrapOr(0) == 1
376 onClick={this.handleCommentUpvote}
377 data-tippy-content={i18n.t("upvote")}
378 aria-label={i18n.t("upvote")}
380 <Icon icon="arrow-up1" classes="icon-inline" />
382 this.state.upvotes !== this.state.score && (
384 {numToSI(this.state.upvotes)}
388 {this.props.enableDownvotes && (
390 className={`btn btn-link btn-animate ${
391 this.state.my_vote.unwrapOr(0) == -1
395 onClick={this.handleCommentDownvote}
396 data-tippy-content={i18n.t("downvote")}
397 aria-label={i18n.t("downvote")}
399 <Icon icon="arrow-down1" classes="icon-inline" />
401 this.state.upvotes !== this.state.score && (
403 {numToSI(this.state.downvotes)}
409 class="btn btn-link btn-animate text-muted"
410 onClick={linkEvent(this, this.handleReplyClick)}
411 data-tippy-content={i18n.t("reply")}
412 aria-label={i18n.t("reply")}
414 <Icon icon="reply1" classes="icon-inline" />
416 {!this.state.showAdvanced ? (
418 className="btn btn-link btn-animate text-muted"
419 onClick={linkEvent(this, this.handleShowAdvanced)}
420 data-tippy-content={i18n.t("more")}
421 aria-label={i18n.t("more")}
423 <Icon icon="more-vertical" classes="icon-inline" />
427 {!this.myComment && (
429 <button class="btn btn-link btn-animate">
431 className="text-muted"
432 to={`/create_private_message/recipient/${cv.creator.id}`}
433 title={i18n.t("message").toLowerCase()}
439 class="btn btn-link btn-animate text-muted"
442 this.handleShowReportDialog
444 data-tippy-content={i18n.t(
447 aria-label={i18n.t("show_report_dialog")}
452 class="btn btn-link btn-animate text-muted"
455 this.handleBlockUserClick
457 data-tippy-content={i18n.t("block_user")}
458 aria-label={i18n.t("block_user")}
460 <Icon icon="slash" />
465 class="btn btn-link btn-animate text-muted"
468 this.handleSaveCommentClick
471 cv.saved ? i18n.t("unsave") : i18n.t("save")
474 cv.saved ? i18n.t("unsave") : i18n.t("save")
477 {this.state.saveLoading ? (
482 classes={`icon-inline ${
483 cv.saved && "text-warning"
489 className="btn btn-link btn-animate text-muted"
490 onClick={linkEvent(this, this.handleViewSource)}
491 data-tippy-content={i18n.t("view_source")}
492 aria-label={i18n.t("view_source")}
496 classes={`icon-inline ${
497 this.state.viewSource && "text-success"
504 class="btn btn-link btn-animate text-muted"
509 data-tippy-content={i18n.t("edit")}
510 aria-label={i18n.t("edit")}
512 <Icon icon="edit" classes="icon-inline" />
515 class="btn btn-link btn-animate text-muted"
518 this.handleDeleteClick
533 classes={`icon-inline ${
534 cv.comment.deleted && "text-danger"
539 {(canModOnSelf || canAdminOnSelf) && (
541 class="btn btn-link btn-animate text-muted"
544 this.handleDistinguishClick
547 !cv.comment.distinguished
548 ? i18n.t("distinguish")
549 : i18n.t("undistinguish")
552 !cv.comment.distinguished
553 ? i18n.t("distinguish")
554 : i18n.t("undistinguish")
559 classes={`icon-inline ${
560 cv.comment.distinguished &&
568 {/* Admins and mods can remove comments */}
569 {(canMod_ || canAdmin_) && (
571 {!cv.comment.removed ? (
573 class="btn btn-link btn-animate text-muted"
576 this.handleModRemoveShow
578 aria-label={i18n.t("remove")}
584 class="btn btn-link btn-animate text-muted"
587 this.handleModRemoveSubmit
589 aria-label={i18n.t("restore")}
596 {/* Mods can ban from community, and appoint as mods to community */}
600 (!cv.creator_banned_from_community ? (
602 class="btn btn-link btn-animate text-muted"
605 this.handleModBanFromCommunityShow
607 aria-label={i18n.t("ban")}
613 class="btn btn-link btn-animate text-muted"
616 this.handleModBanFromCommunitySubmit
618 aria-label={i18n.t("unban")}
623 {!cv.creator_banned_from_community &&
624 (!this.state.showConfirmAppointAsMod ? (
626 class="btn btn-link btn-animate text-muted"
629 this.handleShowConfirmAppointAsMod
633 ? i18n.t("remove_as_mod")
634 : i18n.t("appoint_as_mod")
638 ? i18n.t("remove_as_mod")
639 : i18n.t("appoint_as_mod")}
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.handleAddModToCommunity
655 aria-label={i18n.t("yes")}
660 class="btn btn-link btn-animate text-muted"
663 this.handleCancelConfirmAppointAsMod
665 aria-label={i18n.t("no")}
673 {/* Community creators and admins can transfer community to another mod */}
674 {(amCommunityCreator_ || canAdmin_) &&
677 (!this.state.showConfirmTransferCommunity ? (
679 class="btn btn-link btn-animate text-muted"
682 this.handleShowConfirmTransferCommunity
684 aria-label={i18n.t("transfer_community")}
686 {i18n.t("transfer_community")}
691 class="btn btn-link btn-animate text-muted"
692 aria-label={i18n.t("are_you_sure")}
694 {i18n.t("are_you_sure")}
697 class="btn btn-link btn-animate text-muted"
700 this.handleTransferCommunity
702 aria-label={i18n.t("yes")}
707 class="btn btn-link btn-animate text-muted"
711 .handleCancelShowConfirmTransferCommunity
713 aria-label={i18n.t("no")}
719 {/* Admins can ban from all, and appoint other admins */}
725 class="btn btn-link btn-animate text-muted"
728 this.handlePurgePersonShow
730 aria-label={i18n.t("purge_user")}
732 {i18n.t("purge_user")}
735 class="btn btn-link btn-animate text-muted"
738 this.handlePurgeCommentShow
740 aria-label={i18n.t("purge_comment")}
742 {i18n.t("purge_comment")}
745 {!isBanned(cv.creator) ? (
747 class="btn btn-link btn-animate text-muted"
750 this.handleModBanShow
752 aria-label={i18n.t("ban_from_site")}
754 {i18n.t("ban_from_site")}
758 class="btn btn-link btn-animate text-muted"
761 this.handleModBanSubmit
763 aria-label={i18n.t("unban_from_site")}
765 {i18n.t("unban_from_site")}
770 {!isBanned(cv.creator) &&
772 (!this.state.showConfirmAppointAsAdmin ? (
774 class="btn btn-link btn-animate text-muted"
777 this.handleShowConfirmAppointAsAdmin
781 ? i18n.t("remove_as_admin")
782 : i18n.t("appoint_as_admin")
786 ? i18n.t("remove_as_admin")
787 : i18n.t("appoint_as_admin")}
791 <button class="btn btn-link btn-animate text-muted">
792 {i18n.t("are_you_sure")}
795 class="btn btn-link btn-animate text-muted"
800 aria-label={i18n.t("yes")}
805 class="btn btn-link btn-animate text-muted"
808 this.handleCancelConfirmAppointAsAdmin
810 aria-label={i18n.t("no")}
823 {/* end of button group */}
828 {showMoreChildren && (
830 className={`details ml-1 comment-node py-2 ${
831 !this.props.noBorder ? "border-top border-light" : ""
833 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
836 class="btn btn-link text-muted"
837 onClick={linkEvent(this, this.handleFetchChildren)}
839 {i18n.t("x_more_replies", {
840 count: node.comment_view.counts.child_count,
841 formattedCount: numToSI(node.comment_view.counts.child_count),
847 {/* end of details */}
848 {this.state.showRemoveDialog && (
851 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
855 htmlFor={`mod-remove-reason-${cv.comment.id}`}
861 id={`mod-remove-reason-${cv.comment.id}`}
862 class="form-control mr-2"
863 placeholder={i18n.t("reason")}
864 value={toUndefined(this.state.removeReason)}
865 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
869 class="btn btn-secondary"
870 aria-label={i18n.t("remove_comment")}
872 {i18n.t("remove_comment")}
876 {this.state.showReportDialog && (
879 onSubmit={linkEvent(this, this.handleReportSubmit)}
881 <label class="sr-only" htmlFor={`report-reason-${cv.comment.id}`}>
887 id={`report-reason-${cv.comment.id}`}
888 class="form-control mr-2"
889 placeholder={i18n.t("reason")}
890 value={this.state.reportReason}
891 onInput={linkEvent(this, this.handleReportReasonChange)}
895 class="btn btn-secondary"
896 aria-label={i18n.t("create_report")}
898 {i18n.t("create_report")}
902 {this.state.showBanDialog && (
903 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
904 <div class="form-group row col-12">
906 class="col-form-label"
907 htmlFor={`mod-ban-reason-${cv.comment.id}`}
913 id={`mod-ban-reason-${cv.comment.id}`}
914 class="form-control mr-2"
915 placeholder={i18n.t("reason")}
916 value={toUndefined(this.state.banReason)}
917 onInput={linkEvent(this, this.handleModBanReasonChange)}
920 class="col-form-label"
921 htmlFor={`mod-ban-expires-${cv.comment.id}`}
927 id={`mod-ban-expires-${cv.comment.id}`}
928 class="form-control mr-2"
929 placeholder={i18n.t("number_of_days")}
930 value={toUndefined(this.state.banExpireDays)}
931 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
933 <div class="form-group">
934 <div class="form-check">
936 class="form-check-input"
937 id="mod-ban-remove-data"
939 checked={this.state.removeData}
940 onChange={linkEvent(this, this.handleModRemoveDataChange)}
943 class="form-check-label"
944 htmlFor="mod-ban-remove-data"
945 title={i18n.t("remove_content_more")}
947 {i18n.t("remove_content")}
952 {/* TODO hold off on expires until later */}
953 {/* <div class="form-group row"> */}
954 {/* <label class="col-form-label">Expires</label> */}
955 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
957 <div class="form-group row">
960 class="btn btn-secondary"
961 aria-label={i18n.t("ban")}
963 {i18n.t("ban")} {cv.creator.name}
969 {this.state.showPurgeDialog && (
970 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
972 <label class="sr-only" htmlFor="purge-reason">
978 class="form-control my-3"
979 placeholder={i18n.t("reason")}
980 value={toUndefined(this.state.purgeReason)}
981 onInput={linkEvent(this, this.handlePurgeReasonChange)}
983 <div class="form-group row col-12">
984 {this.state.purgeLoading ? (
989 class="btn btn-secondary"
990 aria-label={purgeTypeText}
998 {this.state.showReply && (
1001 onReplyCancel={this.handleReplyCancel}
1002 disabled={this.props.locked}
1006 {!this.state.collapsed && node.children.length > 0 && (
1008 nodes={node.children}
1009 locked={this.props.locked}
1010 moderators={this.props.moderators}
1011 admins={this.props.admins}
1012 maxCommentsShown={None}
1013 enableDownvotes={this.props.enableDownvotes}
1014 viewType={this.props.viewType}
1017 {/* A collapsed clearfix */}
1018 {this.state.collapsed && <div class="row col-12"></div>}
1023 get commentReplyOrMentionRead(): boolean {
1024 let cv = this.props.node.comment_view;
1026 if (this.isPersonMentionType(cv)) {
1027 return cv.person_mention.read;
1028 } else if (this.isCommentReplyType(cv)) {
1029 return cv.comment_reply.read;
1035 linkBtn(small = false) {
1036 let cv = this.props.node.comment_view;
1037 let classnames = classNames("btn btn-link btn-animate text-muted", {
1041 let title = this.props.showContext
1042 ? i18n.t("show_context")
1048 className={classnames}
1049 to={`/comment/${cv.comment.id}`}
1052 <Icon icon="link" classes="icon-inline" />
1055 <a className={classnames} title={title} href={cv.comment.ap_id}>
1056 <Icon icon="fedilink" classes="icon-inline" />
1067 get myComment(): boolean {
1068 return UserService.Instance.myUserInfo
1071 m.local_user_view.person.id == this.props.node.comment_view.creator.id
1076 get isPostCreator(): boolean {
1078 this.props.node.comment_view.creator.id ==
1079 this.props.node.comment_view.post.creator_id
1083 get commentUnlessRemoved(): string {
1084 let comment = this.props.node.comment_view.comment;
1085 return comment.removed
1086 ? `*${i18n.t("removed")}*`
1088 ? `*${i18n.t("deleted")}*`
1092 handleReplyClick(i: CommentNode) {
1093 i.state.showReply = true;
1094 i.setState(i.state);
1097 handleEditClick(i: CommentNode) {
1098 i.state.showEdit = true;
1099 i.setState(i.state);
1102 handleBlockUserClick(i: CommentNode) {
1103 let blockUserForm = new BlockPerson({
1104 person_id: i.props.node.comment_view.creator.id,
1106 auth: auth().unwrap(),
1108 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1111 handleDeleteClick(i: CommentNode) {
1112 let comment = i.props.node.comment_view.comment;
1113 let deleteForm = new DeleteComment({
1114 comment_id: comment.id,
1115 deleted: !comment.deleted,
1116 auth: auth().unwrap(),
1118 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1121 handleSaveCommentClick(i: CommentNode) {
1122 let cv = i.props.node.comment_view;
1123 let save = cv.saved == undefined ? true : !cv.saved;
1124 let form = new SaveComment({
1125 comment_id: cv.comment.id,
1127 auth: auth().unwrap(),
1130 WebSocketService.Instance.send(wsClient.saveComment(form));
1132 i.state.saveLoading = true;
1133 i.setState(this.state);
1136 handleReplyCancel() {
1137 this.state.showReply = false;
1138 this.state.showEdit = false;
1139 this.setState(this.state);
1142 handleCommentUpvote(event: any) {
1143 event.preventDefault();
1144 let myVote = this.state.my_vote.unwrapOr(0);
1145 let newVote = myVote == 1 ? 0 : 1;
1149 this.state.upvotes--;
1150 } else if (myVote == -1) {
1151 this.state.downvotes--;
1152 this.state.upvotes++;
1153 this.state.score += 2;
1155 this.state.upvotes++;
1159 this.state.my_vote = Some(newVote);
1161 let form = new CreateCommentLike({
1162 comment_id: this.props.node.comment_view.comment.id,
1164 auth: auth().unwrap(),
1166 WebSocketService.Instance.send(wsClient.likeComment(form));
1167 this.setState(this.state);
1171 handleCommentDownvote(event: any) {
1172 event.preventDefault();
1173 let myVote = this.state.my_vote.unwrapOr(0);
1174 let newVote = myVote == -1 ? 0 : -1;
1177 this.state.score -= 2;
1178 this.state.upvotes--;
1179 this.state.downvotes++;
1180 } else if (myVote == -1) {
1181 this.state.downvotes--;
1184 this.state.downvotes++;
1188 this.state.my_vote = Some(newVote);
1190 let form = new CreateCommentLike({
1191 comment_id: this.props.node.comment_view.comment.id,
1193 auth: auth().unwrap(),
1196 WebSocketService.Instance.send(wsClient.likeComment(form));
1197 this.setState(this.state);
1201 handleShowReportDialog(i: CommentNode) {
1202 i.state.showReportDialog = !i.state.showReportDialog;
1203 i.setState(i.state);
1206 handleReportReasonChange(i: CommentNode, event: any) {
1207 i.state.reportReason = event.target.value;
1208 i.setState(i.state);
1211 handleReportSubmit(i: CommentNode) {
1212 let comment = i.props.node.comment_view.comment;
1213 let form = new CreateCommentReport({
1214 comment_id: comment.id,
1215 reason: i.state.reportReason,
1216 auth: auth().unwrap(),
1218 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1220 i.state.showReportDialog = false;
1221 i.setState(i.state);
1224 handleModRemoveShow(i: CommentNode) {
1225 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1226 i.state.showBanDialog = false;
1227 i.setState(i.state);
1230 handleModRemoveReasonChange(i: CommentNode, event: any) {
1231 i.state.removeReason = Some(event.target.value);
1232 i.setState(i.state);
1235 handleModRemoveDataChange(i: CommentNode, event: any) {
1236 i.state.removeData = event.target.checked;
1237 i.setState(i.state);
1240 handleModRemoveSubmit(i: CommentNode) {
1241 let comment = i.props.node.comment_view.comment;
1242 let form = new RemoveComment({
1243 comment_id: comment.id,
1244 removed: !comment.removed,
1245 reason: i.state.removeReason,
1246 auth: auth().unwrap(),
1248 WebSocketService.Instance.send(wsClient.removeComment(form));
1250 i.state.showRemoveDialog = false;
1251 i.setState(i.state);
1254 handleDistinguishClick(i: CommentNode) {
1255 let comment = i.props.node.comment_view.comment;
1256 let form = new EditComment({
1257 comment_id: comment.id,
1258 form_id: None, // TODO not sure about this
1260 distinguished: Some(!comment.distinguished),
1261 auth: auth().unwrap(),
1263 WebSocketService.Instance.send(wsClient.editComment(form));
1264 i.setState(i.state);
1267 isPersonMentionType(
1268 item: CommentView | PersonMentionView | CommentReplyView
1269 ): item is PersonMentionView {
1270 return (item as PersonMentionView).person_mention?.id !== undefined;
1274 item: CommentView | PersonMentionView | CommentReplyView
1275 ): item is CommentReplyView {
1276 return (item as CommentReplyView).comment_reply?.id !== undefined;
1279 handleMarkRead(i: CommentNode) {
1280 if (i.isPersonMentionType(i.props.node.comment_view)) {
1281 let form = new MarkPersonMentionAsRead({
1282 person_mention_id: i.props.node.comment_view.person_mention.id,
1283 read: !i.props.node.comment_view.person_mention.read,
1284 auth: auth().unwrap(),
1286 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1287 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1288 let form = new MarkCommentReplyAsRead({
1289 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1290 read: !i.props.node.comment_view.comment_reply.read,
1291 auth: auth().unwrap(),
1293 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1296 i.state.readLoading = true;
1297 i.setState(this.state);
1300 handleModBanFromCommunityShow(i: CommentNode) {
1301 i.state.showBanDialog = true;
1302 i.state.banType = BanType.Community;
1303 i.state.showRemoveDialog = false;
1304 i.setState(i.state);
1307 handleModBanShow(i: CommentNode) {
1308 i.state.showBanDialog = true;
1309 i.state.banType = BanType.Site;
1310 i.state.showRemoveDialog = false;
1311 i.setState(i.state);
1314 handleModBanReasonChange(i: CommentNode, event: any) {
1315 i.state.banReason = Some(event.target.value);
1316 i.setState(i.state);
1319 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1320 i.state.banExpireDays = Some(event.target.value);
1321 i.setState(i.state);
1324 handleModBanFromCommunitySubmit(i: CommentNode) {
1325 i.state.banType = BanType.Community;
1326 i.setState(i.state);
1327 i.handleModBanBothSubmit(i);
1330 handleModBanSubmit(i: CommentNode) {
1331 i.state.banType = BanType.Site;
1332 i.setState(i.state);
1333 i.handleModBanBothSubmit(i);
1336 handleModBanBothSubmit(i: CommentNode) {
1337 let cv = i.props.node.comment_view;
1339 if (i.state.banType == BanType.Community) {
1340 // If its an unban, restore all their data
1341 let ban = !cv.creator_banned_from_community;
1343 i.state.removeData = false;
1345 let form = new BanFromCommunity({
1346 person_id: cv.creator.id,
1347 community_id: cv.community.id,
1349 remove_data: Some(i.state.removeData),
1350 reason: i.state.banReason,
1351 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1352 auth: auth().unwrap(),
1354 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1356 // If its an unban, restore all their data
1357 let ban = !cv.creator.banned;
1359 i.state.removeData = false;
1361 let form = new BanPerson({
1362 person_id: cv.creator.id,
1364 remove_data: Some(i.state.removeData),
1365 reason: i.state.banReason,
1366 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1367 auth: auth().unwrap(),
1369 WebSocketService.Instance.send(wsClient.banPerson(form));
1372 i.state.showBanDialog = false;
1373 i.setState(i.state);
1376 handlePurgePersonShow(i: CommentNode) {
1377 i.state.showPurgeDialog = true;
1378 i.state.purgeType = PurgeType.Person;
1379 i.state.showRemoveDialog = false;
1380 i.setState(i.state);
1383 handlePurgeCommentShow(i: CommentNode) {
1384 i.state.showPurgeDialog = true;
1385 i.state.purgeType = PurgeType.Comment;
1386 i.state.showRemoveDialog = false;
1387 i.setState(i.state);
1390 handlePurgeReasonChange(i: CommentNode, event: any) {
1391 i.state.purgeReason = Some(event.target.value);
1392 i.setState(i.state);
1395 handlePurgeSubmit(i: CommentNode, event: any) {
1396 event.preventDefault();
1398 if (i.state.purgeType == PurgeType.Person) {
1399 let form = new PurgePerson({
1400 person_id: i.props.node.comment_view.creator.id,
1401 reason: i.state.purgeReason,
1402 auth: auth().unwrap(),
1404 WebSocketService.Instance.send(wsClient.purgePerson(form));
1405 } else if (i.state.purgeType == PurgeType.Comment) {
1406 let form = new PurgeComment({
1407 comment_id: i.props.node.comment_view.comment.id,
1408 reason: i.state.purgeReason,
1409 auth: auth().unwrap(),
1411 WebSocketService.Instance.send(wsClient.purgeComment(form));
1414 i.state.purgeLoading = true;
1415 i.setState(i.state);
1418 handleShowConfirmAppointAsMod(i: CommentNode) {
1419 i.state.showConfirmAppointAsMod = true;
1420 i.setState(i.state);
1423 handleCancelConfirmAppointAsMod(i: CommentNode) {
1424 i.state.showConfirmAppointAsMod = false;
1425 i.setState(i.state);
1428 handleAddModToCommunity(i: CommentNode) {
1429 let cv = i.props.node.comment_view;
1430 let form = new AddModToCommunity({
1431 person_id: cv.creator.id,
1432 community_id: cv.community.id,
1433 added: !isMod(i.props.moderators, cv.creator.id),
1434 auth: auth().unwrap(),
1436 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1437 i.state.showConfirmAppointAsMod = false;
1438 i.setState(i.state);
1441 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1442 i.state.showConfirmAppointAsAdmin = true;
1443 i.setState(i.state);
1446 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1447 i.state.showConfirmAppointAsAdmin = false;
1448 i.setState(i.state);
1451 handleAddAdmin(i: CommentNode) {
1452 let creatorId = i.props.node.comment_view.creator.id;
1453 let form = new AddAdmin({
1454 person_id: creatorId,
1455 added: !isAdmin(i.props.admins, creatorId),
1456 auth: auth().unwrap(),
1458 WebSocketService.Instance.send(wsClient.addAdmin(form));
1459 i.state.showConfirmAppointAsAdmin = false;
1460 i.setState(i.state);
1463 handleShowConfirmTransferCommunity(i: CommentNode) {
1464 i.state.showConfirmTransferCommunity = true;
1465 i.setState(i.state);
1468 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1469 i.state.showConfirmTransferCommunity = false;
1470 i.setState(i.state);
1473 handleTransferCommunity(i: CommentNode) {
1474 let cv = i.props.node.comment_view;
1475 let form = new TransferCommunity({
1476 community_id: cv.community.id,
1477 person_id: cv.creator.id,
1478 auth: auth().unwrap(),
1480 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1481 i.state.showConfirmTransferCommunity = false;
1482 i.setState(i.state);
1485 handleShowConfirmTransferSite(i: CommentNode) {
1486 i.state.showConfirmTransferSite = true;
1487 i.setState(i.state);
1490 handleCancelShowConfirmTransferSite(i: CommentNode) {
1491 i.state.showConfirmTransferSite = false;
1492 i.setState(i.state);
1495 get isCommentNew(): boolean {
1496 let now = moment.utc().subtract(10, "minutes");
1497 let then = moment.utc(this.props.node.comment_view.comment.published);
1498 return now.isBefore(then);
1501 handleCommentCollapse(i: CommentNode) {
1502 i.state.collapsed = !i.state.collapsed;
1503 i.setState(i.state);
1507 handleViewSource(i: CommentNode) {
1508 i.state.viewSource = !i.state.viewSource;
1509 i.setState(i.state);
1512 handleShowAdvanced(i: CommentNode) {
1513 i.state.showAdvanced = !i.state.showAdvanced;
1514 i.setState(i.state);
1518 handleFetchChildren(i: CommentNode) {
1519 let form = new GetComments({
1520 post_id: Some(i.props.node.comment_view.post.id),
1521 parent_id: Some(i.props.node.comment_view.comment.id),
1522 max_depth: Some(commentTreeMaxDepth),
1526 type_: Some(ListingType.All),
1527 community_name: None,
1529 saved_only: Some(false),
1530 auth: auth(false).ok(),
1533 WebSocketService.Instance.send(wsClient.getComments(form));
1537 if (this.state.my_vote.unwrapOr(0) == 1) {
1539 } else if (this.state.my_vote.unwrapOr(0) == -1) {
1540 return "text-danger";
1542 return "text-muted";
1546 get pointsTippy(): string {
1547 let points = i18n.t("number_of_points", {
1548 count: this.state.score,
1549 formattedCount: this.state.score,
1552 let upvotes = i18n.t("number_of_upvotes", {
1553 count: this.state.upvotes,
1554 formattedCount: this.state.upvotes,
1557 let downvotes = i18n.t("number_of_downvotes", {
1558 count: this.state.downvotes,
1559 formattedCount: this.state.downvotes,
1562 return `${points} • ${upvotes} • ${downvotes}`;
1565 get expandText(): string {
1566 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");