]> Untitled Git - lemmy-ui.git/blob - src/shared/components/comment/comment-node.tsx
Removing monads. Fixes #884 (#886)
[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   CommentNode as CommentNodeI,
11   CommentReplyView,
12   CommentView,
13   CommunityModeratorView,
14   CreateCommentLike,
15   CreateCommentReport,
16   DeleteComment,
17   EditComment,
18   GetComments,
19   Language,
20   ListingType,
21   MarkCommentReplyAsRead,
22   MarkPersonMentionAsRead,
23   PersonMentionView,
24   PersonViewSafe,
25   PurgeComment,
26   PurgePerson,
27   RemoveComment,
28   SaveComment,
29   TransferCommunity,
30 } from "lemmy-js-client";
31 import moment from "moment";
32 import { i18n } from "../../i18next";
33 import { BanType, CommentViewType, PurgeType } from "../../interfaces";
34 import { UserService, WebSocketService } from "../../services";
35 import {
36   amCommunityCreator,
37   canAdmin,
38   canMod,
39   colorList,
40   commentTreeMaxDepth,
41   futureDaysToUnixTime,
42   isAdmin,
43   isBanned,
44   isMod,
45   mdToHtml,
46   mdToHtmlNoImages,
47   myAuth,
48   numToSI,
49   setupTippy,
50   showScores,
51   wsClient,
52 } from "../../utils";
53 import { Icon, PurgeWarning, Spinner } from "../common/icon";
54 import { MomentTime } from "../common/moment-time";
55 import { CommunityLink } from "../community/community-link";
56 import { PersonListing } from "../person/person-listing";
57 import { CommentForm } from "./comment-form";
58 import { CommentNodes } from "./comment-nodes";
59
60 interface CommentNodeState {
61   showReply: boolean;
62   showEdit: boolean;
63   showRemoveDialog: boolean;
64   removeReason?: string;
65   showBanDialog: boolean;
66   removeData: boolean;
67   banReason?: string;
68   banExpireDays?: number;
69   banType: BanType;
70   showPurgeDialog: boolean;
71   purgeReason?: string;
72   purgeType: PurgeType;
73   purgeLoading: boolean;
74   showConfirmTransferSite: boolean;
75   showConfirmTransferCommunity: boolean;
76   showConfirmAppointAsMod: boolean;
77   showConfirmAppointAsAdmin: boolean;
78   collapsed: boolean;
79   viewSource: boolean;
80   showAdvanced: boolean;
81   showReportDialog: boolean;
82   reportReason?: string;
83   my_vote?: number;
84   score: number;
85   upvotes: number;
86   downvotes: number;
87   readLoading: boolean;
88   saveLoading: boolean;
89 }
90
91 interface CommentNodeProps {
92   node: CommentNodeI;
93   moderators?: CommunityModeratorView[];
94   admins?: PersonViewSafe[];
95   noBorder?: boolean;
96   noIndent?: boolean;
97   viewOnly?: boolean;
98   locked?: boolean;
99   markable?: boolean;
100   showContext?: boolean;
101   showCommunity?: boolean;
102   enableDownvotes?: boolean;
103   viewType: CommentViewType;
104   allLanguages: Language[];
105   siteLanguages: number[];
106   hideImages?: boolean;
107 }
108
109 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
110   state: CommentNodeState = {
111     showReply: false,
112     showEdit: false,
113     showRemoveDialog: false,
114     showBanDialog: false,
115     removeData: false,
116     banType: BanType.Community,
117     showPurgeDialog: false,
118     purgeLoading: false,
119     purgeType: PurgeType.Person,
120     collapsed: false,
121     viewSource: false,
122     showAdvanced: false,
123     showConfirmTransferSite: false,
124     showConfirmTransferCommunity: false,
125     showConfirmAppointAsMod: false,
126     showConfirmAppointAsAdmin: false,
127     showReportDialog: false,
128     my_vote: this.props.node.comment_view.my_vote,
129     score: this.props.node.comment_view.counts.score,
130     upvotes: this.props.node.comment_view.counts.upvotes,
131     downvotes: this.props.node.comment_view.counts.downvotes,
132     readLoading: false,
133     saveLoading: false,
134   };
135
136   constructor(props: any, context: any) {
137     super(props, context);
138
139     this.handleReplyCancel = this.handleReplyCancel.bind(this);
140     this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
141     this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
142   }
143
144   // TODO see if there's a better way to do this, and all willReceiveProps
145   componentWillReceiveProps(nextProps: CommentNodeProps) {
146     let cv = nextProps.node.comment_view;
147     this.setState({
148       my_vote: cv.my_vote,
149       upvotes: cv.counts.upvotes,
150       downvotes: cv.counts.downvotes,
151       score: cv.counts.score,
152       readLoading: false,
153       saveLoading: false,
154     });
155   }
156
157   render() {
158     let node = this.props.node;
159     let cv = this.props.node.comment_view;
160
161     let purgeTypeText =
162       this.state.purgeType == PurgeType.Comment
163         ? i18n.t("purge_comment")
164         : `${i18n.t("purge")} ${cv.creator.name}`;
165
166     let canMod_ = canMod(
167       cv.creator.id,
168       this.props.moderators,
169       this.props.admins
170     );
171     let canModOnSelf = canMod(
172       cv.creator.id,
173       this.props.moderators,
174       this.props.admins,
175       UserService.Instance.myUserInfo,
176       true
177     );
178     let canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
179     let canAdminOnSelf = canAdmin(
180       cv.creator.id,
181       this.props.admins,
182       UserService.Instance.myUserInfo,
183       true
184     );
185     let isMod_ = isMod(cv.creator.id, this.props.moderators);
186     let isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
187     let amCommunityCreator_ = amCommunityCreator(
188       cv.creator.id,
189       this.props.moderators
190     );
191
192     let borderColor = this.props.node.depth
193       ? colorList[(this.props.node.depth - 1) % colorList.length]
194       : colorList[0];
195     let moreRepliesBorderColor = this.props.node.depth
196       ? colorList[this.props.node.depth % colorList.length]
197       : colorList[0];
198
199     let showMoreChildren =
200       this.props.viewType == CommentViewType.Tree &&
201       !this.state.collapsed &&
202       node.children.length == 0 &&
203       node.comment_view.counts.child_count > 0;
204
205     return (
206       <div
207         className={`comment ${
208           this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
209         }`}
210       >
211         <div
212           id={`comment-${cv.comment.id}`}
213           className={classNames(`details comment-node py-2`, {
214             "border-top border-light": !this.props.noBorder,
215             mark:
216               this.isCommentNew ||
217               this.props.node.comment_view.comment.distinguished,
218           })}
219           style={
220             !this.props.noIndent && this.props.node.depth
221               ? `border-left: 2px ${borderColor} solid !important`
222               : ""
223           }
224         >
225           <div
226             className={classNames({
227               "ml-2": !this.props.noIndent && this.props.node.depth,
228             })}
229           >
230             <div className="d-flex flex-wrap align-items-center text-muted small">
231               <span className="mr-2">
232                 <PersonListing person={cv.creator} />
233               </span>
234               {cv.comment.distinguished && (
235                 <Icon icon="shield" inline classes={`text-danger mr-2`} />
236               )}
237               {isMod_ && (
238                 <div className="badge badge-light d-none d-sm-inline mr-2">
239                   {i18n.t("mod")}
240                 </div>
241               )}
242               {isAdmin_ && (
243                 <div className="badge badge-light d-none d-sm-inline mr-2">
244                   {i18n.t("admin")}
245                 </div>
246               )}
247               {this.isPostCreator && (
248                 <div className="badge badge-light d-none d-sm-inline mr-2">
249                   {i18n.t("creator")}
250                 </div>
251               )}
252               {cv.creator.bot_account && (
253                 <div className="badge badge-light d-none d-sm-inline mr-2">
254                   {i18n.t("bot_account").toLowerCase()}
255                 </div>
256               )}
257               {(cv.creator_banned_from_community || isBanned(cv.creator)) && (
258                 <div className="badge badge-danger mr-2">
259                   {i18n.t("banned")}
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: this.state.score,
298                         formattedCount: 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/recipient/${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")}
603                                   >
604                                     {i18n.t("ban")}
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: EditComment = {
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_: ListingType.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: this.state.score,
1565       formattedCount: this.state.score,
1566     });
1567
1568     let upvotes = i18n.t("number_of_upvotes", {
1569       count: this.state.upvotes,
1570       formattedCount: this.state.upvotes,
1571     });
1572
1573     let downvotes = i18n.t("number_of_downvotes", {
1574       count: this.state.downvotes,
1575       formattedCount: 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 }