]> Untitled Git - lemmy-ui.git/blob - src/shared/components/comment/comment-node.tsx
Re-organized components folder. (#339)
[lemmy-ui.git] / src / shared / components / comment / comment-node.tsx
1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
3 import {
4   AddAdmin,
5   AddModToCommunity,
6   BanFromCommunity,
7   BanPerson,
8   CommentView,
9   CommunityModeratorView,
10   CreateCommentLike,
11   DeleteComment,
12   MarkCommentAsRead,
13   MarkPersonMentionAsRead,
14   PersonMentionView,
15   PersonViewSafe,
16   RemoveComment,
17   SaveComment,
18   TransferCommunity,
19   TransferSite,
20 } from "lemmy-js-client";
21 import moment from "moment";
22 import { i18n } from "../../i18next";
23 import { BanType, CommentNode as CommentNodeI } from "../../interfaces";
24 import { UserService, WebSocketService } from "../../services";
25 import {
26   authField,
27   canMod,
28   colorList,
29   getUnixTime,
30   isMod,
31   mdToHtml,
32   setupTippy,
33   showScores,
34   wsClient,
35 } from "../../utils";
36 import { Icon, Spinner } from "../common/icon";
37 import { MomentTime } from "../common/moment-time";
38 import { CommunityLink } from "../community/community-link";
39 import { PersonListing } from "../person/person-listing";
40 import { CommentForm } from "./comment-form";
41 import { CommentNodes } from "./comment-nodes";
42
43 interface CommentNodeState {
44   showReply: boolean;
45   showEdit: boolean;
46   showRemoveDialog: boolean;
47   removeReason: string;
48   showBanDialog: boolean;
49   removeData: boolean;
50   banReason: string;
51   banExpires: string;
52   banType: BanType;
53   showConfirmTransferSite: boolean;
54   showConfirmTransferCommunity: boolean;
55   showConfirmAppointAsMod: boolean;
56   showConfirmAppointAsAdmin: boolean;
57   collapsed: boolean;
58   viewSource: boolean;
59   showAdvanced: boolean;
60   my_vote: number;
61   score: number;
62   upvotes: number;
63   downvotes: number;
64   borderColor: string;
65   readLoading: boolean;
66   saveLoading: boolean;
67 }
68
69 interface CommentNodeProps {
70   node: CommentNodeI;
71   noBorder?: boolean;
72   noIndent?: boolean;
73   viewOnly?: boolean;
74   locked?: boolean;
75   markable?: boolean;
76   showContext?: boolean;
77   moderators: CommunityModeratorView[];
78   admins: PersonViewSafe[];
79   // TODO is this necessary, can't I get it from the node itself?
80   postCreatorId?: number;
81   showCommunity?: boolean;
82   enableDownvotes: boolean;
83 }
84
85 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
86   private emptyState: CommentNodeState = {
87     showReply: false,
88     showEdit: false,
89     showRemoveDialog: false,
90     removeReason: null,
91     showBanDialog: false,
92     removeData: false,
93     banReason: null,
94     banExpires: null,
95     banType: BanType.Community,
96     collapsed: false,
97     viewSource: false,
98     showAdvanced: false,
99     showConfirmTransferSite: false,
100     showConfirmTransferCommunity: false,
101     showConfirmAppointAsMod: false,
102     showConfirmAppointAsAdmin: false,
103     my_vote: this.props.node.comment_view.my_vote,
104     score: this.props.node.comment_view.counts.score,
105     upvotes: this.props.node.comment_view.counts.upvotes,
106     downvotes: this.props.node.comment_view.counts.downvotes,
107     borderColor: this.props.node.depth
108       ? colorList[this.props.node.depth % colorList.length]
109       : colorList[0],
110     readLoading: false,
111     saveLoading: false,
112   };
113
114   constructor(props: any, context: any) {
115     super(props, context);
116
117     this.state = this.emptyState;
118     this.handleReplyCancel = this.handleReplyCancel.bind(this);
119     this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
120     this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
121   }
122
123   // TODO see if there's a better way to do this, and all willReceiveProps
124   componentWillReceiveProps(nextProps: CommentNodeProps) {
125     let cv = nextProps.node.comment_view;
126     this.state.my_vote = cv.my_vote;
127     this.state.upvotes = cv.counts.upvotes;
128     this.state.downvotes = cv.counts.downvotes;
129     this.state.score = cv.counts.score;
130     this.state.readLoading = false;
131     this.state.saveLoading = false;
132     this.setState(this.state);
133   }
134
135   render() {
136     let node = this.props.node;
137     let cv = this.props.node.comment_view;
138     return (
139       <div
140         className={`comment ${
141           cv.comment.parent_id && !this.props.noIndent ? "ml-1" : ""
142         }`}
143       >
144         <div
145           id={`comment-${cv.comment.id}`}
146           className={`details comment-node py-2 ${
147             !this.props.noBorder ? "border-top border-light" : ""
148           } ${this.isCommentNew ? "mark" : ""}`}
149           style={
150             !this.props.noIndent &&
151             cv.comment.parent_id &&
152             `border-left: 2px ${this.state.borderColor} solid !important`
153           }
154         >
155           <div
156             class={`${!this.props.noIndent && cv.comment.parent_id && "ml-2"}`}
157           >
158             <div class="d-flex flex-wrap align-items-center text-muted small">
159               <span class="mr-2">
160                 <PersonListing person={cv.creator} />
161               </span>
162
163               {this.isMod && (
164                 <div className="badge badge-light d-none d-sm-inline mr-2">
165                   {i18n.t("mod")}
166                 </div>
167               )}
168               {this.isAdmin && (
169                 <div className="badge badge-light d-none d-sm-inline mr-2">
170                   {i18n.t("admin")}
171                 </div>
172               )}
173               {this.isPostCreator && (
174                 <div className="badge badge-light d-none d-sm-inline mr-2">
175                   {i18n.t("creator")}
176                 </div>
177               )}
178               {(cv.creator_banned_from_community || cv.creator.banned) && (
179                 <div className="badge badge-danger mr-2">
180                   {i18n.t("banned")}
181                 </div>
182               )}
183               {this.props.showCommunity && (
184                 <>
185                   <span class="mx-1">{i18n.t("to")}</span>
186                   <CommunityLink community={cv.community} />
187                   <span class="mx-2">•</span>
188                   <Link className="mr-2" to={`/post/${cv.post.id}`}>
189                     {cv.post.name}
190                   </Link>
191                 </>
192               )}
193               <button
194                 class="btn btn-sm text-muted"
195                 onClick={linkEvent(this, this.handleCommentCollapse)}
196                 aria-label={this.expandText}
197                 data-tippy-content={this.expandText}
198               >
199                 {this.state.collapsed ? (
200                   <Icon icon="plus-square" classes="icon-inline" />
201                 ) : (
202                   <Icon icon="minus-square" classes="icon-inline" />
203                 )}
204               </button>
205               {this.linkBtn(true)}
206               {/* This is an expanding spacer for mobile */}
207               <div className="mr-lg-5 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
208               {showScores() && (
209                 <>
210                   <a
211                     className={`unselectable pointer ${this.scoreColor}`}
212                     onClick={linkEvent(node, this.handleCommentUpvote)}
213                     data-tippy-content={this.pointsTippy}
214                   >
215                     <span
216                       class="mr-1 font-weight-bold"
217                       aria-label={i18n.t("number_of_points", {
218                         count: this.state.score,
219                       })}
220                     >
221                       {this.state.score}
222                     </span>
223                   </a>
224                   <span className="mr-1">•</span>
225                 </>
226               )}
227               <span>
228                 <MomentTime data={cv.comment} />
229               </span>
230             </div>
231             {/* end of user row */}
232             {this.state.showEdit && (
233               <CommentForm
234                 node={node}
235                 edit
236                 onReplyCancel={this.handleReplyCancel}
237                 disabled={this.props.locked}
238                 focus
239               />
240             )}
241             {!this.state.showEdit && !this.state.collapsed && (
242               <div>
243                 {this.state.viewSource ? (
244                   <pre>{this.commentUnlessRemoved}</pre>
245                 ) : (
246                   <div
247                     className="md-div"
248                     dangerouslySetInnerHTML={mdToHtml(
249                       this.commentUnlessRemoved
250                     )}
251                   />
252                 )}
253                 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
254                   {this.props.showContext && this.linkBtn()}
255                   {this.props.markable && (
256                     <button
257                       class="btn btn-link btn-animate text-muted"
258                       onClick={linkEvent(this, this.handleMarkRead)}
259                       data-tippy-content={
260                         this.commentOrMentionRead
261                           ? i18n.t("mark_as_unread")
262                           : i18n.t("mark_as_read")
263                       }
264                       aria-label={
265                         this.commentOrMentionRead
266                           ? i18n.t("mark_as_unread")
267                           : i18n.t("mark_as_read")
268                       }
269                     >
270                       {this.state.readLoading ? (
271                         this.loadingIcon
272                       ) : (
273                         <Icon
274                           icon="check"
275                           classes={`icon-inline ${
276                             this.commentOrMentionRead && "text-success"
277                           }`}
278                         />
279                       )}
280                     </button>
281                   )}
282                   {UserService.Instance.localUserView && !this.props.viewOnly && (
283                     <>
284                       <button
285                         className={`btn btn-link btn-animate ${
286                           this.state.my_vote == 1 ? "text-info" : "text-muted"
287                         }`}
288                         onClick={linkEvent(node, this.handleCommentUpvote)}
289                         data-tippy-content={i18n.t("upvote")}
290                         aria-label={i18n.t("upvote")}
291                       >
292                         <Icon icon="arrow-up1" classes="icon-inline" />
293                         {showScores() &&
294                           this.state.upvotes !== this.state.score && (
295                             <span class="ml-1">{this.state.upvotes}</span>
296                           )}
297                       </button>
298                       {this.props.enableDownvotes && (
299                         <button
300                           className={`btn btn-link btn-animate ${
301                             this.state.my_vote == -1
302                               ? "text-danger"
303                               : "text-muted"
304                           }`}
305                           onClick={linkEvent(node, this.handleCommentDownvote)}
306                           data-tippy-content={i18n.t("downvote")}
307                           aria-label={i18n.t("downvote")}
308                         >
309                           <Icon icon="arrow-down1" classes="icon-inline" />
310                           {showScores() &&
311                             this.state.upvotes !== this.state.score && (
312                               <span class="ml-1">{this.state.downvotes}</span>
313                             )}
314                         </button>
315                       )}
316                       <button
317                         class="btn btn-link btn-animate text-muted"
318                         onClick={linkEvent(this, this.handleReplyClick)}
319                         data-tippy-content={i18n.t("reply")}
320                         aria-label={i18n.t("reply")}
321                       >
322                         <Icon icon="reply1" classes="icon-inline" />
323                       </button>
324                       {!this.state.showAdvanced ? (
325                         <button
326                           className="btn btn-link btn-animate text-muted"
327                           onClick={linkEvent(this, this.handleShowAdvanced)}
328                           data-tippy-content={i18n.t("more")}
329                           aria-label={i18n.t("more")}
330                         >
331                           <Icon icon="more-vertical" classes="icon-inline" />
332                         </button>
333                       ) : (
334                         <>
335                           {!this.myComment && (
336                             <button class="btn btn-link btn-animate">
337                               <Link
338                                 className="text-muted"
339                                 to={`/create_private_message/recipient/${cv.creator.id}`}
340                                 title={i18n.t("message").toLowerCase()}
341                               >
342                                 <Icon icon="mail" />
343                               </Link>
344                             </button>
345                           )}
346                           <button
347                             class="btn btn-link btn-animate text-muted"
348                             onClick={linkEvent(
349                               this,
350                               this.handleSaveCommentClick
351                             )}
352                             data-tippy-content={
353                               cv.saved ? i18n.t("unsave") : i18n.t("save")
354                             }
355                             aria-label={
356                               cv.saved ? i18n.t("unsave") : i18n.t("save")
357                             }
358                           >
359                             {this.state.saveLoading ? (
360                               this.loadingIcon
361                             ) : (
362                               <Icon
363                                 icon="star"
364                                 classes={`icon-inline ${
365                                   cv.saved && "text-warning"
366                                 }`}
367                               />
368                             )}
369                           </button>
370                           <button
371                             className="btn btn-link btn-animate text-muted"
372                             onClick={linkEvent(this, this.handleViewSource)}
373                             data-tippy-content={i18n.t("view_source")}
374                             aria-label={i18n.t("view_source")}
375                           >
376                             <Icon
377                               icon="file-text"
378                               classes={`icon-inline ${
379                                 this.state.viewSource && "text-success"
380                               }`}
381                             />
382                           </button>
383                           {this.myComment && (
384                             <>
385                               <button
386                                 class="btn btn-link btn-animate text-muted"
387                                 onClick={linkEvent(this, this.handleEditClick)}
388                                 data-tippy-content={i18n.t("edit")}
389                                 aria-label={i18n.t("edit")}
390                               >
391                                 <Icon icon="edit" classes="icon-inline" />
392                               </button>
393                               <button
394                                 class="btn btn-link btn-animate text-muted"
395                                 onClick={linkEvent(
396                                   this,
397                                   this.handleDeleteClick
398                                 )}
399                                 data-tippy-content={
400                                   !cv.comment.deleted
401                                     ? i18n.t("delete")
402                                     : i18n.t("restore")
403                                 }
404                                 aria-label={
405                                   !cv.comment.deleted
406                                     ? i18n.t("delete")
407                                     : i18n.t("restore")
408                                 }
409                               >
410                                 <Icon
411                                   icon="trash"
412                                   classes={`icon-inline ${
413                                     cv.comment.deleted && "text-danger"
414                                   }`}
415                                 />
416                               </button>
417                             </>
418                           )}
419                           {/* Admins and mods can remove comments */}
420                           {(this.canMod || this.canAdmin) && (
421                             <>
422                               {!cv.comment.removed ? (
423                                 <button
424                                   class="btn btn-link btn-animate text-muted"
425                                   onClick={linkEvent(
426                                     this,
427                                     this.handleModRemoveShow
428                                   )}
429                                   aria-label={i18n.t("remove")}
430                                 >
431                                   {i18n.t("remove")}
432                                 </button>
433                               ) : (
434                                 <button
435                                   class="btn btn-link btn-animate text-muted"
436                                   onClick={linkEvent(
437                                     this,
438                                     this.handleModRemoveSubmit
439                                   )}
440                                   aria-label={i18n.t("restore")}
441                                 >
442                                   {i18n.t("restore")}
443                                 </button>
444                               )}
445                             </>
446                           )}
447                           {/* Mods can ban from community, and appoint as mods to community */}
448                           {this.canMod && (
449                             <>
450                               {!this.isMod &&
451                                 (!cv.creator_banned_from_community ? (
452                                   <button
453                                     class="btn btn-link btn-animate text-muted"
454                                     onClick={linkEvent(
455                                       this,
456                                       this.handleModBanFromCommunityShow
457                                     )}
458                                     aria-label={i18n.t("ban")}
459                                   >
460                                     {i18n.t("ban")}
461                                   </button>
462                                 ) : (
463                                   <button
464                                     class="btn btn-link btn-animate text-muted"
465                                     onClick={linkEvent(
466                                       this,
467                                       this.handleModBanFromCommunitySubmit
468                                     )}
469                                     aria-label={i18n.t("unban")}
470                                   >
471                                     {i18n.t("unban")}
472                                   </button>
473                                 ))}
474                               {!cv.creator_banned_from_community &&
475                                 (!this.state.showConfirmAppointAsMod ? (
476                                   <button
477                                     class="btn btn-link btn-animate text-muted"
478                                     onClick={linkEvent(
479                                       this,
480                                       this.handleShowConfirmAppointAsMod
481                                     )}
482                                     aria-label={
483                                       this.isMod
484                                         ? i18n.t("remove_as_mod")
485                                         : i18n.t("appoint_as_mod")
486                                     }
487                                   >
488                                     {this.isMod
489                                       ? i18n.t("remove_as_mod")
490                                       : i18n.t("appoint_as_mod")}
491                                   </button>
492                                 ) : (
493                                   <>
494                                     <button
495                                       class="btn btn-link btn-animate text-muted"
496                                       aria-label={i18n.t("are_you_sure")}
497                                     >
498                                       {i18n.t("are_you_sure")}
499                                     </button>
500                                     <button
501                                       class="btn btn-link btn-animate text-muted"
502                                       onClick={linkEvent(
503                                         this,
504                                         this.handleAddModToCommunity
505                                       )}
506                                       aria-label={i18n.t("yes")}
507                                     >
508                                       {i18n.t("yes")}
509                                     </button>
510                                     <button
511                                       class="btn btn-link btn-animate text-muted"
512                                       onClick={linkEvent(
513                                         this,
514                                         this.handleCancelConfirmAppointAsMod
515                                       )}
516                                       aria-label={i18n.t("no")}
517                                     >
518                                       {i18n.t("no")}
519                                     </button>
520                                   </>
521                                 ))}
522                             </>
523                           )}
524                           {/* Community creators and admins can transfer community to another mod */}
525                           {(this.amCommunityCreator || this.canAdmin) &&
526                             this.isMod &&
527                             cv.creator.local &&
528                             (!this.state.showConfirmTransferCommunity ? (
529                               <button
530                                 class="btn btn-link btn-animate text-muted"
531                                 onClick={linkEvent(
532                                   this,
533                                   this.handleShowConfirmTransferCommunity
534                                 )}
535                                 aria-label={i18n.t("transfer_community")}
536                               >
537                                 {i18n.t("transfer_community")}
538                               </button>
539                             ) : (
540                               <>
541                                 <button
542                                   class="btn btn-link btn-animate text-muted"
543                                   aria-label={i18n.t("are_you_sure")}
544                                 >
545                                   {i18n.t("are_you_sure")}
546                                 </button>
547                                 <button
548                                   class="btn btn-link btn-animate text-muted"
549                                   onClick={linkEvent(
550                                     this,
551                                     this.handleTransferCommunity
552                                   )}
553                                   aria-label={i18n.t("yes")}
554                                 >
555                                   {i18n.t("yes")}
556                                 </button>
557                                 <button
558                                   class="btn btn-link btn-animate text-muted"
559                                   onClick={linkEvent(
560                                     this,
561                                     this
562                                       .handleCancelShowConfirmTransferCommunity
563                                   )}
564                                   aria-label={i18n.t("no")}
565                                 >
566                                   {i18n.t("no")}
567                                 </button>
568                               </>
569                             ))}
570                           {/* Admins can ban from all, and appoint other admins */}
571                           {this.canAdmin && (
572                             <>
573                               {!this.isAdmin &&
574                                 (!cv.creator.banned ? (
575                                   <button
576                                     class="btn btn-link btn-animate text-muted"
577                                     onClick={linkEvent(
578                                       this,
579                                       this.handleModBanShow
580                                     )}
581                                     aria-label={i18n.t("ban_from_site")}
582                                   >
583                                     {i18n.t("ban_from_site")}
584                                   </button>
585                                 ) : (
586                                   <button
587                                     class="btn btn-link btn-animate text-muted"
588                                     onClick={linkEvent(
589                                       this,
590                                       this.handleModBanSubmit
591                                     )}
592                                     aria-label={i18n.t("unban_from_site")}
593                                   >
594                                     {i18n.t("unban_from_site")}
595                                   </button>
596                                 ))}
597                               {!cv.creator.banned &&
598                                 cv.creator.local &&
599                                 (!this.state.showConfirmAppointAsAdmin ? (
600                                   <button
601                                     class="btn btn-link btn-animate text-muted"
602                                     onClick={linkEvent(
603                                       this,
604                                       this.handleShowConfirmAppointAsAdmin
605                                     )}
606                                     aria-label={
607                                       this.isAdmin
608                                         ? i18n.t("remove_as_admin")
609                                         : i18n.t("appoint_as_admin")
610                                     }
611                                   >
612                                     {this.isAdmin
613                                       ? i18n.t("remove_as_admin")
614                                       : i18n.t("appoint_as_admin")}
615                                   </button>
616                                 ) : (
617                                   <>
618                                     <button class="btn btn-link btn-animate text-muted">
619                                       {i18n.t("are_you_sure")}
620                                     </button>
621                                     <button
622                                       class="btn btn-link btn-animate text-muted"
623                                       onClick={linkEvent(
624                                         this,
625                                         this.handleAddAdmin
626                                       )}
627                                       aria-label={i18n.t("yes")}
628                                     >
629                                       {i18n.t("yes")}
630                                     </button>
631                                     <button
632                                       class="btn btn-link btn-animate text-muted"
633                                       onClick={linkEvent(
634                                         this,
635                                         this.handleCancelConfirmAppointAsAdmin
636                                       )}
637                                       aria-label={i18n.t("no")}
638                                     >
639                                       {i18n.t("no")}
640                                     </button>
641                                   </>
642                                 ))}
643                             </>
644                           )}
645                           {/* Site Creator can transfer to another admin */}
646                           {this.amSiteCreator &&
647                             this.isAdmin &&
648                             cv.creator.local &&
649                             (!this.state.showConfirmTransferSite ? (
650                               <button
651                                 class="btn btn-link btn-animate text-muted"
652                                 onClick={linkEvent(
653                                   this,
654                                   this.handleShowConfirmTransferSite
655                                 )}
656                                 aria-label={i18n.t("transfer_site")}
657                               >
658                                 {i18n.t("transfer_site")}
659                               </button>
660                             ) : (
661                               <>
662                                 <button
663                                   class="btn btn-link btn-animate text-muted"
664                                   aria-label={i18n.t("are_you_sure")}
665                                 >
666                                   {i18n.t("are_you_sure")}
667                                 </button>
668                                 <button
669                                   class="btn btn-link btn-animate text-muted"
670                                   onClick={linkEvent(
671                                     this,
672                                     this.handleTransferSite
673                                   )}
674                                   aria-label={i18n.t("yes")}
675                                 >
676                                   {i18n.t("yes")}
677                                 </button>
678                                 <button
679                                   class="btn btn-link btn-animate text-muted"
680                                   onClick={linkEvent(
681                                     this,
682                                     this.handleCancelShowConfirmTransferSite
683                                   )}
684                                   aria-label={i18n.t("no")}
685                                 >
686                                   {i18n.t("no")}
687                                 </button>
688                               </>
689                             ))}
690                         </>
691                       )}
692                     </>
693                   )}
694                 </div>
695                 {/* end of button group */}
696               </div>
697             )}
698           </div>
699         </div>
700         {/* end of details */}
701         {this.state.showRemoveDialog && (
702           <form
703             class="form-inline"
704             onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
705           >
706             <label
707               class="sr-only"
708               htmlFor={`mod-remove-reason-${cv.comment.id}`}
709             >
710               {i18n.t("reason")}
711             </label>
712             <input
713               type="text"
714               id={`mod-remove-reason-${cv.comment.id}`}
715               class="form-control mr-2"
716               placeholder={i18n.t("reason")}
717               value={this.state.removeReason}
718               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
719             />
720             <button
721               type="submit"
722               class="btn btn-secondary"
723               aria-label={i18n.t("remove_comment")}
724             >
725               {i18n.t("remove_comment")}
726             </button>
727           </form>
728         )}
729         {this.state.showBanDialog && (
730           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
731             <div class="form-group row">
732               <label
733                 class="col-form-label"
734                 htmlFor={`mod-ban-reason-${cv.comment.id}`}
735               >
736                 {i18n.t("reason")}
737               </label>
738               <input
739                 type="text"
740                 id={`mod-ban-reason-${cv.comment.id}`}
741                 class="form-control mr-2"
742                 placeholder={i18n.t("reason")}
743                 value={this.state.banReason}
744                 onInput={linkEvent(this, this.handleModBanReasonChange)}
745               />
746               <div class="form-group">
747                 <div class="form-check">
748                   <input
749                     class="form-check-input"
750                     id="mod-ban-remove-data"
751                     type="checkbox"
752                     checked={this.state.removeData}
753                     onChange={linkEvent(this, this.handleModRemoveDataChange)}
754                   />
755                   <label class="form-check-label" htmlFor="mod-ban-remove-data">
756                     {i18n.t("remove_posts_comments")}
757                   </label>
758                 </div>
759               </div>
760             </div>
761             {/* TODO hold off on expires until later */}
762             {/* <div class="form-group row"> */}
763             {/*   <label class="col-form-label">Expires</label> */}
764             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
765             {/* </div> */}
766             <div class="form-group row">
767               <button
768                 type="submit"
769                 class="btn btn-secondary"
770                 aria-label={i18n.t("ban")}
771               >
772                 {i18n.t("ban")} {cv.creator.name}
773               </button>
774             </div>
775           </form>
776         )}
777         {this.state.showReply && (
778           <CommentForm
779             node={node}
780             onReplyCancel={this.handleReplyCancel}
781             disabled={this.props.locked}
782             focus
783           />
784         )}
785         {node.children && !this.state.collapsed && (
786           <CommentNodes
787             nodes={node.children}
788             locked={this.props.locked}
789             moderators={this.props.moderators}
790             admins={this.props.admins}
791             postCreatorId={this.props.postCreatorId}
792             enableDownvotes={this.props.enableDownvotes}
793           />
794         )}
795         {/* A collapsed clearfix */}
796         {this.state.collapsed && <div class="row col-12"></div>}
797       </div>
798     );
799   }
800
801   get commentOrMentionRead() {
802     let cv = this.props.node.comment_view;
803     return this.isPersonMentionType(cv)
804       ? cv.person_mention.read
805       : cv.comment.read;
806   }
807
808   linkBtn(small = false) {
809     let cv = this.props.node.comment_view;
810     return (
811       <Link
812         className={`btn ${small && "btn-sm"} btn-link btn-animate text-muted`}
813         to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
814         title={this.props.showContext ? i18n.t("show_context") : i18n.t("link")}
815       >
816         <Icon icon="link" classes="icon-inline" />
817       </Link>
818     );
819   }
820
821   get loadingIcon() {
822     return <Spinner />;
823   }
824
825   get myComment(): boolean {
826     return (
827       this.props.node.comment_view.creator.id ==
828       UserService.Instance.localUserView?.person.id
829     );
830   }
831
832   get isMod(): boolean {
833     return (
834       this.props.moderators &&
835       isMod(
836         this.props.moderators.map(m => m.moderator.id),
837         this.props.node.comment_view.creator.id
838       )
839     );
840   }
841
842   get isAdmin(): boolean {
843     return (
844       this.props.admins &&
845       isMod(
846         this.props.admins.map(a => a.person.id),
847         this.props.node.comment_view.creator.id
848       )
849     );
850   }
851
852   get isPostCreator(): boolean {
853     return this.props.node.comment_view.creator.id == this.props.postCreatorId;
854   }
855
856   get canMod(): boolean {
857     if (this.props.admins && this.props.moderators) {
858       let adminsThenMods = this.props.admins
859         .map(a => a.person.id)
860         .concat(this.props.moderators.map(m => m.moderator.id));
861
862       return canMod(
863         UserService.Instance.localUserView,
864         adminsThenMods,
865         this.props.node.comment_view.creator.id
866       );
867     } else {
868       return false;
869     }
870   }
871
872   get canAdmin(): boolean {
873     return (
874       this.props.admins &&
875       canMod(
876         UserService.Instance.localUserView,
877         this.props.admins.map(a => a.person.id),
878         this.props.node.comment_view.creator.id
879       )
880     );
881   }
882
883   get amCommunityCreator(): boolean {
884     return (
885       this.props.moderators &&
886       UserService.Instance.localUserView &&
887       this.props.node.comment_view.creator.id !=
888         UserService.Instance.localUserView.person.id &&
889       UserService.Instance.localUserView.person.id ==
890         this.props.moderators[0].moderator.id
891     );
892   }
893
894   get amSiteCreator(): boolean {
895     return (
896       this.props.admins &&
897       UserService.Instance.localUserView &&
898       this.props.node.comment_view.creator.id !=
899         UserService.Instance.localUserView.person.id &&
900       UserService.Instance.localUserView.person.id ==
901         this.props.admins[0].person.id
902     );
903   }
904
905   get commentUnlessRemoved(): string {
906     let comment = this.props.node.comment_view.comment;
907     return comment.removed
908       ? `*${i18n.t("removed")}*`
909       : comment.deleted
910       ? `*${i18n.t("deleted")}*`
911       : comment.content;
912   }
913
914   handleReplyClick(i: CommentNode) {
915     i.state.showReply = true;
916     i.setState(i.state);
917   }
918
919   handleEditClick(i: CommentNode) {
920     i.state.showEdit = true;
921     i.setState(i.state);
922   }
923
924   handleDeleteClick(i: CommentNode) {
925     let comment = i.props.node.comment_view.comment;
926     let deleteForm: DeleteComment = {
927       comment_id: comment.id,
928       deleted: !comment.deleted,
929       auth: authField(),
930     };
931     WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
932   }
933
934   handleSaveCommentClick(i: CommentNode) {
935     let cv = i.props.node.comment_view;
936     let save = cv.saved == undefined ? true : !cv.saved;
937     let form: SaveComment = {
938       comment_id: cv.comment.id,
939       save,
940       auth: authField(),
941     };
942
943     WebSocketService.Instance.send(wsClient.saveComment(form));
944
945     i.state.saveLoading = true;
946     i.setState(this.state);
947   }
948
949   handleReplyCancel() {
950     this.state.showReply = false;
951     this.state.showEdit = false;
952     this.setState(this.state);
953   }
954
955   handleCommentUpvote(i: CommentNodeI, event: any) {
956     event.preventDefault();
957     let new_vote = this.state.my_vote == 1 ? 0 : 1;
958
959     if (this.state.my_vote == 1) {
960       this.state.score--;
961       this.state.upvotes--;
962     } else if (this.state.my_vote == -1) {
963       this.state.downvotes--;
964       this.state.upvotes++;
965       this.state.score += 2;
966     } else {
967       this.state.upvotes++;
968       this.state.score++;
969     }
970
971     this.state.my_vote = new_vote;
972
973     let form: CreateCommentLike = {
974       comment_id: i.comment_view.comment.id,
975       score: this.state.my_vote,
976       auth: authField(),
977     };
978
979     WebSocketService.Instance.send(wsClient.likeComment(form));
980     this.setState(this.state);
981     setupTippy();
982   }
983
984   handleCommentDownvote(i: CommentNodeI, event: any) {
985     event.preventDefault();
986     let new_vote = this.state.my_vote == -1 ? 0 : -1;
987
988     if (this.state.my_vote == 1) {
989       this.state.score -= 2;
990       this.state.upvotes--;
991       this.state.downvotes++;
992     } else if (this.state.my_vote == -1) {
993       this.state.downvotes--;
994       this.state.score++;
995     } else {
996       this.state.downvotes++;
997       this.state.score--;
998     }
999
1000     this.state.my_vote = new_vote;
1001
1002     let form: CreateCommentLike = {
1003       comment_id: i.comment_view.comment.id,
1004       score: this.state.my_vote,
1005       auth: authField(),
1006     };
1007
1008     WebSocketService.Instance.send(wsClient.likeComment(form));
1009     this.setState(this.state);
1010     setupTippy();
1011   }
1012
1013   handleModRemoveShow(i: CommentNode) {
1014     i.state.showRemoveDialog = true;
1015     i.setState(i.state);
1016   }
1017
1018   handleModRemoveReasonChange(i: CommentNode, event: any) {
1019     i.state.removeReason = event.target.value;
1020     i.setState(i.state);
1021   }
1022
1023   handleModRemoveDataChange(i: CommentNode, event: any) {
1024     i.state.removeData = event.target.checked;
1025     i.setState(i.state);
1026   }
1027
1028   handleModRemoveSubmit(i: CommentNode) {
1029     let comment = i.props.node.comment_view.comment;
1030     let form: RemoveComment = {
1031       comment_id: comment.id,
1032       removed: !comment.removed,
1033       reason: i.state.removeReason,
1034       auth: authField(),
1035     };
1036     WebSocketService.Instance.send(wsClient.removeComment(form));
1037
1038     i.state.showRemoveDialog = false;
1039     i.setState(i.state);
1040   }
1041
1042   isPersonMentionType(
1043     item: CommentView | PersonMentionView
1044   ): item is PersonMentionView {
1045     return (item as PersonMentionView).person_mention?.id !== undefined;
1046   }
1047
1048   handleMarkRead(i: CommentNode) {
1049     if (i.isPersonMentionType(i.props.node.comment_view)) {
1050       let form: MarkPersonMentionAsRead = {
1051         person_mention_id: i.props.node.comment_view.person_mention.id,
1052         read: !i.props.node.comment_view.person_mention.read,
1053         auth: authField(),
1054       };
1055       WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
1056     } else {
1057       let form: MarkCommentAsRead = {
1058         comment_id: i.props.node.comment_view.comment.id,
1059         read: !i.props.node.comment_view.comment.read,
1060         auth: authField(),
1061       };
1062       WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
1063     }
1064
1065     i.state.readLoading = true;
1066     i.setState(this.state);
1067   }
1068
1069   handleModBanFromCommunityShow(i: CommentNode) {
1070     i.state.showBanDialog = !i.state.showBanDialog;
1071     i.state.banType = BanType.Community;
1072     i.setState(i.state);
1073   }
1074
1075   handleModBanShow(i: CommentNode) {
1076     i.state.showBanDialog = !i.state.showBanDialog;
1077     i.state.banType = BanType.Site;
1078     i.setState(i.state);
1079   }
1080
1081   handleModBanReasonChange(i: CommentNode, event: any) {
1082     i.state.banReason = event.target.value;
1083     i.setState(i.state);
1084   }
1085
1086   handleModBanExpiresChange(i: CommentNode, event: any) {
1087     i.state.banExpires = event.target.value;
1088     i.setState(i.state);
1089   }
1090
1091   handleModBanFromCommunitySubmit(i: CommentNode) {
1092     i.state.banType = BanType.Community;
1093     i.setState(i.state);
1094     i.handleModBanBothSubmit(i);
1095   }
1096
1097   handleModBanSubmit(i: CommentNode) {
1098     i.state.banType = BanType.Site;
1099     i.setState(i.state);
1100     i.handleModBanBothSubmit(i);
1101   }
1102
1103   handleModBanBothSubmit(i: CommentNode) {
1104     let cv = i.props.node.comment_view;
1105
1106     if (i.state.banType == BanType.Community) {
1107       // If its an unban, restore all their data
1108       let ban = !cv.creator_banned_from_community;
1109       if (ban == false) {
1110         i.state.removeData = false;
1111       }
1112       let form: BanFromCommunity = {
1113         person_id: cv.creator.id,
1114         community_id: cv.community.id,
1115         ban,
1116         remove_data: i.state.removeData,
1117         reason: i.state.banReason,
1118         expires: getUnixTime(i.state.banExpires),
1119         auth: authField(),
1120       };
1121       WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1122     } else {
1123       // If its an unban, restore all their data
1124       let ban = !cv.creator.banned;
1125       if (ban == false) {
1126         i.state.removeData = false;
1127       }
1128       let form: BanPerson = {
1129         person_id: cv.creator.id,
1130         ban,
1131         remove_data: i.state.removeData,
1132         reason: i.state.banReason,
1133         expires: getUnixTime(i.state.banExpires),
1134         auth: authField(),
1135       };
1136       WebSocketService.Instance.send(wsClient.banPerson(form));
1137     }
1138
1139     i.state.showBanDialog = false;
1140     i.setState(i.state);
1141   }
1142
1143   handleShowConfirmAppointAsMod(i: CommentNode) {
1144     i.state.showConfirmAppointAsMod = true;
1145     i.setState(i.state);
1146   }
1147
1148   handleCancelConfirmAppointAsMod(i: CommentNode) {
1149     i.state.showConfirmAppointAsMod = false;
1150     i.setState(i.state);
1151   }
1152
1153   handleAddModToCommunity(i: CommentNode) {
1154     let cv = i.props.node.comment_view;
1155     let form: AddModToCommunity = {
1156       person_id: cv.creator.id,
1157       community_id: cv.community.id,
1158       added: !i.isMod,
1159       auth: authField(),
1160     };
1161     WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1162     i.state.showConfirmAppointAsMod = false;
1163     i.setState(i.state);
1164   }
1165
1166   handleShowConfirmAppointAsAdmin(i: CommentNode) {
1167     i.state.showConfirmAppointAsAdmin = true;
1168     i.setState(i.state);
1169   }
1170
1171   handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1172     i.state.showConfirmAppointAsAdmin = false;
1173     i.setState(i.state);
1174   }
1175
1176   handleAddAdmin(i: CommentNode) {
1177     let form: AddAdmin = {
1178       person_id: i.props.node.comment_view.creator.id,
1179       added: !i.isAdmin,
1180       auth: authField(),
1181     };
1182     WebSocketService.Instance.send(wsClient.addAdmin(form));
1183     i.state.showConfirmAppointAsAdmin = false;
1184     i.setState(i.state);
1185   }
1186
1187   handleShowConfirmTransferCommunity(i: CommentNode) {
1188     i.state.showConfirmTransferCommunity = true;
1189     i.setState(i.state);
1190   }
1191
1192   handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1193     i.state.showConfirmTransferCommunity = false;
1194     i.setState(i.state);
1195   }
1196
1197   handleTransferCommunity(i: CommentNode) {
1198     let cv = i.props.node.comment_view;
1199     let form: TransferCommunity = {
1200       community_id: cv.community.id,
1201       person_id: cv.creator.id,
1202       auth: authField(),
1203     };
1204     WebSocketService.Instance.send(wsClient.transferCommunity(form));
1205     i.state.showConfirmTransferCommunity = false;
1206     i.setState(i.state);
1207   }
1208
1209   handleShowConfirmTransferSite(i: CommentNode) {
1210     i.state.showConfirmTransferSite = true;
1211     i.setState(i.state);
1212   }
1213
1214   handleCancelShowConfirmTransferSite(i: CommentNode) {
1215     i.state.showConfirmTransferSite = false;
1216     i.setState(i.state);
1217   }
1218
1219   handleTransferSite(i: CommentNode) {
1220     let form: TransferSite = {
1221       person_id: i.props.node.comment_view.creator.id,
1222       auth: authField(),
1223     };
1224     WebSocketService.Instance.send(wsClient.transferSite(form));
1225     i.state.showConfirmTransferSite = false;
1226     i.setState(i.state);
1227   }
1228
1229   get isCommentNew(): boolean {
1230     let now = moment.utc().subtract(10, "minutes");
1231     let then = moment.utc(this.props.node.comment_view.comment.published);
1232     return now.isBefore(then);
1233   }
1234
1235   handleCommentCollapse(i: CommentNode) {
1236     i.state.collapsed = !i.state.collapsed;
1237     i.setState(i.state);
1238     setupTippy();
1239   }
1240
1241   handleViewSource(i: CommentNode) {
1242     i.state.viewSource = !i.state.viewSource;
1243     i.setState(i.state);
1244   }
1245
1246   handleShowAdvanced(i: CommentNode) {
1247     i.state.showAdvanced = !i.state.showAdvanced;
1248     i.setState(i.state);
1249     setupTippy();
1250   }
1251
1252   get scoreColor() {
1253     if (this.state.my_vote == 1) {
1254       return "text-info";
1255     } else if (this.state.my_vote == -1) {
1256       return "text-danger";
1257     } else {
1258       return "text-muted";
1259     }
1260   }
1261
1262   get pointsTippy(): string {
1263     let points = i18n.t("number_of_points", {
1264       count: this.state.score,
1265     });
1266
1267     let upvotes = i18n.t("number_of_upvotes", {
1268       count: this.state.upvotes,
1269     });
1270
1271     let downvotes = i18n.t("number_of_downvotes", {
1272       count: this.state.downvotes,
1273     });
1274
1275     return `${points} • ${upvotes} • ${downvotes}`;
1276   }
1277
1278   get expandText(): string {
1279     return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
1280   }
1281 }