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