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 hideImages?: boolean;
110 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
111 private emptyState: CommentNodeState = {
114 showRemoveDialog: false,
116 showBanDialog: false,
120 banType: BanType.Community,
121 showPurgeDialog: false,
124 purgeType: PurgeType.Person,
128 showConfirmTransferSite: false,
129 showConfirmTransferCommunity: false,
130 showConfirmAppointAsMod: false,
131 showConfirmAppointAsAdmin: false,
132 showReportDialog: false,
134 my_vote: this.props.node.comment_view.my_vote,
135 score: this.props.node.comment_view.counts.score,
136 upvotes: this.props.node.comment_view.counts.upvotes,
137 downvotes: this.props.node.comment_view.counts.downvotes,
142 constructor(props: any, context: any) {
143 super(props, context);
145 this.state = this.emptyState;
146 this.handleReplyCancel = this.handleReplyCancel.bind(this);
147 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
148 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
151 // TODO see if there's a better way to do this, and all willReceiveProps
152 componentWillReceiveProps(nextProps: CommentNodeProps) {
153 let cv = nextProps.node.comment_view;
156 upvotes: cv.counts.upvotes,
157 downvotes: cv.counts.downvotes,
158 score: cv.counts.score,
165 let node = this.props.node;
166 let cv = this.props.node.comment_view;
168 let purgeTypeText: string;
169 if (this.state.purgeType == PurgeType.Comment) {
170 purgeTypeText = i18n.t("purge_comment");
171 } else if (this.state.purgeType == PurgeType.Person) {
172 purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
175 let canMod_ = canMod(
176 this.props.moderators,
180 let canModOnSelf = canMod(
181 this.props.moderators,
184 UserService.Instance.myUserInfo,
187 let canAdmin_ = canAdmin(this.props.admins, cv.creator.id);
188 let canAdminOnSelf = canAdmin(
191 UserService.Instance.myUserInfo,
194 let isMod_ = isMod(this.props.moderators, cv.creator.id);
195 let isAdmin_ = isAdmin(this.props.admins, cv.creator.id);
196 let amCommunityCreator_ = amCommunityCreator(
197 this.props.moderators,
201 let borderColor = this.props.node.depth
202 ? colorList[(this.props.node.depth - 1) % colorList.length]
204 let moreRepliesBorderColor = this.props.node.depth
205 ? colorList[this.props.node.depth % colorList.length]
208 let showMoreChildren =
209 this.props.viewType == CommentViewType.Tree &&
210 !this.state.collapsed &&
211 node.children.length == 0 &&
212 node.comment_view.counts.child_count > 0;
216 className={`comment ${
217 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
221 id={`comment-${cv.comment.id}`}
222 className={classNames(`details comment-node py-2`, {
223 "border-top border-light": !this.props.noBorder,
226 this.props.node.comment_view.comment.distinguished,
229 !this.props.noIndent &&
230 this.props.node.depth &&
231 `border-left: 2px ${borderColor} solid !important`
236 !this.props.noIndent && this.props.node.depth && "ml-2"
239 <div className="d-flex flex-wrap align-items-center text-muted small">
240 <span className="mr-2">
241 <PersonListing person={cv.creator} />
243 {cv.comment.distinguished && (
244 <Icon icon="shield" inline classes={`text-danger mr-2`} />
247 <div className="badge badge-light d-none d-sm-inline mr-2">
252 <div className="badge badge-light d-none d-sm-inline mr-2">
256 {this.isPostCreator && (
257 <div className="badge badge-light d-none d-sm-inline mr-2">
261 {cv.creator.bot_account && (
262 <div className="badge badge-light d-none d-sm-inline mr-2">
263 {i18n.t("bot_account").toLowerCase()}
266 {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
267 <div className="badge badge-danger mr-2">
271 {this.props.showCommunity && (
273 <span className="mx-1">{i18n.t("to")}</span>
274 <CommunityLink community={cv.community} />
275 <span className="mx-2">•</span>
276 <Link className="mr-2" to={`/post/${cv.post.id}`}>
282 className="btn btn-sm text-muted"
283 onClick={linkEvent(this, this.handleCommentCollapse)}
284 aria-label={this.expandText}
285 data-tippy-content={this.expandText}
287 {this.state.collapsed ? (
288 <Icon icon="plus-square" classes="icon-inline" />
290 <Icon icon="minus-square" classes="icon-inline" />
294 {/* This is an expanding spacer for mobile */}
295 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
299 className={`unselectable pointer ${this.scoreColor}`}
300 onClick={this.handleCommentUpvote}
301 data-tippy-content={this.pointsTippy}
304 className="mr-1 font-weight-bold"
305 aria-label={i18n.t("number_of_points", {
306 count: this.state.score,
307 formattedCount: this.state.score,
310 {numToSI(this.state.score)}
313 <span className="mr-1">•</span>
318 published={cv.comment.published}
319 updated={cv.comment.updated}
323 {/* end of user row */}
324 {this.state.showEdit && (
328 onReplyCancel={this.handleReplyCancel}
329 disabled={this.props.locked}
331 allLanguages={this.props.allLanguages}
334 {!this.state.showEdit && !this.state.collapsed && (
336 {this.state.viewSource ? (
337 <pre>{this.commentUnlessRemoved}</pre>
341 dangerouslySetInnerHTML={
342 this.props.hideImages
343 ? mdToHtmlNoImages(this.commentUnlessRemoved)
344 : mdToHtml(this.commentUnlessRemoved)
348 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
349 {this.props.showContext && this.linkBtn()}
350 {this.props.markable && (
352 className="btn btn-link btn-animate text-muted"
353 onClick={linkEvent(this, this.handleMarkRead)}
355 this.commentReplyOrMentionRead
356 ? i18n.t("mark_as_unread")
357 : i18n.t("mark_as_read")
360 this.commentReplyOrMentionRead
361 ? i18n.t("mark_as_unread")
362 : i18n.t("mark_as_read")
365 {this.state.readLoading ? (
370 classes={`icon-inline ${
371 this.commentReplyOrMentionRead && "text-success"
377 {UserService.Instance.myUserInfo.isSome() &&
378 !this.props.viewOnly && (
381 className={`btn btn-link btn-animate ${
382 this.state.my_vote.unwrapOr(0) == 1
386 onClick={this.handleCommentUpvote}
387 data-tippy-content={i18n.t("upvote")}
388 aria-label={i18n.t("upvote")}
390 <Icon icon="arrow-up1" classes="icon-inline" />
392 this.state.upvotes !== this.state.score && (
393 <span className="ml-1">
394 {numToSI(this.state.upvotes)}
398 {this.props.enableDownvotes && (
400 className={`btn btn-link btn-animate ${
401 this.state.my_vote.unwrapOr(0) == -1
405 onClick={this.handleCommentDownvote}
406 data-tippy-content={i18n.t("downvote")}
407 aria-label={i18n.t("downvote")}
409 <Icon icon="arrow-down1" classes="icon-inline" />
411 this.state.upvotes !== this.state.score && (
412 <span className="ml-1">
413 {numToSI(this.state.downvotes)}
419 className="btn btn-link btn-animate text-muted"
420 onClick={linkEvent(this, this.handleReplyClick)}
421 data-tippy-content={i18n.t("reply")}
422 aria-label={i18n.t("reply")}
424 <Icon icon="reply1" classes="icon-inline" />
426 {!this.state.showAdvanced ? (
428 className="btn btn-link btn-animate text-muted"
429 onClick={linkEvent(this, this.handleShowAdvanced)}
430 data-tippy-content={i18n.t("more")}
431 aria-label={i18n.t("more")}
433 <Icon icon="more-vertical" classes="icon-inline" />
437 {!this.myComment && (
439 <button className="btn btn-link btn-animate">
441 className="text-muted"
442 to={`/create_private_message/recipient/${cv.creator.id}`}
443 title={i18n.t("message").toLowerCase()}
449 className="btn btn-link btn-animate text-muted"
452 this.handleShowReportDialog
454 data-tippy-content={i18n.t(
457 aria-label={i18n.t("show_report_dialog")}
462 className="btn btn-link btn-animate text-muted"
465 this.handleBlockUserClick
467 data-tippy-content={i18n.t("block_user")}
468 aria-label={i18n.t("block_user")}
470 <Icon icon="slash" />
475 className="btn btn-link btn-animate text-muted"
478 this.handleSaveCommentClick
481 cv.saved ? i18n.t("unsave") : i18n.t("save")
484 cv.saved ? i18n.t("unsave") : i18n.t("save")
487 {this.state.saveLoading ? (
492 classes={`icon-inline ${
493 cv.saved && "text-warning"
499 className="btn btn-link btn-animate text-muted"
500 onClick={linkEvent(this, this.handleViewSource)}
501 data-tippy-content={i18n.t("view_source")}
502 aria-label={i18n.t("view_source")}
506 classes={`icon-inline ${
507 this.state.viewSource && "text-success"
514 className="btn btn-link btn-animate text-muted"
519 data-tippy-content={i18n.t("edit")}
520 aria-label={i18n.t("edit")}
522 <Icon icon="edit" classes="icon-inline" />
525 className="btn btn-link btn-animate text-muted"
528 this.handleDeleteClick
543 classes={`icon-inline ${
544 cv.comment.deleted && "text-danger"
549 {(canModOnSelf || canAdminOnSelf) && (
551 className="btn btn-link btn-animate text-muted"
554 this.handleDistinguishClick
557 !cv.comment.distinguished
558 ? i18n.t("distinguish")
559 : i18n.t("undistinguish")
562 !cv.comment.distinguished
563 ? i18n.t("distinguish")
564 : i18n.t("undistinguish")
569 classes={`icon-inline ${
570 cv.comment.distinguished &&
578 {/* Admins and mods can remove comments */}
579 {(canMod_ || canAdmin_) && (
581 {!cv.comment.removed ? (
583 className="btn btn-link btn-animate text-muted"
586 this.handleModRemoveShow
588 aria-label={i18n.t("remove")}
594 className="btn btn-link btn-animate text-muted"
597 this.handleModRemoveSubmit
599 aria-label={i18n.t("restore")}
606 {/* Mods can ban from community, and appoint as mods to community */}
610 (!cv.creator_banned_from_community ? (
612 className="btn btn-link btn-animate text-muted"
615 this.handleModBanFromCommunityShow
617 aria-label={i18n.t("ban")}
623 className="btn btn-link btn-animate text-muted"
626 this.handleModBanFromCommunitySubmit
628 aria-label={i18n.t("unban")}
633 {!cv.creator_banned_from_community &&
634 (!this.state.showConfirmAppointAsMod ? (
636 className="btn btn-link btn-animate text-muted"
639 this.handleShowConfirmAppointAsMod
643 ? i18n.t("remove_as_mod")
644 : i18n.t("appoint_as_mod")
648 ? i18n.t("remove_as_mod")
649 : i18n.t("appoint_as_mod")}
654 className="btn btn-link btn-animate text-muted"
655 aria-label={i18n.t("are_you_sure")}
657 {i18n.t("are_you_sure")}
660 className="btn btn-link btn-animate text-muted"
663 this.handleAddModToCommunity
665 aria-label={i18n.t("yes")}
670 className="btn btn-link btn-animate text-muted"
673 this.handleCancelConfirmAppointAsMod
675 aria-label={i18n.t("no")}
683 {/* Community creators and admins can transfer community to another mod */}
684 {(amCommunityCreator_ || canAdmin_) &&
687 (!this.state.showConfirmTransferCommunity ? (
689 className="btn btn-link btn-animate text-muted"
692 this.handleShowConfirmTransferCommunity
694 aria-label={i18n.t("transfer_community")}
696 {i18n.t("transfer_community")}
701 className="btn btn-link btn-animate text-muted"
702 aria-label={i18n.t("are_you_sure")}
704 {i18n.t("are_you_sure")}
707 className="btn btn-link btn-animate text-muted"
710 this.handleTransferCommunity
712 aria-label={i18n.t("yes")}
717 className="btn btn-link btn-animate text-muted"
721 .handleCancelShowConfirmTransferCommunity
723 aria-label={i18n.t("no")}
729 {/* Admins can ban from all, and appoint other admins */}
735 className="btn btn-link btn-animate text-muted"
738 this.handlePurgePersonShow
740 aria-label={i18n.t("purge_user")}
742 {i18n.t("purge_user")}
745 className="btn btn-link btn-animate text-muted"
748 this.handlePurgeCommentShow
750 aria-label={i18n.t("purge_comment")}
752 {i18n.t("purge_comment")}
755 {!isBanned(cv.creator) ? (
757 className="btn btn-link btn-animate text-muted"
760 this.handleModBanShow
762 aria-label={i18n.t("ban_from_site")}
764 {i18n.t("ban_from_site")}
768 className="btn btn-link btn-animate text-muted"
771 this.handleModBanSubmit
773 aria-label={i18n.t("unban_from_site")}
775 {i18n.t("unban_from_site")}
780 {!isBanned(cv.creator) &&
782 (!this.state.showConfirmAppointAsAdmin ? (
784 className="btn btn-link btn-animate text-muted"
787 this.handleShowConfirmAppointAsAdmin
791 ? i18n.t("remove_as_admin")
792 : i18n.t("appoint_as_admin")
796 ? i18n.t("remove_as_admin")
797 : i18n.t("appoint_as_admin")}
801 <button className="btn btn-link btn-animate text-muted">
802 {i18n.t("are_you_sure")}
805 className="btn btn-link btn-animate text-muted"
810 aria-label={i18n.t("yes")}
815 className="btn btn-link btn-animate text-muted"
818 this.handleCancelConfirmAppointAsAdmin
820 aria-label={i18n.t("no")}
833 {/* end of button group */}
838 {showMoreChildren && (
840 className={`details ml-1 comment-node py-2 ${
841 !this.props.noBorder ? "border-top border-light" : ""
843 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
846 className="btn btn-link text-muted"
847 onClick={linkEvent(this, this.handleFetchChildren)}
849 {i18n.t("x_more_replies", {
850 count: node.comment_view.counts.child_count,
851 formattedCount: numToSI(node.comment_view.counts.child_count),
857 {/* end of details */}
858 {this.state.showRemoveDialog && (
860 className="form-inline"
861 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
865 htmlFor={`mod-remove-reason-${cv.comment.id}`}
871 id={`mod-remove-reason-${cv.comment.id}`}
872 className="form-control mr-2"
873 placeholder={i18n.t("reason")}
874 value={toUndefined(this.state.removeReason)}
875 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
879 className="btn btn-secondary"
880 aria-label={i18n.t("remove_comment")}
882 {i18n.t("remove_comment")}
886 {this.state.showReportDialog && (
888 className="form-inline"
889 onSubmit={linkEvent(this, this.handleReportSubmit)}
893 htmlFor={`report-reason-${cv.comment.id}`}
900 id={`report-reason-${cv.comment.id}`}
901 className="form-control mr-2"
902 placeholder={i18n.t("reason")}
903 value={this.state.reportReason}
904 onInput={linkEvent(this, this.handleReportReasonChange)}
908 className="btn btn-secondary"
909 aria-label={i18n.t("create_report")}
911 {i18n.t("create_report")}
915 {this.state.showBanDialog && (
916 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
917 <div className="form-group row col-12">
919 className="col-form-label"
920 htmlFor={`mod-ban-reason-${cv.comment.id}`}
926 id={`mod-ban-reason-${cv.comment.id}`}
927 className="form-control mr-2"
928 placeholder={i18n.t("reason")}
929 value={toUndefined(this.state.banReason)}
930 onInput={linkEvent(this, this.handleModBanReasonChange)}
933 className="col-form-label"
934 htmlFor={`mod-ban-expires-${cv.comment.id}`}
940 id={`mod-ban-expires-${cv.comment.id}`}
941 className="form-control mr-2"
942 placeholder={i18n.t("number_of_days")}
943 value={toUndefined(this.state.banExpireDays)}
944 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
946 <div className="form-group">
947 <div className="form-check">
949 className="form-check-input"
950 id="mod-ban-remove-data"
952 checked={this.state.removeData}
953 onChange={linkEvent(this, this.handleModRemoveDataChange)}
956 className="form-check-label"
957 htmlFor="mod-ban-remove-data"
958 title={i18n.t("remove_content_more")}
960 {i18n.t("remove_content")}
965 {/* TODO hold off on expires until later */}
966 {/* <div class="form-group row"> */}
967 {/* <label class="col-form-label">Expires</label> */}
968 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
970 <div className="form-group row">
973 className="btn btn-secondary"
974 aria-label={i18n.t("ban")}
976 {i18n.t("ban")} {cv.creator.name}
982 {this.state.showPurgeDialog && (
983 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
985 <label className="sr-only" htmlFor="purge-reason">
991 className="form-control my-3"
992 placeholder={i18n.t("reason")}
993 value={toUndefined(this.state.purgeReason)}
994 onInput={linkEvent(this, this.handlePurgeReasonChange)}
996 <div className="form-group row col-12">
997 {this.state.purgeLoading ? (
1002 className="btn btn-secondary"
1003 aria-label={purgeTypeText}
1011 {this.state.showReply && (
1014 onReplyCancel={this.handleReplyCancel}
1015 disabled={this.props.locked}
1017 allLanguages={this.props.allLanguages}
1020 {!this.state.collapsed && node.children.length > 0 && (
1022 nodes={node.children}
1023 locked={this.props.locked}
1024 moderators={this.props.moderators}
1025 admins={this.props.admins}
1026 maxCommentsShown={None}
1027 enableDownvotes={this.props.enableDownvotes}
1028 viewType={this.props.viewType}
1029 allLanguages={this.props.allLanguages}
1030 hideImages={this.props.hideImages}
1033 {/* A collapsed clearfix */}
1034 {this.state.collapsed && <div className="row col-12"></div>}
1039 get commentReplyOrMentionRead(): boolean {
1040 let cv = this.props.node.comment_view;
1042 if (this.isPersonMentionType(cv)) {
1043 return cv.person_mention.read;
1044 } else if (this.isCommentReplyType(cv)) {
1045 return cv.comment_reply.read;
1051 linkBtn(small = false) {
1052 let cv = this.props.node.comment_view;
1053 let classnames = classNames("btn btn-link btn-animate text-muted", {
1057 let title = this.props.showContext
1058 ? i18n.t("show_context")
1064 className={classnames}
1065 to={`/comment/${cv.comment.id}`}
1068 <Icon icon="link" classes="icon-inline" />
1071 <a className={classnames} title={title} href={cv.comment.ap_id}>
1072 <Icon icon="fedilink" classes="icon-inline" />
1083 get myComment(): boolean {
1084 return UserService.Instance.myUserInfo
1087 m.local_user_view.person.id == this.props.node.comment_view.creator.id
1092 get isPostCreator(): boolean {
1094 this.props.node.comment_view.creator.id ==
1095 this.props.node.comment_view.post.creator_id
1099 get commentUnlessRemoved(): string {
1100 let comment = this.props.node.comment_view.comment;
1101 return comment.removed
1102 ? `*${i18n.t("removed")}*`
1104 ? `*${i18n.t("deleted")}*`
1108 handleReplyClick(i: CommentNode) {
1109 i.setState({ showReply: true });
1112 handleEditClick(i: CommentNode) {
1113 i.setState({ showEdit: true });
1116 handleBlockUserClick(i: CommentNode) {
1117 let blockUserForm = new BlockPerson({
1118 person_id: i.props.node.comment_view.creator.id,
1120 auth: auth().unwrap(),
1122 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1125 handleDeleteClick(i: CommentNode) {
1126 let comment = i.props.node.comment_view.comment;
1127 let deleteForm = new DeleteComment({
1128 comment_id: comment.id,
1129 deleted: !comment.deleted,
1130 auth: auth().unwrap(),
1132 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1135 handleSaveCommentClick(i: CommentNode) {
1136 let cv = i.props.node.comment_view;
1137 let save = cv.saved == undefined ? true : !cv.saved;
1138 let form = new SaveComment({
1139 comment_id: cv.comment.id,
1141 auth: auth().unwrap(),
1144 WebSocketService.Instance.send(wsClient.saveComment(form));
1146 i.setState({ saveLoading: true });
1149 handleReplyCancel() {
1150 this.setState({ showReply: false, showEdit: false });
1153 handleCommentUpvote(event: any) {
1154 event.preventDefault();
1155 let myVote = this.state.my_vote.unwrapOr(0);
1156 let newVote = myVote == 1 ? 0 : 1;
1160 score: this.state.score - 1,
1161 upvotes: this.state.upvotes - 1,
1163 } else if (myVote == -1) {
1165 downvotes: this.state.downvotes - 1,
1166 upvotes: this.state.upvotes + 1,
1167 score: this.state.score + 2,
1171 score: this.state.score + 1,
1172 upvotes: this.state.upvotes + 1,
1176 this.setState({ my_vote: Some(newVote) });
1178 let form = new CreateCommentLike({
1179 comment_id: this.props.node.comment_view.comment.id,
1181 auth: auth().unwrap(),
1183 WebSocketService.Instance.send(wsClient.likeComment(form));
1187 handleCommentDownvote(event: any) {
1188 event.preventDefault();
1189 let myVote = this.state.my_vote.unwrapOr(0);
1190 let newVote = myVote == -1 ? 0 : -1;
1194 downvotes: this.state.downvotes + 1,
1195 upvotes: this.state.upvotes - 1,
1196 score: this.state.score - 2,
1198 } else if (myVote == -1) {
1200 downvotes: this.state.downvotes - 1,
1201 score: this.state.score + 1,
1205 downvotes: this.state.downvotes + 1,
1206 score: this.state.score - 1,
1210 this.setState({ my_vote: Some(newVote) });
1212 let form = new CreateCommentLike({
1213 comment_id: this.props.node.comment_view.comment.id,
1215 auth: auth().unwrap(),
1218 WebSocketService.Instance.send(wsClient.likeComment(form));
1222 handleShowReportDialog(i: CommentNode) {
1223 i.setState({ showReportDialog: !i.state.showReportDialog });
1226 handleReportReasonChange(i: CommentNode, event: any) {
1227 i.setState({ reportReason: event.target.value });
1230 handleReportSubmit(i: CommentNode) {
1231 let comment = i.props.node.comment_view.comment;
1232 let form = new CreateCommentReport({
1233 comment_id: comment.id,
1234 reason: i.state.reportReason,
1235 auth: auth().unwrap(),
1237 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1239 i.setState({ showReportDialog: false });
1242 handleModRemoveShow(i: CommentNode) {
1244 showRemoveDialog: !i.state.showRemoveDialog,
1245 showBanDialog: false,
1249 handleModRemoveReasonChange(i: CommentNode, event: any) {
1250 i.setState({ removeReason: Some(event.target.value) });
1253 handleModRemoveDataChange(i: CommentNode, event: any) {
1254 i.setState({ removeData: event.target.checked });
1257 handleModRemoveSubmit(i: CommentNode) {
1258 let comment = i.props.node.comment_view.comment;
1259 let form = new RemoveComment({
1260 comment_id: comment.id,
1261 removed: !comment.removed,
1262 reason: i.state.removeReason,
1263 auth: auth().unwrap(),
1265 WebSocketService.Instance.send(wsClient.removeComment(form));
1267 i.setState({ showRemoveDialog: false });
1270 handleDistinguishClick(i: CommentNode) {
1271 let comment = i.props.node.comment_view.comment;
1272 let form = new EditComment({
1273 comment_id: comment.id,
1274 form_id: None, // TODO not sure about this
1276 distinguished: Some(!comment.distinguished),
1277 language_id: Some(comment.language_id),
1278 auth: auth().unwrap(),
1280 WebSocketService.Instance.send(wsClient.editComment(form));
1281 i.setState(i.state);
1284 isPersonMentionType(
1285 item: CommentView | PersonMentionView | CommentReplyView
1286 ): item is PersonMentionView {
1287 return (item as PersonMentionView).person_mention?.id !== undefined;
1291 item: CommentView | PersonMentionView | CommentReplyView
1292 ): item is CommentReplyView {
1293 return (item as CommentReplyView).comment_reply?.id !== undefined;
1296 handleMarkRead(i: CommentNode) {
1297 if (i.isPersonMentionType(i.props.node.comment_view)) {
1298 let form = new MarkPersonMentionAsRead({
1299 person_mention_id: i.props.node.comment_view.person_mention.id,
1300 read: !i.props.node.comment_view.person_mention.read,
1301 auth: auth().unwrap(),
1303 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1304 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1305 let form = new MarkCommentReplyAsRead({
1306 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1307 read: !i.props.node.comment_view.comment_reply.read,
1308 auth: auth().unwrap(),
1310 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1313 i.setState({ readLoading: true });
1316 handleModBanFromCommunityShow(i: CommentNode) {
1318 showBanDialog: true,
1319 banType: BanType.Community,
1320 showRemoveDialog: false,
1324 handleModBanShow(i: CommentNode) {
1326 showBanDialog: true,
1327 banType: BanType.Site,
1328 showRemoveDialog: false,
1332 handleModBanReasonChange(i: CommentNode, event: any) {
1333 i.setState({ banReason: Some(event.target.value) });
1336 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1337 i.setState({ banExpireDays: Some(event.target.value) });
1340 handleModBanFromCommunitySubmit(i: CommentNode) {
1341 i.setState({ banType: BanType.Community });
1342 i.handleModBanBothSubmit(i);
1345 handleModBanSubmit(i: CommentNode) {
1346 i.setState({ banType: BanType.Site });
1347 i.handleModBanBothSubmit(i);
1350 handleModBanBothSubmit(i: CommentNode) {
1351 let cv = i.props.node.comment_view;
1353 if (i.state.banType == BanType.Community) {
1354 // If its an unban, restore all their data
1355 let ban = !cv.creator_banned_from_community;
1357 i.setState({ removeData: false });
1359 let form = new BanFromCommunity({
1360 person_id: cv.creator.id,
1361 community_id: cv.community.id,
1363 remove_data: Some(i.state.removeData),
1364 reason: i.state.banReason,
1365 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1366 auth: auth().unwrap(),
1368 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1370 // If its an unban, restore all their data
1371 let ban = !cv.creator.banned;
1373 i.setState({ removeData: false });
1375 let form = new BanPerson({
1376 person_id: cv.creator.id,
1378 remove_data: Some(i.state.removeData),
1379 reason: i.state.banReason,
1380 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1381 auth: auth().unwrap(),
1383 WebSocketService.Instance.send(wsClient.banPerson(form));
1386 i.setState({ showBanDialog: false });
1389 handlePurgePersonShow(i: CommentNode) {
1391 showPurgeDialog: true,
1392 purgeType: PurgeType.Person,
1393 showRemoveDialog: false,
1397 handlePurgeCommentShow(i: CommentNode) {
1399 showPurgeDialog: true,
1400 purgeType: PurgeType.Comment,
1401 showRemoveDialog: false,
1405 handlePurgeReasonChange(i: CommentNode, event: any) {
1406 i.setState({ purgeReason: Some(event.target.value) });
1409 handlePurgeSubmit(i: CommentNode, event: any) {
1410 event.preventDefault();
1412 if (i.state.purgeType == PurgeType.Person) {
1413 let form = new PurgePerson({
1414 person_id: i.props.node.comment_view.creator.id,
1415 reason: i.state.purgeReason,
1416 auth: auth().unwrap(),
1418 WebSocketService.Instance.send(wsClient.purgePerson(form));
1419 } else if (i.state.purgeType == PurgeType.Comment) {
1420 let form = new PurgeComment({
1421 comment_id: i.props.node.comment_view.comment.id,
1422 reason: i.state.purgeReason,
1423 auth: auth().unwrap(),
1425 WebSocketService.Instance.send(wsClient.purgeComment(form));
1428 i.setState({ purgeLoading: true });
1431 handleShowConfirmAppointAsMod(i: CommentNode) {
1432 i.setState({ showConfirmAppointAsMod: true });
1435 handleCancelConfirmAppointAsMod(i: CommentNode) {
1436 i.setState({ showConfirmAppointAsMod: false });
1439 handleAddModToCommunity(i: CommentNode) {
1440 let cv = i.props.node.comment_view;
1441 let form = new AddModToCommunity({
1442 person_id: cv.creator.id,
1443 community_id: cv.community.id,
1444 added: !isMod(i.props.moderators, cv.creator.id),
1445 auth: auth().unwrap(),
1447 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1448 i.setState({ showConfirmAppointAsMod: false });
1451 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1452 i.setState({ showConfirmAppointAsAdmin: true });
1455 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1456 i.setState({ showConfirmAppointAsAdmin: false });
1459 handleAddAdmin(i: CommentNode) {
1460 let creatorId = i.props.node.comment_view.creator.id;
1461 let form = new AddAdmin({
1462 person_id: creatorId,
1463 added: !isAdmin(i.props.admins, creatorId),
1464 auth: auth().unwrap(),
1466 WebSocketService.Instance.send(wsClient.addAdmin(form));
1467 i.setState({ showConfirmAppointAsAdmin: false });
1470 handleShowConfirmTransferCommunity(i: CommentNode) {
1471 i.setState({ showConfirmTransferCommunity: true });
1474 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1475 i.setState({ showConfirmTransferCommunity: false });
1478 handleTransferCommunity(i: CommentNode) {
1479 let cv = i.props.node.comment_view;
1480 let form = new TransferCommunity({
1481 community_id: cv.community.id,
1482 person_id: cv.creator.id,
1483 auth: auth().unwrap(),
1485 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1486 i.setState({ showConfirmTransferCommunity: false });
1489 handleShowConfirmTransferSite(i: CommentNode) {
1490 i.setState({ showConfirmTransferSite: true });
1493 handleCancelShowConfirmTransferSite(i: CommentNode) {
1494 i.setState({ showConfirmTransferSite: false });
1497 get isCommentNew(): boolean {
1498 let now = moment.utc().subtract(10, "minutes");
1499 let then = moment.utc(this.props.node.comment_view.comment.published);
1500 return now.isBefore(then);
1503 handleCommentCollapse(i: CommentNode) {
1504 i.setState({ collapsed: !i.state.collapsed });
1508 handleViewSource(i: CommentNode) {
1509 i.setState({ viewSource: !i.state.viewSource });
1512 handleShowAdvanced(i: CommentNode) {
1513 i.setState({ showAdvanced: !i.state.showAdvanced });
1517 handleFetchChildren(i: CommentNode) {
1518 let form = new GetComments({
1519 post_id: Some(i.props.node.comment_view.post.id),
1520 parent_id: Some(i.props.node.comment_view.comment.id),
1521 max_depth: Some(commentTreeMaxDepth),
1525 type_: Some(ListingType.All),
1526 community_name: None,
1528 saved_only: Some(false),
1529 auth: auth(false).ok(),
1532 WebSocketService.Instance.send(wsClient.getComments(form));
1536 if (this.state.my_vote.unwrapOr(0) == 1) {
1538 } else if (this.state.my_vote.unwrapOr(0) == -1) {
1539 return "text-danger";
1541 return "text-muted";
1545 get pointsTippy(): string {
1546 let points = i18n.t("number_of_points", {
1547 count: this.state.score,
1548 formattedCount: this.state.score,
1551 let upvotes = i18n.t("number_of_upvotes", {
1552 count: this.state.upvotes,
1553 formattedCount: this.state.upvotes,
1556 let downvotes = i18n.t("number_of_downvotes", {
1557 count: this.state.downvotes,
1558 formattedCount: this.state.downvotes,
1561 return `${points} • ${upvotes} • ${downvotes}`;
1564 get expandText(): string {
1565 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");