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