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