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