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