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