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