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