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