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