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;
152 upvotes: cv.counts.upvotes,
153 downvotes: cv.counts.downvotes,
154 score: cv.counts.score,
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 canModOnSelf = canMod(
177 this.props.moderators,
180 UserService.Instance.myUserInfo,
183 let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
184 let canAdminOnSelf = canAdmin(
187 UserService.Instance.myUserInfo,
190 let isMod_ = isMod(this.props.moderators, cv.creator.id);
191 let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
192 let amCommunityCreator_ = amCommunityCreator(
193 this.props.moderators,
197 let borderColor = this.props.node.depth
198 ? colorList[(this.props.node.depth - 1) % colorList.length]
200 let moreRepliesBorderColor = this.props.node.depth
201 ? colorList[this.props.node.depth % colorList.length]
204 let showMoreChildren =
205 this.props.viewType == CommentViewType.Tree &&
206 !this.state.collapsed &&
207 node.children.length == 0 &&
208 node.comment_view.counts.child_count > 0;
212 className={`comment ${
213 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
217 id={`comment-${cv.comment.id}`}
218 className={classNames(`details comment-node py-2`, {
219 "border-top border-light": !this.props.noBorder,
222 this.props.node.comment_view.comment.distinguished,
225 !this.props.noIndent &&
226 this.props.node.depth &&
227 `border-left: 2px ${borderColor} solid !important`
232 !this.props.noIndent && this.props.node.depth && "ml-2"
235 <div className="d-flex flex-wrap align-items-center text-muted small">
236 <span className="mr-2">
237 <PersonListing person={cv.creator} />
239 {cv.comment.distinguished && (
240 <Icon icon="shield" inline classes={`text-danger mr-2`} />
243 <div className="badge badge-light d-none d-sm-inline mr-2">
248 <div className="badge badge-light d-none d-sm-inline mr-2">
252 {this.isPostCreator && (
253 <div className="badge badge-light d-none d-sm-inline mr-2">
257 {cv.creator.bot_account && (
258 <div className="badge badge-light d-none d-sm-inline mr-2">
259 {i18n.t("bot_account").toLowerCase()}
262 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
263 <div className="badge badge-danger mr-2">
267 {this.props.showCommunity && (
269 <span className="mx-1">{i18n.t("to")}</span>
270 <CommunityLink community={cv.community} />
271 <span className="mx-2">•</span>
272 <Link className="mr-2" to={`/post/${cv.post.id}`}>
278 className="btn btn-sm text-muted"
279 onClick={linkEvent(this, this.handleCommentCollapse)}
280 aria-label={this.expandText}
281 data-tippy-content={this.expandText}
283 {this.state.collapsed ? (
284 <Icon icon="plus-square" classes="icon-inline" />
286 <Icon icon="minus-square" classes="icon-inline" />
290 {/* This is an expanding spacer for mobile */}
291 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
295 className={`unselectable pointer ${this.scoreColor}`}
296 onClick={this.handleCommentUpvote}
297 data-tippy-content={this.pointsTippy}
300 className="mr-1 font-weight-bold"
301 aria-label={i18n.t("number_of_points", {
302 count: this.state.score,
303 formattedCount: this.state.score,
306 {numToSI(this.state.score)}
309 <span className="mr-1">•</span>
314 published={cv.comment.published}
315 updated={cv.comment.updated}
319 {/* end of user row */}
320 {this.state.showEdit && (
324 onReplyCancel={this.handleReplyCancel}
325 disabled={this.props.locked}
329 {!this.state.showEdit && !this.state.collapsed && (
331 {this.state.viewSource ? (
332 <pre>{this.commentUnlessRemoved}</pre>
336 dangerouslySetInnerHTML={mdToHtml(
337 this.commentUnlessRemoved
341 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
342 {this.props.showContext && this.linkBtn()}
343 {this.props.markable && (
345 className="btn btn-link btn-animate text-muted"
346 onClick={linkEvent(this, this.handleMarkRead)}
348 this.commentReplyOrMentionRead
349 ? i18n.t("mark_as_unread")
350 : i18n.t("mark_as_read")
353 this.commentReplyOrMentionRead
354 ? i18n.t("mark_as_unread")
355 : i18n.t("mark_as_read")
358 {this.state.readLoading ? (
363 classes={`icon-inline ${
364 this.commentReplyOrMentionRead && "text-success"
370 {UserService.Instance.myUserInfo.isSome() &&
371 !this.props.viewOnly && (
374 className={`btn btn-link btn-animate ${
375 this.state.my_vote.unwrapOr(0) == 1
379 onClick={this.handleCommentUpvote}
380 data-tippy-content={i18n.t("upvote")}
381 aria-label={i18n.t("upvote")}
383 <Icon icon="arrow-up1" classes="icon-inline" />
385 this.state.upvotes !== this.state.score && (
386 <span className="ml-1">
387 {numToSI(this.state.upvotes)}
391 {this.props.enableDownvotes && (
393 className={`btn btn-link btn-animate ${
394 this.state.my_vote.unwrapOr(0) == -1
398 onClick={this.handleCommentDownvote}
399 data-tippy-content={i18n.t("downvote")}
400 aria-label={i18n.t("downvote")}
402 <Icon icon="arrow-down1" classes="icon-inline" />
404 this.state.upvotes !== this.state.score && (
405 <span className="ml-1">
406 {numToSI(this.state.downvotes)}
412 className="btn btn-link btn-animate text-muted"
413 onClick={linkEvent(this, this.handleReplyClick)}
414 data-tippy-content={i18n.t("reply")}
415 aria-label={i18n.t("reply")}
417 <Icon icon="reply1" classes="icon-inline" />
419 {!this.state.showAdvanced ? (
421 className="btn btn-link btn-animate text-muted"
422 onClick={linkEvent(this, this.handleShowAdvanced)}
423 data-tippy-content={i18n.t("more")}
424 aria-label={i18n.t("more")}
426 <Icon icon="more-vertical" classes="icon-inline" />
430 {!this.myComment && (
432 <button className="btn btn-link btn-animate">
434 className="text-muted"
435 to={`/create_private_message/recipient/${cv.creator.id}`}
436 title={i18n.t("message").toLowerCase()}
442 className="btn btn-link btn-animate text-muted"
445 this.handleShowReportDialog
447 data-tippy-content={i18n.t(
450 aria-label={i18n.t("show_report_dialog")}
455 className="btn btn-link btn-animate text-muted"
458 this.handleBlockUserClick
460 data-tippy-content={i18n.t("block_user")}
461 aria-label={i18n.t("block_user")}
463 <Icon icon="slash" />
468 className="btn btn-link btn-animate text-muted"
471 this.handleSaveCommentClick
474 cv.saved ? i18n.t("unsave") : i18n.t("save")
477 cv.saved ? i18n.t("unsave") : i18n.t("save")
480 {this.state.saveLoading ? (
485 classes={`icon-inline ${
486 cv.saved && "text-warning"
492 className="btn btn-link btn-animate text-muted"
493 onClick={linkEvent(this, this.handleViewSource)}
494 data-tippy-content={i18n.t("view_source")}
495 aria-label={i18n.t("view_source")}
499 classes={`icon-inline ${
500 this.state.viewSource && "text-success"
507 className="btn btn-link btn-animate text-muted"
512 data-tippy-content={i18n.t("edit")}
513 aria-label={i18n.t("edit")}
515 <Icon icon="edit" classes="icon-inline" />
518 className="btn btn-link btn-animate text-muted"
521 this.handleDeleteClick
536 classes={`icon-inline ${
537 cv.comment.deleted && "text-danger"
542 {(canModOnSelf || canAdminOnSelf) && (
544 className="btn btn-link btn-animate text-muted"
547 this.handleDistinguishClick
550 !cv.comment.distinguished
551 ? i18n.t("distinguish")
552 : i18n.t("undistinguish")
555 !cv.comment.distinguished
556 ? i18n.t("distinguish")
557 : i18n.t("undistinguish")
562 classes={`icon-inline ${
563 cv.comment.distinguished &&
571 {/* Admins and mods can remove comments */}
572 {(canMod_ || canAdmin_) && (
574 {!cv.comment.removed ? (
576 className="btn btn-link btn-animate text-muted"
579 this.handleModRemoveShow
581 aria-label={i18n.t("remove")}
587 className="btn btn-link btn-animate text-muted"
590 this.handleModRemoveSubmit
592 aria-label={i18n.t("restore")}
599 {/* Mods can ban from community, and appoint as mods to community */}
603 (!cv.creator_banned_from_community ? (
605 className="btn btn-link btn-animate text-muted"
608 this.handleModBanFromCommunityShow
610 aria-label={i18n.t("ban")}
616 className="btn btn-link btn-animate text-muted"
619 this.handleModBanFromCommunitySubmit
621 aria-label={i18n.t("unban")}
626 {!cv.creator_banned_from_community &&
627 (!this.state.showConfirmAppointAsMod ? (
629 className="btn btn-link btn-animate text-muted"
632 this.handleShowConfirmAppointAsMod
636 ? i18n.t("remove_as_mod")
637 : i18n.t("appoint_as_mod")
641 ? i18n.t("remove_as_mod")
642 : i18n.t("appoint_as_mod")}
647 className="btn btn-link btn-animate text-muted"
648 aria-label={i18n.t("are_you_sure")}
650 {i18n.t("are_you_sure")}
653 className="btn btn-link btn-animate text-muted"
656 this.handleAddModToCommunity
658 aria-label={i18n.t("yes")}
663 className="btn btn-link btn-animate text-muted"
666 this.handleCancelConfirmAppointAsMod
668 aria-label={i18n.t("no")}
676 {/* Community creators and admins can transfer community to another mod */}
677 {(amCommunityCreator_ || canAdmin_) &&
680 (!this.state.showConfirmTransferCommunity ? (
682 className="btn btn-link btn-animate text-muted"
685 this.handleShowConfirmTransferCommunity
687 aria-label={i18n.t("transfer_community")}
689 {i18n.t("transfer_community")}
694 className="btn btn-link btn-animate text-muted"
695 aria-label={i18n.t("are_you_sure")}
697 {i18n.t("are_you_sure")}
700 className="btn btn-link btn-animate text-muted"
703 this.handleTransferCommunity
705 aria-label={i18n.t("yes")}
710 className="btn btn-link btn-animate text-muted"
714 .handleCancelShowConfirmTransferCommunity
716 aria-label={i18n.t("no")}
722 {/* Admins can ban from all, and appoint other admins */}
728 className="btn btn-link btn-animate text-muted"
731 this.handlePurgePersonShow
733 aria-label={i18n.t("purge_user")}
735 {i18n.t("purge_user")}
738 className="btn btn-link btn-animate text-muted"
741 this.handlePurgeCommentShow
743 aria-label={i18n.t("purge_comment")}
745 {i18n.t("purge_comment")}
748 {!isBanned(cv.creator) ? (
750 className="btn btn-link btn-animate text-muted"
753 this.handleModBanShow
755 aria-label={i18n.t("ban_from_site")}
757 {i18n.t("ban_from_site")}
761 className="btn btn-link btn-animate text-muted"
764 this.handleModBanSubmit
766 aria-label={i18n.t("unban_from_site")}
768 {i18n.t("unban_from_site")}
773 {!isBanned(cv.creator) &&
775 (!this.state.showConfirmAppointAsAdmin ? (
777 className="btn btn-link btn-animate text-muted"
780 this.handleShowConfirmAppointAsAdmin
784 ? i18n.t("remove_as_admin")
785 : i18n.t("appoint_as_admin")
789 ? i18n.t("remove_as_admin")
790 : i18n.t("appoint_as_admin")}
794 <button className="btn btn-link btn-animate text-muted">
795 {i18n.t("are_you_sure")}
798 className="btn btn-link btn-animate text-muted"
803 aria-label={i18n.t("yes")}
808 className="btn btn-link btn-animate text-muted"
811 this.handleCancelConfirmAppointAsAdmin
813 aria-label={i18n.t("no")}
826 {/* end of button group */}
831 {showMoreChildren && (
833 className={`details ml-1 comment-node py-2 ${
834 !this.props.noBorder ? "border-top border-light" : ""
836 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
839 className="btn btn-link text-muted"
840 onClick={linkEvent(this, this.handleFetchChildren)}
842 {i18n.t("x_more_replies", {
843 count: node.comment_view.counts.child_count,
844 formattedCount: numToSI(node.comment_view.counts.child_count),
850 {/* end of details */}
851 {this.state.showRemoveDialog && (
853 className="form-inline"
854 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
858 htmlFor={`mod-remove-reason-${cv.comment.id}`}
864 id={`mod-remove-reason-${cv.comment.id}`}
865 className="form-control mr-2"
866 placeholder={i18n.t("reason")}
867 value={toUndefined(this.state.removeReason)}
868 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
872 className="btn btn-secondary"
873 aria-label={i18n.t("remove_comment")}
875 {i18n.t("remove_comment")}
879 {this.state.showReportDialog && (
881 className="form-inline"
882 onSubmit={linkEvent(this, this.handleReportSubmit)}
886 htmlFor={`report-reason-${cv.comment.id}`}
893 id={`report-reason-${cv.comment.id}`}
894 className="form-control mr-2"
895 placeholder={i18n.t("reason")}
896 value={this.state.reportReason}
897 onInput={linkEvent(this, this.handleReportReasonChange)}
901 className="btn btn-secondary"
902 aria-label={i18n.t("create_report")}
904 {i18n.t("create_report")}
908 {this.state.showBanDialog && (
909 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
910 <div className="form-group row col-12">
912 className="col-form-label"
913 htmlFor={`mod-ban-reason-${cv.comment.id}`}
919 id={`mod-ban-reason-${cv.comment.id}`}
920 className="form-control mr-2"
921 placeholder={i18n.t("reason")}
922 value={toUndefined(this.state.banReason)}
923 onInput={linkEvent(this, this.handleModBanReasonChange)}
926 className="col-form-label"
927 htmlFor={`mod-ban-expires-${cv.comment.id}`}
933 id={`mod-ban-expires-${cv.comment.id}`}
934 className="form-control mr-2"
935 placeholder={i18n.t("number_of_days")}
936 value={toUndefined(this.state.banExpireDays)}
937 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
939 <div className="form-group">
940 <div className="form-check">
942 className="form-check-input"
943 id="mod-ban-remove-data"
945 checked={this.state.removeData}
946 onChange={linkEvent(this, this.handleModRemoveDataChange)}
949 className="form-check-label"
950 htmlFor="mod-ban-remove-data"
951 title={i18n.t("remove_content_more")}
953 {i18n.t("remove_content")}
958 {/* TODO hold off on expires until later */}
959 {/* <div class="form-group row"> */}
960 {/* <label class="col-form-label">Expires</label> */}
961 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
963 <div className="form-group row">
966 className="btn btn-secondary"
967 aria-label={i18n.t("ban")}
969 {i18n.t("ban")} {cv.creator.name}
975 {this.state.showPurgeDialog && (
976 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
978 <label className="sr-only" htmlFor="purge-reason">
984 className="form-control my-3"
985 placeholder={i18n.t("reason")}
986 value={toUndefined(this.state.purgeReason)}
987 onInput={linkEvent(this, this.handlePurgeReasonChange)}
989 <div className="form-group row col-12">
990 {this.state.purgeLoading ? (
995 className="btn btn-secondary"
996 aria-label={purgeTypeText}
1004 {this.state.showReply && (
1007 onReplyCancel={this.handleReplyCancel}
1008 disabled={this.props.locked}
1012 {!this.state.collapsed && node.children.length > 0 && (
1014 nodes={node.children}
1015 locked={this.props.locked}
1016 moderators={this.props.moderators}
1017 admins={this.props.admins}
1018 maxCommentsShown={None}
1019 enableDownvotes={this.props.enableDownvotes}
1020 viewType={this.props.viewType}
1023 {/* A collapsed clearfix */}
1024 {this.state.collapsed && <div className="row col-12"></div>}
1029 get commentReplyOrMentionRead(): boolean {
1030 let cv = this.props.node.comment_view;
1032 if (this.isPersonMentionType(cv)) {
1033 return cv.person_mention.read;
1034 } else if (this.isCommentReplyType(cv)) {
1035 return cv.comment_reply.read;
1041 linkBtn(small = false) {
1042 let cv = this.props.node.comment_view;
1043 let classnames = classNames("btn btn-link btn-animate text-muted", {
1047 let title = this.props.showContext
1048 ? i18n.t("show_context")
1054 className={classnames}
1055 to={`/comment/${cv.comment.id}`}
1058 <Icon icon="link" classes="icon-inline" />
1061 <a className={classnames} title={title} href={cv.comment.ap_id}>
1062 <Icon icon="fedilink" classes="icon-inline" />
1073 get myComment(): boolean {
1074 return UserService.Instance.myUserInfo
1077 m.local_user_view.person.id == this.props.node.comment_view.creator.id
1082 get isPostCreator(): boolean {
1084 this.props.node.comment_view.creator.id ==
1085 this.props.node.comment_view.post.creator_id
1089 get commentUnlessRemoved(): string {
1090 let comment = this.props.node.comment_view.comment;
1091 return comment.removed
1092 ? `*${i18n.t("removed")}*`
1094 ? `*${i18n.t("deleted")}*`
1098 handleReplyClick(i: CommentNode) {
1099 i.setState({ showReply: true });
1102 handleEditClick(i: CommentNode) {
1103 i.setState({ showEdit: true });
1106 handleBlockUserClick(i: CommentNode) {
1107 let blockUserForm = new BlockPerson({
1108 person_id: i.props.node.comment_view.creator.id,
1110 auth: auth().unwrap(),
1112 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1115 handleDeleteClick(i: CommentNode) {
1116 let comment = i.props.node.comment_view.comment;
1117 let deleteForm = new DeleteComment({
1118 comment_id: comment.id,
1119 deleted: !comment.deleted,
1120 auth: auth().unwrap(),
1122 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1125 handleSaveCommentClick(i: CommentNode) {
1126 let cv = i.props.node.comment_view;
1127 let save = cv.saved == undefined ? true : !cv.saved;
1128 let form = new SaveComment({
1129 comment_id: cv.comment.id,
1131 auth: auth().unwrap(),
1134 WebSocketService.Instance.send(wsClient.saveComment(form));
1136 i.setState({ saveLoading: true });
1139 handleReplyCancel() {
1140 this.setState({ showReply: false, showEdit: false });
1143 handleCommentUpvote(event: any) {
1144 event.preventDefault();
1145 let myVote = this.state.my_vote.unwrapOr(0);
1146 let newVote = myVote == 1 ? 0 : 1;
1150 score: this.state.score - 1,
1151 upvotes: this.state.upvotes - 1,
1153 } else if (myVote == -1) {
1155 downvotes: this.state.downvotes - 1,
1156 upvotes: this.state.upvotes + 1,
1157 score: this.state.score + 2,
1161 score: this.state.score + 1,
1162 upvotes: this.state.upvotes + 1,
1166 this.setState({ my_vote: Some(newVote) });
1168 let form = new CreateCommentLike({
1169 comment_id: this.props.node.comment_view.comment.id,
1171 auth: auth().unwrap(),
1173 WebSocketService.Instance.send(wsClient.likeComment(form));
1177 handleCommentDownvote(event: any) {
1178 event.preventDefault();
1179 let myVote = this.state.my_vote.unwrapOr(0);
1180 let newVote = myVote == -1 ? 0 : -1;
1184 downvotes: this.state.downvotes + 1,
1185 upvotes: this.state.upvotes - 1,
1186 score: this.state.score - 2,
1188 } else if (myVote == -1) {
1190 downvotes: this.state.downvotes - 1,
1191 score: this.state.score + 1,
1195 downvotes: this.state.downvotes + 1,
1196 score: this.state.score - 1,
1200 this.setState({ my_vote: Some(newVote) });
1202 let form = new CreateCommentLike({
1203 comment_id: this.props.node.comment_view.comment.id,
1205 auth: auth().unwrap(),
1208 WebSocketService.Instance.send(wsClient.likeComment(form));
1212 handleShowReportDialog(i: CommentNode) {
1213 i.setState({ showReportDialog: !i.state.showReportDialog });
1216 handleReportReasonChange(i: CommentNode, event: any) {
1217 i.setState({ reportReason: event.target.value });
1220 handleReportSubmit(i: CommentNode) {
1221 let comment = i.props.node.comment_view.comment;
1222 let form = new CreateCommentReport({
1223 comment_id: comment.id,
1224 reason: i.state.reportReason,
1225 auth: auth().unwrap(),
1227 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1229 i.setState({ showReportDialog: false });
1232 handleModRemoveShow(i: CommentNode) {
1234 showRemoveDialog: !i.state.showRemoveDialog,
1235 showBanDialog: false,
1239 handleModRemoveReasonChange(i: CommentNode, event: any) {
1240 i.setState({ removeReason: Some(event.target.value) });
1243 handleModRemoveDataChange(i: CommentNode, event: any) {
1244 i.setState({ removeData: event.target.checked });
1247 handleModRemoveSubmit(i: CommentNode) {
1248 let comment = i.props.node.comment_view.comment;
1249 let form = new RemoveComment({
1250 comment_id: comment.id,
1251 removed: !comment.removed,
1252 reason: i.state.removeReason,
1253 auth: auth().unwrap(),
1255 WebSocketService.Instance.send(wsClient.removeComment(form));
1257 i.setState({ showRemoveDialog: false });
1260 handleDistinguishClick(i: CommentNode) {
1261 let comment = i.props.node.comment_view.comment;
1262 let form = new EditComment({
1263 comment_id: comment.id,
1264 form_id: None, // TODO not sure about this
1266 distinguished: Some(!comment.distinguished),
1267 auth: auth().unwrap(),
1269 WebSocketService.Instance.send(wsClient.editComment(form));
1270 i.setState(i.state);
1273 isPersonMentionType(
1274 item: CommentView | PersonMentionView | CommentReplyView
1275 ): item is PersonMentionView {
1276 return (item as PersonMentionView).person_mention?.id !== undefined;
1280 item: CommentView | PersonMentionView | CommentReplyView
1281 ): item is CommentReplyView {
1282 return (item as CommentReplyView).comment_reply?.id !== undefined;
1285 handleMarkRead(i: CommentNode) {
1286 if (i.isPersonMentionType(i.props.node.comment_view)) {
1287 let form = new MarkPersonMentionAsRead({
1288 person_mention_id: i.props.node.comment_view.person_mention.id,
1289 read: !i.props.node.comment_view.person_mention.read,
1290 auth: auth().unwrap(),
1292 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1293 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1294 let form = new MarkCommentReplyAsRead({
1295 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1296 read: !i.props.node.comment_view.comment_reply.read,
1297 auth: auth().unwrap(),
1299 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1302 i.setState({ readLoading: true });
1305 handleModBanFromCommunityShow(i: CommentNode) {
1307 showBanDialog: true,
1308 banType: BanType.Community,
1309 showRemoveDialog: false,
1313 handleModBanShow(i: CommentNode) {
1315 showBanDialog: true,
1316 banType: BanType.Site,
1317 showRemoveDialog: false,
1321 handleModBanReasonChange(i: CommentNode, event: any) {
1322 i.setState({ banReason: Some(event.target.value) });
1325 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1326 i.setState({ banExpireDays: Some(event.target.value) });
1329 handleModBanFromCommunitySubmit(i: CommentNode) {
1330 i.setState({ banType: BanType.Community });
1331 i.handleModBanBothSubmit(i);
1334 handleModBanSubmit(i: CommentNode) {
1335 i.setState({ banType: BanType.Site });
1336 i.handleModBanBothSubmit(i);
1339 handleModBanBothSubmit(i: CommentNode) {
1340 let cv = i.props.node.comment_view;
1342 if (i.state.banType == BanType.Community) {
1343 // If its an unban, restore all their data
1344 let ban = !cv.creator_banned_from_community;
1346 i.setState({ removeData: false });
1348 let form = new BanFromCommunity({
1349 person_id: cv.creator.id,
1350 community_id: cv.community.id,
1352 remove_data: Some(i.state.removeData),
1353 reason: i.state.banReason,
1354 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1355 auth: auth().unwrap(),
1357 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1359 // If its an unban, restore all their data
1360 let ban = !cv.creator.banned;
1362 i.setState({ removeData: false });
1364 let form = new BanPerson({
1365 person_id: cv.creator.id,
1367 remove_data: Some(i.state.removeData),
1368 reason: i.state.banReason,
1369 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1370 auth: auth().unwrap(),
1372 WebSocketService.Instance.send(wsClient.banPerson(form));
1375 i.setState({ showBanDialog: false });
1378 handlePurgePersonShow(i: CommentNode) {
1380 showPurgeDialog: true,
1381 purgeType: PurgeType.Person,
1382 showRemoveDialog: false,
1386 handlePurgeCommentShow(i: CommentNode) {
1388 showPurgeDialog: true,
1389 purgeType: PurgeType.Comment,
1390 showRemoveDialog: false,
1394 handlePurgeReasonChange(i: CommentNode, event: any) {
1395 i.setState({ purgeReason: Some(event.target.value) });
1398 handlePurgeSubmit(i: CommentNode, event: any) {
1399 event.preventDefault();
1401 if (i.state.purgeType == PurgeType.Person) {
1402 let form = new PurgePerson({
1403 person_id: i.props.node.comment_view.creator.id,
1404 reason: i.state.purgeReason,
1405 auth: auth().unwrap(),
1407 WebSocketService.Instance.send(wsClient.purgePerson(form));
1408 } else if (i.state.purgeType == PurgeType.Comment) {
1409 let form = new PurgeComment({
1410 comment_id: i.props.node.comment_view.comment.id,
1411 reason: i.state.purgeReason,
1412 auth: auth().unwrap(),
1414 WebSocketService.Instance.send(wsClient.purgeComment(form));
1417 i.setState({ purgeLoading: true });
1420 handleShowConfirmAppointAsMod(i: CommentNode) {
1421 i.setState({ showConfirmAppointAsMod: true });
1424 handleCancelConfirmAppointAsMod(i: CommentNode) {
1425 i.setState({ showConfirmAppointAsMod: false });
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.setState({ showConfirmAppointAsMod: false });
1440 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1441 i.setState({ showConfirmAppointAsAdmin: true });
1444 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1445 i.setState({ showConfirmAppointAsAdmin: false });
1448 handleAddAdmin(i: CommentNode) {
1449 let creatorId = i.props.node.comment_view.creator.id;
1450 let form = new AddAdmin({
1451 person_id: creatorId,
1452 added: !isAdmin(i.props.admins, creatorId),
1453 auth: auth().unwrap(),
1455 WebSocketService.Instance.send(wsClient.addAdmin(form));
1456 i.setState({ showConfirmAppointAsAdmin: false });
1459 handleShowConfirmTransferCommunity(i: CommentNode) {
1460 i.setState({ showConfirmTransferCommunity: true });
1463 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1464 i.setState({ showConfirmTransferCommunity: false });
1467 handleTransferCommunity(i: CommentNode) {
1468 let cv = i.props.node.comment_view;
1469 let form = new TransferCommunity({
1470 community_id: cv.community.id,
1471 person_id: cv.creator.id,
1472 auth: auth().unwrap(),
1474 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1475 i.setState({ showConfirmTransferCommunity: false });
1478 handleShowConfirmTransferSite(i: CommentNode) {
1479 i.setState({ showConfirmTransferSite: true });
1482 handleCancelShowConfirmTransferSite(i: CommentNode) {
1483 i.setState({ showConfirmTransferSite: false });
1486 get isCommentNew(): boolean {
1487 let now = moment.utc().subtract(10, "minutes");
1488 let then = moment.utc(this.props.node.comment_view.comment.published);
1489 return now.isBefore(then);
1492 handleCommentCollapse(i: CommentNode) {
1493 i.setState({ collapsed: !i.state.collapsed });
1497 handleViewSource(i: CommentNode) {
1498 i.setState({ viewSource: !i.state.viewSource });
1501 handleShowAdvanced(i: CommentNode) {
1502 i.setState({ showAdvanced: !i.state.showAdvanced });
1506 handleFetchChildren(i: CommentNode) {
1507 let form = new GetComments({
1508 post_id: Some(i.props.node.comment_view.post.id),
1509 parent_id: Some(i.props.node.comment_view.comment.id),
1510 max_depth: Some(commentTreeMaxDepth),
1514 type_: Some(ListingType.All),
1515 community_name: None,
1517 saved_only: Some(false),
1518 auth: auth(false).ok(),
1521 WebSocketService.Instance.send(wsClient.getComments(form));
1525 if (this.state.my_vote.unwrapOr(0) == 1) {
1527 } else if (this.state.my_vote.unwrapOr(0) == -1) {
1528 return "text-danger";
1530 return "text-muted";
1534 get pointsTippy(): string {
1535 let points = i18n.t("number_of_points", {
1536 count: this.state.score,
1537 formattedCount: this.state.score,
1540 let upvotes = i18n.t("number_of_upvotes", {
1541 count: this.state.upvotes,
1542 formattedCount: this.state.upvotes,
1545 let downvotes = i18n.t("number_of_downvotes", {
1546 count: this.state.downvotes,
1547 formattedCount: this.state.downvotes,
1550 return `${points} • ${upvotes} • ${downvotes}`;
1553 get expandText(): string {
1554 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");