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