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