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