]> Untitled Git - lemmy.git/blob - ui/src/components/comment-node.tsx
Some comment-node additions
[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={i18n.t('number_of_points', {
202                     count: this.state.score,
203                   })}
204                 >
205                   <svg class="icon icon-inline mr-1">
206                     <use xlinkHref="#icon-zap"></use>
207                   </svg>
208                   {this.state.score}
209                 </span>
210               </li>
211               <li className="list-inline-item">•</li>
212               <li className="list-inline-item">
213                 <span>
214                   <MomentTime data={node.comment} />
215                 </span>
216               </li>
217               <li className="list-inline-item">
218                 <div
219                   className="unselectable pointer text-monospace"
220                   onClick={linkEvent(this, this.handleCommentCollapse)}
221                 >
222                   {this.state.collapsed ? (
223                     <svg class="icon icon-inline">
224                       <use xlinkHref="#icon-plus-square"></use>
225                     </svg>
226                   ) : (
227                     <svg class="icon icon-inline">
228                       <use xlinkHref="#icon-minus-square"></use>
229                     </svg>
230                   )}
231                 </div>
232               </li>
233             </ul>
234             {this.state.showEdit && (
235               <CommentForm
236                 node={node}
237                 edit
238                 onReplyCancel={this.handleReplyCancel}
239                 disabled={this.props.locked}
240               />
241             )}
242             {!this.state.showEdit && !this.state.collapsed && (
243               <div>
244                 {this.state.viewSource ? (
245                   <pre>{this.commentUnlessRemoved}</pre>
246                 ) : (
247                   <div
248                     className="md-div"
249                     dangerouslySetInnerHTML={mdToHtml(
250                       this.commentUnlessRemoved
251                     )}
252                   />
253                 )}
254                 <ul class="list-inline mb-0 text-muted font-weight-bold h5">
255                   {this.props.markable && (
256                     <li className="list-inline-item-action">
257                       <span
258                         class="pointer"
259                         onClick={linkEvent(this, this.handleMarkRead)}
260                         data-tippy-content={
261                           node.comment.read
262                             ? i18n.t('mark_as_unread')
263                             : i18n.t('mark_as_read')
264                         }
265                       >
266                         <svg
267                           class={`icon icon-inline ${node.comment.read &&
268                             'text-success'}`}
269                         >
270                           <use xlinkHref="#icon-check"></use>
271                         </svg>
272                       </span>
273                     </li>
274                   )}
275                   {UserService.Instance.user && !this.props.viewOnly && (
276                     <>
277                       <li className="list-inline-item-action">
278                         <button
279                           className={`vote-animate btn btn-link p-0 mb-1 ${
280                             this.state.my_vote == 1 ? 'text-info' : 'text-muted'
281                           }`}
282                           onClick={linkEvent(node, this.handleCommentUpvote)}
283                           data-tippy-content={i18n.t('upvote')}
284                         >
285                           <svg class="icon icon-inline">
286                             <use xlinkHref="#icon-arrow-up"></use>
287                           </svg>
288                           {this.state.upvotes !== this.state.score && (
289                             <span class="ml-1">{this.state.upvotes}</span>
290                           )}
291                         </button>
292                       </li>
293                       {WebSocketService.Instance.site.enable_downvotes && (
294                         <li className="list-inline-item-action">
295                           <button
296                             className={`vote-animate btn btn-link p-0 mb-1 ${
297                               this.state.my_vote == -1
298                                 ? 'text-danger'
299                                 : 'text-muted'
300                             }`}
301                             onClick={linkEvent(
302                               node,
303                               this.handleCommentDownvote
304                             )}
305                             data-tippy-content={i18n.t('downvote')}
306                           >
307                             <svg class="icon icon-inline">
308                               <use xlinkHref="#icon-arrow-down"></use>
309                             </svg>
310                             {this.state.upvotes !== this.state.score && (
311                               <span class="ml-1">{this.state.downvotes}</span>
312                             )}
313                           </button>
314                         </li>
315                       )}
316                       <li className="list-inline-item-action">
317                         <span
318                           class="pointer"
319                           onClick={linkEvent(this, this.handleReplyClick)}
320                           data-tippy-content={i18n.t('reply')}
321                         >
322                           <svg class="icon icon-inline">
323                             <use xlinkHref="#icon-reply1"></use>
324                           </svg>
325                         </span>
326                       </li>
327                       <li className="list-inline-item-action">
328                         <Link
329                           className="text-muted"
330                           to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
331                           title={i18n.t('link')}
332                         >
333                           <svg class="icon icon-inline">
334                             <use xlinkHref="#icon-link"></use>
335                           </svg>
336                         </Link>
337                       </li>
338                       {!this.state.showAdvanced ? (
339                         <li className="list-inline-item-action">
340                           <span
341                             className="unselectable pointer"
342                             onClick={linkEvent(this, this.handleShowAdvanced)}
343                             data-tippy-content={i18n.t('more')}
344                           >
345                             <svg class="icon icon-inline">
346                               <use xlinkHref="#icon-more-vertical"></use>
347                             </svg>
348                           </span>
349                         </li>
350                       ) : (
351                         <>
352                           {!this.myComment && (
353                             <li className="list-inline-item-action">
354                               <Link
355                                 class="text-muted"
356                                 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
357                                 title={i18n.t('message').toLowerCase()}
358                               >
359                                 <svg class="icon">
360                                   <use xlinkHref="#icon-mail"></use>
361                                 </svg>
362                               </Link>
363                             </li>
364                           )}
365                           <li className="list-inline-item-action">
366                             <span
367                               class="pointer"
368                               onClick={linkEvent(
369                                 this,
370                                 this.handleSaveCommentClick
371                               )}
372                               data-tippy-content={
373                                 node.comment.saved
374                                   ? i18n.t('unsave')
375                                   : i18n.t('save')
376                               }
377                             >
378                               <svg
379                                 class={`icon icon-inline ${node.comment.saved &&
380                                   'text-warning'}`}
381                               >
382                                 <use xlinkHref="#icon-star"></use>
383                               </svg>
384                             </span>
385                           </li>
386                           <li className="list-inline-item-action">
387                             <span
388                               className="pointer"
389                               onClick={linkEvent(this, this.handleViewSource)}
390                               data-tippy-content={i18n.t('view_source')}
391                             >
392                               <svg
393                                 class={`icon icon-inline ${this.state
394                                   .viewSource && 'text-success'}`}
395                               >
396                                 <use xlinkHref="#icon-file-text"></use>
397                               </svg>
398                             </span>
399                           </li>
400                           {this.myComment && (
401                             <>
402                               <li className="list-inline-item-action">•</li>
403                               <li className="list-inline-item-action">
404                                 <span
405                                   class="pointer"
406                                   onClick={linkEvent(
407                                     this,
408                                     this.handleEditClick
409                                   )}
410                                   data-tippy-content={i18n.t('edit')}
411                                 >
412                                   <svg class="icon icon-inline">
413                                     <use xlinkHref="#icon-edit"></use>
414                                   </svg>
415                                 </span>
416                               </li>
417                               <li className="list-inline-item-action">
418                                 <span
419                                   class="pointer"
420                                   onClick={linkEvent(
421                                     this,
422                                     this.handleDeleteClick
423                                   )}
424                                   data-tippy-content={
425                                     !node.comment.deleted
426                                       ? i18n.t('delete')
427                                       : i18n.t('restore')
428                                   }
429                                 >
430                                   <svg
431                                     class={`icon icon-inline ${node.comment
432                                       .deleted && 'text-danger'}`}
433                                   >
434                                     <use xlinkHref="#icon-trash"></use>
435                                   </svg>
436                                 </span>
437                               </li>
438                             </>
439                           )}
440                           {/* Admins and mods can remove comments */}
441                           {(this.canMod || this.canAdmin) && (
442                             <>
443                               <li className="list-inline-item-action">
444                                 {!node.comment.removed ? (
445                                   <span
446                                     class="pointer"
447                                     onClick={linkEvent(
448                                       this,
449                                       this.handleModRemoveShow
450                                     )}
451                                   >
452                                     {i18n.t('remove')}
453                                   </span>
454                                 ) : (
455                                   <span
456                                     class="pointer"
457                                     onClick={linkEvent(
458                                       this,
459                                       this.handleModRemoveSubmit
460                                     )}
461                                   >
462                                     {i18n.t('restore')}
463                                   </span>
464                                 )}
465                               </li>
466                             </>
467                           )}
468                           {/* Mods can ban from community, and appoint as mods to community */}
469                           {this.canMod && (
470                             <>
471                               {!this.isMod && (
472                                 <li className="list-inline-item-action">
473                                   {!node.comment.banned_from_community ? (
474                                     <span
475                                       class="pointer"
476                                       onClick={linkEvent(
477                                         this,
478                                         this.handleModBanFromCommunityShow
479                                       )}
480                                     >
481                                       {i18n.t('ban')}
482                                     </span>
483                                   ) : (
484                                     <span
485                                       class="pointer"
486                                       onClick={linkEvent(
487                                         this,
488                                         this.handleModBanFromCommunitySubmit
489                                       )}
490                                     >
491                                       {i18n.t('unban')}
492                                     </span>
493                                   )}
494                                 </li>
495                               )}
496                               {!node.comment.banned_from_community && (
497                                 <li className="list-inline-item-action">
498                                   {!this.state.showConfirmAppointAsMod ? (
499                                     <span
500                                       class="pointer"
501                                       onClick={linkEvent(
502                                         this,
503                                         this.handleShowConfirmAppointAsMod
504                                       )}
505                                     >
506                                       {this.isMod
507                                         ? i18n.t('remove_as_mod')
508                                         : i18n.t('appoint_as_mod')}
509                                     </span>
510                                   ) : (
511                                     <>
512                                       <span class="d-inline-block mr-1">
513                                         {i18n.t('are_you_sure')}
514                                       </span>
515                                       <span
516                                         class="pointer d-inline-block mr-1"
517                                         onClick={linkEvent(
518                                           this,
519                                           this.handleAddModToCommunity
520                                         )}
521                                       >
522                                         {i18n.t('yes')}
523                                       </span>
524                                       <span
525                                         class="pointer d-inline-block"
526                                         onClick={linkEvent(
527                                           this,
528                                           this.handleCancelConfirmAppointAsMod
529                                         )}
530                                       >
531                                         {i18n.t('no')}
532                                       </span>
533                                     </>
534                                   )}
535                                 </li>
536                               )}
537                             </>
538                           )}
539                           {/* Community creators and admins can transfer community to another mod */}
540                           {(this.amCommunityCreator || this.canAdmin) &&
541                             this.isMod && (
542                               <li className="list-inline-item-action">
543                                 {!this.state.showConfirmTransferCommunity ? (
544                                   <span
545                                     class="pointer"
546                                     onClick={linkEvent(
547                                       this,
548                                       this.handleShowConfirmTransferCommunity
549                                     )}
550                                   >
551                                     {i18n.t('transfer_community')}
552                                   </span>
553                                 ) : (
554                                   <>
555                                     <span class="d-inline-block mr-1">
556                                       {i18n.t('are_you_sure')}
557                                     </span>
558                                     <span
559                                       class="pointer d-inline-block mr-1"
560                                       onClick={linkEvent(
561                                         this,
562                                         this.handleTransferCommunity
563                                       )}
564                                     >
565                                       {i18n.t('yes')}
566                                     </span>
567                                     <span
568                                       class="pointer d-inline-block"
569                                       onClick={linkEvent(
570                                         this,
571                                         this
572                                           .handleCancelShowConfirmTransferCommunity
573                                       )}
574                                     >
575                                       {i18n.t('no')}
576                                     </span>
577                                   </>
578                                 )}
579                               </li>
580                             )}
581                           {/* Admins can ban from all, and appoint other admins */}
582                           {this.canAdmin && (
583                             <>
584                               {!this.isAdmin && (
585                                 <li className="list-inline-item-action">
586                                   {!node.comment.banned ? (
587                                     <span
588                                       class="pointer"
589                                       onClick={linkEvent(
590                                         this,
591                                         this.handleModBanShow
592                                       )}
593                                     >
594                                       {i18n.t('ban_from_site')}
595                                     </span>
596                                   ) : (
597                                     <span
598                                       class="pointer"
599                                       onClick={linkEvent(
600                                         this,
601                                         this.handleModBanSubmit
602                                       )}
603                                     >
604                                       {i18n.t('unban_from_site')}
605                                     </span>
606                                   )}
607                                 </li>
608                               )}
609                               {!node.comment.banned && (
610                                 <li className="list-inline-item-action">
611                                   {!this.state.showConfirmAppointAsAdmin ? (
612                                     <span
613                                       class="pointer"
614                                       onClick={linkEvent(
615                                         this,
616                                         this.handleShowConfirmAppointAsAdmin
617                                       )}
618                                     >
619                                       {this.isAdmin
620                                         ? i18n.t('remove_as_admin')
621                                         : i18n.t('appoint_as_admin')}
622                                     </span>
623                                   ) : (
624                                     <>
625                                       <span class="d-inline-block mr-1">
626                                         {i18n.t('are_you_sure')}
627                                       </span>
628                                       <span
629                                         class="pointer d-inline-block mr-1"
630                                         onClick={linkEvent(
631                                           this,
632                                           this.handleAddAdmin
633                                         )}
634                                       >
635                                         {i18n.t('yes')}
636                                       </span>
637                                       <span
638                                         class="pointer d-inline-block"
639                                         onClick={linkEvent(
640                                           this,
641                                           this.handleCancelConfirmAppointAsAdmin
642                                         )}
643                                       >
644                                         {i18n.t('no')}
645                                       </span>
646                                     </>
647                                   )}
648                                 </li>
649                               )}
650                             </>
651                           )}
652                           {/* Site Creator can transfer to another admin */}
653                           {this.amSiteCreator && this.isAdmin && (
654                             <li className="list-inline-item-action">
655                               {!this.state.showConfirmTransferSite ? (
656                                 <span
657                                   class="pointer"
658                                   onClick={linkEvent(
659                                     this,
660                                     this.handleShowConfirmTransferSite
661                                   )}
662                                 >
663                                   {i18n.t('transfer_site')}
664                                 </span>
665                               ) : (
666                                 <>
667                                   <span class="d-inline-block mr-1">
668                                     {i18n.t('are_you_sure')}
669                                   </span>
670                                   <span
671                                     class="pointer d-inline-block mr-1"
672                                     onClick={linkEvent(
673                                       this,
674                                       this.handleTransferSite
675                                     )}
676                                   >
677                                     {i18n.t('yes')}
678                                   </span>
679                                   <span
680                                     class="pointer d-inline-block"
681                                     onClick={linkEvent(
682                                       this,
683                                       this.handleCancelShowConfirmTransferSite
684                                     )}
685                                   >
686                                     {i18n.t('no')}
687                                   </span>
688                                 </>
689                               )}
690                             </li>
691                           )}
692                         </>
693                       )}
694                     </>
695                   )}
696                 </ul>
697               </div>
698             )}
699           </div>
700         </div>
701         {/* end of details */}
702         {this.state.showRemoveDialog && (
703           <form
704             class="form-inline"
705             onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
706           >
707             <input
708               type="text"
709               class="form-control mr-2"
710               placeholder={i18n.t('reason')}
711               value={this.state.removeReason}
712               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
713             />
714             <button type="submit" class="btn btn-secondary">
715               {i18n.t('remove_comment')}
716             </button>
717           </form>
718         )}
719         {this.state.showBanDialog && (
720           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
721             <div class="form-group row">
722               <label class="col-form-label">{i18n.t('reason')}</label>
723               <input
724                 type="text"
725                 class="form-control mr-2"
726                 placeholder={i18n.t('reason')}
727                 value={this.state.banReason}
728                 onInput={linkEvent(this, this.handleModBanReasonChange)}
729               />
730             </div>
731             {/* TODO hold off on expires until later */}
732             {/* <div class="form-group row"> */}
733             {/*   <label class="col-form-label">Expires</label> */}
734             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
735             {/* </div> */}
736             <div class="form-group row">
737               <button type="submit" class="btn btn-secondary">
738                 {i18n.t('ban')} {node.comment.creator_name}
739               </button>
740             </div>
741           </form>
742         )}
743         {this.state.showReply && (
744           <CommentForm
745             node={node}
746             onReplyCancel={this.handleReplyCancel}
747             disabled={this.props.locked}
748           />
749         )}
750         {node.children && !this.state.collapsed && (
751           <CommentNodes
752             nodes={node.children}
753             locked={this.props.locked}
754             moderators={this.props.moderators}
755             admins={this.props.admins}
756             postCreatorId={this.props.postCreatorId}
757             sort={this.props.sort}
758             sortType={this.props.sortType}
759           />
760         )}
761         {/* A collapsed clearfix */}
762         {this.state.collapsed && <div class="row col-12"></div>}
763       </div>
764     );
765   }
766
767   get myComment(): boolean {
768     return (
769       UserService.Instance.user &&
770       this.props.node.comment.creator_id == UserService.Instance.user.id
771     );
772   }
773
774   get isMod(): boolean {
775     return (
776       this.props.moderators &&
777       isMod(
778         this.props.moderators.map(m => m.user_id),
779         this.props.node.comment.creator_id
780       )
781     );
782   }
783
784   get isAdmin(): boolean {
785     return (
786       this.props.admins &&
787       isMod(
788         this.props.admins.map(a => a.id),
789         this.props.node.comment.creator_id
790       )
791     );
792   }
793
794   get isPostCreator(): boolean {
795     return this.props.node.comment.creator_id == this.props.postCreatorId;
796   }
797
798   get canMod(): boolean {
799     if (this.props.admins && this.props.moderators) {
800       let adminsThenMods = this.props.admins
801         .map(a => a.id)
802         .concat(this.props.moderators.map(m => m.user_id));
803
804       return canMod(
805         UserService.Instance.user,
806         adminsThenMods,
807         this.props.node.comment.creator_id
808       );
809     } else {
810       return false;
811     }
812   }
813
814   get canAdmin(): boolean {
815     return (
816       this.props.admins &&
817       canMod(
818         UserService.Instance.user,
819         this.props.admins.map(a => a.id),
820         this.props.node.comment.creator_id
821       )
822     );
823   }
824
825   get amCommunityCreator(): boolean {
826     return (
827       this.props.moderators &&
828       UserService.Instance.user &&
829       this.props.node.comment.creator_id != UserService.Instance.user.id &&
830       UserService.Instance.user.id == this.props.moderators[0].user_id
831     );
832   }
833
834   get amSiteCreator(): boolean {
835     return (
836       this.props.admins &&
837       UserService.Instance.user &&
838       this.props.node.comment.creator_id != UserService.Instance.user.id &&
839       UserService.Instance.user.id == this.props.admins[0].id
840     );
841   }
842
843   get commentUnlessRemoved(): string {
844     let node = this.props.node;
845     return node.comment.removed
846       ? `*${i18n.t('removed')}*`
847       : node.comment.deleted
848       ? `*${i18n.t('deleted')}*`
849       : node.comment.content;
850   }
851
852   handleReplyClick(i: CommentNode) {
853     i.state.showReply = true;
854     i.setState(i.state);
855   }
856
857   handleEditClick(i: CommentNode) {
858     i.state.showEdit = true;
859     i.setState(i.state);
860   }
861
862   handleDeleteClick(i: CommentNode) {
863     let deleteForm: CommentFormI = {
864       content: i.props.node.comment.content,
865       edit_id: i.props.node.comment.id,
866       creator_id: i.props.node.comment.creator_id,
867       post_id: i.props.node.comment.post_id,
868       parent_id: i.props.node.comment.parent_id,
869       deleted: !i.props.node.comment.deleted,
870       auth: null,
871     };
872     WebSocketService.Instance.editComment(deleteForm);
873   }
874
875   handleSaveCommentClick(i: CommentNode) {
876     let saved =
877       i.props.node.comment.saved == undefined
878         ? true
879         : !i.props.node.comment.saved;
880     let form: SaveCommentForm = {
881       comment_id: i.props.node.comment.id,
882       save: saved,
883     };
884
885     WebSocketService.Instance.saveComment(form);
886   }
887
888   handleReplyCancel() {
889     this.state.showReply = false;
890     this.state.showEdit = false;
891     this.setState(this.state);
892   }
893
894   handleCommentUpvote(i: CommentNodeI) {
895     let new_vote = this.state.my_vote == 1 ? 0 : 1;
896
897     if (this.state.my_vote == 1) {
898       this.state.score--;
899       this.state.upvotes--;
900     } else if (this.state.my_vote == -1) {
901       this.state.downvotes--;
902       this.state.upvotes++;
903       this.state.score += 2;
904     } else {
905       this.state.upvotes++;
906       this.state.score++;
907     }
908
909     this.state.my_vote = new_vote;
910
911     let form: CommentLikeForm = {
912       comment_id: i.comment.id,
913       post_id: i.comment.post_id,
914       score: this.state.my_vote,
915     };
916
917     WebSocketService.Instance.likeComment(form);
918     this.setState(this.state);
919   }
920
921   handleCommentDownvote(i: CommentNodeI) {
922     let new_vote = this.state.my_vote == -1 ? 0 : -1;
923
924     if (this.state.my_vote == 1) {
925       this.state.score -= 2;
926       this.state.upvotes--;
927       this.state.downvotes++;
928     } else if (this.state.my_vote == -1) {
929       this.state.downvotes--;
930       this.state.score++;
931     } else {
932       this.state.downvotes++;
933       this.state.score--;
934     }
935
936     this.state.my_vote = new_vote;
937
938     let form: CommentLikeForm = {
939       comment_id: i.comment.id,
940       post_id: i.comment.post_id,
941       score: this.state.my_vote,
942     };
943
944     WebSocketService.Instance.likeComment(form);
945     this.setState(this.state);
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 }