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