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