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