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