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}
244 icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
245 classes="icon-inline"
248 <span className="mr-2">
249 <PersonListing person={cv.creator} />
251 {cv.comment.distinguished && (
252 <Icon icon="shield" inline classes={`text-danger mr-2`} />
254 {this.isPostCreator && (
255 <div className="badge badge-light d-none d-sm-inline mr-2">
260 <div className="badge d-none d-sm-inline mr-2">
265 <div className="badge d-none d-sm-inline mr-2">
269 {cv.creator.bot_account && (
270 <div className="badge d-none d-sm-inline mr-2">
271 {i18n.t("bot_account").toLowerCase()}
274 {this.props.showCommunity && (
276 <span className="mx-1">{i18n.t("to")}</span>
277 <CommunityLink community={cv.community} />
278 <span className="mx-2">•</span>
279 <Link className="mr-2" to={`/post/${cv.post.id}`}>
285 {cv.comment.language_id !== 0 && (
286 <span className="badge d-none d-sm-inline mr-2">
288 this.props.allLanguages.find(
289 lang => lang.id === cv.comment.language_id
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" />
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: Number(this.state.score),
307 formattedCount: numToSI(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}
332 siteLanguages={this.props.siteLanguages}
335 {!this.state.showEdit && !this.state.collapsed && (
337 {this.state.viewSource ? (
338 <pre>{this.commentUnlessRemoved}</pre>
342 dangerouslySetInnerHTML={
343 this.props.hideImages
344 ? mdToHtmlNoImages(this.commentUnlessRemoved)
345 : mdToHtml(this.commentUnlessRemoved)
349 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
350 {this.props.showContext && this.linkBtn()}
351 {this.props.markable && (
353 className="btn btn-link btn-animate text-muted"
354 onClick={linkEvent(this, this.handleMarkRead)}
356 this.commentReplyOrMentionRead
357 ? i18n.t("mark_as_unread")
358 : i18n.t("mark_as_read")
361 this.commentReplyOrMentionRead
362 ? i18n.t("mark_as_unread")
363 : i18n.t("mark_as_read")
366 {this.state.readLoading ? (
371 classes={`icon-inline ${
372 this.commentReplyOrMentionRead && "text-success"
378 {UserService.Instance.myUserInfo && !this.props.viewOnly && (
381 className={`btn btn-link btn-animate ${
382 this.state.my_vote === 1 ? "text-info" : "text-muted"
384 onClick={this.handleCommentUpvote}
385 data-tippy-content={i18n.t("upvote")}
386 aria-label={i18n.t("upvote")}
387 aria-pressed={this.state.my_vote === 1}
389 <Icon icon="arrow-up1" classes="icon-inline" />
391 this.state.upvotes !== this.state.score && (
392 <span className="ml-1">
393 {numToSI(this.state.upvotes)}
397 {this.props.enableDownvotes && (
399 className={`btn btn-link btn-animate ${
400 this.state.my_vote === -1
404 onClick={this.handleCommentDownvote}
405 data-tippy-content={i18n.t("downvote")}
406 aria-label={i18n.t("downvote")}
407 aria-pressed={this.state.my_vote === -1}
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/${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"
515 onClick={linkEvent(this, this.handleEditClick)}
516 data-tippy-content={i18n.t("edit")}
517 aria-label={i18n.t("edit")}
519 <Icon icon="edit" classes="icon-inline" />
522 className="btn btn-link btn-animate text-muted"
525 this.handleDeleteClick
540 classes={`icon-inline ${
541 cv.comment.deleted && "text-danger"
546 {(canModOnSelf || canAdminOnSelf) && (
548 className="btn btn-link btn-animate text-muted"
551 this.handleDistinguishClick
554 !cv.comment.distinguished
555 ? i18n.t("distinguish")
556 : i18n.t("undistinguish")
559 !cv.comment.distinguished
560 ? i18n.t("distinguish")
561 : i18n.t("undistinguish")
566 classes={`icon-inline ${
567 cv.comment.distinguished && "text-danger"
574 {/* Admins and mods can remove comments */}
575 {(canMod_ || canAdmin_) && (
577 {!cv.comment.removed ? (
579 className="btn btn-link btn-animate text-muted"
582 this.handleModRemoveShow
584 aria-label={i18n.t("remove")}
590 className="btn btn-link btn-animate text-muted"
593 this.handleModRemoveSubmit
595 aria-label={i18n.t("restore")}
602 {/* Mods can ban from community, and appoint as mods to community */}
606 (!cv.creator_banned_from_community ? (
608 className="btn btn-link btn-animate text-muted"
611 this.handleModBanFromCommunityShow
613 aria-label={i18n.t("ban_from_community")}
615 {i18n.t("ban_from_community")}
619 className="btn btn-link btn-animate text-muted"
622 this.handleModBanFromCommunitySubmit
624 aria-label={i18n.t("unban")}
629 {!cv.creator_banned_from_community &&
630 (!this.state.showConfirmAppointAsMod ? (
632 className="btn btn-link btn-animate text-muted"
635 this.handleShowConfirmAppointAsMod
639 ? i18n.t("remove_as_mod")
640 : i18n.t("appoint_as_mod")
644 ? i18n.t("remove_as_mod")
645 : i18n.t("appoint_as_mod")}
650 className="btn btn-link btn-animate text-muted"
651 aria-label={i18n.t("are_you_sure")}
653 {i18n.t("are_you_sure")}
656 className="btn btn-link btn-animate text-muted"
659 this.handleAddModToCommunity
661 aria-label={i18n.t("yes")}
666 className="btn btn-link btn-animate text-muted"
669 this.handleCancelConfirmAppointAsMod
671 aria-label={i18n.t("no")}
679 {/* Community creators and admins can transfer community to another mod */}
680 {(amCommunityCreator_ || canAdmin_) &&
683 (!this.state.showConfirmTransferCommunity ? (
685 className="btn btn-link btn-animate text-muted"
688 this.handleShowConfirmTransferCommunity
690 aria-label={i18n.t("transfer_community")}
692 {i18n.t("transfer_community")}
697 className="btn btn-link btn-animate text-muted"
698 aria-label={i18n.t("are_you_sure")}
700 {i18n.t("are_you_sure")}
703 className="btn btn-link btn-animate text-muted"
706 this.handleTransferCommunity
708 aria-label={i18n.t("yes")}
713 className="btn btn-link btn-animate text-muted"
717 .handleCancelShowConfirmTransferCommunity
719 aria-label={i18n.t("no")}
725 {/* Admins can ban from all, and appoint other admins */}
731 className="btn btn-link btn-animate text-muted"
734 this.handlePurgePersonShow
736 aria-label={i18n.t("purge_user")}
738 {i18n.t("purge_user")}
741 className="btn btn-link btn-animate text-muted"
744 this.handlePurgeCommentShow
746 aria-label={i18n.t("purge_comment")}
748 {i18n.t("purge_comment")}
751 {!isBanned(cv.creator) ? (
753 className="btn btn-link btn-animate text-muted"
756 this.handleModBanShow
758 aria-label={i18n.t("ban_from_site")}
760 {i18n.t("ban_from_site")}
764 className="btn btn-link btn-animate text-muted"
767 this.handleModBanSubmit
769 aria-label={i18n.t("unban_from_site")}
771 {i18n.t("unban_from_site")}
776 {!isBanned(cv.creator) &&
778 (!this.state.showConfirmAppointAsAdmin ? (
780 className="btn btn-link btn-animate text-muted"
783 this.handleShowConfirmAppointAsAdmin
787 ? i18n.t("remove_as_admin")
788 : i18n.t("appoint_as_admin")
792 ? i18n.t("remove_as_admin")
793 : i18n.t("appoint_as_admin")}
797 <button className="btn btn-link btn-animate text-muted">
798 {i18n.t("are_you_sure")}
801 className="btn btn-link btn-animate text-muted"
806 aria-label={i18n.t("yes")}
811 className="btn btn-link btn-animate text-muted"
814 this.handleCancelConfirmAppointAsAdmin
816 aria-label={i18n.t("no")}
829 {/* end of button group */}
834 {showMoreChildren && (
836 className={`details ml-1 comment-node py-2 ${
837 !this.props.noBorder ? "border-top border-light" : ""
839 style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
842 className="btn btn-link text-muted"
843 onClick={linkEvent(this, this.handleFetchChildren)}
845 {i18n.t("x_more_replies", {
846 count: node.comment_view.counts.child_count,
847 formattedCount: numToSI(node.comment_view.counts.child_count),
853 {/* end of details */}
854 {this.state.showRemoveDialog && (
856 className="form-inline"
857 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
861 htmlFor={`mod-remove-reason-${cv.comment.id}`}
867 id={`mod-remove-reason-${cv.comment.id}`}
868 className="form-control mr-2"
869 placeholder={i18n.t("reason")}
870 value={this.state.removeReason}
871 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
875 className="btn btn-secondary"
876 aria-label={i18n.t("remove_comment")}
878 {i18n.t("remove_comment")}
882 {this.state.showReportDialog && (
884 className="form-inline"
885 onSubmit={linkEvent(this, this.handleReportSubmit)}
889 htmlFor={`report-reason-${cv.comment.id}`}
896 id={`report-reason-${cv.comment.id}`}
897 className="form-control mr-2"
898 placeholder={i18n.t("reason")}
899 value={this.state.reportReason}
900 onInput={linkEvent(this, this.handleReportReasonChange)}
904 className="btn btn-secondary"
905 aria-label={i18n.t("create_report")}
907 {i18n.t("create_report")}
911 {this.state.showBanDialog && (
912 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
913 <div className="form-group row col-12">
915 className="col-form-label"
916 htmlFor={`mod-ban-reason-${cv.comment.id}`}
922 id={`mod-ban-reason-${cv.comment.id}`}
923 className="form-control mr-2"
924 placeholder={i18n.t("reason")}
925 value={this.state.banReason}
926 onInput={linkEvent(this, this.handleModBanReasonChange)}
929 className="col-form-label"
930 htmlFor={`mod-ban-expires-${cv.comment.id}`}
936 id={`mod-ban-expires-${cv.comment.id}`}
937 className="form-control mr-2"
938 placeholder={i18n.t("number_of_days")}
939 value={this.state.banExpireDays}
940 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
942 <div className="form-group">
943 <div className="form-check">
945 className="form-check-input"
946 id="mod-ban-remove-data"
948 checked={this.state.removeData}
949 onChange={linkEvent(this, this.handleModRemoveDataChange)}
952 className="form-check-label"
953 htmlFor="mod-ban-remove-data"
954 title={i18n.t("remove_content_more")}
956 {i18n.t("remove_content")}
961 {/* TODO hold off on expires until later */}
962 {/* <div class="form-group row"> */}
963 {/* <label class="col-form-label">Expires</label> */}
964 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
966 <div className="form-group row">
969 className="btn btn-secondary"
970 aria-label={i18n.t("ban")}
972 {i18n.t("ban")} {cv.creator.name}
978 {this.state.showPurgeDialog && (
979 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
981 <label className="sr-only" htmlFor="purge-reason">
987 className="form-control my-3"
988 placeholder={i18n.t("reason")}
989 value={this.state.purgeReason}
990 onInput={linkEvent(this, this.handlePurgeReasonChange)}
992 <div className="form-group row col-12">
993 {this.state.purgeLoading ? (
998 className="btn btn-secondary"
999 aria-label={purgeTypeText}
1007 {this.state.showReply && (
1010 onReplyCancel={this.handleReplyCancel}
1011 disabled={this.props.locked}
1013 allLanguages={this.props.allLanguages}
1014 siteLanguages={this.props.siteLanguages}
1017 {!this.state.collapsed && node.children.length > 0 && (
1019 nodes={node.children}
1020 locked={this.props.locked}
1021 moderators={this.props.moderators}
1022 admins={this.props.admins}
1023 enableDownvotes={this.props.enableDownvotes}
1024 viewType={this.props.viewType}
1025 allLanguages={this.props.allLanguages}
1026 siteLanguages={this.props.siteLanguages}
1027 hideImages={this.props.hideImages}
1030 {/* A collapsed clearfix */}
1031 {this.state.collapsed && <div className="row col-12"></div>}
1036 get commentReplyOrMentionRead(): boolean {
1037 const cv = this.props.node.comment_view;
1039 if (this.isPersonMentionType(cv)) {
1040 return cv.person_mention.read;
1041 } else if (this.isCommentReplyType(cv)) {
1042 return cv.comment_reply.read;
1048 linkBtn(small = false) {
1049 const cv = this.props.node.comment_view;
1050 const classnames = classNames("btn btn-link btn-animate text-muted", {
1054 const title = this.props.showContext
1055 ? i18n.t("show_context")
1058 // The context button should show the parent comment by default
1059 const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1064 className={classnames}
1065 to={`/comment/${parentCommentId}`}
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 {
1085 UserService.Instance.myUserInfo?.local_user_view.person.id ==
1086 this.props.node.comment_view.creator.id
1090 get isPostCreator(): boolean {
1092 this.props.node.comment_view.creator.id ==
1093 this.props.node.comment_view.post.creator_id
1097 get commentUnlessRemoved(): string {
1098 const comment = this.props.node.comment_view.comment;
1099 return comment.removed
1100 ? `*${i18n.t("removed")}*`
1102 ? `*${i18n.t("deleted")}*`
1106 handleReplyClick(i: CommentNode) {
1107 i.setState({ showReply: true });
1110 handleEditClick(i: CommentNode) {
1111 i.setState({ showEdit: true });
1114 handleBlockUserClick(i: CommentNode) {
1115 const auth = myAuth();
1117 const blockUserForm: BlockPerson = {
1118 person_id: i.props.node.comment_view.creator.id,
1122 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1126 handleDeleteClick(i: CommentNode) {
1127 const comment = i.props.node.comment_view.comment;
1128 const auth = myAuth();
1130 const deleteForm: DeleteComment = {
1131 comment_id: comment.id,
1132 deleted: !comment.deleted,
1135 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
1139 handleSaveCommentClick(i: CommentNode) {
1140 const cv = i.props.node.comment_view;
1141 const save = cv.saved == undefined ? true : !cv.saved;
1142 const auth = myAuth();
1144 const form: SaveComment = {
1145 comment_id: cv.comment.id,
1150 WebSocketService.Instance.send(wsClient.saveComment(form));
1152 i.setState({ saveLoading: true });
1156 handleReplyCancel() {
1157 this.setState({ showReply: false, showEdit: false });
1160 handleCommentUpvote(event: any) {
1161 event.preventDefault();
1162 const myVote = this.state.my_vote;
1163 const newVote = myVote == 1 ? 0 : 1;
1167 score: this.state.score - 1,
1168 upvotes: this.state.upvotes - 1,
1170 } else if (myVote == -1) {
1172 downvotes: this.state.downvotes - 1,
1173 upvotes: this.state.upvotes + 1,
1174 score: this.state.score + 2,
1178 score: this.state.score + 1,
1179 upvotes: this.state.upvotes + 1,
1183 this.setState({ my_vote: newVote });
1185 const auth = myAuth();
1187 const form: CreateCommentLike = {
1188 comment_id: this.props.node.comment_view.comment.id,
1192 WebSocketService.Instance.send(wsClient.likeComment(form));
1197 handleCommentDownvote(event: any) {
1198 event.preventDefault();
1199 const myVote = this.state.my_vote;
1200 const newVote = myVote == -1 ? 0 : -1;
1204 downvotes: this.state.downvotes + 1,
1205 upvotes: this.state.upvotes - 1,
1206 score: this.state.score - 2,
1208 } else if (myVote == -1) {
1210 downvotes: this.state.downvotes - 1,
1211 score: this.state.score + 1,
1215 downvotes: this.state.downvotes + 1,
1216 score: this.state.score - 1,
1220 this.setState({ my_vote: newVote });
1222 const auth = myAuth();
1224 const form: CreateCommentLike = {
1225 comment_id: this.props.node.comment_view.comment.id,
1230 WebSocketService.Instance.send(wsClient.likeComment(form));
1235 handleShowReportDialog(i: CommentNode) {
1236 i.setState({ showReportDialog: !i.state.showReportDialog });
1239 handleReportReasonChange(i: CommentNode, event: any) {
1240 i.setState({ reportReason: event.target.value });
1243 handleReportSubmit(i: CommentNode) {
1244 const comment = i.props.node.comment_view.comment;
1245 const reason = i.state.reportReason;
1246 const auth = myAuth();
1247 if (reason && auth) {
1248 const form: CreateCommentReport = {
1249 comment_id: comment.id,
1253 WebSocketService.Instance.send(wsClient.createCommentReport(form));
1254 i.setState({ showReportDialog: false });
1258 handleModRemoveShow(i: CommentNode) {
1260 showRemoveDialog: !i.state.showRemoveDialog,
1261 showBanDialog: false,
1265 handleModRemoveReasonChange(i: CommentNode, event: any) {
1266 i.setState({ removeReason: event.target.value });
1269 handleModRemoveDataChange(i: CommentNode, event: any) {
1270 i.setState({ removeData: event.target.checked });
1273 handleModRemoveSubmit(i: CommentNode) {
1274 const comment = i.props.node.comment_view.comment;
1275 const auth = myAuth();
1277 const form: RemoveComment = {
1278 comment_id: comment.id,
1279 removed: !comment.removed,
1280 reason: i.state.removeReason,
1283 WebSocketService.Instance.send(wsClient.removeComment(form));
1285 i.setState({ showRemoveDialog: false });
1289 handleDistinguishClick(i: CommentNode) {
1290 const comment = i.props.node.comment_view.comment;
1291 const auth = myAuth();
1293 const form: DistinguishComment = {
1294 comment_id: comment.id,
1295 distinguished: !comment.distinguished,
1298 WebSocketService.Instance.send(wsClient.editComment(form));
1299 i.setState(i.state);
1303 isPersonMentionType(
1304 item: CommentView | PersonMentionView | CommentReplyView
1305 ): item is PersonMentionView {
1306 return (item as PersonMentionView).person_mention?.id !== undefined;
1310 item: CommentView | PersonMentionView | CommentReplyView
1311 ): item is CommentReplyView {
1312 return (item as CommentReplyView).comment_reply?.id !== undefined;
1315 handleMarkRead(i: CommentNode) {
1316 const auth = myAuth();
1318 if (i.isPersonMentionType(i.props.node.comment_view)) {
1319 const form: MarkPersonMentionAsRead = {
1320 person_mention_id: i.props.node.comment_view.person_mention.id,
1321 read: !i.props.node.comment_view.person_mention.read,
1324 WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1325 } else if (i.isCommentReplyType(i.props.node.comment_view)) {
1326 const form: MarkCommentReplyAsRead = {
1327 comment_reply_id: i.props.node.comment_view.comment_reply.id,
1328 read: !i.props.node.comment_view.comment_reply.read,
1331 WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
1334 i.setState({ readLoading: true });
1338 handleModBanFromCommunityShow(i: CommentNode) {
1340 showBanDialog: true,
1341 banType: BanType.Community,
1342 showRemoveDialog: false,
1346 handleModBanShow(i: CommentNode) {
1348 showBanDialog: true,
1349 banType: BanType.Site,
1350 showRemoveDialog: false,
1354 handleModBanReasonChange(i: CommentNode, event: any) {
1355 i.setState({ banReason: event.target.value });
1358 handleModBanExpireDaysChange(i: CommentNode, event: any) {
1359 i.setState({ banExpireDays: event.target.value });
1362 handleModBanFromCommunitySubmit(i: CommentNode) {
1363 i.setState({ banType: BanType.Community });
1364 i.handleModBanBothSubmit(i);
1367 handleModBanSubmit(i: CommentNode) {
1368 i.setState({ banType: BanType.Site });
1369 i.handleModBanBothSubmit(i);
1372 handleModBanBothSubmit(i: CommentNode) {
1373 const cv = i.props.node.comment_view;
1374 const auth = myAuth();
1376 if (i.state.banType == BanType.Community) {
1377 // If its an unban, restore all their data
1378 const ban = !cv.creator_banned_from_community;
1380 i.setState({ removeData: false });
1382 const form: BanFromCommunity = {
1383 person_id: cv.creator.id,
1384 community_id: cv.community.id,
1386 remove_data: i.state.removeData,
1387 reason: i.state.banReason,
1388 expires: futureDaysToUnixTime(i.state.banExpireDays),
1391 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1393 // If its an unban, restore all their data
1394 const ban = !cv.creator.banned;
1396 i.setState({ removeData: false });
1398 const form: BanPerson = {
1399 person_id: cv.creator.id,
1401 remove_data: i.state.removeData,
1402 reason: i.state.banReason,
1403 expires: futureDaysToUnixTime(i.state.banExpireDays),
1406 WebSocketService.Instance.send(wsClient.banPerson(form));
1409 i.setState({ showBanDialog: false });
1413 handlePurgePersonShow(i: CommentNode) {
1415 showPurgeDialog: true,
1416 purgeType: PurgeType.Person,
1417 showRemoveDialog: false,
1421 handlePurgeCommentShow(i: CommentNode) {
1423 showPurgeDialog: true,
1424 purgeType: PurgeType.Comment,
1425 showRemoveDialog: false,
1429 handlePurgeReasonChange(i: CommentNode, event: any) {
1430 i.setState({ purgeReason: event.target.value });
1433 handlePurgeSubmit(i: CommentNode, event: any) {
1434 event.preventDefault();
1435 const auth = myAuth();
1437 if (i.state.purgeType == PurgeType.Person) {
1438 const form: PurgePerson = {
1439 person_id: i.props.node.comment_view.creator.id,
1440 reason: i.state.purgeReason,
1443 WebSocketService.Instance.send(wsClient.purgePerson(form));
1444 } else if (i.state.purgeType == PurgeType.Comment) {
1445 const form: PurgeComment = {
1446 comment_id: i.props.node.comment_view.comment.id,
1447 reason: i.state.purgeReason,
1450 WebSocketService.Instance.send(wsClient.purgeComment(form));
1453 i.setState({ purgeLoading: true });
1457 handleShowConfirmAppointAsMod(i: CommentNode) {
1458 i.setState({ showConfirmAppointAsMod: true });
1461 handleCancelConfirmAppointAsMod(i: CommentNode) {
1462 i.setState({ showConfirmAppointAsMod: false });
1465 handleAddModToCommunity(i: CommentNode) {
1466 const cv = i.props.node.comment_view;
1467 const auth = myAuth();
1469 const form: AddModToCommunity = {
1470 person_id: cv.creator.id,
1471 community_id: cv.community.id,
1472 added: !isMod(cv.creator.id, i.props.moderators),
1475 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1476 i.setState({ showConfirmAppointAsMod: false });
1480 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1481 i.setState({ showConfirmAppointAsAdmin: true });
1484 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1485 i.setState({ showConfirmAppointAsAdmin: false });
1488 handleAddAdmin(i: CommentNode) {
1489 const auth = myAuth();
1491 const creatorId = i.props.node.comment_view.creator.id;
1492 const form: AddAdmin = {
1493 person_id: creatorId,
1494 added: !isAdmin(creatorId, i.props.admins),
1497 WebSocketService.Instance.send(wsClient.addAdmin(form));
1498 i.setState({ showConfirmAppointAsAdmin: false });
1502 handleShowConfirmTransferCommunity(i: CommentNode) {
1503 i.setState({ showConfirmTransferCommunity: true });
1506 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1507 i.setState({ showConfirmTransferCommunity: false });
1510 handleTransferCommunity(i: CommentNode) {
1511 const cv = i.props.node.comment_view;
1512 const auth = myAuth();
1514 const form: TransferCommunity = {
1515 community_id: cv.community.id,
1516 person_id: cv.creator.id,
1519 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1520 i.setState({ showConfirmTransferCommunity: false });
1524 handleShowConfirmTransferSite(i: CommentNode) {
1525 i.setState({ showConfirmTransferSite: true });
1528 handleCancelShowConfirmTransferSite(i: CommentNode) {
1529 i.setState({ showConfirmTransferSite: false });
1532 get isCommentNew(): boolean {
1533 const now = moment.utc().subtract(10, "minutes");
1534 const then = moment.utc(this.props.node.comment_view.comment.published);
1535 return now.isBefore(then);
1538 handleCommentCollapse(i: CommentNode) {
1539 i.setState({ collapsed: !i.state.collapsed });
1543 handleViewSource(i: CommentNode) {
1544 i.setState({ viewSource: !i.state.viewSource });
1547 handleShowAdvanced(i: CommentNode) {
1548 i.setState({ showAdvanced: !i.state.showAdvanced });
1552 handleFetchChildren(i: CommentNode) {
1553 const form: GetComments = {
1554 post_id: i.props.node.comment_view.post.id,
1555 parent_id: i.props.node.comment_view.comment.id,
1556 max_depth: commentTreeMaxDepth,
1560 auth: myAuth(false),
1563 WebSocketService.Instance.send(wsClient.getComments(form));
1567 if (this.state.my_vote == 1) {
1569 } else if (this.state.my_vote == -1) {
1570 return "text-danger";
1572 return "text-muted";
1576 get pointsTippy(): string {
1577 const points = i18n.t("number_of_points", {
1578 count: Number(this.state.score),
1579 formattedCount: numToSI(this.state.score),
1582 const upvotes = i18n.t("number_of_upvotes", {
1583 count: Number(this.state.upvotes),
1584 formattedCount: numToSI(this.state.upvotes),
1587 const downvotes = i18n.t("number_of_downvotes", {
1588 count: Number(this.state.downvotes),
1589 formattedCount: numToSI(this.state.downvotes),
1592 return `${points} • ${upvotes} • ${downvotes}`;
1595 get expandText(): string {
1596 return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");