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