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