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