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