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