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