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