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,
22 MarkCommentReplyAsRead,
23 MarkPersonMentionAsRead,
32 } from "lemmy-js-client";
33 import moment from "moment";
34 import { i18n } from "../../i18next";
35 import { BanType, CommentViewType, PurgeType } from "../../interfaces";
36 import { UserService, WebSocketService } from "../../services";
54 import { Icon, PurgeWarning, Spinner } from "../common/icon";
55 import { MomentTime } from "../common/moment-time";
56 import { CommunityLink } from "../community/community-link";
57 import { PersonListing } from "../person/person-listing";
58 import { CommentForm } from "./comment-form";
59 import { CommentNodes } from "./comment-nodes";
61 interface CommentNodeState {
64 showRemoveDialog: boolean;
65 removeReason: Option<string>;
66 showBanDialog: boolean;
68 banReason: Option<string>;
69 banExpireDays: Option<number>;
71 showPurgeDialog: boolean;
72 purgeReason: Option<string>;
74 purgeLoading: boolean;
75 showConfirmTransferSite: boolean;
76 showConfirmTransferCommunity: boolean;
77 showConfirmAppointAsMod: boolean;
78 showConfirmAppointAsAdmin: boolean;
81 showAdvanced: boolean;
82 showReportDialog: boolean;
84 my_vote: Option<number>;
92 interface CommentNodeProps {
94 moderators: Option<CommunityModeratorView[]>;
95 admins: Option<PersonViewSafe[]>;
101 showContext?: boolean;
102 showCommunity?: boolean;
103 enableDownvotes: boolean;
104 viewType: CommentViewType;
105 allLanguages: Language[];
108 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
109 private emptyState: CommentNodeState = {
112 showRemoveDialog: false,
114 showBanDialog: false,
118 banType: BanType.Community,
119 showPurgeDialog: false,
122 purgeType: PurgeType.Person,
126 showConfirmTransferSite: false,
127 showConfirmTransferCommunity: false,
128 showConfirmAppointAsMod: false,
129 showConfirmAppointAsAdmin: false,
130 showReportDialog: false,
132 my_vote: this.props.node.comment_view.my_vote,
133 score: this.props.node.comment_view.counts.score,
134 upvotes: this.props.node.comment_view.counts.upvotes,
135 downvotes: this.props.node.comment_view.counts.downvotes,
140 constructor(props: any, context: any) {
141 super(props, context);
143 this.state = this.emptyState;
144 this.handleReplyCancel = this.handleReplyCancel.bind(this);
145 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
146 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
149 // TODO see if there's a better way to do this, and all willReceiveProps
150 componentWillReceiveProps(nextProps: CommentNodeProps) {
151 let cv = nextProps.node.comment_view;
154 upvotes: cv.counts.upvotes,
155 downvotes: cv.counts.downvotes,
156 score: cv.counts.score,
163 let node = this.props.node;
164 let cv = this.props.node.comment_view;
166 let purgeTypeText: string;
167 if (this.state.purgeType == PurgeType.Comment) {
168 purgeTypeText = i18n.t("purge_comment");
169 } else if (this.state.purgeType == PurgeType.Person) {
170 purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
173 let canMod_ = canMod(
174 this.props.moderators,
178 let canModOnSelf = canMod(
179 this.props.moderators,
182 UserService.Instance.myUserInfo,
185 let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
186 let canAdminOnSelf = canAdmin(
189 UserService.Instance.myUserInfo,
192 let isMod_ = isMod(this.props.moderators, cv.creator.id);
193 let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
194 let amCommunityCreator_ = amCommunityCreator(
195 this.props.moderators,
199 let borderColor = this.props.node.depth
200 ? colorList[(this.props.node.depth - 1) % colorList.length]
202 let moreRepliesBorderColor = this.props.node.depth
203 ? colorList[this.props.node.depth % colorList.length]
206 let showMoreChildren =
207 this.props.viewType == CommentViewType.Tree &&
208 !this.state.collapsed &&
209 node.children.length == 0 &&
210 node.comment_view.counts.child_count > 0;
214 className={`comment ${
215 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
219 id={`comment-${cv.comment.id}`}
220 className={classNames(`details comment-node py-2`, {
221 "border-top border-light": !this.props.noBorder,
224 this.props.node.comment_view.comment.distinguished,
227 !this.props.noIndent &&
228 this.props.node.depth &&
229 `border-left: 2px ${borderColor} solid !important`
234 !this.props.noIndent && this.props.node.depth && "ml-2"
237 <div className="d-flex flex-wrap align-items-center text-muted small">
238 <span className="mr-2">
239 <PersonListing person={cv.creator} />
241 {cv.comment.distinguished && (
242 <Icon icon="shield" inline classes={`text-danger mr-2`} />
245 <div className="badge badge-light d-none d-sm-inline mr-2">
250 <div className="badge badge-light d-none d-sm-inline mr-2">
254 {this.isPostCreator && (
255 <div className="badge badge-light d-none d-sm-inline mr-2">
259 {cv.creator.bot_account && (
260 <div className="badge badge-light d-none d-sm-inline mr-2">
261 {i18n.t("bot_account").toLowerCase()}
264 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
265 <div className="badge badge-danger mr-2">
269 {this.props.showCommunity && (
271 <span className="mx-1">{i18n.t("to")}</span>
272 <CommunityLink community={cv.community} />
273 <span className="mx-2">•</span>
274 <Link className="mr-2" to={`/post/${cv.post.id}`}>
280 className="btn btn-sm text-muted"
281 onClick={linkEvent(this, this.handleCommentCollapse)}
282 aria-label={this.expandText}
283 data-tippy-content={this.expandText}
285 {this.state.collapsed ? (
286 <Icon icon="plus-square" classes="icon-inline" />
288 <Icon icon="minus-square" classes="icon-inline" />
292 {/* This is an expanding spacer for mobile */}
293 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
297 className={`unselectable pointer ${this.scoreColor}`}
298 onClick={this.handleCommentUpvote}
299 data-tippy-content={this.pointsTippy}
302 className="mr-1 font-weight-bold"
303 aria-label={i18n.t("number_of_points", {
304 count: this.state.score,
305 formattedCount: this.state.score,
308 {numToSI(this.state.score)}
311 <span className="mr-1">•</span>
316 published={cv.comment.published}
317 updated={cv.comment.updated}
321 {/* end of user row */}
322 {this.state.showEdit && (
326 onReplyCancel={this.handleReplyCancel}
327 disabled={this.props.locked}
329 allLanguages={this.props.allLanguages}
332 {!this.state.showEdit && !this.state.collapsed && (
334 {this.state.viewSource ? (
335 <pre>{this.commentUnlessRemoved}</pre>
339 dangerouslySetInnerHTML={mdToHtml(
340 this.commentUnlessRemoved
344 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
345 {this.props.showContext && this.linkBtn()}
346 {this.props.markable && (
348 className="btn btn-link btn-animate text-muted"
349 onClick={linkEvent(this, this.handleMarkRead)}
351 this.commentReplyOrMentionRead
352 ? i18n.t("mark_as_unread")
353 : i18n.t("mark_as_read")
356 this.commentReplyOrMentionRead
357 ? i18n.t("mark_as_unread")
358 : i18n.t("mark_as_read")
361 {this.state.readLoading ? (
366 classes={`icon-inline ${
367 this.commentReplyOrMentionRead && "text-success"
373 {UserService.Instance.myUserInfo.isSome() &&
374 !this.props.viewOnly && (
377 className={`btn btn-link btn-animate ${
378 this.state.my_vote.unwrapOr(0) == 1
382 onClick={this.handleCommentUpvote}
383 data-tippy-content={i18n.t("upvote")}
384 aria-label={i18n.t("upvote")}
386 <Icon icon="arrow-up1" classes="icon-inline" />
388 this.state.upvotes !== this.state.score && (
389 <span className="ml-1">
390 {numToSI(this.state.upvotes)}
394 {this.props.enableDownvotes && (
396 className={`btn btn-link btn-animate ${
397 this.state.my_vote.unwrapOr(0) == -1
401 onClick={this.handleCommentDownvote}
402 data-tippy-content={i18n.t("downvote")}
403 aria-label={i18n.t("downvote")}
405 <Icon icon="arrow-down1" classes="icon-inline" />
407 this.state.upvotes !== this.state.score && (
408 <span className="ml-1">
409 {numToSI(this.state.downvotes)}
415 className="btn btn-link btn-animate text-muted"
416 onClick={linkEvent(this, this.handleReplyClick)}
417 data-tippy-content={i18n.t("reply")}
418 aria-label={i18n.t("reply")}
420 <Icon icon="reply1" classes="icon-inline" />
422 {!this.state.showAdvanced ? (
424 className="btn btn-link btn-animate text-muted"
425 onClick={linkEvent(this, this.handleShowAdvanced)}
426 data-tippy-content={i18n.t("more")}
427 aria-label={i18n.t("more")}
429 <Icon icon="more-vertical" classes="icon-inline" />
433 {!this.myComment && (
435 <button className="btn btn-link btn-animate">
437 className="text-muted"
438 to={`/create_private_message/recipient/${cv.creator.id}`}
439 title={i18n.t("message").toLowerCase()}
445 className="btn btn-link btn-animate text-muted"
448 this.handleShowReportDialog
450 data-tippy-content={i18n.t(
453 aria-label={i18n.t("show_report_dialog")}
458 className="btn btn-link btn-animate text-muted"
461 this.handleBlockUserClick
463 data-tippy-content={i18n.t("block_user")}
464 aria-label={i18n.t("block_user")}
466 <Icon icon="slash" />
471 className="btn btn-link btn-animate text-muted"
474 this.handleSaveCommentClick
477 cv.saved ? i18n.t("unsave") : i18n.t("save")
480 cv.saved ? i18n.t("unsave") : i18n.t("save")
483 {this.state.saveLoading ? (
488 classes={`icon-inline ${
489 cv.saved && "text-warning"
495 className="btn btn-link btn-animate text-muted"
496 onClick={linkEvent(this, this.handleViewSource)}
497 data-tippy-content={i18n.t("view_source")}
498 aria-label={i18n.t("view_source")}
502 classes={`icon-inline ${
503 this.state.viewSource && "text-success"
510 className="btn btn-link btn-animate text-muted"
515 data-tippy-content={i18n.t("edit")}
516 aria-label={i18n.t("edit")}
518 <Icon icon="edit" classes="icon-inline" />
521 className="btn btn-link btn-animate text-muted"
524 this.handleDeleteClick
539 classes={`icon-inline ${
540 cv.comment.deleted && "text-danger"
545 {(canModOnSelf || canAdminOnSelf) && (
547 className="btn btn-link btn-animate text-muted"
550 this.handleDistinguishClick
553 !cv.comment.distinguished
554 ? i18n.t("distinguish")
555 : i18n.t("undistinguish")
558 !cv.comment.distinguished
559 ? i18n.t("distinguish")
560 : i18n.t("undistinguish")
565 classes={`icon-inline ${
566 cv.comment.distinguished &&
574 {/* Admins and mods can remove comments */}
575 {(canMod_ || canAdmin_) && (
577 {!cv.comment.removed ? (
579 className="btn btn-link btn-animate text-muted"
582 this.handleModRemoveShow
584 aria-label={i18n.t("remove")}
590 className="btn btn-link btn-animate text-muted"
593 this.handleModRemoveSubmit
595 aria-label={i18n.t("restore")}
602 {/* Mods can ban from community, and appoint as mods to community */}
606 (!cv.creator_banned_from_community ? (
608 className="btn btn-link btn-animate text-muted"
611 this.handleModBanFromCommunityShow
613 aria-label={i18n.t("ban")}
619 className="btn btn-link btn-animate text-muted"
622 this.handleModBanFromCommunitySubmit
624 aria-label={i18n.t("unban")}
629 {!cv.creator_banned_from_community &&
630 (!this.state.showConfirmAppointAsMod ? (
632 className="btn btn-link btn-animate text-muted"
635 this.handleShowConfirmAppointAsMod
639 ? i18n.t("remove_as_mod")
640 : i18n.t("appoint_as_mod")
644 ? i18n.t("remove_as_mod")
645 : i18n.t("appoint_as_mod")}
650 className="btn btn-link btn-animate text-muted"
651 aria-label={i18n.t("are_you_sure")}
653 {i18n.t("are_you_sure")}
656 className="btn btn-link btn-animate text-muted"
659 this.handleAddModToCommunity
661 aria-label={i18n.t("yes")}
666 className="btn btn-link btn-animate text-muted"
669 this.handleCancelConfirmAppointAsMod
671 aria-label={i18n.t("no")}
679 {/* Community creators and admins can transfer community to another mod */}
680 {(amCommunityCreator_ || canAdmin_) &&
683 (!this.state.showConfirmTransferCommunity ? (
685 className="btn btn-link btn-animate text-muted"
688 this.handleShowConfirmTransferCommunity
690 aria-label={i18n.t("transfer_community")}
692 {i18n.t("transfer_community")}
697 className="btn btn-link btn-animate text-muted"
698 aria-label={i18n.t("are_you_sure")}
700 {i18n.t("are_you_sure")}
703 className="btn btn-link btn-animate text-muted"
706 this.handleTransferCommunity
708 aria-label={i18n.t("yes")}
713 className="btn btn-link btn-animate text-muted"
717 .handleCancelShowConfirmTransferCommunity
719 aria-label={i18n.t("no")}
725 {/* Admins can ban from all, and appoint other admins */}
731 className="btn btn-link btn-animate text-muted"
734 this.handlePurgePersonShow
736 aria-label={i18n.t("purge_user")}
738 {i18n.t("purge_user")}
741 className="btn btn-link btn-animate text-muted"
744 this.handlePurgeCommentShow
746 aria-label={i18n.t("purge_comment")}
748 {i18n.t("purge_comment")}
751 {!isBanned(cv.creator) ? (
753 className="btn btn-link btn-animate text-muted"
756 this.handleModBanShow
758 aria-label={i18n.t("ban_from_site")}
760 {i18n.t("ban_from_site")}
764 className="btn btn-link btn-animate text-muted"
767 this.handleModBanSubmit
769 aria-label={i18n.t("unban_from_site")}
771 {i18n.t("unban_from_site")}
776 {!isBanned(cv.creator) &&
778 (!this.state.showConfirmAppointAsAdmin ? (
780 className="btn btn-link btn-animate text-muted"
783 this.handleShowConfirmAppointAsAdmin
787 ? i18n.t("remove_as_admin")
788 : i18n.t("appoint_as_admin")
792 ? i18n.t("remove_as_admin")
793 : i18n.t("appoint_as_admin")}
797 <button className="btn btn-link btn-animate text-muted">
798 {i18n.t("are_you_sure")}
801 className="btn btn-link btn-animate text-muted"
806 aria-label={i18n.t("yes")}
811 className="btn btn-link btn-animate text-muted"
814 this.handleCancelConfirmAppointAsAdmin
816 aria-label={i18n.t("no")}
829 {/* end of button group */}
834 {showMoreChildren && (
836 className={`details ml-1 comment-node py-2 ${
837 !this.props.noBorder ? "border-top border-light" : ""
839 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
842 className="btn btn-link text-muted"
843 onClick={linkEvent(this, this.handleFetchChildren)}
845 {i18n.t("x_more_replies", {
846 count: node.comment_view.counts.child_count,
847 formattedCount: numToSI(node.comment_view.counts.child_count),
853 {/* end of details */}
854 {this.state.showRemoveDialog && (
856 className="form-inline"
857 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
861 htmlFor={`mod-remove-reason-${cv.comment.id}`}
867 id={`mod-remove-reason-${cv.comment.id}`}
868 className="form-control mr-2"
869 placeholder={i18n.t("reason")}
870 value={toUndefined(this.state.removeReason)}
871 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
875 className="btn btn-secondary"
876 aria-label={i18n.t("remove_comment")}
878 {i18n.t("remove_comment")}
882 {this.state.showReportDialog && (
884 className="form-inline"
885 onSubmit={linkEvent(this, this.handleReportSubmit)}
889 htmlFor={`report-reason-${cv.comment.id}`}
896 id={`report-reason-${cv.comment.id}`}
897 className="form-control mr-2"
898 placeholder={i18n.t("reason")}
899 value={this.state.reportReason}
900 onInput={linkEvent(this, this.handleReportReasonChange)}
904 className="btn btn-secondary"
905 aria-label={i18n.t("create_report")}
907 {i18n.t("create_report")}
911 {this.state.showBanDialog && (
912 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
913 <div className="form-group row col-12">
915 className="col-form-label"
916 htmlFor={`mod-ban-reason-${cv.comment.id}`}
922 id={`mod-ban-reason-${cv.comment.id}`}
923 className="form-control mr-2"
924 placeholder={i18n.t("reason")}
925 value={toUndefined(this.state.banReason)}
926 onInput={linkEvent(this, this.handleModBanReasonChange)}
929 className="col-form-label"
930 htmlFor={`mod-ban-expires-${cv.comment.id}`}
936 id={`mod-ban-expires-${cv.comment.id}`}
937 className="form-control mr-2"
938 placeholder={i18n.t("number_of_days")}
939 value={toUndefined(this.state.banExpireDays)}
940 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
942 <div className="form-group">
943 <div className="form-check">
945 className="form-check-input"
946 id="mod-ban-remove-data"
948 checked={this.state.removeData}
949 onChange={linkEvent(this, this.handleModRemoveDataChange)}
952 className="form-check-label"
953 htmlFor="mod-ban-remove-data"
954 title={i18n.t("remove_content_more")}
956 {i18n.t("remove_content")}
961 {/* TODO hold off on expires until later */}
962 {/* <div class="form-group row"> */}
963 {/* <label class="col-form-label">Expires</label> */}
964 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
966 <div className="form-group row">
969 className="btn btn-secondary"
970 aria-label={i18n.t("ban")}
972 {i18n.t("ban")} {cv.creator.name}
978 {this.state.showPurgeDialog && (
979 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
981 <label className="sr-only" htmlFor="purge-reason">
987 className="form-control my-3"
988 placeholder={i18n.t("reason")}
989 value={toUndefined(this.state.purgeReason)}
990 onInput={linkEvent(this, this.handlePurgeReasonChange)}
992 <div className="form-group row col-12">
993 {this.state.purgeLoading ? (
998 className="btn btn-secondary"
999 aria-label={purgeTypeText}
1007 {this.state.showReply && (
1010 onReplyCancel={this.handleReplyCancel}
1011 disabled={this.props.locked}
1013 allLanguages={this.props.allLanguages}
1016 {!this.state.collapsed && node.children.length > 0 && (
1018 nodes={node.children}
1019 locked={this.props.locked}
1020 moderators={this.props.moderators}
1021 admins={this.props.admins}
1022 maxCommentsShown={None}
1023 enableDownvotes={this.props.enableDownvotes}
1024 viewType={this.props.viewType}
1025 allLanguages={this.props.allLanguages}
1028 {/* A collapsed clearfix */}
1029 {this.state.collapsed && <div className="row col-12"></div>}
1034 get commentReplyOrMentionRead(): boolean {
1035 let cv = this.props.node.comment_view;
1037 if (this.isPersonMentionType(cv)) {
1038 return cv.person_mention.read;
1039 } else if (this.isCommentReplyType(cv)) {
1040 return cv.comment_reply.read;
1046 linkBtn(small = false) {
1047 let cv = this.props.node.comment_view;
1048 let classnames = classNames("btn btn-link btn-animate text-muted", {
1052 let title = this.props.showContext
1053 ? i18n.t("show_context")
1059 className={classnames}
1060 to={`/comment/${cv.comment.id}`}
1063 <Icon icon="link" classes="icon-inline" />
1066 <a className={classnames} title={title} href={cv.comment.ap_id}>
1067 <Icon icon="fedilink" classes="icon-inline" />
1078 get myComment(): boolean {
1079 return UserService.Instance.myUserInfo
1082 m.local_user_view.person.id == this.props.node.comment_view.creator.id
1087 get isPostCreator(): boolean {
1089 this.props.node.comment_view.creator.id ==
1090 this.props.node.comment_view.post.creator_id
1094 get commentUnlessRemoved(): string {
1095 let comment = this.props.node.comment_view.comment;
1096 return comment.removed
1097 ? `*${i18n.t("removed")}*`
1099 ? `*${i18n.t("deleted")}*`
1103 handleReplyClick(i: CommentNode) {
1104 i.setState({ showReply: true });
1107 handleEditClick(i: CommentNode) {
1108 i.setState({ showEdit: true });
1111 handleBlockUserClick(i: CommentNode) {
1112 let blockUserForm = new BlockPerson({
1113 person_id: i.props.node.comment_view.creator.id,
1115 auth: auth().unwrap(),
1117 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1120 handleDeleteClick(i: CommentNode) {
1121 let comment = i.props.node.comment_view.comment;
1122 let deleteForm = new DeleteComment({
1123 comment_id: comment.id,
1124 deleted: !comment.deleted,
1125 auth: auth().unwrap(),
1127 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1130 handleSaveCommentClick(i: CommentNode) {
1131 let cv = i.props.node.comment_view;
1132 let save = cv.saved == undefined ? true : !cv.saved;
1133 let form = new SaveComment({
1134 comment_id: cv.comment.id,
1136 auth: auth().unwrap(),
1139 WebSocketService.Instance.send(wsClient.saveComment(form));
1141 i.setState({ saveLoading: true });
1144 handleReplyCancel() {
1145 this.setState({ showReply: false, showEdit: false });
1148 handleCommentUpvote(event: any) {
1149 event.preventDefault();
1150 let myVote = this.state.my_vote.unwrapOr(0);
1151 let newVote = myVote == 1 ? 0 : 1;
1155 score: this.state.score - 1,
1156 upvotes: this.state.upvotes - 1,
1158 } else if (myVote == -1) {
1160 downvotes: this.state.downvotes - 1,
1161 upvotes: this.state.upvotes + 1,
1162 score: this.state.score + 2,
1166 score: this.state.score + 1,
1167 upvotes: this.state.upvotes + 1,
1171 this.setState({ my_vote: Some(newVote) });
1173 let form = new CreateCommentLike({
1174 comment_id: this.props.node.comment_view.comment.id,
1176 auth: auth().unwrap(),
1178 WebSocketService.Instance.send(wsClient.likeComment(form));
1182 handleCommentDownvote(event: any) {
1183 event.preventDefault();
1184 let myVote = this.state.my_vote.unwrapOr(0);
1185 let newVote = myVote == -1 ? 0 : -1;
1189 downvotes: this.state.downvotes + 1,
1190 upvotes: this.state.upvotes - 1,
1191 score: this.state.score - 2,
1193 } else if (myVote == -1) {
1195 downvotes: this.state.downvotes - 1,
1196 score: this.state.score + 1,
1200 downvotes: this.state.downvotes + 1,
1201 score: this.state.score - 1,
1205 this.setState({ my_vote: Some(newVote) });
1207 let form = new CreateCommentLike({
1208 comment_id: this.props.node.comment_view.comment.id,
1210 auth: auth().unwrap(),
1213 WebSocketService.Instance.send(wsClient.likeComment(form));
1217 handleShowReportDialog(i: CommentNode) {
1218 i.setState({ showReportDialog: !i.state.showReportDialog });
1221 handleReportReasonChange(i: CommentNode, event: any) {
1222 i.setState({ reportReason: event.target.value });
1225 handleReportSubmit(i: CommentNode) {
1226 let comment = i.props.node.comment_view.comment;
1227 let form = new CreateCommentReport({
1228 comment_id: comment.id,
1229 reason: i.state.reportReason,
1230 auth: auth().unwrap(),
1232 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1234 i.setState({ showReportDialog: false });
1237 handleModRemoveShow(i: CommentNode) {
1239 showRemoveDialog: !i.state.showRemoveDialog,
1240 showBanDialog: false,
1244 handleModRemoveReasonChange(i: CommentNode, event: any) {
1245 i.setState({ removeReason: Some(event.target.value) });
1248 handleModRemoveDataChange(i: CommentNode, event: any) {
1249 i.setState({ removeData: event.target.checked });
1252 handleModRemoveSubmit(i: CommentNode) {
1253 let comment = i.props.node.comment_view.comment;
1254 let form = new RemoveComment({
1255 comment_id: comment.id,
1256 removed: !comment.removed,
1257 reason: i.state.removeReason,
1258 auth: auth().unwrap(),
1260 WebSocketService.Instance.send(wsClient.removeComment(form));
1262 i.setState({ showRemoveDialog: false });
1265 handleDistinguishClick(i: CommentNode) {
1266 let comment = i.props.node.comment_view.comment;
1267 let form = new EditComment({
1268 comment_id: comment.id,
1269 form_id: None, // TODO not sure about this
1271 distinguished: Some(!comment.distinguished),
1272 language_id: Some(comment.language_id),
1273 auth: auth().unwrap(),
1275 WebSocketService.Instance.send(wsClient.editComment(form));
1276 i.setState(i.state);
1279 isPersonMentionType(
1280 item: CommentView | PersonMentionView | CommentReplyView
1281 ): item is PersonMentionView {
1282 return (item as PersonMentionView).person_mention?.id !== undefined;
1286 item: CommentView | PersonMentionView | CommentReplyView
1287 ): item is CommentReplyView {
1288 return (item as CommentReplyView).comment_reply?.id !== undefined;
1291 handleMarkRead(i: CommentNode) {
1292 if (i.isPersonMentionType(i.props.node.comment_view)) {
1293 let form = new MarkPersonMentionAsRead({
1294 person_mention_id: i.props.node.comment_view.person_mention.id,
1295 read: !i.props.node.comment_view.person_mention.read,
1296 auth: auth().unwrap(),
1298 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1299 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1300 let form = new MarkCommentReplyAsRead({
1301 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1302 read: !i.props.node.comment_view.comment_reply.read,
1303 auth: auth().unwrap(),
1305 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1308 i.setState({ readLoading: true });
1311 handleModBanFromCommunityShow(i: CommentNode) {
1313 showBanDialog: true,
1314 banType: BanType.Community,
1315 showRemoveDialog: false,
1319 handleModBanShow(i: CommentNode) {
1321 showBanDialog: true,
1322 banType: BanType.Site,
1323 showRemoveDialog: false,
1327 handleModBanReasonChange(i: CommentNode, event: any) {
1328 i.setState({ banReason: Some(event.target.value) });
1331 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1332 i.setState({ banExpireDays: Some(event.target.value) });
1335 handleModBanFromCommunitySubmit(i: CommentNode) {
1336 i.setState({ banType: BanType.Community });
1337 i.handleModBanBothSubmit(i);
1340 handleModBanSubmit(i: CommentNode) {
1341 i.setState({ banType: BanType.Site });
1342 i.handleModBanBothSubmit(i);
1345 handleModBanBothSubmit(i: CommentNode) {
1346 let cv = i.props.node.comment_view;
1348 if (i.state.banType == BanType.Community) {
1349 // If its an unban, restore all their data
1350 let ban = !cv.creator_banned_from_community;
1352 i.setState({ removeData: false });
1354 let form = new BanFromCommunity({
1355 person_id: cv.creator.id,
1356 community_id: cv.community.id,
1358 remove_data: Some(i.state.removeData),
1359 reason: i.state.banReason,
1360 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1361 auth: auth().unwrap(),
1363 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1365 // If its an unban, restore all their data
1366 let ban = !cv.creator.banned;
1368 i.setState({ removeData: false });
1370 let form = new BanPerson({
1371 person_id: cv.creator.id,
1373 remove_data: Some(i.state.removeData),
1374 reason: i.state.banReason,
1375 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1376 auth: auth().unwrap(),
1378 WebSocketService.Instance.send(wsClient.banPerson(form));
1381 i.setState({ showBanDialog: false });
1384 handlePurgePersonShow(i: CommentNode) {
1386 showPurgeDialog: true,
1387 purgeType: PurgeType.Person,
1388 showRemoveDialog: false,
1392 handlePurgeCommentShow(i: CommentNode) {
1394 showPurgeDialog: true,
1395 purgeType: PurgeType.Comment,
1396 showRemoveDialog: false,
1400 handlePurgeReasonChange(i: CommentNode, event: any) {
1401 i.setState({ purgeReason: Some(event.target.value) });
1404 handlePurgeSubmit(i: CommentNode, event: any) {
1405 event.preventDefault();
1407 if (i.state.purgeType == PurgeType.Person) {
1408 let form = new PurgePerson({
1409 person_id: i.props.node.comment_view.creator.id,
1410 reason: i.state.purgeReason,
1411 auth: auth().unwrap(),
1413 WebSocketService.Instance.send(wsClient.purgePerson(form));
1414 } else if (i.state.purgeType == PurgeType.Comment) {
1415 let form = new PurgeComment({
1416 comment_id: i.props.node.comment_view.comment.id,
1417 reason: i.state.purgeReason,
1418 auth: auth().unwrap(),
1420 WebSocketService.Instance.send(wsClient.purgeComment(form));
1423 i.setState({ purgeLoading: true });
1426 handleShowConfirmAppointAsMod(i: CommentNode) {
1427 i.setState({ showConfirmAppointAsMod: true });
1430 handleCancelConfirmAppointAsMod(i: CommentNode) {
1431 i.setState({ showConfirmAppointAsMod: false });
1434 handleAddModToCommunity(i: CommentNode) {
1435 let cv = i.props.node.comment_view;
1436 let form = new AddModToCommunity({
1437 person_id: cv.creator.id,
1438 community_id: cv.community.id,
1439 added: !isMod(i.props.moderators, cv.creator.id),
1440 auth: auth().unwrap(),
1442 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1443 i.setState({ showConfirmAppointAsMod: false });
1446 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1447 i.setState({ showConfirmAppointAsAdmin: true });
1450 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1451 i.setState({ showConfirmAppointAsAdmin: false });
1454 handleAddAdmin(i: CommentNode) {
1455 let creatorId = i.props.node.comment_view.creator.id;
1456 let form = new AddAdmin({
1457 person_id: creatorId,
1458 added: !isAdmin(i.props.admins, creatorId),
1459 auth: auth().unwrap(),
1461 WebSocketService.Instance.send(wsClient.addAdmin(form));
1462 i.setState({ showConfirmAppointAsAdmin: false });
1465 handleShowConfirmTransferCommunity(i: CommentNode) {
1466 i.setState({ showConfirmTransferCommunity: true });
1469 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1470 i.setState({ showConfirmTransferCommunity: false });
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.setState({ showConfirmTransferCommunity: false });
1484 handleShowConfirmTransferSite(i: CommentNode) {
1485 i.setState({ showConfirmTransferSite: true });
1488 handleCancelShowConfirmTransferSite(i: CommentNode) {
1489 i.setState({ showConfirmTransferSite: false });
1492 get isCommentNew(): boolean {
1493 let now = moment.utc().subtract(10, "minutes");
1494 let then = moment.utc(this.props.node.comment_view.comment.published);
1495 return now.isBefore(then);
1498 handleCommentCollapse(i: CommentNode) {
1499 i.setState({ collapsed: !i.state.collapsed });
1503 handleViewSource(i: CommentNode) {
1504 i.setState({ viewSource: !i.state.viewSource });
1507 handleShowAdvanced(i: CommentNode) {
1508 i.setState({ showAdvanced: !i.state.showAdvanced });
1512 handleFetchChildren(i: CommentNode) {
1513 let form = new GetComments({
1514 post_id: Some(i.props.node.comment_view.post.id),
1515 parent_id: Some(i.props.node.comment_view.comment.id),
1516 max_depth: Some(commentTreeMaxDepth),
1520 type_: Some(ListingType.All),
1521 community_name: None,
1523 saved_only: Some(false),
1524 auth: auth(false).ok(),
1527 WebSocketService.Instance.send(wsClient.getComments(form));
1531 if (this.state.my_vote.unwrapOr(0) == 1) {
1533 } else if (this.state.my_vote.unwrapOr(0) == -1) {
1534 return "text-danger";
1536 return "text-muted";
1540 get pointsTippy(): string {
1541 let points = i18n.t("number_of_points", {
1542 count: this.state.score,
1543 formattedCount: this.state.score,
1546 let upvotes = i18n.t("number_of_upvotes", {
1547 count: this.state.upvotes,
1548 formattedCount: this.state.upvotes,
1551 let downvotes = i18n.t("number_of_downvotes", {
1552 count: this.state.downvotes,
1553 formattedCount: this.state.downvotes,
1556 return `${points} • ${upvotes} • ${downvotes}`;
1559 get expandText(): string {
1560 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");