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