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";
55 import { Icon, PurgeWarning, Spinner } from "../common/icon";
56 import { MomentTime } from "../common/moment-time";
57 import { CommunityLink } from "../community/community-link";
58 import { PersonListing } from "../person/person-listing";
59 import { CommentForm } from "./comment-form";
60 import { CommentNodes } from "./comment-nodes";
62 interface CommentNodeState {
65 showRemoveDialog: boolean;
66 removeReason: Option<string>;
67 showBanDialog: boolean;
69 banReason: Option<string>;
70 banExpireDays: Option<number>;
72 showPurgeDialog: boolean;
73 purgeReason: Option<string>;
75 purgeLoading: boolean;
76 showConfirmTransferSite: boolean;
77 showConfirmTransferCommunity: boolean;
78 showConfirmAppointAsMod: boolean;
79 showConfirmAppointAsAdmin: boolean;
82 showAdvanced: boolean;
83 showReportDialog: boolean;
85 my_vote: Option<number>;
93 interface CommentNodeProps {
95 moderators: Option<CommunityModeratorView[]>;
96 admins: Option<PersonViewSafe[]>;
102 showContext?: boolean;
103 showCommunity?: boolean;
104 enableDownvotes: boolean;
105 viewType: CommentViewType;
106 allLanguages: Language[];
107 siteLanguages: number[];
108 hideImages?: boolean;
111 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
112 private emptyState: CommentNodeState = {
115 showRemoveDialog: false,
117 showBanDialog: false,
121 banType: BanType.Community,
122 showPurgeDialog: false,
125 purgeType: PurgeType.Person,
129 showConfirmTransferSite: false,
130 showConfirmTransferCommunity: false,
131 showConfirmAppointAsMod: false,
132 showConfirmAppointAsAdmin: false,
133 showReportDialog: false,
135 my_vote: this.props.node.comment_view.my_vote,
136 score: this.props.node.comment_view.counts.score,
137 upvotes: this.props.node.comment_view.counts.upvotes,
138 downvotes: this.props.node.comment_view.counts.downvotes,
143 constructor(props: any, context: any) {
144 super(props, context);
146 this.state = this.emptyState;
147 this.handleReplyCancel = this.handleReplyCancel.bind(this);
148 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
149 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
152 // TODO see if there's a better way to do this, and all willReceiveProps
153 componentWillReceiveProps(nextProps: CommentNodeProps) {
154 let cv = nextProps.node.comment_view;
157 upvotes: cv.counts.upvotes,
158 downvotes: cv.counts.downvotes,
159 score: cv.counts.score,
166 let node = this.props.node;
167 let cv = this.props.node.comment_view;
169 let purgeTypeText: string;
170 if (this.state.purgeType == PurgeType.Comment) {
171 purgeTypeText = i18n.t("purge_comment");
172 } else if (this.state.purgeType == PurgeType.Person) {
173 purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
176 let canMod_ = canMod(
177 this.props.moderators,
181 let canModOnSelf = canMod(
182 this.props.moderators,
185 UserService.Instance.myUserInfo,
188 let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
189 let canAdminOnSelf = canAdmin(
192 UserService.Instance.myUserInfo,
195 let isMod_ = isMod(this.props.moderators, cv.creator.id);
196 let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
197 let amCommunityCreator_ = amCommunityCreator(
198 this.props.moderators,
202 let borderColor = this.props.node.depth
203 ? colorList[(this.props.node.depth - 1) % colorList.length]
205 let moreRepliesBorderColor = this.props.node.depth
206 ? colorList[this.props.node.depth % colorList.length]
209 let showMoreChildren =
210 this.props.viewType == CommentViewType.Tree &&
211 !this.state.collapsed &&
212 node.children.length == 0 &&
213 node.comment_view.counts.child_count > 0;
217 className={`comment ${
218 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
222 id={`comment-${cv.comment.id}`}
223 className={classNames(`details comment-node py-2`, {
224 "border-top border-light": !this.props.noBorder,
227 this.props.node.comment_view.comment.distinguished,
230 !this.props.noIndent &&
231 this.props.node.depth &&
232 `border-left: 2px ${borderColor} solid !important`
237 !this.props.noIndent && this.props.node.depth && "ml-2"
240 <div className="d-flex flex-wrap align-items-center text-muted small">
241 <span className="mr-2">
242 <PersonListing person={cv.creator} />
244 {cv.comment.distinguished && (
245 <Icon icon="shield" inline classes={`text-danger mr-2`} />
248 <div className="badge badge-light d-none d-sm-inline mr-2">
253 <div className="badge badge-light d-none d-sm-inline mr-2">
257 {this.isPostCreator && (
258 <div className="badge badge-light d-none d-sm-inline mr-2">
262 {cv.creator.bot_account && (
263 <div className="badge badge-light d-none d-sm-inline mr-2">
264 {i18n.t("bot_account").toLowerCase()}
267 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
268 <div className="badge badge-danger mr-2">
272 {this.props.showCommunity && (
274 <span className="mx-1">{i18n.t("to")}</span>
275 <CommunityLink community={cv.community} />
276 <span className="mx-2">•</span>
277 <Link className="mr-2" to={`/post/${cv.post.id}`}>
283 className="btn btn-sm text-muted"
284 onClick={linkEvent(this, this.handleCommentCollapse)}
285 aria-label={this.expandText}
286 data-tippy-content={this.expandText}
288 {this.state.collapsed ? (
289 <Icon icon="plus-square" classes="icon-inline" />
291 <Icon icon="minus-square" classes="icon-inline" />
295 {/* This is an expanding spacer for mobile */}
296 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
300 className={`unselectable pointer ${this.scoreColor}`}
301 onClick={this.handleCommentUpvote}
302 data-tippy-content={this.pointsTippy}
305 className="mr-1 font-weight-bold"
306 aria-label={i18n.t("number_of_points", {
307 count: this.state.score,
308 formattedCount: this.state.score,
311 {numToSI(this.state.score)}
314 <span className="mr-1">•</span>
319 published={cv.comment.published}
320 updated={cv.comment.updated}
324 {/* end of user row */}
325 {this.state.showEdit && (
329 onReplyCancel={this.handleReplyCancel}
330 disabled={this.props.locked}
332 allLanguages={this.props.allLanguages}
333 siteLanguages={this.props.siteLanguages}
336 {!this.state.showEdit && !this.state.collapsed && (
338 {this.state.viewSource ? (
339 <pre>{this.commentUnlessRemoved}</pre>
343 dangerouslySetInnerHTML={
344 this.props.hideImages
345 ? mdToHtmlNoImages(this.commentUnlessRemoved)
346 : mdToHtml(this.commentUnlessRemoved)
350 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
351 {this.props.showContext && this.linkBtn()}
352 {this.props.markable && (
354 className="btn btn-link btn-animate text-muted"
355 onClick={linkEvent(this, this.handleMarkRead)}
357 this.commentReplyOrMentionRead
358 ? i18n.t("mark_as_unread")
359 : i18n.t("mark_as_read")
362 this.commentReplyOrMentionRead
363 ? i18n.t("mark_as_unread")
364 : i18n.t("mark_as_read")
367 {this.state.readLoading ? (
372 classes={`icon-inline ${
373 this.commentReplyOrMentionRead && "text-success"
379 {UserService.Instance.myUserInfo.isSome() &&
380 !this.props.viewOnly && (
383 className={`btn btn-link btn-animate ${
384 this.state.my_vote.unwrapOr(0) == 1
388 onClick={this.handleCommentUpvote}
389 data-tippy-content={i18n.t("upvote")}
390 aria-label={i18n.t("upvote")}
392 <Icon icon="arrow-up1" classes="icon-inline" />
394 this.state.upvotes !== this.state.score && (
395 <span className="ml-1">
396 {numToSI(this.state.upvotes)}
400 {this.props.enableDownvotes && (
402 className={`btn btn-link btn-animate ${
403 this.state.my_vote.unwrapOr(0) == -1
407 onClick={this.handleCommentDownvote}
408 data-tippy-content={i18n.t("downvote")}
409 aria-label={i18n.t("downvote")}
411 <Icon icon="arrow-down1" classes="icon-inline" />
413 this.state.upvotes !== this.state.score && (
414 <span className="ml-1">
415 {numToSI(this.state.downvotes)}
421 className="btn btn-link btn-animate text-muted"
422 onClick={linkEvent(this, this.handleReplyClick)}
423 data-tippy-content={i18n.t("reply")}
424 aria-label={i18n.t("reply")}
426 <Icon icon="reply1" classes="icon-inline" />
428 {!this.state.showAdvanced ? (
430 className="btn btn-link btn-animate text-muted"
431 onClick={linkEvent(this, this.handleShowAdvanced)}
432 data-tippy-content={i18n.t("more")}
433 aria-label={i18n.t("more")}
435 <Icon icon="more-vertical" classes="icon-inline" />
439 {!this.myComment && (
441 <button className="btn btn-link btn-animate">
443 className="text-muted"
444 to={`/create_private_message/recipient/${cv.creator.id}`}
445 title={i18n.t("message").toLowerCase()}
451 className="btn btn-link btn-animate text-muted"
454 this.handleShowReportDialog
456 data-tippy-content={i18n.t(
459 aria-label={i18n.t("show_report_dialog")}
464 className="btn btn-link btn-animate text-muted"
467 this.handleBlockUserClick
469 data-tippy-content={i18n.t("block_user")}
470 aria-label={i18n.t("block_user")}
472 <Icon icon="slash" />
477 className="btn btn-link btn-animate text-muted"
480 this.handleSaveCommentClick
483 cv.saved ? i18n.t("unsave") : i18n.t("save")
486 cv.saved ? i18n.t("unsave") : i18n.t("save")
489 {this.state.saveLoading ? (
494 classes={`icon-inline ${
495 cv.saved && "text-warning"
501 className="btn btn-link btn-animate text-muted"
502 onClick={linkEvent(this, this.handleViewSource)}
503 data-tippy-content={i18n.t("view_source")}
504 aria-label={i18n.t("view_source")}
508 classes={`icon-inline ${
509 this.state.viewSource && "text-success"
516 className="btn btn-link btn-animate text-muted"
521 data-tippy-content={i18n.t("edit")}
522 aria-label={i18n.t("edit")}
524 <Icon icon="edit" classes="icon-inline" />
527 className="btn btn-link btn-animate text-muted"
530 this.handleDeleteClick
545 classes={`icon-inline ${
546 cv.comment.deleted && "text-danger"
551 {(canModOnSelf || canAdminOnSelf) && (
553 className="btn btn-link btn-animate text-muted"
556 this.handleDistinguishClick
559 !cv.comment.distinguished
560 ? i18n.t("distinguish")
561 : i18n.t("undistinguish")
564 !cv.comment.distinguished
565 ? i18n.t("distinguish")
566 : i18n.t("undistinguish")
571 classes={`icon-inline ${
572 cv.comment.distinguished &&
580 {/* Admins and mods can remove comments */}
581 {(canMod_ || canAdmin_) && (
583 {!cv.comment.removed ? (
585 className="btn btn-link btn-animate text-muted"
588 this.handleModRemoveShow
590 aria-label={i18n.t("remove")}
596 className="btn btn-link btn-animate text-muted"
599 this.handleModRemoveSubmit
601 aria-label={i18n.t("restore")}
608 {/* Mods can ban from community, and appoint as mods to community */}
612 (!cv.creator_banned_from_community ? (
614 className="btn btn-link btn-animate text-muted"
617 this.handleModBanFromCommunityShow
619 aria-label={i18n.t("ban")}
625 className="btn btn-link btn-animate text-muted"
628 this.handleModBanFromCommunitySubmit
630 aria-label={i18n.t("unban")}
635 {!cv.creator_banned_from_community &&
636 (!this.state.showConfirmAppointAsMod ? (
638 className="btn btn-link btn-animate text-muted"
641 this.handleShowConfirmAppointAsMod
645 ? i18n.t("remove_as_mod")
646 : i18n.t("appoint_as_mod")
650 ? i18n.t("remove_as_mod")
651 : i18n.t("appoint_as_mod")}
656 className="btn btn-link btn-animate text-muted"
657 aria-label={i18n.t("are_you_sure")}
659 {i18n.t("are_you_sure")}
662 className="btn btn-link btn-animate text-muted"
665 this.handleAddModToCommunity
667 aria-label={i18n.t("yes")}
672 className="btn btn-link btn-animate text-muted"
675 this.handleCancelConfirmAppointAsMod
677 aria-label={i18n.t("no")}
685 {/* Community creators and admins can transfer community to another mod */}
686 {(amCommunityCreator_ || canAdmin_) &&
689 (!this.state.showConfirmTransferCommunity ? (
691 className="btn btn-link btn-animate text-muted"
694 this.handleShowConfirmTransferCommunity
696 aria-label={i18n.t("transfer_community")}
698 {i18n.t("transfer_community")}
703 className="btn btn-link btn-animate text-muted"
704 aria-label={i18n.t("are_you_sure")}
706 {i18n.t("are_you_sure")}
709 className="btn btn-link btn-animate text-muted"
712 this.handleTransferCommunity
714 aria-label={i18n.t("yes")}
719 className="btn btn-link btn-animate text-muted"
723 .handleCancelShowConfirmTransferCommunity
725 aria-label={i18n.t("no")}
731 {/* Admins can ban from all, and appoint other admins */}
737 className="btn btn-link btn-animate text-muted"
740 this.handlePurgePersonShow
742 aria-label={i18n.t("purge_user")}
744 {i18n.t("purge_user")}
747 className="btn btn-link btn-animate text-muted"
750 this.handlePurgeCommentShow
752 aria-label={i18n.t("purge_comment")}
754 {i18n.t("purge_comment")}
757 {!isBanned(cv.creator) ? (
759 className="btn btn-link btn-animate text-muted"
762 this.handleModBanShow
764 aria-label={i18n.t("ban_from_site")}
766 {i18n.t("ban_from_site")}
770 className="btn btn-link btn-animate text-muted"
773 this.handleModBanSubmit
775 aria-label={i18n.t("unban_from_site")}
777 {i18n.t("unban_from_site")}
782 {!isBanned(cv.creator) &&
784 (!this.state.showConfirmAppointAsAdmin ? (
786 className="btn btn-link btn-animate text-muted"
789 this.handleShowConfirmAppointAsAdmin
793 ? i18n.t("remove_as_admin")
794 : i18n.t("appoint_as_admin")
798 ? i18n.t("remove_as_admin")
799 : i18n.t("appoint_as_admin")}
803 <button className="btn btn-link btn-animate text-muted">
804 {i18n.t("are_you_sure")}
807 className="btn btn-link btn-animate text-muted"
812 aria-label={i18n.t("yes")}
817 className="btn btn-link btn-animate text-muted"
820 this.handleCancelConfirmAppointAsAdmin
822 aria-label={i18n.t("no")}
835 {/* end of button group */}
840 {showMoreChildren && (
842 className={`details ml-1 comment-node py-2 ${
843 !this.props.noBorder ? "border-top border-light" : ""
845 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
848 className="btn btn-link text-muted"
849 onClick={linkEvent(this, this.handleFetchChildren)}
851 {i18n.t("x_more_replies", {
852 count: node.comment_view.counts.child_count,
853 formattedCount: numToSI(node.comment_view.counts.child_count),
859 {/* end of details */}
860 {this.state.showRemoveDialog && (
862 className="form-inline"
863 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
867 htmlFor={`mod-remove-reason-${cv.comment.id}`}
873 id={`mod-remove-reason-${cv.comment.id}`}
874 className="form-control mr-2"
875 placeholder={i18n.t("reason")}
876 value={toUndefined(this.state.removeReason)}
877 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
881 className="btn btn-secondary"
882 aria-label={i18n.t("remove_comment")}
884 {i18n.t("remove_comment")}
888 {this.state.showReportDialog && (
890 className="form-inline"
891 onSubmit={linkEvent(this, this.handleReportSubmit)}
895 htmlFor={`report-reason-${cv.comment.id}`}
902 id={`report-reason-${cv.comment.id}`}
903 className="form-control mr-2"
904 placeholder={i18n.t("reason")}
905 value={this.state.reportReason}
906 onInput={linkEvent(this, this.handleReportReasonChange)}
910 className="btn btn-secondary"
911 aria-label={i18n.t("create_report")}
913 {i18n.t("create_report")}
917 {this.state.showBanDialog && (
918 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
919 <div className="form-group row col-12">
921 className="col-form-label"
922 htmlFor={`mod-ban-reason-${cv.comment.id}`}
928 id={`mod-ban-reason-${cv.comment.id}`}
929 className="form-control mr-2"
930 placeholder={i18n.t("reason")}
931 value={toUndefined(this.state.banReason)}
932 onInput={linkEvent(this, this.handleModBanReasonChange)}
935 className="col-form-label"
936 htmlFor={`mod-ban-expires-${cv.comment.id}`}
942 id={`mod-ban-expires-${cv.comment.id}`}
943 className="form-control mr-2"
944 placeholder={i18n.t("number_of_days")}
945 value={toUndefined(this.state.banExpireDays)}
946 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
948 <div className="form-group">
949 <div className="form-check">
951 className="form-check-input"
952 id="mod-ban-remove-data"
954 checked={this.state.removeData}
955 onChange={linkEvent(this, this.handleModRemoveDataChange)}
958 className="form-check-label"
959 htmlFor="mod-ban-remove-data"
960 title={i18n.t("remove_content_more")}
962 {i18n.t("remove_content")}
967 {/* TODO hold off on expires until later */}
968 {/* <div class="form-group row"> */}
969 {/* <label class="col-form-label">Expires</label> */}
970 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
972 <div className="form-group row">
975 className="btn btn-secondary"
976 aria-label={i18n.t("ban")}
978 {i18n.t("ban")} {cv.creator.name}
984 {this.state.showPurgeDialog && (
985 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
987 <label className="sr-only" htmlFor="purge-reason">
993 className="form-control my-3"
994 placeholder={i18n.t("reason")}
995 value={toUndefined(this.state.purgeReason)}
996 onInput={linkEvent(this, this.handlePurgeReasonChange)}
998 <div className="form-group row col-12">
999 {this.state.purgeLoading ? (
1004 className="btn btn-secondary"
1005 aria-label={purgeTypeText}
1013 {this.state.showReply && (
1016 onReplyCancel={this.handleReplyCancel}
1017 disabled={this.props.locked}
1019 allLanguages={this.props.allLanguages}
1020 siteLanguages={this.props.siteLanguages}
1023 {!this.state.collapsed && node.children.length > 0 && (
1025 nodes={node.children}
1026 locked={this.props.locked}
1027 moderators={this.props.moderators}
1028 admins={this.props.admins}
1029 maxCommentsShown={None}
1030 enableDownvotes={this.props.enableDownvotes}
1031 viewType={this.props.viewType}
1032 allLanguages={this.props.allLanguages}
1033 siteLanguages={this.props.siteLanguages}
1034 hideImages={this.props.hideImages}
1037 {/* A collapsed clearfix */}
1038 {this.state.collapsed && <div className="row col-12"></div>}
1043 get commentReplyOrMentionRead(): boolean {
1044 let cv = this.props.node.comment_view;
1046 if (this.isPersonMentionType(cv)) {
1047 return cv.person_mention.read;
1048 } else if (this.isCommentReplyType(cv)) {
1049 return cv.comment_reply.read;
1055 linkBtn(small = false) {
1056 let cv = this.props.node.comment_view;
1057 let classnames = classNames("btn btn-link btn-animate text-muted", {
1061 let title = this.props.showContext
1062 ? i18n.t("show_context")
1068 className={classnames}
1069 to={`/comment/${cv.comment.id}`}
1072 <Icon icon="link" classes="icon-inline" />
1075 <a className={classnames} title={title} href={cv.comment.ap_id}>
1076 <Icon icon="fedilink" classes="icon-inline" />
1087 get myComment(): boolean {
1088 return UserService.Instance.myUserInfo
1091 m.local_user_view.person.id == this.props.node.comment_view.creator.id
1096 get isPostCreator(): boolean {
1098 this.props.node.comment_view.creator.id ==
1099 this.props.node.comment_view.post.creator_id
1103 get commentUnlessRemoved(): string {
1104 let comment = this.props.node.comment_view.comment;
1105 return comment.removed
1106 ? `*${i18n.t("removed")}*`
1108 ? `*${i18n.t("deleted")}*`
1112 handleReplyClick(i: CommentNode) {
1113 i.setState({ showReply: true });
1116 handleEditClick(i: CommentNode) {
1117 i.setState({ showEdit: true });
1120 handleBlockUserClick(i: CommentNode) {
1121 let blockUserForm = new BlockPerson({
1122 person_id: i.props.node.comment_view.creator.id,
1124 auth: auth().unwrap(),
1126 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1129 handleDeleteClick(i: CommentNode) {
1130 let comment = i.props.node.comment_view.comment;
1131 let deleteForm = new DeleteComment({
1132 comment_id: comment.id,
1133 deleted: !comment.deleted,
1134 auth: auth().unwrap(),
1136 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1139 handleSaveCommentClick(i: CommentNode) {
1140 let cv = i.props.node.comment_view;
1141 let save = cv.saved == undefined ? true : !cv.saved;
1142 let form = new SaveComment({
1143 comment_id: cv.comment.id,
1145 auth: auth().unwrap(),
1148 WebSocketService.Instance.send(wsClient.saveComment(form));
1150 i.setState({ saveLoading: true });
1153 handleReplyCancel() {
1154 this.setState({ showReply: false, showEdit: false });
1157 handleCommentUpvote(event: any) {
1158 event.preventDefault();
1159 let myVote = this.state.my_vote.unwrapOr(0);
1160 let newVote = myVote == 1 ? 0 : 1;
1164 score: this.state.score - 1,
1165 upvotes: this.state.upvotes - 1,
1167 } else if (myVote == -1) {
1169 downvotes: this.state.downvotes - 1,
1170 upvotes: this.state.upvotes + 1,
1171 score: this.state.score + 2,
1175 score: this.state.score + 1,
1176 upvotes: this.state.upvotes + 1,
1180 this.setState({ my_vote: Some(newVote) });
1182 let form = new CreateCommentLike({
1183 comment_id: this.props.node.comment_view.comment.id,
1185 auth: auth().unwrap(),
1187 WebSocketService.Instance.send(wsClient.likeComment(form));
1191 handleCommentDownvote(event: any) {
1192 event.preventDefault();
1193 let myVote = this.state.my_vote.unwrapOr(0);
1194 let newVote = myVote == -1 ? 0 : -1;
1198 downvotes: this.state.downvotes + 1,
1199 upvotes: this.state.upvotes - 1,
1200 score: this.state.score - 2,
1202 } else if (myVote == -1) {
1204 downvotes: this.state.downvotes - 1,
1205 score: this.state.score + 1,
1209 downvotes: this.state.downvotes + 1,
1210 score: this.state.score - 1,
1214 this.setState({ my_vote: Some(newVote) });
1216 let form = new CreateCommentLike({
1217 comment_id: this.props.node.comment_view.comment.id,
1219 auth: auth().unwrap(),
1222 WebSocketService.Instance.send(wsClient.likeComment(form));
1226 handleShowReportDialog(i: CommentNode) {
1227 i.setState({ showReportDialog: !i.state.showReportDialog });
1230 handleReportReasonChange(i: CommentNode, event: any) {
1231 i.setState({ reportReason: event.target.value });
1234 handleReportSubmit(i: CommentNode) {
1235 let comment = i.props.node.comment_view.comment;
1236 let form = new CreateCommentReport({
1237 comment_id: comment.id,
1238 reason: i.state.reportReason,
1239 auth: auth().unwrap(),
1241 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1243 i.setState({ showReportDialog: false });
1246 handleModRemoveShow(i: CommentNode) {
1248 showRemoveDialog: !i.state.showRemoveDialog,
1249 showBanDialog: false,
1253 handleModRemoveReasonChange(i: CommentNode, event: any) {
1254 i.setState({ removeReason: Some(event.target.value) });
1257 handleModRemoveDataChange(i: CommentNode, event: any) {
1258 i.setState({ removeData: event.target.checked });
1261 handleModRemoveSubmit(i: CommentNode) {
1262 let comment = i.props.node.comment_view.comment;
1263 let form = new RemoveComment({
1264 comment_id: comment.id,
1265 removed: !comment.removed,
1266 reason: i.state.removeReason,
1267 auth: auth().unwrap(),
1269 WebSocketService.Instance.send(wsClient.removeComment(form));
1271 i.setState({ showRemoveDialog: false });
1274 handleDistinguishClick(i: CommentNode) {
1275 let comment = i.props.node.comment_view.comment;
1276 let form = new EditComment({
1277 comment_id: comment.id,
1278 form_id: None, // TODO not sure about this
1280 distinguished: Some(!comment.distinguished),
1281 language_id: Some(comment.language_id),
1282 auth: auth().unwrap(),
1284 WebSocketService.Instance.send(wsClient.editComment(form));
1285 i.setState(i.state);
1288 isPersonMentionType(
1289 item: CommentView | PersonMentionView | CommentReplyView
1290 ): item is PersonMentionView {
1291 return (item as PersonMentionView).person_mention?.id !== undefined;
1295 item: CommentView | PersonMentionView | CommentReplyView
1296 ): item is CommentReplyView {
1297 return (item as CommentReplyView).comment_reply?.id !== undefined;
1300 handleMarkRead(i: CommentNode) {
1301 if (i.isPersonMentionType(i.props.node.comment_view)) {
1302 let form = new MarkPersonMentionAsRead({
1303 person_mention_id: i.props.node.comment_view.person_mention.id,
1304 read: !i.props.node.comment_view.person_mention.read,
1305 auth: auth().unwrap(),
1307 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1308 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1309 let form = new MarkCommentReplyAsRead({
1310 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1311 read: !i.props.node.comment_view.comment_reply.read,
1312 auth: auth().unwrap(),
1314 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1317 i.setState({ readLoading: true });
1320 handleModBanFromCommunityShow(i: CommentNode) {
1322 showBanDialog: true,
1323 banType: BanType.Community,
1324 showRemoveDialog: false,
1328 handleModBanShow(i: CommentNode) {
1330 showBanDialog: true,
1331 banType: BanType.Site,
1332 showRemoveDialog: false,
1336 handleModBanReasonChange(i: CommentNode, event: any) {
1337 i.setState({ banReason: Some(event.target.value) });
1340 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1341 i.setState({ banExpireDays: Some(event.target.value) });
1344 handleModBanFromCommunitySubmit(i: CommentNode) {
1345 i.setState({ banType: BanType.Community });
1346 i.handleModBanBothSubmit(i);
1349 handleModBanSubmit(i: CommentNode) {
1350 i.setState({ banType: BanType.Site });
1351 i.handleModBanBothSubmit(i);
1354 handleModBanBothSubmit(i: CommentNode) {
1355 let cv = i.props.node.comment_view;
1357 if (i.state.banType == BanType.Community) {
1358 // If its an unban, restore all their data
1359 let ban = !cv.creator_banned_from_community;
1361 i.setState({ removeData: false });
1363 let form = new BanFromCommunity({
1364 person_id: cv.creator.id,
1365 community_id: cv.community.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.banFromCommunity(form));
1374 // If its an unban, restore all their data
1375 let ban = !cv.creator.banned;
1377 i.setState({ removeData: false });
1379 let form = new BanPerson({
1380 person_id: cv.creator.id,
1382 remove_data: Some(i.state.removeData),
1383 reason: i.state.banReason,
1384 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1385 auth: auth().unwrap(),
1387 WebSocketService.Instance.send(wsClient.banPerson(form));
1390 i.setState({ showBanDialog: false });
1393 handlePurgePersonShow(i: CommentNode) {
1395 showPurgeDialog: true,
1396 purgeType: PurgeType.Person,
1397 showRemoveDialog: false,
1401 handlePurgeCommentShow(i: CommentNode) {
1403 showPurgeDialog: true,
1404 purgeType: PurgeType.Comment,
1405 showRemoveDialog: false,
1409 handlePurgeReasonChange(i: CommentNode, event: any) {
1410 i.setState({ purgeReason: Some(event.target.value) });
1413 handlePurgeSubmit(i: CommentNode, event: any) {
1414 event.preventDefault();
1416 if (i.state.purgeType == PurgeType.Person) {
1417 let form = new PurgePerson({
1418 person_id: i.props.node.comment_view.creator.id,
1419 reason: i.state.purgeReason,
1420 auth: auth().unwrap(),
1422 WebSocketService.Instance.send(wsClient.purgePerson(form));
1423 } else if (i.state.purgeType == PurgeType.Comment) {
1424 let form = new PurgeComment({
1425 comment_id: i.props.node.comment_view.comment.id,
1426 reason: i.state.purgeReason,
1427 auth: auth().unwrap(),
1429 WebSocketService.Instance.send(wsClient.purgeComment(form));
1432 i.setState({ purgeLoading: true });
1435 handleShowConfirmAppointAsMod(i: CommentNode) {
1436 i.setState({ showConfirmAppointAsMod: true });
1439 handleCancelConfirmAppointAsMod(i: CommentNode) {
1440 i.setState({ showConfirmAppointAsMod: false });
1443 handleAddModToCommunity(i: CommentNode) {
1444 let cv = i.props.node.comment_view;
1445 let form = new AddModToCommunity({
1446 person_id: cv.creator.id,
1447 community_id: cv.community.id,
1448 added: !isMod(i.props.moderators, cv.creator.id),
1449 auth: auth().unwrap(),
1451 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1452 i.setState({ showConfirmAppointAsMod: false });
1455 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1456 i.setState({ showConfirmAppointAsAdmin: true });
1459 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1460 i.setState({ showConfirmAppointAsAdmin: false });
1463 handleAddAdmin(i: CommentNode) {
1464 let creatorId = i.props.node.comment_view.creator.id;
1465 let form = new AddAdmin({
1466 person_id: creatorId,
1467 added: !isAdmin(i.props.admins, creatorId),
1468 auth: auth().unwrap(),
1470 WebSocketService.Instance.send(wsClient.addAdmin(form));
1471 i.setState({ showConfirmAppointAsAdmin: false });
1474 handleShowConfirmTransferCommunity(i: CommentNode) {
1475 i.setState({ showConfirmTransferCommunity: true });
1478 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1479 i.setState({ showConfirmTransferCommunity: false });
1482 handleTransferCommunity(i: CommentNode) {
1483 let cv = i.props.node.comment_view;
1484 let form = new TransferCommunity({
1485 community_id: cv.community.id,
1486 person_id: cv.creator.id,
1487 auth: auth().unwrap(),
1489 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1490 i.setState({ showConfirmTransferCommunity: false });
1493 handleShowConfirmTransferSite(i: CommentNode) {
1494 i.setState({ showConfirmTransferSite: true });
1497 handleCancelShowConfirmTransferSite(i: CommentNode) {
1498 i.setState({ showConfirmTransferSite: false });
1501 get isCommentNew(): boolean {
1502 let now = moment.utc().subtract(10, "minutes");
1503 let then = moment.utc(this.props.node.comment_view.comment.published);
1504 return now.isBefore(then);
1507 handleCommentCollapse(i: CommentNode) {
1508 i.setState({ collapsed: !i.state.collapsed });
1512 handleViewSource(i: CommentNode) {
1513 i.setState({ viewSource: !i.state.viewSource });
1516 handleShowAdvanced(i: CommentNode) {
1517 i.setState({ showAdvanced: !i.state.showAdvanced });
1521 handleFetchChildren(i: CommentNode) {
1522 let form = new GetComments({
1523 post_id: Some(i.props.node.comment_view.post.id),
1524 parent_id: Some(i.props.node.comment_view.comment.id),
1525 max_depth: Some(commentTreeMaxDepth),
1529 type_: Some(ListingType.All),
1530 community_name: None,
1532 saved_only: Some(false),
1533 auth: auth(false).ok(),
1536 WebSocketService.Instance.send(wsClient.getComments(form));
1540 if (this.state.my_vote.unwrapOr(0) == 1) {
1542 } else if (this.state.my_vote.unwrapOr(0) == -1) {
1543 return "text-danger";
1545 return "text-muted";
1549 get pointsTippy(): string {
1550 let points = i18n.t("number_of_points", {
1551 count: this.state.score,
1552 formattedCount: this.state.score,
1555 let upvotes = i18n.t("number_of_upvotes", {
1556 count: this.state.upvotes,
1557 formattedCount: this.state.upvotes,
1560 let downvotes = i18n.t("number_of_downvotes", {
1561 count: this.state.downvotes,
1562 formattedCount: this.state.downvotes,
1565 return `${points} • ${upvotes} • ${downvotes}`;
1568 get expandText(): string {
1569 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");