1 import classNames from "classnames";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
12 CommunityModeratorView,
19 MarkCommentReplyAsRead,
20 MarkPersonMentionAsRead,
28 } from "lemmy-js-client";
29 import moment from "moment";
30 import { i18n } from "../../i18next";
36 } from "../../interfaces";
37 import { UserService, WebSocketService } from "../../services";
57 import { Icon, PurgeWarning, Spinner } from "../common/icon";
58 import { MomentTime } from "../common/moment-time";
59 import { CommunityLink } from "../community/community-link";
60 import { PersonListing } from "../person/person-listing";
61 import { CommentForm } from "./comment-form";
62 import { CommentNodes } from "./comment-nodes";
64 interface CommentNodeState {
67 showRemoveDialog: boolean;
68 removeReason?: string;
69 showBanDialog: boolean;
72 banExpireDays?: number;
74 showPurgeDialog: boolean;
77 purgeLoading: boolean;
78 showConfirmTransferSite: boolean;
79 showConfirmTransferCommunity: boolean;
80 showConfirmAppointAsMod: boolean;
81 showConfirmAppointAsAdmin: boolean;
84 showAdvanced: boolean;
85 showReportDialog: boolean;
86 reportReason?: string;
95 interface CommentNodeProps {
97 moderators?: CommunityModeratorView[];
98 admins?: PersonView[];
104 showContext?: boolean;
105 showCommunity?: boolean;
106 enableDownvotes?: boolean;
107 viewType: CommentViewType;
108 allLanguages: Language[];
109 siteLanguages: number[];
110 hideImages?: boolean;
113 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
114 state: CommentNodeState = {
117 showRemoveDialog: false,
118 showBanDialog: false,
120 banType: BanType.Community,
121 showPurgeDialog: false,
123 purgeType: PurgeType.Person,
127 showConfirmTransferSite: false,
128 showConfirmTransferCommunity: false,
129 showConfirmAppointAsMod: false,
130 showConfirmAppointAsAdmin: false,
131 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.handleReplyCancel = this.handleReplyCancel.bind(this);
144 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
145 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
148 // TODO see if there's a better way to do this, and all willReceiveProps
149 componentWillReceiveProps(nextProps: CommentNodeProps) {
150 const cv = nextProps.node.comment_view;
153 upvotes: cv.counts.upvotes,
154 downvotes: cv.counts.downvotes,
155 score: cv.counts.score,
162 const node = this.props.node;
163 const cv = this.props.node.comment_view;
165 const purgeTypeText =
166 this.state.purgeType == PurgeType.Comment
167 ? i18n.t("purge_comment")
168 : `${i18n.t("purge")} ${cv.creator.name}`;
171 canMod(cv.creator.id, this.props.moderators, this.props.admins) &&
176 this.props.moderators,
178 UserService.Instance.myUserInfo,
180 ) && cv.community.local;
182 canAdmin(cv.creator.id, this.props.admins) && cv.community.local;
183 const canAdminOnSelf =
187 UserService.Instance.myUserInfo,
189 ) && cv.community.local;
190 const isMod_ = isMod(cv.creator.id, this.props.moderators);
192 isAdmin(cv.creator.id, this.props.admins) && cv.community.local;
193 const amCommunityCreator_ = amCommunityCreator(
195 this.props.moderators
198 const borderColor = this.props.node.depth
199 ? colorList[(this.props.node.depth - 1) % colorList.length]
201 const moreRepliesBorderColor = this.props.node.depth
202 ? colorList[this.props.node.depth % colorList.length]
205 const showMoreChildren =
206 this.props.viewType == CommentViewType.Tree &&
207 !this.state.collapsed &&
208 node.children.length == 0 &&
209 node.comment_view.counts.child_count > 0;
213 className={`comment ${
214 this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
218 id={`comment-${cv.comment.id}`}
219 className={classNames(`details comment-node py-2`, {
220 "border-top border-light": !this.props.noBorder,
223 this.props.node.comment_view.comment.distinguished,
226 !this.props.noIndent && this.props.node.depth
227 ? `border-left: 2px ${borderColor} solid !important`
232 className={classNames({
233 "ml-2": !this.props.noIndent && this.props.node.depth,
236 <div className="d-flex flex-wrap align-items-center text-muted small">
238 className="btn btn-sm text-muted mr-2"
239 onClick={linkEvent(this, this.handleCommentCollapse)}
240 aria-label={this.expandText}
241 data-tippy-content={this.expandText}
243 <Icon icon={`${this.state.collapsed ? "plus" : "minus"}-square`} classes="icon-inline" />
245 <span className="mr-2">
246 <PersonListing person={cv.creator} />
248 {cv.comment.distinguished && (
249 <Icon icon="shield" inline classes={`text-danger mr-2`} />
251 {this.isPostCreator && (
252 <div className="badge badge-light d-none d-sm-inline mr-2">
257 <div className="badge d-none d-sm-inline mr-2">
262 <div className="badge d-none d-sm-inline mr-2">
266 {cv.creator.bot_account && (
267 <div className="badge d-none d-sm-inline mr-2">
268 {i18n.t("bot_account").toLowerCase()}
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 {cv.comment.language_id !== 0 && (
283 <span className="badge d-none d-sm-inline mr-2">
285 this.props.allLanguages.find(
286 lang => lang.id === cv.comment.language_id
291 {/* This is an expanding spacer for mobile */}
292 <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
296 className={`unselectable pointer ${this.scoreColor}`}
297 onClick={this.handleCommentUpvote}
298 data-tippy-content={this.pointsTippy}
301 className="mr-1 font-weight-bold"
302 aria-label={i18n.t("number_of_points", {
303 count: Number(this.state.score),
304 formattedCount: numToSI(this.state.score),
307 {numToSI(this.state.score)}
310 <span className="mr-1">•</span>
315 published={cv.comment.published}
316 updated={cv.comment.updated}
320 {/* end of user row */}
321 {this.state.showEdit && (
325 onReplyCancel={this.handleReplyCancel}
326 disabled={this.props.locked}
328 allLanguages={this.props.allLanguages}
329 siteLanguages={this.props.siteLanguages}
332 {!this.state.showEdit && !this.state.collapsed && (
334 {this.state.viewSource ? (
335 <pre>{this.commentUnlessRemoved}</pre>
339 dangerouslySetInnerHTML={
340 this.props.hideImages
341 ? mdToHtmlNoImages(this.commentUnlessRemoved)
342 : mdToHtml(this.commentUnlessRemoved)
346 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
347 {this.props.showContext && this.linkBtn()}
348 {this.props.markable && (
350 className="btn btn-link btn-animate text-muted"
351 onClick={linkEvent(this, this.handleMarkRead)}
353 this.commentReplyOrMentionRead
354 ? i18n.t("mark_as_unread")
355 : i18n.t("mark_as_read")
358 this.commentReplyOrMentionRead
359 ? i18n.t("mark_as_unread")
360 : i18n.t("mark_as_read")
363 {this.state.readLoading ? (
368 classes={`icon-inline ${
369 this.commentReplyOrMentionRead && "text-success"
375 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
378 className={`btn btn-link btn-animate ${
379 this.state.my_vote === 1 ? "text-info" : "text-muted"
381 onClick={this.handleCommentUpvote}
382 data-tippy-content={i18n.t("upvote")}
383 aria-label={i18n.t("upvote")}
384 aria-pressed={this.state.my_vote === 1}
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 === -1
401 onClick={this.handleCommentDownvote}
402 data-tippy-content={i18n.t("downvote")}
403 aria-label={i18n.t("downvote")}
404 aria-pressed={this.state.my_vote === -1}
406 <Icon icon="arrow-down1" classes="icon-inline" />
408 this.state.upvotes !== this.state.score && (
409 <span className="ml-1">
410 {numToSI(this.state.downvotes)}
416 className="btn btn-link btn-animate text-muted"
417 onClick={linkEvent(this, this.handleReplyClick)}
418 data-tippy-content={i18n.t("reply")}
419 aria-label={i18n.t("reply")}
421 <Icon icon="reply1" classes="icon-inline" />
423 {!this.state.showAdvanced ? (
425 className="btn btn-link btn-animate text-muted"
426 onClick={linkEvent(this, this.handleShowAdvanced)}
427 data-tippy-content={i18n.t("more")}
428 aria-label={i18n.t("more")}
430 <Icon icon="more-vertical" classes="icon-inline" />
434 {!this.myComment && (
436 <button className="btn btn-link btn-animate">
438 className="text-muted"
439 to={`/create_private_message/${cv.creator.id}`}
440 title={i18n.t("message").toLowerCase()}
446 className="btn btn-link btn-animate text-muted"
449 this.handleShowReportDialog
451 data-tippy-content={i18n.t(
454 aria-label={i18n.t("show_report_dialog")}
459 className="btn btn-link btn-animate text-muted"
462 this.handleBlockUserClick
464 data-tippy-content={i18n.t("block_user")}
465 aria-label={i18n.t("block_user")}
467 <Icon icon="slash" />
472 className="btn btn-link btn-animate text-muted"
475 this.handleSaveCommentClick
478 cv.saved ? i18n.t("unsave") : i18n.t("save")
481 cv.saved ? i18n.t("unsave") : i18n.t("save")
484 {this.state.saveLoading ? (
489 classes={`icon-inline ${
490 cv.saved && "text-warning"
496 className="btn btn-link btn-animate text-muted"
497 onClick={linkEvent(this, this.handleViewSource)}
498 data-tippy-content={i18n.t("view_source")}
499 aria-label={i18n.t("view_source")}
503 classes={`icon-inline ${
504 this.state.viewSource && "text-success"
511 className="btn btn-link btn-animate text-muted"
512 onClick={linkEvent(this, this.handleEditClick)}
513 data-tippy-content={i18n.t("edit")}
514 aria-label={i18n.t("edit")}
516 <Icon icon="edit" classes="icon-inline" />
519 className="btn btn-link btn-animate text-muted"
522 this.handleDeleteClick
537 classes={`icon-inline ${
538 cv.comment.deleted && "text-danger"
543 {(canModOnSelf || canAdminOnSelf) && (
545 className="btn btn-link btn-animate text-muted"
548 this.handleDistinguishClick
551 !cv.comment.distinguished
552 ? i18n.t("distinguish")
553 : i18n.t("undistinguish")
556 !cv.comment.distinguished
557 ? i18n.t("distinguish")
558 : i18n.t("undistinguish")
563 classes={`icon-inline ${
564 cv.comment.distinguished && "text-danger"
571 {/* Admins and mods can remove comments */}
572 {(canMod_ || canAdmin_) && (
574 {!cv.comment.removed ? (
576 className="btn btn-link btn-animate text-muted"
579 this.handleModRemoveShow
581 aria-label={i18n.t("remove")}
587 className="btn btn-link btn-animate text-muted"
590 this.handleModRemoveSubmit
592 aria-label={i18n.t("restore")}
599 {/* Mods can ban from community, and appoint as mods to community */}
603 (!cv.creator_banned_from_community ? (
605 className="btn btn-link btn-animate text-muted"
608 this.handleModBanFromCommunityShow
610 aria-label={i18n.t("ban_from_community")}
612 {i18n.t("ban_from_community")}
616 className="btn btn-link btn-animate text-muted"
619 this.handleModBanFromCommunitySubmit
621 aria-label={i18n.t("unban")}
626 {!cv.creator_banned_from_community &&
627 (!this.state.showConfirmAppointAsMod ? (
629 className="btn btn-link btn-animate text-muted"
632 this.handleShowConfirmAppointAsMod
636 ? i18n.t("remove_as_mod")
637 : i18n.t("appoint_as_mod")
641 ? i18n.t("remove_as_mod")
642 : i18n.t("appoint_as_mod")}
647 className="btn btn-link btn-animate text-muted"
648 aria-label={i18n.t("are_you_sure")}
650 {i18n.t("are_you_sure")}
653 className="btn btn-link btn-animate text-muted"
656 this.handleAddModToCommunity
658 aria-label={i18n.t("yes")}
663 className="btn btn-link btn-animate text-muted"
666 this.handleCancelConfirmAppointAsMod
668 aria-label={i18n.t("no")}
676 {/* Community creators and admins can transfer community to another mod */}
677 {(amCommunityCreator_ || canAdmin_) &&
680 (!this.state.showConfirmTransferCommunity ? (
682 className="btn btn-link btn-animate text-muted"
685 this.handleShowConfirmTransferCommunity
687 aria-label={i18n.t("transfer_community")}
689 {i18n.t("transfer_community")}
694 className="btn btn-link btn-animate text-muted"
695 aria-label={i18n.t("are_you_sure")}
697 {i18n.t("are_you_sure")}
700 className="btn btn-link btn-animate text-muted"
703 this.handleTransferCommunity
705 aria-label={i18n.t("yes")}
710 className="btn btn-link btn-animate text-muted"
714 .handleCancelShowConfirmTransferCommunity
716 aria-label={i18n.t("no")}
722 {/* Admins can ban from all, and appoint other admins */}
728 className="btn btn-link btn-animate text-muted"
731 this.handlePurgePersonShow
733 aria-label={i18n.t("purge_user")}
735 {i18n.t("purge_user")}
738 className="btn btn-link btn-animate text-muted"
741 this.handlePurgeCommentShow
743 aria-label={i18n.t("purge_comment")}
745 {i18n.t("purge_comment")}
748 {!isBanned(cv.creator) ? (
750 className="btn btn-link btn-animate text-muted"
753 this.handleModBanShow
755 aria-label={i18n.t("ban_from_site")}
757 {i18n.t("ban_from_site")}
761 className="btn btn-link btn-animate text-muted"
764 this.handleModBanSubmit
766 aria-label={i18n.t("unban_from_site")}
768 {i18n.t("unban_from_site")}
773 {!isBanned(cv.creator) &&
775 (!this.state.showConfirmAppointAsAdmin ? (
777 className="btn btn-link btn-animate text-muted"
780 this.handleShowConfirmAppointAsAdmin
784 ? i18n.t("remove_as_admin")
785 : i18n.t("appoint_as_admin")
789 ? i18n.t("remove_as_admin")
790 : i18n.t("appoint_as_admin")}
794 <button className="btn btn-link btn-animate text-muted">
795 {i18n.t("are_you_sure")}
798 className="btn btn-link btn-animate text-muted"
803 aria-label={i18n.t("yes")}
808 className="btn btn-link btn-animate text-muted"
811 this.handleCancelConfirmAppointAsAdmin
813 aria-label={i18n.t("no")}
826 {/* end of button group */}
831 {showMoreChildren && (
833 className={`details ml-1 comment-node py-2 ${
834 !this.props.noBorder ? "border-top border-light" : ""
836 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
839 className="btn btn-link text-muted"
840 onClick={linkEvent(this, this.handleFetchChildren)}
842 {i18n.t("x_more_replies", {
843 count: node.comment_view.counts.child_count,
844 formattedCount: numToSI(node.comment_view.counts.child_count),
850 {/* end of details */}
851 {this.state.showRemoveDialog && (
853 className="form-inline"
854 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
858 htmlFor={`mod-remove-reason-${cv.comment.id}`}
864 id={`mod-remove-reason-${cv.comment.id}`}
865 className="form-control mr-2"
866 placeholder={i18n.t("reason")}
867 value={this.state.removeReason}
868 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
872 className="btn btn-secondary"
873 aria-label={i18n.t("remove_comment")}
875 {i18n.t("remove_comment")}
879 {this.state.showReportDialog && (
881 className="form-inline"
882 onSubmit={linkEvent(this, this.handleReportSubmit)}
886 htmlFor={`report-reason-${cv.comment.id}`}
893 id={`report-reason-${cv.comment.id}`}
894 className="form-control mr-2"
895 placeholder={i18n.t("reason")}
896 value={this.state.reportReason}
897 onInput={linkEvent(this, this.handleReportReasonChange)}
901 className="btn btn-secondary"
902 aria-label={i18n.t("create_report")}
904 {i18n.t("create_report")}
908 {this.state.showBanDialog && (
909 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
910 <div className="form-group row col-12">
912 className="col-form-label"
913 htmlFor={`mod-ban-reason-${cv.comment.id}`}
919 id={`mod-ban-reason-${cv.comment.id}`}
920 className="form-control mr-2"
921 placeholder={i18n.t("reason")}
922 value={this.state.banReason}
923 onInput={linkEvent(this, this.handleModBanReasonChange)}
926 className="col-form-label"
927 htmlFor={`mod-ban-expires-${cv.comment.id}`}
933 id={`mod-ban-expires-${cv.comment.id}`}
934 className="form-control mr-2"
935 placeholder={i18n.t("number_of_days")}
936 value={this.state.banExpireDays}
937 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
939 <div className="form-group">
940 <div className="form-check">
942 className="form-check-input"
943 id="mod-ban-remove-data"
945 checked={this.state.removeData}
946 onChange={linkEvent(this, this.handleModRemoveDataChange)}
949 className="form-check-label"
950 htmlFor="mod-ban-remove-data"
951 title={i18n.t("remove_content_more")}
953 {i18n.t("remove_content")}
958 {/* TODO hold off on expires until later */}
959 {/* <div class="form-group row"> */}
960 {/* <label class="col-form-label">Expires</label> */}
961 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
963 <div className="form-group row">
966 className="btn btn-secondary"
967 aria-label={i18n.t("ban")}
969 {i18n.t("ban")} {cv.creator.name}
975 {this.state.showPurgeDialog && (
976 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
978 <label className="sr-only" htmlFor="purge-reason">
984 className="form-control my-3"
985 placeholder={i18n.t("reason")}
986 value={this.state.purgeReason}
987 onInput={linkEvent(this, this.handlePurgeReasonChange)}
989 <div className="form-group row col-12">
990 {this.state.purgeLoading ? (
995 className="btn btn-secondary"
996 aria-label={purgeTypeText}
1004 {this.state.showReply && (
1007 onReplyCancel={this.handleReplyCancel}
1008 disabled={this.props.locked}
1010 allLanguages={this.props.allLanguages}
1011 siteLanguages={this.props.siteLanguages}
1014 {!this.state.collapsed && node.children.length > 0 && (
1016 nodes={node.children}
1017 locked={this.props.locked}
1018 moderators={this.props.moderators}
1019 admins={this.props.admins}
1020 enableDownvotes={this.props.enableDownvotes}
1021 viewType={this.props.viewType}
1022 allLanguages={this.props.allLanguages}
1023 siteLanguages={this.props.siteLanguages}
1024 hideImages={this.props.hideImages}
1027 {/* A collapsed clearfix */}
1028 {this.state.collapsed && <div className="row col-12"></div>}
1033 get commentReplyOrMentionRead(): boolean {
1034 const cv = this.props.node.comment_view;
1036 if (this.isPersonMentionType(cv)) {
1037 return cv.person_mention.read;
1038 } else if (this.isCommentReplyType(cv)) {
1039 return cv.comment_reply.read;
1045 linkBtn(small = false) {
1046 const cv = this.props.node.comment_view;
1047 const classnames = classNames("btn btn-link btn-animate text-muted", {
1051 const title = this.props.showContext
1052 ? i18n.t("show_context")
1055 // The context button should show the parent comment by default
1056 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1061 className={classnames}
1062 to={`/comment/${parentCommentId}`}
1065 <Icon icon="link" classes="icon-inline" />
1068 <a className={classnames} title={title} href={cv.comment.ap_id}>
1069 <Icon icon="fedilink" classes="icon-inline" />
1080 get myComment(): boolean {
1082 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1083 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 const 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 const auth = myAuth();
1114 const blockUserForm: BlockPerson = {
1115 person_id: i.props.node.comment_view.creator.id,
1119 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1123 handleDeleteClick(i: CommentNode) {
1124 const comment = i.props.node.comment_view.comment;
1125 const auth = myAuth();
1127 const deleteForm: DeleteComment = {
1128 comment_id: comment.id,
1129 deleted: !comment.deleted,
1132 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1136 handleSaveCommentClick(i: CommentNode) {
1137 const cv = i.props.node.comment_view;
1138 const save = cv.saved == undefined ? true : !cv.saved;
1139 const auth = myAuth();
1141 const form: SaveComment = {
1142 comment_id: cv.comment.id,
1147 WebSocketService.Instance.send(wsClient.saveComment(form));
1149 i.setState({ saveLoading: true });
1153 handleReplyCancel() {
1154 this.setState({ showReply: false, showEdit: false });
1157 handleCommentUpvote(event: any) {
1158 event.preventDefault();
1159 const myVote = this.state.my_vote;
1160 const 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: newVote });
1182 const auth = myAuth();
1184 const form: CreateCommentLike = {
1185 comment_id: this.props.node.comment_view.comment.id,
1189 WebSocketService.Instance.send(wsClient.likeComment(form));
1194 handleCommentDownvote(event: any) {
1195 event.preventDefault();
1196 const myVote = this.state.my_vote;
1197 const newVote = myVote == -1 ? 0 : -1;
1201 downvotes: this.state.downvotes + 1,
1202 upvotes: this.state.upvotes - 1,
1203 score: this.state.score - 2,
1205 } else if (myVote == -1) {
1207 downvotes: this.state.downvotes - 1,
1208 score: this.state.score + 1,
1212 downvotes: this.state.downvotes + 1,
1213 score: this.state.score - 1,
1217 this.setState({ my_vote: newVote });
1219 const auth = myAuth();
1221 const form: CreateCommentLike = {
1222 comment_id: this.props.node.comment_view.comment.id,
1227 WebSocketService.Instance.send(wsClient.likeComment(form));
1232 handleShowReportDialog(i: CommentNode) {
1233 i.setState({ showReportDialog: !i.state.showReportDialog });
1236 handleReportReasonChange(i: CommentNode, event: any) {
1237 i.setState({ reportReason: event.target.value });
1240 handleReportSubmit(i: CommentNode) {
1241 const comment = i.props.node.comment_view.comment;
1242 const reason = i.state.reportReason;
1243 const auth = myAuth();
1244 if (reason && auth) {
1245 const form: CreateCommentReport = {
1246 comment_id: comment.id,
1250 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1251 i.setState({ showReportDialog: false });
1255 handleModRemoveShow(i: CommentNode) {
1257 showRemoveDialog: !i.state.showRemoveDialog,
1258 showBanDialog: false,
1262 handleModRemoveReasonChange(i: CommentNode, event: any) {
1263 i.setState({ removeReason: event.target.value });
1266 handleModRemoveDataChange(i: CommentNode, event: any) {
1267 i.setState({ removeData: event.target.checked });
1270 handleModRemoveSubmit(i: CommentNode) {
1271 const comment = i.props.node.comment_view.comment;
1272 const auth = myAuth();
1274 const form: RemoveComment = {
1275 comment_id: comment.id,
1276 removed: !comment.removed,
1277 reason: i.state.removeReason,
1280 WebSocketService.Instance.send(wsClient.removeComment(form));
1282 i.setState({ showRemoveDialog: false });
1286 handleDistinguishClick(i: CommentNode) {
1287 const comment = i.props.node.comment_view.comment;
1288 const auth = myAuth();
1290 const form: DistinguishComment = {
1291 comment_id: comment.id,
1292 distinguished: !comment.distinguished,
1295 WebSocketService.Instance.send(wsClient.editComment(form));
1296 i.setState(i.state);
1300 isPersonMentionType(
1301 item: CommentView | PersonMentionView | CommentReplyView
1302 ): item is PersonMentionView {
1303 return (item as PersonMentionView).person_mention?.id !== undefined;
1307 item: CommentView | PersonMentionView | CommentReplyView
1308 ): item is CommentReplyView {
1309 return (item as CommentReplyView).comment_reply?.id !== undefined;
1312 handleMarkRead(i: CommentNode) {
1313 const auth = myAuth();
1315 if (i.isPersonMentionType(i.props.node.comment_view)) {
1316 const form: MarkPersonMentionAsRead = {
1317 person_mention_id: i.props.node.comment_view.person_mention.id,
1318 read: !i.props.node.comment_view.person_mention.read,
1321 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1322 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1323 const form: MarkCommentReplyAsRead = {
1324 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1325 read: !i.props.node.comment_view.comment_reply.read,
1328 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1331 i.setState({ readLoading: true });
1335 handleModBanFromCommunityShow(i: CommentNode) {
1337 showBanDialog: true,
1338 banType: BanType.Community,
1339 showRemoveDialog: false,
1343 handleModBanShow(i: CommentNode) {
1345 showBanDialog: true,
1346 banType: BanType.Site,
1347 showRemoveDialog: false,
1351 handleModBanReasonChange(i: CommentNode, event: any) {
1352 i.setState({ banReason: event.target.value });
1355 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1356 i.setState({ banExpireDays: event.target.value });
1359 handleModBanFromCommunitySubmit(i: CommentNode) {
1360 i.setState({ banType: BanType.Community });
1361 i.handleModBanBothSubmit(i);
1364 handleModBanSubmit(i: CommentNode) {
1365 i.setState({ banType: BanType.Site });
1366 i.handleModBanBothSubmit(i);
1369 handleModBanBothSubmit(i: CommentNode) {
1370 const cv = i.props.node.comment_view;
1371 const auth = myAuth();
1373 if (i.state.banType == BanType.Community) {
1374 // If its an unban, restore all their data
1375 const ban = !cv.creator_banned_from_community;
1377 i.setState({ removeData: false });
1379 const form: BanFromCommunity = {
1380 person_id: cv.creator.id,
1381 community_id: cv.community.id,
1383 remove_data: i.state.removeData,
1384 reason: i.state.banReason,
1385 expires: futureDaysToUnixTime(i.state.banExpireDays),
1388 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1390 // If its an unban, restore all their data
1391 const ban = !cv.creator.banned;
1393 i.setState({ removeData: false });
1395 const form: BanPerson = {
1396 person_id: cv.creator.id,
1398 remove_data: i.state.removeData,
1399 reason: i.state.banReason,
1400 expires: futureDaysToUnixTime(i.state.banExpireDays),
1403 WebSocketService.Instance.send(wsClient.banPerson(form));
1406 i.setState({ showBanDialog: false });
1410 handlePurgePersonShow(i: CommentNode) {
1412 showPurgeDialog: true,
1413 purgeType: PurgeType.Person,
1414 showRemoveDialog: false,
1418 handlePurgeCommentShow(i: CommentNode) {
1420 showPurgeDialog: true,
1421 purgeType: PurgeType.Comment,
1422 showRemoveDialog: false,
1426 handlePurgeReasonChange(i: CommentNode, event: any) {
1427 i.setState({ purgeReason: event.target.value });
1430 handlePurgeSubmit(i: CommentNode, event: any) {
1431 event.preventDefault();
1432 const auth = myAuth();
1434 if (i.state.purgeType == PurgeType.Person) {
1435 const form: PurgePerson = {
1436 person_id: i.props.node.comment_view.creator.id,
1437 reason: i.state.purgeReason,
1440 WebSocketService.Instance.send(wsClient.purgePerson(form));
1441 } else if (i.state.purgeType == PurgeType.Comment) {
1442 const form: PurgeComment = {
1443 comment_id: i.props.node.comment_view.comment.id,
1444 reason: i.state.purgeReason,
1447 WebSocketService.Instance.send(wsClient.purgeComment(form));
1450 i.setState({ purgeLoading: true });
1454 handleShowConfirmAppointAsMod(i: CommentNode) {
1455 i.setState({ showConfirmAppointAsMod: true });
1458 handleCancelConfirmAppointAsMod(i: CommentNode) {
1459 i.setState({ showConfirmAppointAsMod: false });
1462 handleAddModToCommunity(i: CommentNode) {
1463 const cv = i.props.node.comment_view;
1464 const auth = myAuth();
1466 const form: AddModToCommunity = {
1467 person_id: cv.creator.id,
1468 community_id: cv.community.id,
1469 added: !isMod(cv.creator.id, i.props.moderators),
1472 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1473 i.setState({ showConfirmAppointAsMod: false });
1477 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1478 i.setState({ showConfirmAppointAsAdmin: true });
1481 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1482 i.setState({ showConfirmAppointAsAdmin: false });
1485 handleAddAdmin(i: CommentNode) {
1486 const auth = myAuth();
1488 const creatorId = i.props.node.comment_view.creator.id;
1489 const form: AddAdmin = {
1490 person_id: creatorId,
1491 added: !isAdmin(creatorId, i.props.admins),
1494 WebSocketService.Instance.send(wsClient.addAdmin(form));
1495 i.setState({ showConfirmAppointAsAdmin: false });
1499 handleShowConfirmTransferCommunity(i: CommentNode) {
1500 i.setState({ showConfirmTransferCommunity: true });
1503 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1504 i.setState({ showConfirmTransferCommunity: false });
1507 handleTransferCommunity(i: CommentNode) {
1508 const cv = i.props.node.comment_view;
1509 const auth = myAuth();
1511 const form: TransferCommunity = {
1512 community_id: cv.community.id,
1513 person_id: cv.creator.id,
1516 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1517 i.setState({ showConfirmTransferCommunity: false });
1521 handleShowConfirmTransferSite(i: CommentNode) {
1522 i.setState({ showConfirmTransferSite: true });
1525 handleCancelShowConfirmTransferSite(i: CommentNode) {
1526 i.setState({ showConfirmTransferSite: false });
1529 get isCommentNew(): boolean {
1530 const now = moment.utc().subtract(10, "minutes");
1531 const then = moment.utc(this.props.node.comment_view.comment.published);
1532 return now.isBefore(then);
1535 handleCommentCollapse(i: CommentNode) {
1536 i.setState({ collapsed: !i.state.collapsed });
1540 handleViewSource(i: CommentNode) {
1541 i.setState({ viewSource: !i.state.viewSource });
1544 handleShowAdvanced(i: CommentNode) {
1545 i.setState({ showAdvanced: !i.state.showAdvanced });
1549 handleFetchChildren(i: CommentNode) {
1550 const form: GetComments = {
1551 post_id: i.props.node.comment_view.post.id,
1552 parent_id: i.props.node.comment_view.comment.id,
1553 max_depth: commentTreeMaxDepth,
1557 auth: myAuth(false),
1560 WebSocketService.Instance.send(wsClient.getComments(form));
1564 if (this.state.my_vote == 1) {
1566 } else if (this.state.my_vote == -1) {
1567 return "text-danger";
1569 return "text-muted";
1573 get pointsTippy(): string {
1574 const points = i18n.t("number_of_points", {
1575 count: Number(this.state.score),
1576 formattedCount: numToSI(this.state.score),
1579 const upvotes = i18n.t("number_of_upvotes", {
1580 count: Number(this.state.upvotes),
1581 formattedCount: numToSI(this.state.upvotes),
1584 const downvotes = i18n.t("number_of_downvotes", {
1585 count: Number(this.state.downvotes),
1586 formattedCount: numToSI(this.state.downvotes),
1589 return `${points} • ${upvotes} • ${downvotes}`;
1592 get expandText(): string {
1593 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");