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