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