]> Untitled Git - lemmy-ui.git/blob - src/shared/components/comment/comment-node.tsx
Use http client (#1081)
[lemmy-ui.git] / src / shared / components / comment / comment-node.tsx
1 import classNames from "classnames";
2 import { Component, InfernoNode, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
4 import {
5   AddAdmin,
6   AddModToCommunity,
7   BanFromCommunity,
8   BanPerson,
9   BlockPerson,
10   CommentId,
11   CommentReplyView,
12   CommentView,
13   CommunityModeratorView,
14   CreateComment,
15   CreateCommentLike,
16   CreateCommentReport,
17   DeleteComment,
18   DistinguishComment,
19   EditComment,
20   GetComments,
21   Language,
22   MarkCommentReplyAsRead,
23   MarkPersonMentionAsRead,
24   PersonMentionView,
25   PersonView,
26   PurgeComment,
27   PurgePerson,
28   RemoveComment,
29   SaveComment,
30   TransferCommunity,
31 } from "lemmy-js-client";
32 import moment from "moment";
33 import { i18n } from "../../i18next";
34 import {
35   BanType,
36   CommentNodeI,
37   CommentViewType,
38   PurgeType,
39   VoteType,
40 } from "../../interfaces";
41 import { UserService } from "../../services";
42 import {
43   amCommunityCreator,
44   canAdmin,
45   canMod,
46   colorList,
47   commentTreeMaxDepth,
48   futureDaysToUnixTime,
49   getCommentParentId,
50   isAdmin,
51   isBanned,
52   isMod,
53   mdToHtml,
54   mdToHtmlNoImages,
55   myAuth,
56   myAuthRequired,
57   newVote,
58   numToSI,
59   setupTippy,
60   showScores,
61 } from "../../utils";
62 import { Icon, PurgeWarning, Spinner } from "../common/icon";
63 import { MomentTime } from "../common/moment-time";
64 import { CommunityLink } from "../community/community-link";
65 import { PersonListing } from "../person/person-listing";
66 import { CommentForm } from "./comment-form";
67 import { CommentNodes } from "./comment-nodes";
68
69 interface CommentNodeState {
70   showReply: boolean;
71   showEdit: boolean;
72   showRemoveDialog: boolean;
73   removeReason?: string;
74   showBanDialog: boolean;
75   removeData: boolean;
76   banReason?: string;
77   banExpireDays?: number;
78   banType: BanType;
79   showPurgeDialog: boolean;
80   purgeReason?: string;
81   purgeType: PurgeType;
82   showConfirmTransferSite: boolean;
83   showConfirmTransferCommunity: boolean;
84   showConfirmAppointAsMod: boolean;
85   showConfirmAppointAsAdmin: boolean;
86   collapsed: boolean;
87   viewSource: boolean;
88   showAdvanced: boolean;
89   showReportDialog: boolean;
90   reportReason?: string;
91   createOrEditCommentLoading: boolean;
92   upvoteLoading: boolean;
93   downvoteLoading: boolean;
94   saveLoading: boolean;
95   readLoading: boolean;
96   blockPersonLoading: boolean;
97   deleteLoading: boolean;
98   removeLoading: boolean;
99   distinguishLoading: boolean;
100   banLoading: boolean;
101   addModLoading: boolean;
102   addAdminLoading: boolean;
103   transferCommunityLoading: boolean;
104   fetchChildrenLoading: boolean;
105   reportLoading: boolean;
106   purgeLoading: boolean;
107 }
108
109 interface CommentNodeProps {
110   node: CommentNodeI;
111   moderators?: CommunityModeratorView[];
112   admins?: PersonView[];
113   noBorder?: boolean;
114   noIndent?: boolean;
115   viewOnly?: boolean;
116   locked?: boolean;
117   markable?: boolean;
118   showContext?: boolean;
119   showCommunity?: boolean;
120   enableDownvotes?: boolean;
121   viewType: CommentViewType;
122   allLanguages: Language[];
123   siteLanguages: number[];
124   hideImages?: boolean;
125   finished: Map<CommentId, boolean | undefined>;
126   onSaveComment(form: SaveComment): void;
127   onCommentReplyRead(form: MarkCommentReplyAsRead): void;
128   onPersonMentionRead(form: MarkPersonMentionAsRead): void;
129   onCreateComment(form: EditComment | CreateComment): void;
130   onEditComment(form: EditComment | CreateComment): void;
131   onCommentVote(form: CreateCommentLike): void;
132   onBlockPerson(form: BlockPerson): void;
133   onDeleteComment(form: DeleteComment): void;
134   onRemoveComment(form: RemoveComment): void;
135   onDistinguishComment(form: DistinguishComment): void;
136   onAddModToCommunity(form: AddModToCommunity): void;
137   onAddAdmin(form: AddAdmin): void;
138   onBanPersonFromCommunity(form: BanFromCommunity): void;
139   onBanPerson(form: BanPerson): void;
140   onTransferCommunity(form: TransferCommunity): void;
141   onFetchChildren?(form: GetComments): void;
142   onCommentReport(form: CreateCommentReport): void;
143   onPurgePerson(form: PurgePerson): void;
144   onPurgeComment(form: PurgeComment): void;
145 }
146
147 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
148   state: CommentNodeState = {
149     showReply: false,
150     showEdit: false,
151     showRemoveDialog: false,
152     showBanDialog: false,
153     removeData: false,
154     banType: BanType.Community,
155     showPurgeDialog: false,
156     purgeType: PurgeType.Person,
157     collapsed: false,
158     viewSource: false,
159     showAdvanced: false,
160     showConfirmTransferSite: false,
161     showConfirmTransferCommunity: false,
162     showConfirmAppointAsMod: false,
163     showConfirmAppointAsAdmin: false,
164     showReportDialog: false,
165     createOrEditCommentLoading: false,
166     upvoteLoading: false,
167     downvoteLoading: false,
168     saveLoading: false,
169     readLoading: false,
170     blockPersonLoading: false,
171     deleteLoading: false,
172     removeLoading: false,
173     distinguishLoading: false,
174     banLoading: false,
175     addModLoading: false,
176     addAdminLoading: false,
177     transferCommunityLoading: false,
178     fetchChildrenLoading: false,
179     reportLoading: false,
180     purgeLoading: false,
181   };
182
183   constructor(props: any, context: any) {
184     super(props, context);
185
186     this.handleReplyCancel = this.handleReplyCancel.bind(this);
187   }
188
189   get commentView(): CommentView {
190     return this.props.node.comment_view;
191   }
192
193   get commentId(): CommentId {
194     return this.commentView.comment.id;
195   }
196
197   componentWillReceiveProps(
198     nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
199   ): void {
200     if (this.props != nextProps) {
201       this.setState({
202         showReply: false,
203         showEdit: false,
204         showRemoveDialog: false,
205         showBanDialog: false,
206         removeData: false,
207         banType: BanType.Community,
208         showPurgeDialog: false,
209         purgeType: PurgeType.Person,
210         collapsed: false,
211         viewSource: false,
212         showAdvanced: false,
213         showConfirmTransferSite: false,
214         showConfirmTransferCommunity: false,
215         showConfirmAppointAsMod: false,
216         showConfirmAppointAsAdmin: false,
217         showReportDialog: false,
218         createOrEditCommentLoading: false,
219         upvoteLoading: false,
220         downvoteLoading: false,
221         saveLoading: false,
222         readLoading: false,
223         blockPersonLoading: false,
224         deleteLoading: false,
225         removeLoading: false,
226         distinguishLoading: false,
227         banLoading: false,
228         addModLoading: false,
229         addAdminLoading: false,
230         transferCommunityLoading: false,
231         fetchChildrenLoading: false,
232         reportLoading: false,
233         purgeLoading: false,
234       });
235     }
236   }
237
238   render() {
239     const node = this.props.node;
240     const cv = this.commentView;
241
242     const purgeTypeText =
243       this.state.purgeType == PurgeType.Comment
244         ? i18n.t("purge_comment")
245         : `${i18n.t("purge")} ${cv.creator.name}`;
246
247     const canMod_ = canMod(
248       cv.creator.id,
249       this.props.moderators,
250       this.props.admins
251     );
252     const canModOnSelf = canMod(
253       cv.creator.id,
254       this.props.moderators,
255       this.props.admins,
256       UserService.Instance.myUserInfo,
257       true
258     );
259     const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
260     const canAdminOnSelf = canAdmin(
261       cv.creator.id,
262       this.props.admins,
263       UserService.Instance.myUserInfo,
264       true
265     );
266     const isMod_ = isMod(cv.creator.id, this.props.moderators);
267     const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
268     const amCommunityCreator_ = amCommunityCreator(
269       cv.creator.id,
270       this.props.moderators
271     );
272
273     const borderColor = this.props.node.depth
274       ? colorList[(this.props.node.depth - 1) % colorList.length]
275       : colorList[0];
276     const moreRepliesBorderColor = this.props.node.depth
277       ? colorList[this.props.node.depth % colorList.length]
278       : colorList[0];
279
280     const showMoreChildren =
281       this.props.viewType == CommentViewType.Tree &&
282       !this.state.collapsed &&
283       node.children.length == 0 &&
284       node.comment_view.counts.child_count > 0;
285
286     return (
287       <div
288         className={`comment ${
289           this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
290         }`}
291       >
292         <div
293           id={`comment-${cv.comment.id}`}
294           className={classNames(`details comment-node py-2`, {
295             "border-top border-light": !this.props.noBorder,
296             mark: this.isCommentNew || this.commentView.comment.distinguished,
297           })}
298           style={
299             !this.props.noIndent && this.props.node.depth
300               ? `border-left: 2px ${borderColor} solid !important`
301               : ""
302           }
303         >
304           <div
305             className={classNames({
306               "ml-2": !this.props.noIndent && this.props.node.depth,
307             })}
308           >
309             <div className="d-flex flex-wrap align-items-center text-muted small">
310               <button
311                 className="btn btn-sm text-muted mr-2"
312                 onClick={linkEvent(this, this.handleCommentCollapse)}
313                 aria-label={this.expandText}
314                 data-tippy-content={this.expandText}
315               >
316                 <Icon
317                   icon={`${this.state.collapsed ? "plus" : "minus"}-square`}
318                   classes="icon-inline"
319                 />
320               </button>
321               <span className="mr-2">
322                 <PersonListing person={cv.creator} />
323               </span>
324               {cv.comment.distinguished && (
325                 <Icon icon="shield" inline classes={`text-danger mr-2`} />
326               )}
327               {this.isPostCreator && (
328                 <div className="badge badge-light d-none d-sm-inline mr-2">
329                   {i18n.t("creator")}
330                 </div>
331               )}
332               {isMod_ && (
333                 <div className="badge d-none d-sm-inline mr-2">
334                   {i18n.t("mod")}
335                 </div>
336               )}
337               {isAdmin_ && (
338                 <div className="badge d-none d-sm-inline mr-2">
339                   {i18n.t("admin")}
340                 </div>
341               )}
342               {cv.creator.bot_account && (
343                 <div className="badge d-none d-sm-inline mr-2">
344                   {i18n.t("bot_account").toLowerCase()}
345                 </div>
346               )}
347               {this.props.showCommunity && (
348                 <>
349                   <span className="mx-1">{i18n.t("to")}</span>
350                   <CommunityLink community={cv.community} />
351                   <span className="mx-2">•</span>
352                   <Link className="mr-2" to={`/post/${cv.post.id}`}>
353                     {cv.post.name}
354                   </Link>
355                 </>
356               )}
357               {this.linkBtn(true)}
358               {cv.comment.language_id !== 0 && (
359                 <span className="badge d-none d-sm-inline mr-2">
360                   {
361                     this.props.allLanguages.find(
362                       lang => lang.id === cv.comment.language_id
363                     )?.name
364                   }
365                 </span>
366               )}
367               {/* This is an expanding spacer for mobile */}
368               <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" />
369               {showScores() && (
370                 <>
371                   <a
372                     className={`unselectable pointer ${this.scoreColor}`}
373                     onClick={linkEvent(this, this.handleUpvote)}
374                     data-tippy-content={this.pointsTippy}
375                   >
376                     {this.state.upvoteLoading ? (
377                       <Spinner />
378                     ) : (
379                       <span
380                         className="mr-1 font-weight-bold"
381                         aria-label={i18n.t("number_of_points", {
382                           count: Number(this.commentView.counts.score),
383                           formattedCount: numToSI(
384                             this.commentView.counts.score
385                           ),
386                         })}
387                       >
388                         {numToSI(this.commentView.counts.score)}
389                       </span>
390                     )}
391                   </a>
392                   <span className="mr-1">•</span>
393                 </>
394               )}
395               <span>
396                 <MomentTime
397                   published={cv.comment.published}
398                   updated={cv.comment.updated}
399                 />
400               </span>
401             </div>
402             {/* end of user row */}
403             {this.state.showEdit && (
404               <CommentForm
405                 node={node}
406                 edit
407                 onReplyCancel={this.handleReplyCancel}
408                 disabled={this.props.locked}
409                 finished={this.props.finished.get(
410                   this.props.node.comment_view.comment.id
411                 )}
412                 focus
413                 allLanguages={this.props.allLanguages}
414                 siteLanguages={this.props.siteLanguages}
415                 onUpsertComment={this.props.onEditComment}
416               />
417             )}
418             {!this.state.showEdit && !this.state.collapsed && (
419               <div>
420                 {this.state.viewSource ? (
421                   <pre>{this.commentUnlessRemoved}</pre>
422                 ) : (
423                   <div
424                     className="md-div"
425                     dangerouslySetInnerHTML={
426                       this.props.hideImages
427                         ? mdToHtmlNoImages(this.commentUnlessRemoved)
428                         : mdToHtml(this.commentUnlessRemoved)
429                     }
430                   />
431                 )}
432                 <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
433                   {this.props.showContext && this.linkBtn()}
434                   {this.props.markable && (
435                     <button
436                       className="btn btn-link btn-animate text-muted"
437                       onClick={linkEvent(this, this.handleMarkAsRead)}
438                       data-tippy-content={
439                         this.commentReplyOrMentionRead
440                           ? i18n.t("mark_as_unread")
441                           : i18n.t("mark_as_read")
442                       }
443                       aria-label={
444                         this.commentReplyOrMentionRead
445                           ? i18n.t("mark_as_unread")
446                           : i18n.t("mark_as_read")
447                       }
448                     >
449                       {this.state.readLoading ? (
450                         <Spinner />
451                       ) : (
452                         <Icon
453                           icon="check"
454                           classes={`icon-inline ${
455                             this.commentReplyOrMentionRead && "text-success"
456                           }`}
457                         />
458                       )}
459                     </button>
460                   )}
461                   {UserService.Instance.myUserInfo && !this.props.viewOnly && (
462                     <>
463                       <button
464                         className={`btn btn-link btn-animate ${
465                           this.commentView.my_vote === 1
466                             ? "text-info"
467                             : "text-muted"
468                         }`}
469                         onClick={linkEvent(this, this.handleUpvote)}
470                         data-tippy-content={i18n.t("upvote")}
471                         aria-label={i18n.t("upvote")}
472                         aria-pressed={this.commentView.my_vote === 1}
473                       >
474                         {this.state.upvoteLoading ? (
475                           <Spinner />
476                         ) : (
477                           <>
478                             <Icon icon="arrow-up1" classes="icon-inline" />
479                             {showScores() &&
480                               this.commentView.counts.upvotes !==
481                                 this.commentView.counts.score && (
482                                 <span className="ml-1">
483                                   {numToSI(this.commentView.counts.upvotes)}
484                                 </span>
485                               )}
486                           </>
487                         )}
488                       </button>
489                       {this.props.enableDownvotes && (
490                         <button
491                           className={`btn btn-link btn-animate ${
492                             this.commentView.my_vote === -1
493                               ? "text-danger"
494                               : "text-muted"
495                           }`}
496                           onClick={linkEvent(this, this.handleDownvote)}
497                           data-tippy-content={i18n.t("downvote")}
498                           aria-label={i18n.t("downvote")}
499                           aria-pressed={this.commentView.my_vote === -1}
500                         >
501                           {this.state.downvoteLoading ? (
502                             <Spinner />
503                           ) : (
504                             <>
505                               <Icon icon="arrow-down1" classes="icon-inline" />
506                               {showScores() &&
507                                 this.commentView.counts.upvotes !==
508                                   this.commentView.counts.score && (
509                                   <span className="ml-1">
510                                     {numToSI(this.commentView.counts.downvotes)}
511                                   </span>
512                                 )}
513                             </>
514                           )}
515                         </button>
516                       )}
517                       <button
518                         className="btn btn-link btn-animate text-muted"
519                         onClick={linkEvent(this, this.handleReplyClick)}
520                         data-tippy-content={i18n.t("reply")}
521                         aria-label={i18n.t("reply")}
522                       >
523                         <Icon icon="reply1" classes="icon-inline" />
524                       </button>
525                       {!this.state.showAdvanced ? (
526                         <button
527                           className="btn btn-link btn-animate text-muted"
528                           onClick={linkEvent(this, this.handleShowAdvanced)}
529                           data-tippy-content={i18n.t("more")}
530                           aria-label={i18n.t("more")}
531                         >
532                           <Icon icon="more-vertical" classes="icon-inline" />
533                         </button>
534                       ) : (
535                         <>
536                           {!this.myComment && (
537                             <>
538                               <Link
539                                 className="btn btn-link btn-animate text-muted"
540                                 to={`/create_private_message/${cv.creator.id}`}
541                                 title={i18n.t("message").toLowerCase()}
542                               >
543                                 <Icon icon="mail" />
544                               </Link>
545                               <button
546                                 className="btn btn-link btn-animate text-muted"
547                                 onClick={linkEvent(
548                                   this,
549                                   this.handleShowReportDialog
550                                 )}
551                                 data-tippy-content={i18n.t(
552                                   "show_report_dialog"
553                                 )}
554                                 aria-label={i18n.t("show_report_dialog")}
555                               >
556                                 <Icon icon="flag" />
557                               </button>
558                               <button
559                                 className="btn btn-link btn-animate text-muted"
560                                 onClick={linkEvent(
561                                   this,
562                                   this.handleBlockPerson
563                                 )}
564                                 data-tippy-content={i18n.t("block_user")}
565                                 aria-label={i18n.t("block_user")}
566                               >
567                                 {this.state.blockPersonLoading ? (
568                                   <Spinner />
569                                 ) : (
570                                   <Icon icon="slash" />
571                                 )}
572                               </button>
573                             </>
574                           )}
575                           <button
576                             className="btn btn-link btn-animate text-muted"
577                             onClick={linkEvent(this, this.handleSaveComment)}
578                             data-tippy-content={
579                               cv.saved ? i18n.t("unsave") : i18n.t("save")
580                             }
581                             aria-label={
582                               cv.saved ? i18n.t("unsave") : i18n.t("save")
583                             }
584                           >
585                             {this.state.saveLoading ? (
586                               <Spinner />
587                             ) : (
588                               <Icon
589                                 icon="star"
590                                 classes={`icon-inline ${
591                                   cv.saved && "text-warning"
592                                 }`}
593                               />
594                             )}
595                           </button>
596                           <button
597                             className="btn btn-link btn-animate text-muted"
598                             onClick={linkEvent(this, this.handleViewSource)}
599                             data-tippy-content={i18n.t("view_source")}
600                             aria-label={i18n.t("view_source")}
601                           >
602                             <Icon
603                               icon="file-text"
604                               classes={`icon-inline ${
605                                 this.state.viewSource && "text-success"
606                               }`}
607                             />
608                           </button>
609                           {this.myComment && (
610                             <>
611                               <button
612                                 className="btn btn-link btn-animate text-muted"
613                                 onClick={linkEvent(this, this.handleEditClick)}
614                                 data-tippy-content={i18n.t("edit")}
615                                 aria-label={i18n.t("edit")}
616                               >
617                                 <Icon icon="edit" classes="icon-inline" />
618                               </button>
619                               <button
620                                 className="btn btn-link btn-animate text-muted"
621                                 onClick={linkEvent(
622                                   this,
623                                   this.handleDeleteComment
624                                 )}
625                                 data-tippy-content={
626                                   !cv.comment.deleted
627                                     ? i18n.t("delete")
628                                     : i18n.t("restore")
629                                 }
630                                 aria-label={
631                                   !cv.comment.deleted
632                                     ? i18n.t("delete")
633                                     : i18n.t("restore")
634                                 }
635                               >
636                                 {this.state.deleteLoading ? (
637                                   <Spinner />
638                                 ) : (
639                                   <Icon
640                                     icon="trash"
641                                     classes={`icon-inline ${
642                                       cv.comment.deleted && "text-danger"
643                                     }`}
644                                   />
645                                 )}
646                               </button>
647
648                               {(canModOnSelf || canAdminOnSelf) && (
649                                 <button
650                                   className="btn btn-link btn-animate text-muted"
651                                   onClick={linkEvent(
652                                     this,
653                                     this.handleDistinguishComment
654                                   )}
655                                   data-tippy-content={
656                                     !cv.comment.distinguished
657                                       ? i18n.t("distinguish")
658                                       : i18n.t("undistinguish")
659                                   }
660                                   aria-label={
661                                     !cv.comment.distinguished
662                                       ? i18n.t("distinguish")
663                                       : i18n.t("undistinguish")
664                                   }
665                                 >
666                                   <Icon
667                                     icon="shield"
668                                     classes={`icon-inline ${
669                                       cv.comment.distinguished && "text-danger"
670                                     }`}
671                                   />
672                                 </button>
673                               )}
674                             </>
675                           )}
676                           {/* Admins and mods can remove comments */}
677                           {(canMod_ || canAdmin_) && (
678                             <>
679                               {!cv.comment.removed ? (
680                                 <button
681                                   className="btn btn-link btn-animate text-muted"
682                                   onClick={linkEvent(
683                                     this,
684                                     this.handleModRemoveShow
685                                   )}
686                                   aria-label={i18n.t("remove")}
687                                 >
688                                   {i18n.t("remove")}
689                                 </button>
690                               ) : (
691                                 <button
692                                   className="btn btn-link btn-animate text-muted"
693                                   onClick={linkEvent(
694                                     this,
695                                     this.handleRemoveComment
696                                   )}
697                                   aria-label={i18n.t("restore")}
698                                 >
699                                   {this.state.removeLoading ? (
700                                     <Spinner />
701                                   ) : (
702                                     i18n.t("restore")
703                                   )}
704                                 </button>
705                               )}
706                             </>
707                           )}
708                           {/* Mods can ban from community, and appoint as mods to community */}
709                           {canMod_ && (
710                             <>
711                               {!isMod_ &&
712                                 (!cv.creator_banned_from_community ? (
713                                   <button
714                                     className="btn btn-link btn-animate text-muted"
715                                     onClick={linkEvent(
716                                       this,
717                                       this.handleModBanFromCommunityShow
718                                     )}
719                                     aria-label={i18n.t("ban_from_community")}
720                                   >
721                                     {i18n.t("ban_from_community")}
722                                   </button>
723                                 ) : (
724                                   <button
725                                     className="btn btn-link btn-animate text-muted"
726                                     onClick={linkEvent(
727                                       this,
728                                       this.handleBanPersonFromCommunity
729                                     )}
730                                     aria-label={i18n.t("unban")}
731                                   >
732                                     {this.state.banLoading ? (
733                                       <Spinner />
734                                     ) : (
735                                       i18n.t("unban")
736                                     )}
737                                   </button>
738                                 ))}
739                               {!cv.creator_banned_from_community &&
740                                 (!this.state.showConfirmAppointAsMod ? (
741                                   <button
742                                     className="btn btn-link btn-animate text-muted"
743                                     onClick={linkEvent(
744                                       this,
745                                       this.handleShowConfirmAppointAsMod
746                                     )}
747                                     aria-label={
748                                       isMod_
749                                         ? i18n.t("remove_as_mod")
750                                         : i18n.t("appoint_as_mod")
751                                     }
752                                   >
753                                     {isMod_
754                                       ? i18n.t("remove_as_mod")
755                                       : i18n.t("appoint_as_mod")}
756                                   </button>
757                                 ) : (
758                                   <>
759                                     <button
760                                       className="btn btn-link btn-animate text-muted"
761                                       aria-label={i18n.t("are_you_sure")}
762                                     >
763                                       {i18n.t("are_you_sure")}
764                                     </button>
765                                     <button
766                                       className="btn btn-link btn-animate text-muted"
767                                       onClick={linkEvent(
768                                         this,
769                                         this.handleAddModToCommunity
770                                       )}
771                                       aria-label={i18n.t("yes")}
772                                     >
773                                       {this.state.addModLoading ? (
774                                         <Spinner />
775                                       ) : (
776                                         i18n.t("yes")
777                                       )}
778                                     </button>
779                                     <button
780                                       className="btn btn-link btn-animate text-muted"
781                                       onClick={linkEvent(
782                                         this,
783                                         this.handleCancelConfirmAppointAsMod
784                                       )}
785                                       aria-label={i18n.t("no")}
786                                     >
787                                       {i18n.t("no")}
788                                     </button>
789                                   </>
790                                 ))}
791                             </>
792                           )}
793                           {/* Community creators and admins can transfer community to another mod */}
794                           {(amCommunityCreator_ || canAdmin_) &&
795                             isMod_ &&
796                             cv.creator.local &&
797                             (!this.state.showConfirmTransferCommunity ? (
798                               <button
799                                 className="btn btn-link btn-animate text-muted"
800                                 onClick={linkEvent(
801                                   this,
802                                   this.handleShowConfirmTransferCommunity
803                                 )}
804                                 aria-label={i18n.t("transfer_community")}
805                               >
806                                 {i18n.t("transfer_community")}
807                               </button>
808                             ) : (
809                               <>
810                                 <button
811                                   className="btn btn-link btn-animate text-muted"
812                                   aria-label={i18n.t("are_you_sure")}
813                                 >
814                                   {i18n.t("are_you_sure")}
815                                 </button>
816                                 <button
817                                   className="btn btn-link btn-animate text-muted"
818                                   onClick={linkEvent(
819                                     this,
820                                     this.handleTransferCommunity
821                                   )}
822                                   aria-label={i18n.t("yes")}
823                                 >
824                                   {this.state.transferCommunityLoading ? (
825                                     <Spinner />
826                                   ) : (
827                                     i18n.t("yes")
828                                   )}
829                                 </button>
830                                 <button
831                                   className="btn btn-link btn-animate text-muted"
832                                   onClick={linkEvent(
833                                     this,
834                                     this
835                                       .handleCancelShowConfirmTransferCommunity
836                                   )}
837                                   aria-label={i18n.t("no")}
838                                 >
839                                   {i18n.t("no")}
840                                 </button>
841                               </>
842                             ))}
843                           {/* Admins can ban from all, and appoint other admins */}
844                           {canAdmin_ && (
845                             <>
846                               {!isAdmin_ && (
847                                 <>
848                                   <button
849                                     className="btn btn-link btn-animate text-muted"
850                                     onClick={linkEvent(
851                                       this,
852                                       this.handlePurgePersonShow
853                                     )}
854                                     aria-label={i18n.t("purge_user")}
855                                   >
856                                     {i18n.t("purge_user")}
857                                   </button>
858                                   <button
859                                     className="btn btn-link btn-animate text-muted"
860                                     onClick={linkEvent(
861                                       this,
862                                       this.handlePurgeCommentShow
863                                     )}
864                                     aria-label={i18n.t("purge_comment")}
865                                   >
866                                     {i18n.t("purge_comment")}
867                                   </button>
868
869                                   {!isBanned(cv.creator) ? (
870                                     <button
871                                       className="btn btn-link btn-animate text-muted"
872                                       onClick={linkEvent(
873                                         this,
874                                         this.handleModBanShow
875                                       )}
876                                       aria-label={i18n.t("ban_from_site")}
877                                     >
878                                       {i18n.t("ban_from_site")}
879                                     </button>
880                                   ) : (
881                                     <button
882                                       className="btn btn-link btn-animate text-muted"
883                                       onClick={linkEvent(
884                                         this,
885                                         this.handleBanPerson
886                                       )}
887                                       aria-label={i18n.t("unban_from_site")}
888                                     >
889                                       {this.state.banLoading ? (
890                                         <Spinner />
891                                       ) : (
892                                         i18n.t("unban_from_site")
893                                       )}
894                                     </button>
895                                   )}
896                                 </>
897                               )}
898                               {!isBanned(cv.creator) &&
899                                 cv.creator.local &&
900                                 (!this.state.showConfirmAppointAsAdmin ? (
901                                   <button
902                                     className="btn btn-link btn-animate text-muted"
903                                     onClick={linkEvent(
904                                       this,
905                                       this.handleShowConfirmAppointAsAdmin
906                                     )}
907                                     aria-label={
908                                       isAdmin_
909                                         ? i18n.t("remove_as_admin")
910                                         : i18n.t("appoint_as_admin")
911                                     }
912                                   >
913                                     {isAdmin_
914                                       ? i18n.t("remove_as_admin")
915                                       : i18n.t("appoint_as_admin")}
916                                   </button>
917                                 ) : (
918                                   <>
919                                     <button className="btn btn-link btn-animate text-muted">
920                                       {i18n.t("are_you_sure")}
921                                     </button>
922                                     <button
923                                       className="btn btn-link btn-animate text-muted"
924                                       onClick={linkEvent(
925                                         this,
926                                         this.handleAddAdmin
927                                       )}
928                                       aria-label={i18n.t("yes")}
929                                     >
930                                       {this.state.addAdminLoading ? (
931                                         <Spinner />
932                                       ) : (
933                                         i18n.t("yes")
934                                       )}
935                                     </button>
936                                     <button
937                                       className="btn btn-link btn-animate text-muted"
938                                       onClick={linkEvent(
939                                         this,
940                                         this.handleCancelConfirmAppointAsAdmin
941                                       )}
942                                       aria-label={i18n.t("no")}
943                                     >
944                                       {i18n.t("no")}
945                                     </button>
946                                   </>
947                                 ))}
948                             </>
949                           )}
950                         </>
951                       )}
952                     </>
953                   )}
954                 </div>
955                 {/* end of button group */}
956               </div>
957             )}
958           </div>
959         </div>
960         {showMoreChildren && (
961           <div
962             className={`details ml-1 comment-node py-2 ${
963               !this.props.noBorder ? "border-top border-light" : ""
964             }`}
965             style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
966           >
967             <button
968               className="btn btn-link text-muted"
969               onClick={linkEvent(this, this.handleFetchChildren)}
970             >
971               {this.state.fetchChildrenLoading ? (
972                 <Spinner />
973               ) : (
974                 <>
975                   {i18n.t("x_more_replies", {
976                     count: node.comment_view.counts.child_count,
977                     formattedCount: numToSI(
978                       node.comment_view.counts.child_count
979                     ),
980                   })}{" "}
981                   âž”
982                 </>
983               )}
984             </button>
985           </div>
986         )}
987         {/* end of details */}
988         {this.state.showRemoveDialog && (
989           <form
990             className="form-inline"
991             onSubmit={linkEvent(this, this.handleRemoveComment)}
992           >
993             <label
994               className="sr-only"
995               htmlFor={`mod-remove-reason-${cv.comment.id}`}
996             >
997               {i18n.t("reason")}
998             </label>
999             <input
1000               type="text"
1001               id={`mod-remove-reason-${cv.comment.id}`}
1002               className="form-control mr-2"
1003               placeholder={i18n.t("reason")}
1004               value={this.state.removeReason}
1005               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1006             />
1007             <button
1008               type="submit"
1009               className="btn btn-secondary"
1010               aria-label={i18n.t("remove_comment")}
1011             >
1012               {i18n.t("remove_comment")}
1013             </button>
1014           </form>
1015         )}
1016         {this.state.showReportDialog && (
1017           <form
1018             className="form-inline"
1019             onSubmit={linkEvent(this, this.handleReportComment)}
1020           >
1021             <label
1022               className="sr-only"
1023               htmlFor={`report-reason-${cv.comment.id}`}
1024             >
1025               {i18n.t("reason")}
1026             </label>
1027             <input
1028               type="text"
1029               required
1030               id={`report-reason-${cv.comment.id}`}
1031               className="form-control mr-2"
1032               placeholder={i18n.t("reason")}
1033               value={this.state.reportReason}
1034               onInput={linkEvent(this, this.handleReportReasonChange)}
1035             />
1036             <button
1037               type="submit"
1038               className="btn btn-secondary"
1039               aria-label={i18n.t("create_report")}
1040             >
1041               {i18n.t("create_report")}
1042             </button>
1043           </form>
1044         )}
1045         {this.state.showBanDialog && (
1046           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1047             <div className="form-group row col-12">
1048               <label
1049                 className="col-form-label"
1050                 htmlFor={`mod-ban-reason-${cv.comment.id}`}
1051               >
1052                 {i18n.t("reason")}
1053               </label>
1054               <input
1055                 type="text"
1056                 id={`mod-ban-reason-${cv.comment.id}`}
1057                 className="form-control mr-2"
1058                 placeholder={i18n.t("reason")}
1059                 value={this.state.banReason}
1060                 onInput={linkEvent(this, this.handleModBanReasonChange)}
1061               />
1062               <label
1063                 className="col-form-label"
1064                 htmlFor={`mod-ban-expires-${cv.comment.id}`}
1065               >
1066                 {i18n.t("expires")}
1067               </label>
1068               <input
1069                 type="number"
1070                 id={`mod-ban-expires-${cv.comment.id}`}
1071                 className="form-control mr-2"
1072                 placeholder={i18n.t("number_of_days")}
1073                 value={this.state.banExpireDays}
1074                 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1075               />
1076               <div className="form-group">
1077                 <div className="form-check">
1078                   <input
1079                     className="form-check-input"
1080                     id="mod-ban-remove-data"
1081                     type="checkbox"
1082                     checked={this.state.removeData}
1083                     onChange={linkEvent(this, this.handleModRemoveDataChange)}
1084                   />
1085                   <label
1086                     className="form-check-label"
1087                     htmlFor="mod-ban-remove-data"
1088                     title={i18n.t("remove_content_more")}
1089                   >
1090                     {i18n.t("remove_content")}
1091                   </label>
1092                 </div>
1093               </div>
1094             </div>
1095             {/* TODO hold off on expires until later */}
1096             {/* <div class="form-group row"> */}
1097             {/*   <label class="col-form-label">Expires</label> */}
1098             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1099             {/* </div> */}
1100             <div className="form-group row">
1101               <button
1102                 type="submit"
1103                 className="btn btn-secondary"
1104                 aria-label={i18n.t("ban")}
1105               >
1106                 {this.state.banLoading ? (
1107                   <Spinner />
1108                 ) : (
1109                   <span>
1110                     {i18n.t("ban")} {cv.creator.name}
1111                   </span>
1112                 )}
1113               </button>
1114             </div>
1115           </form>
1116         )}
1117
1118         {this.state.showPurgeDialog && (
1119           <form onSubmit={linkEvent(this, this.handlePurgeBothSubmit)}>
1120             <PurgeWarning />
1121             <label className="sr-only" htmlFor="purge-reason">
1122               {i18n.t("reason")}
1123             </label>
1124             <input
1125               type="text"
1126               id="purge-reason"
1127               className="form-control my-3"
1128               placeholder={i18n.t("reason")}
1129               value={this.state.purgeReason}
1130               onInput={linkEvent(this, this.handlePurgeReasonChange)}
1131             />
1132             <div className="form-group row col-12">
1133               {this.state.purgeLoading ? (
1134                 <Spinner />
1135               ) : (
1136                 <button
1137                   type="submit"
1138                   className="btn btn-secondary"
1139                   aria-label={purgeTypeText}
1140                 >
1141                   {purgeTypeText}
1142                 </button>
1143               )}
1144             </div>
1145           </form>
1146         )}
1147         {this.state.showReply && (
1148           <CommentForm
1149             node={node}
1150             onReplyCancel={this.handleReplyCancel}
1151             disabled={this.props.locked}
1152             finished={this.props.finished.get(
1153               this.props.node.comment_view.comment.id
1154             )}
1155             focus
1156             allLanguages={this.props.allLanguages}
1157             siteLanguages={this.props.siteLanguages}
1158             onUpsertComment={this.props.onCreateComment}
1159           />
1160         )}
1161         {!this.state.collapsed && node.children.length > 0 && (
1162           <CommentNodes
1163             nodes={node.children}
1164             locked={this.props.locked}
1165             moderators={this.props.moderators}
1166             admins={this.props.admins}
1167             enableDownvotes={this.props.enableDownvotes}
1168             viewType={this.props.viewType}
1169             allLanguages={this.props.allLanguages}
1170             siteLanguages={this.props.siteLanguages}
1171             hideImages={this.props.hideImages}
1172             finished={this.props.finished}
1173             onCommentReplyRead={this.props.onCommentReplyRead}
1174             onPersonMentionRead={this.props.onPersonMentionRead}
1175             onCreateComment={this.props.onCreateComment}
1176             onEditComment={this.props.onEditComment}
1177             onCommentVote={this.props.onCommentVote}
1178             onBlockPerson={this.props.onBlockPerson}
1179             onSaveComment={this.props.onSaveComment}
1180             onDeleteComment={this.props.onDeleteComment}
1181             onRemoveComment={this.props.onRemoveComment}
1182             onDistinguishComment={this.props.onDistinguishComment}
1183             onAddModToCommunity={this.props.onAddModToCommunity}
1184             onAddAdmin={this.props.onAddAdmin}
1185             onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
1186             onBanPerson={this.props.onBanPerson}
1187             onTransferCommunity={this.props.onTransferCommunity}
1188             onFetchChildren={this.props.onFetchChildren}
1189             onCommentReport={this.props.onCommentReport}
1190             onPurgePerson={this.props.onPurgePerson}
1191             onPurgeComment={this.props.onPurgeComment}
1192           />
1193         )}
1194         {/* A collapsed clearfix */}
1195         {this.state.collapsed && <div className="row col-12"></div>}
1196       </div>
1197     );
1198   }
1199
1200   get commentReplyOrMentionRead(): boolean {
1201     const cv = this.commentView;
1202
1203     if (this.isPersonMentionType(cv)) {
1204       return cv.person_mention.read;
1205     } else if (this.isCommentReplyType(cv)) {
1206       return cv.comment_reply.read;
1207     } else {
1208       return false;
1209     }
1210   }
1211
1212   linkBtn(small = false) {
1213     const cv = this.commentView;
1214     const classnames = classNames("btn btn-link btn-animate text-muted", {
1215       "btn-sm": small,
1216     });
1217
1218     const title = this.props.showContext
1219       ? i18n.t("show_context")
1220       : i18n.t("link");
1221
1222     // The context button should show the parent comment by default
1223     const parentCommentId = getCommentParentId(cv.comment) ?? cv.comment.id;
1224
1225     return (
1226       <>
1227         <Link
1228           className={classnames}
1229           to={`/comment/${parentCommentId}`}
1230           title={title}
1231         >
1232           <Icon icon="link" classes="icon-inline" />
1233         </Link>
1234         {
1235           <a className={classnames} title={title} href={cv.comment.ap_id}>
1236             <Icon icon="fedilink" classes="icon-inline" />
1237           </a>
1238         }
1239       </>
1240     );
1241   }
1242
1243   get myComment(): boolean {
1244     return (
1245       UserService.Instance.myUserInfo?.local_user_view.person.id ==
1246       this.commentView.creator.id
1247     );
1248   }
1249
1250   get isPostCreator(): boolean {
1251     return this.commentView.creator.id == this.commentView.post.creator_id;
1252   }
1253
1254   get scoreColor() {
1255     if (this.commentView.my_vote == 1) {
1256       return "text-info";
1257     } else if (this.commentView.my_vote == -1) {
1258       return "text-danger";
1259     } else {
1260       return "text-muted";
1261     }
1262   }
1263
1264   get pointsTippy(): string {
1265     const points = i18n.t("number_of_points", {
1266       count: Number(this.commentView.counts.score),
1267       formattedCount: numToSI(this.commentView.counts.score),
1268     });
1269
1270     const upvotes = i18n.t("number_of_upvotes", {
1271       count: Number(this.commentView.counts.upvotes),
1272       formattedCount: numToSI(this.commentView.counts.upvotes),
1273     });
1274
1275     const downvotes = i18n.t("number_of_downvotes", {
1276       count: Number(this.commentView.counts.downvotes),
1277       formattedCount: numToSI(this.commentView.counts.downvotes),
1278     });
1279
1280     return `${points} â€¢ ${upvotes} â€¢ ${downvotes}`;
1281   }
1282
1283   get expandText(): string {
1284     return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
1285   }
1286
1287   get commentUnlessRemoved(): string {
1288     const comment = this.commentView.comment;
1289     return comment.removed
1290       ? `*${i18n.t("removed")}*`
1291       : comment.deleted
1292       ? `*${i18n.t("deleted")}*`
1293       : comment.content;
1294   }
1295
1296   handleReplyClick(i: CommentNode) {
1297     i.setState({ showReply: true });
1298   }
1299
1300   handleEditClick(i: CommentNode) {
1301     i.setState({ showEdit: true });
1302   }
1303
1304   handleReplyCancel() {
1305     this.setState({ showReply: false, showEdit: false });
1306   }
1307
1308   handleShowReportDialog(i: CommentNode) {
1309     i.setState({ showReportDialog: !i.state.showReportDialog });
1310   }
1311
1312   handleReportReasonChange(i: CommentNode, event: any) {
1313     i.setState({ reportReason: event.target.value });
1314   }
1315
1316   handleModRemoveShow(i: CommentNode) {
1317     i.setState({
1318       showRemoveDialog: !i.state.showRemoveDialog,
1319       showBanDialog: false,
1320     });
1321   }
1322
1323   handleModRemoveReasonChange(i: CommentNode, event: any) {
1324     i.setState({ removeReason: event.target.value });
1325   }
1326
1327   handleModRemoveDataChange(i: CommentNode, event: any) {
1328     i.setState({ removeData: event.target.checked });
1329   }
1330
1331   isPersonMentionType(
1332     item: CommentView | PersonMentionView | CommentReplyView
1333   ): item is PersonMentionView {
1334     return (item as PersonMentionView).person_mention?.id !== undefined;
1335   }
1336
1337   isCommentReplyType(
1338     item: CommentView | PersonMentionView | CommentReplyView
1339   ): item is CommentReplyView {
1340     return (item as CommentReplyView).comment_reply?.id !== undefined;
1341   }
1342
1343   handleModBanFromCommunityShow(i: CommentNode) {
1344     i.setState({
1345       showBanDialog: true,
1346       banType: BanType.Community,
1347       showRemoveDialog: false,
1348     });
1349   }
1350
1351   handleModBanShow(i: CommentNode) {
1352     i.setState({
1353       showBanDialog: true,
1354       banType: BanType.Site,
1355       showRemoveDialog: false,
1356     });
1357   }
1358
1359   handleModBanReasonChange(i: CommentNode, event: any) {
1360     i.setState({ banReason: event.target.value });
1361   }
1362
1363   handleModBanExpireDaysChange(i: CommentNode, event: any) {
1364     i.setState({ banExpireDays: event.target.value });
1365   }
1366
1367   handlePurgePersonShow(i: CommentNode) {
1368     i.setState({
1369       showPurgeDialog: true,
1370       purgeType: PurgeType.Person,
1371       showRemoveDialog: false,
1372     });
1373   }
1374
1375   handlePurgeCommentShow(i: CommentNode) {
1376     i.setState({
1377       showPurgeDialog: true,
1378       purgeType: PurgeType.Comment,
1379       showRemoveDialog: false,
1380     });
1381   }
1382
1383   handlePurgeReasonChange(i: CommentNode, event: any) {
1384     i.setState({ purgeReason: event.target.value });
1385   }
1386
1387   handleShowConfirmAppointAsMod(i: CommentNode) {
1388     i.setState({ showConfirmAppointAsMod: true });
1389   }
1390
1391   handleCancelConfirmAppointAsMod(i: CommentNode) {
1392     i.setState({ showConfirmAppointAsMod: false });
1393   }
1394
1395   handleShowConfirmAppointAsAdmin(i: CommentNode) {
1396     i.setState({ showConfirmAppointAsAdmin: true });
1397   }
1398
1399   handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1400     i.setState({ showConfirmAppointAsAdmin: false });
1401   }
1402
1403   handleShowConfirmTransferCommunity(i: CommentNode) {
1404     i.setState({ showConfirmTransferCommunity: true });
1405   }
1406
1407   handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1408     i.setState({ showConfirmTransferCommunity: false });
1409   }
1410
1411   handleShowConfirmTransferSite(i: CommentNode) {
1412     i.setState({ showConfirmTransferSite: true });
1413   }
1414
1415   handleCancelShowConfirmTransferSite(i: CommentNode) {
1416     i.setState({ showConfirmTransferSite: false });
1417   }
1418
1419   get isCommentNew(): boolean {
1420     const now = moment.utc().subtract(10, "minutes");
1421     const then = moment.utc(this.commentView.comment.published);
1422     return now.isBefore(then);
1423   }
1424
1425   handleCommentCollapse(i: CommentNode) {
1426     i.setState({ collapsed: !i.state.collapsed });
1427     setupTippy();
1428   }
1429
1430   handleViewSource(i: CommentNode) {
1431     i.setState({ viewSource: !i.state.viewSource });
1432   }
1433
1434   handleShowAdvanced(i: CommentNode) {
1435     i.setState({ showAdvanced: !i.state.showAdvanced });
1436     setupTippy();
1437   }
1438
1439   handleSaveComment(i: CommentNode) {
1440     i.setState({ saveLoading: true });
1441
1442     i.props.onSaveComment({
1443       comment_id: i.commentView.comment.id,
1444       save: !i.commentView.saved,
1445       auth: myAuthRequired(),
1446     });
1447   }
1448
1449   handleUpvote(i: CommentNode) {
1450     i.setState({ upvoteLoading: true });
1451     i.props.onCommentVote({
1452       comment_id: i.commentId,
1453       score: newVote(VoteType.Upvote, i.commentView.my_vote),
1454       auth: myAuthRequired(),
1455     });
1456   }
1457
1458   handleDownvote(i: CommentNode) {
1459     i.setState({ downvoteLoading: true });
1460     i.props.onCommentVote({
1461       comment_id: i.commentId,
1462       score: newVote(VoteType.Downvote, i.commentView.my_vote),
1463       auth: myAuthRequired(),
1464     });
1465   }
1466
1467   handleBlockPerson(i: CommentNode) {
1468     i.setState({ blockPersonLoading: true });
1469     i.props.onBlockPerson({
1470       person_id: i.commentView.creator.id,
1471       block: true,
1472       auth: myAuthRequired(),
1473     });
1474   }
1475
1476   handleMarkAsRead(i: CommentNode) {
1477     i.setState({ readLoading: true });
1478     const cv = i.commentView;
1479     if (i.isPersonMentionType(cv)) {
1480       i.props.onPersonMentionRead({
1481         person_mention_id: cv.person_mention.id,
1482         read: !cv.person_mention.read,
1483         auth: myAuthRequired(),
1484       });
1485     } else if (i.isCommentReplyType(cv)) {
1486       i.props.onCommentReplyRead({
1487         comment_reply_id: cv.comment_reply.id,
1488         read: !cv.comment_reply.read,
1489         auth: myAuthRequired(),
1490       });
1491     }
1492   }
1493
1494   handleDeleteComment(i: CommentNode) {
1495     i.setState({ deleteLoading: true });
1496     i.props.onDeleteComment({
1497       comment_id: i.commentId,
1498       deleted: !i.commentView.comment.deleted,
1499       auth: myAuthRequired(),
1500     });
1501   }
1502
1503   handleRemoveComment(i: CommentNode, event: any) {
1504     event.preventDefault();
1505     i.setState({ removeLoading: true });
1506     i.props.onRemoveComment({
1507       comment_id: i.commentId,
1508       removed: !i.commentView.comment.removed,
1509       auth: myAuthRequired(),
1510     });
1511   }
1512
1513   handleDistinguishComment(i: CommentNode) {
1514     i.setState({ distinguishLoading: true });
1515     i.props.onDistinguishComment({
1516       comment_id: i.commentId,
1517       distinguished: !i.commentView.comment.distinguished,
1518       auth: myAuthRequired(),
1519     });
1520   }
1521
1522   handleBanPersonFromCommunity(i: CommentNode) {
1523     i.setState({ banLoading: true });
1524     i.props.onBanPersonFromCommunity({
1525       community_id: i.commentView.community.id,
1526       person_id: i.commentView.creator.id,
1527       ban: !i.commentView.creator_banned_from_community,
1528       reason: i.state.banReason,
1529       remove_data: i.state.removeData,
1530       expires: futureDaysToUnixTime(i.state.banExpireDays),
1531       auth: myAuthRequired(),
1532     });
1533   }
1534
1535   handleBanPerson(i: CommentNode) {
1536     i.setState({ banLoading: true });
1537     i.props.onBanPerson({
1538       person_id: i.commentView.creator.id,
1539       ban: !i.commentView.creator_banned_from_community,
1540       reason: i.state.banReason,
1541       remove_data: i.state.removeData,
1542       expires: futureDaysToUnixTime(i.state.banExpireDays),
1543       auth: myAuthRequired(),
1544     });
1545   }
1546
1547   handleModBanBothSubmit(i: CommentNode, event: any) {
1548     event.preventDefault();
1549     if (i.state.banType == BanType.Community) {
1550       i.handleBanPersonFromCommunity(i);
1551     } else {
1552       i.handleBanPerson(i);
1553     }
1554   }
1555
1556   handleAddModToCommunity(i: CommentNode) {
1557     i.setState({ addModLoading: true });
1558
1559     const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
1560     i.props.onAddModToCommunity({
1561       community_id: i.commentView.community.id,
1562       person_id: i.commentView.creator.id,
1563       added,
1564       auth: myAuthRequired(),
1565     });
1566   }
1567
1568   handleAddAdmin(i: CommentNode) {
1569     i.setState({ addAdminLoading: true });
1570
1571     const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
1572     i.props.onAddAdmin({
1573       person_id: i.commentView.creator.id,
1574       added,
1575       auth: myAuthRequired(),
1576     });
1577   }
1578
1579   handleTransferCommunity(i: CommentNode) {
1580     i.setState({ transferCommunityLoading: true });
1581     i.props.onTransferCommunity({
1582       community_id: i.commentView.community.id,
1583       person_id: i.commentView.creator.id,
1584       auth: myAuthRequired(),
1585     });
1586   }
1587
1588   handleReportComment(i: CommentNode, event: any) {
1589     event.preventDefault();
1590     i.setState({ reportLoading: true });
1591     i.props.onCommentReport({
1592       comment_id: i.commentId,
1593       reason: i.state.reportReason ?? "",
1594       auth: myAuthRequired(),
1595     });
1596   }
1597
1598   handlePurgeBothSubmit(i: CommentNode, event: any) {
1599     event.preventDefault();
1600     i.setState({ purgeLoading: true });
1601
1602     if (i.state.purgeType == PurgeType.Person) {
1603       i.props.onPurgePerson({
1604         person_id: i.commentView.creator.id,
1605         reason: i.state.purgeReason,
1606         auth: myAuthRequired(),
1607       });
1608     } else {
1609       i.props.onPurgeComment({
1610         comment_id: i.commentId,
1611         reason: i.state.purgeReason,
1612         auth: myAuthRequired(),
1613       });
1614     }
1615   }
1616
1617   handleFetchChildren(i: CommentNode) {
1618     i.setState({ fetchChildrenLoading: true });
1619     i.props.onFetchChildren?.({
1620       parent_id: i.commentId,
1621       max_depth: commentTreeMaxDepth,
1622       limit: 999, // TODO
1623       type_: "All",
1624       saved_only: false,
1625       auth: myAuth(),
1626     });
1627   }
1628 }