]> Untitled Git - lemmy.git/blob - ui/src/components/comment-node.tsx
Adding loading indicators to save and mark as read. #519
[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   moderators: Array<CommunityUser>;
70   admins: Array<UserView>;
71   // TODO is this necessary, can't I get it from the node itself?
72   postCreatorId?: number;
73   showCommunity?: boolean;
74   sort?: CommentSortType;
75   sortType?: SortType;
76 }
77
78 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
79   private emptyState: CommentNodeState = {
80     showReply: false,
81     showEdit: false,
82     showRemoveDialog: false,
83     removeReason: null,
84     showBanDialog: false,
85     banReason: null,
86     banExpires: null,
87     banType: BanType.Community,
88     collapsed: false,
89     viewSource: false,
90     showAdvanced: false,
91     showConfirmTransferSite: false,
92     showConfirmTransferCommunity: false,
93     showConfirmAppointAsMod: false,
94     showConfirmAppointAsAdmin: false,
95     my_vote: this.props.node.comment.my_vote,
96     score: this.props.node.comment.score,
97     upvotes: this.props.node.comment.upvotes,
98     downvotes: this.props.node.comment.downvotes,
99     borderColor: this.props.node.comment.depth
100       ? colorList[this.props.node.comment.depth % colorList.length]
101       : colorList[0],
102     readLoading: false,
103     saveLoading: false,
104   };
105
106   constructor(props: any, context: any) {
107     super(props, context);
108
109     this.state = this.emptyState;
110     this.handleReplyCancel = this.handleReplyCancel.bind(this);
111     this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
112     this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
113   }
114
115   componentWillReceiveProps(nextProps: CommentNodeProps) {
116     this.state.my_vote = nextProps.node.comment.my_vote;
117     this.state.upvotes = nextProps.node.comment.upvotes;
118     this.state.downvotes = nextProps.node.comment.downvotes;
119     this.state.score = nextProps.node.comment.score;
120     this.state.readLoading = false;
121     this.state.saveLoading = false;
122     this.setState(this.state);
123   }
124
125   render() {
126     let node = this.props.node;
127     return (
128       <div
129         className={`comment ${
130           node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
131         }`}
132       >
133         <div
134           id={`comment-${node.comment.id}`}
135           className={`details comment-node border-top border-light ${
136             this.isCommentNew ? 'mark' : ''
137           }`}
138           style={
139             !this.props.noIndent &&
140             this.props.node.comment.parent_id &&
141             `border-left: 2px ${this.state.borderColor} solid !important`
142           }
143         >
144           <div
145             class={`${!this.props.noIndent &&
146               this.props.node.comment.parent_id &&
147               'ml-2'}`}
148           >
149             <ul class="list-inline mb-1 text-muted small">
150               <li className="mt-1 list-inline-item">
151                 <Link
152                   className="text-body font-weight-bold"
153                   to={`/u/${node.comment.creator_name}`}
154                 >
155                   {node.comment.creator_avatar && showAvatars() && (
156                     <img
157                       height="32"
158                       width="32"
159                       src={pictshareAvatarThumbnail(
160                         node.comment.creator_avatar
161                       )}
162                       class="rounded-circle mr-1"
163                     />
164                   )}
165                   <span>{node.comment.creator_name}</span>
166                 </Link>
167               </li>
168               {this.isMod && (
169                 <li className="list-inline-item badge badge-light">
170                   {i18n.t('mod')}
171                 </li>
172               )}
173               {this.isAdmin && (
174                 <li className="list-inline-item badge badge-light">
175                   {i18n.t('admin')}
176                 </li>
177               )}
178               {this.isPostCreator && (
179                 <li className="list-inline-item badge badge-light">
180                   {i18n.t('creator')}
181                 </li>
182               )}
183               {(node.comment.banned_from_community || node.comment.banned) && (
184                 <li className="list-inline-item badge badge-danger">
185                   {i18n.t('banned')}
186                 </li>
187               )}
188               <li className="list-inline-item">
189                 <div
190                   className="unselectable pointer text-monospace"
191                   onClick={linkEvent(this, this.handleCommentCollapse)}
192                 >
193                   {this.state.collapsed ? (
194                     <svg class="icon icon-inline">
195                       <use xlinkHref="#icon-plus-square"></use>
196                     </svg>
197                   ) : (
198                     <svg class="icon icon-inline">
199                       <use xlinkHref="#icon-minus-square"></use>
200                     </svg>
201                   )}
202                 </div>
203               </li>
204               {this.props.showCommunity && (
205                 <li className="list-inline-item">
206                   <span> {i18n.t('to')} </span>
207                   <Link to={`/c/${node.comment.community_name}`}>
208                     {node.comment.community_name}
209                   </Link>
210                 </li>
211               )}
212               <li className="list-inline-item">•</li>
213               <li className="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.markable && (
254                     <li className="list-inline-item">
255                       <button
256                         class="btn btn-link btn-sm btn-animate text-muted"
257                         onClick={linkEvent(this, this.handleMarkRead)}
258                         data-tippy-content={
259                           node.comment.read
260                             ? i18n.t('mark_as_unread')
261                             : i18n.t('mark_as_read')
262                         }
263                       >
264                         {this.state.readLoading ? (
265                           this.loadingIcon
266                         ) : (
267                           <svg
268                             class={`icon icon-inline ${node.comment.read &&
269                               'text-success'}`}
270                           >
271                             <use xlinkHref="#icon-check"></use>
272                           </svg>
273                         )}
274                       </button>
275                     </li>
276                   )}
277                   {UserService.Instance.user && !this.props.viewOnly && (
278                     <>
279                       <li className="list-inline-item">
280                         <button
281                           className={`btn btn-link btn-sm btn-animate ${
282                             this.state.my_vote == 1 ? 'text-info' : 'text-muted'
283                           }`}
284                           onClick={linkEvent(node, this.handleCommentUpvote)}
285                           data-tippy-content={i18n.t('upvote')}
286                         >
287                           <svg class="icon icon-inline">
288                             <use xlinkHref="#icon-arrow-up"></use>
289                           </svg>
290                           {this.state.upvotes !== this.state.score && (
291                             <span class="ml-1">{this.state.upvotes}</span>
292                           )}
293                         </button>
294                       </li>
295                       {WebSocketService.Instance.site.enable_downvotes && (
296                         <li className="list-inline-item">
297                           <button
298                             className={`btn btn-link btn-sm btn-animate ${
299                               this.state.my_vote == -1
300                                 ? 'text-danger'
301                                 : 'text-muted'
302                             }`}
303                             onClick={linkEvent(
304                               node,
305                               this.handleCommentDownvote
306                             )}
307                             data-tippy-content={i18n.t('downvote')}
308                           >
309                             <svg class="icon icon-inline">
310                               <use xlinkHref="#icon-arrow-down"></use>
311                             </svg>
312                             {this.state.upvotes !== this.state.score && (
313                               <span class="ml-1">{this.state.downvotes}</span>
314                             )}
315                           </button>
316                         </li>
317                       )}
318                       <li className="list-inline-item">
319                         <button
320                           class="btn btn-link btn-sm btn-animate text-muted"
321                           onClick={linkEvent(this, this.handleSaveCommentClick)}
322                           data-tippy-content={
323                             node.comment.saved
324                               ? i18n.t('unsave')
325                               : i18n.t('save')
326                           }
327                         >
328                           {this.state.saveLoading ? (
329                             this.loadingIcon
330                           ) : (
331                             <svg
332                               class={`icon icon-inline ${node.comment.saved &&
333                                 'text-warning'}`}
334                             >
335                               <use xlinkHref="#icon-star"></use>
336                             </svg>
337                           )}
338                         </button>
339                       </li>
340                       <li className="list-inline-item">
341                         <button
342                           class="btn btn-link btn-sm btn-animate text-muted"
343                           onClick={linkEvent(this, this.handleReplyClick)}
344                           data-tippy-content={i18n.t('reply')}
345                         >
346                           <svg class="icon icon-inline">
347                             <use xlinkHref="#icon-reply1"></use>
348                           </svg>
349                         </button>
350                       </li>
351                       {this.props.markable && this.linkBtn}
352                       {!this.state.showAdvanced ? (
353                         <li className="list-inline-item">
354                           <button
355                             className="btn btn-link btn-sm 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-sm 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.markable && this.linkBtn}
380                           <li className="list-inline-item">
381                             <button
382                               className="btn btn-link btn-sm 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-sm 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-sm 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-sm btn-animate text-muted"
767           to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
768           title={i18n.t('link')}
769         >
770           <svg class="icon icon-inline">
771             <use xlinkHref="#icon-link"></use>
772           </svg>
773         </Link>
774       </li>
775     );
776   }
777
778   get loadingIcon() {
779     return (
780       <svg class="icon icon-spinner spin">
781         <use xlinkHref="#icon-spinner"></use>
782       </svg>
783     );
784   }
785
786   get myComment(): boolean {
787     return (
788       UserService.Instance.user &&
789       this.props.node.comment.creator_id == UserService.Instance.user.id
790     );
791   }
792
793   get isMod(): boolean {
794     return (
795       this.props.moderators &&
796       isMod(
797         this.props.moderators.map(m => m.user_id),
798         this.props.node.comment.creator_id
799       )
800     );
801   }
802
803   get isAdmin(): boolean {
804     return (
805       this.props.admins &&
806       isMod(
807         this.props.admins.map(a => a.id),
808         this.props.node.comment.creator_id
809       )
810     );
811   }
812
813   get isPostCreator(): boolean {
814     return this.props.node.comment.creator_id == this.props.postCreatorId;
815   }
816
817   get canMod(): boolean {
818     if (this.props.admins && this.props.moderators) {
819       let adminsThenMods = this.props.admins
820         .map(a => a.id)
821         .concat(this.props.moderators.map(m => m.user_id));
822
823       return canMod(
824         UserService.Instance.user,
825         adminsThenMods,
826         this.props.node.comment.creator_id
827       );
828     } else {
829       return false;
830     }
831   }
832
833   get canAdmin(): boolean {
834     return (
835       this.props.admins &&
836       canMod(
837         UserService.Instance.user,
838         this.props.admins.map(a => a.id),
839         this.props.node.comment.creator_id
840       )
841     );
842   }
843
844   get amCommunityCreator(): boolean {
845     return (
846       this.props.moderators &&
847       UserService.Instance.user &&
848       this.props.node.comment.creator_id != UserService.Instance.user.id &&
849       UserService.Instance.user.id == this.props.moderators[0].user_id
850     );
851   }
852
853   get amSiteCreator(): boolean {
854     return (
855       this.props.admins &&
856       UserService.Instance.user &&
857       this.props.node.comment.creator_id != UserService.Instance.user.id &&
858       UserService.Instance.user.id == this.props.admins[0].id
859     );
860   }
861
862   get commentUnlessRemoved(): string {
863     let node = this.props.node;
864     return node.comment.removed
865       ? `*${i18n.t('removed')}*`
866       : node.comment.deleted
867       ? `*${i18n.t('deleted')}*`
868       : node.comment.content;
869   }
870
871   handleReplyClick(i: CommentNode) {
872     i.state.showReply = true;
873     i.setState(i.state);
874   }
875
876   handleEditClick(i: CommentNode) {
877     i.state.showEdit = true;
878     i.setState(i.state);
879   }
880
881   handleDeleteClick(i: CommentNode) {
882     let deleteForm: CommentFormI = {
883       content: i.props.node.comment.content,
884       edit_id: i.props.node.comment.id,
885       creator_id: i.props.node.comment.creator_id,
886       post_id: i.props.node.comment.post_id,
887       parent_id: i.props.node.comment.parent_id,
888       deleted: !i.props.node.comment.deleted,
889       auth: null,
890     };
891     WebSocketService.Instance.editComment(deleteForm);
892   }
893
894   handleSaveCommentClick(i: CommentNode) {
895     let saved =
896       i.props.node.comment.saved == undefined
897         ? true
898         : !i.props.node.comment.saved;
899     let form: SaveCommentForm = {
900       comment_id: i.props.node.comment.id,
901       save: saved,
902     };
903
904     WebSocketService.Instance.saveComment(form);
905
906     i.state.saveLoading = true;
907     i.setState(this.state);
908   }
909
910   handleReplyCancel() {
911     this.state.showReply = false;
912     this.state.showEdit = false;
913     this.setState(this.state);
914   }
915
916   handleCommentUpvote(i: CommentNodeI) {
917     let new_vote = this.state.my_vote == 1 ? 0 : 1;
918
919     if (this.state.my_vote == 1) {
920       this.state.score--;
921       this.state.upvotes--;
922     } else if (this.state.my_vote == -1) {
923       this.state.downvotes--;
924       this.state.upvotes++;
925       this.state.score += 2;
926     } else {
927       this.state.upvotes++;
928       this.state.score++;
929     }
930
931     this.state.my_vote = new_vote;
932
933     let form: CommentLikeForm = {
934       comment_id: i.comment.id,
935       post_id: i.comment.post_id,
936       score: this.state.my_vote,
937     };
938
939     WebSocketService.Instance.likeComment(form);
940     this.setState(this.state);
941     setupTippy();
942   }
943
944   handleCommentDownvote(i: CommentNodeI) {
945     let new_vote = this.state.my_vote == -1 ? 0 : -1;
946
947     if (this.state.my_vote == 1) {
948       this.state.score -= 2;
949       this.state.upvotes--;
950       this.state.downvotes++;
951     } else if (this.state.my_vote == -1) {
952       this.state.downvotes--;
953       this.state.score++;
954     } else {
955       this.state.downvotes++;
956       this.state.score--;
957     }
958
959     this.state.my_vote = new_vote;
960
961     let form: CommentLikeForm = {
962       comment_id: i.comment.id,
963       post_id: i.comment.post_id,
964       score: this.state.my_vote,
965     };
966
967     WebSocketService.Instance.likeComment(form);
968     this.setState(this.state);
969     setupTippy();
970   }
971
972   handleModRemoveShow(i: CommentNode) {
973     i.state.showRemoveDialog = true;
974     i.setState(i.state);
975   }
976
977   handleModRemoveReasonChange(i: CommentNode, event: any) {
978     i.state.removeReason = event.target.value;
979     i.setState(i.state);
980   }
981
982   handleModRemoveSubmit(i: CommentNode) {
983     event.preventDefault();
984     let form: CommentFormI = {
985       content: i.props.node.comment.content,
986       edit_id: i.props.node.comment.id,
987       creator_id: i.props.node.comment.creator_id,
988       post_id: i.props.node.comment.post_id,
989       parent_id: i.props.node.comment.parent_id,
990       removed: !i.props.node.comment.removed,
991       reason: i.state.removeReason,
992       auth: null,
993     };
994     WebSocketService.Instance.editComment(form);
995
996     i.state.showRemoveDialog = false;
997     i.setState(i.state);
998   }
999
1000   handleMarkRead(i: CommentNode) {
1001     // if it has a user_mention_id field, then its a mention
1002     if (i.props.node.comment.user_mention_id) {
1003       let form: EditUserMentionForm = {
1004         user_mention_id: i.props.node.comment.user_mention_id,
1005         read: !i.props.node.comment.read,
1006       };
1007       WebSocketService.Instance.editUserMention(form);
1008     } else {
1009       let form: CommentFormI = {
1010         content: i.props.node.comment.content,
1011         edit_id: i.props.node.comment.id,
1012         creator_id: i.props.node.comment.creator_id,
1013         post_id: i.props.node.comment.post_id,
1014         parent_id: i.props.node.comment.parent_id,
1015         read: !i.props.node.comment.read,
1016         auth: null,
1017       };
1018       WebSocketService.Instance.editComment(form);
1019     }
1020
1021     i.state.readLoading = true;
1022     i.setState(this.state);
1023   }
1024
1025   handleModBanFromCommunityShow(i: CommentNode) {
1026     i.state.showBanDialog = !i.state.showBanDialog;
1027     i.state.banType = BanType.Community;
1028     i.setState(i.state);
1029   }
1030
1031   handleModBanShow(i: CommentNode) {
1032     i.state.showBanDialog = !i.state.showBanDialog;
1033     i.state.banType = BanType.Site;
1034     i.setState(i.state);
1035   }
1036
1037   handleModBanReasonChange(i: CommentNode, event: any) {
1038     i.state.banReason = event.target.value;
1039     i.setState(i.state);
1040   }
1041
1042   handleModBanExpiresChange(i: CommentNode, event: any) {
1043     i.state.banExpires = event.target.value;
1044     i.setState(i.state);
1045   }
1046
1047   handleModBanFromCommunitySubmit(i: CommentNode) {
1048     i.state.banType = BanType.Community;
1049     i.setState(i.state);
1050     i.handleModBanBothSubmit(i);
1051   }
1052
1053   handleModBanSubmit(i: CommentNode) {
1054     i.state.banType = BanType.Site;
1055     i.setState(i.state);
1056     i.handleModBanBothSubmit(i);
1057   }
1058
1059   handleModBanBothSubmit(i: CommentNode) {
1060     event.preventDefault();
1061
1062     if (i.state.banType == BanType.Community) {
1063       let form: BanFromCommunityForm = {
1064         user_id: i.props.node.comment.creator_id,
1065         community_id: i.props.node.comment.community_id,
1066         ban: !i.props.node.comment.banned_from_community,
1067         reason: i.state.banReason,
1068         expires: getUnixTime(i.state.banExpires),
1069       };
1070       WebSocketService.Instance.banFromCommunity(form);
1071     } else {
1072       let form: BanUserForm = {
1073         user_id: i.props.node.comment.creator_id,
1074         ban: !i.props.node.comment.banned,
1075         reason: i.state.banReason,
1076         expires: getUnixTime(i.state.banExpires),
1077       };
1078       WebSocketService.Instance.banUser(form);
1079     }
1080
1081     i.state.showBanDialog = false;
1082     i.setState(i.state);
1083   }
1084
1085   handleShowConfirmAppointAsMod(i: CommentNode) {
1086     i.state.showConfirmAppointAsMod = true;
1087     i.setState(i.state);
1088   }
1089
1090   handleCancelConfirmAppointAsMod(i: CommentNode) {
1091     i.state.showConfirmAppointAsMod = false;
1092     i.setState(i.state);
1093   }
1094
1095   handleAddModToCommunity(i: CommentNode) {
1096     let form: AddModToCommunityForm = {
1097       user_id: i.props.node.comment.creator_id,
1098       community_id: i.props.node.comment.community_id,
1099       added: !i.isMod,
1100     };
1101     WebSocketService.Instance.addModToCommunity(form);
1102     i.state.showConfirmAppointAsMod = false;
1103     i.setState(i.state);
1104   }
1105
1106   handleShowConfirmAppointAsAdmin(i: CommentNode) {
1107     i.state.showConfirmAppointAsAdmin = true;
1108     i.setState(i.state);
1109   }
1110
1111   handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1112     i.state.showConfirmAppointAsAdmin = false;
1113     i.setState(i.state);
1114   }
1115
1116   handleAddAdmin(i: CommentNode) {
1117     let form: AddAdminForm = {
1118       user_id: i.props.node.comment.creator_id,
1119       added: !i.isAdmin,
1120     };
1121     WebSocketService.Instance.addAdmin(form);
1122     i.state.showConfirmAppointAsAdmin = false;
1123     i.setState(i.state);
1124   }
1125
1126   handleShowConfirmTransferCommunity(i: CommentNode) {
1127     i.state.showConfirmTransferCommunity = true;
1128     i.setState(i.state);
1129   }
1130
1131   handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1132     i.state.showConfirmTransferCommunity = false;
1133     i.setState(i.state);
1134   }
1135
1136   handleTransferCommunity(i: CommentNode) {
1137     let form: TransferCommunityForm = {
1138       community_id: i.props.node.comment.community_id,
1139       user_id: i.props.node.comment.creator_id,
1140     };
1141     WebSocketService.Instance.transferCommunity(form);
1142     i.state.showConfirmTransferCommunity = false;
1143     i.setState(i.state);
1144   }
1145
1146   handleShowConfirmTransferSite(i: CommentNode) {
1147     i.state.showConfirmTransferSite = true;
1148     i.setState(i.state);
1149   }
1150
1151   handleCancelShowConfirmTransferSite(i: CommentNode) {
1152     i.state.showConfirmTransferSite = false;
1153     i.setState(i.state);
1154   }
1155
1156   handleTransferSite(i: CommentNode) {
1157     let form: TransferSiteForm = {
1158       user_id: i.props.node.comment.creator_id,
1159     };
1160     WebSocketService.Instance.transferSite(form);
1161     i.state.showConfirmTransferSite = false;
1162     i.setState(i.state);
1163   }
1164
1165   get isCommentNew(): boolean {
1166     let now = moment.utc().subtract(10, 'minutes');
1167     let then = moment.utc(this.props.node.comment.published);
1168     return now.isBefore(then);
1169   }
1170
1171   handleCommentCollapse(i: CommentNode) {
1172     i.state.collapsed = !i.state.collapsed;
1173     i.setState(i.state);
1174   }
1175
1176   handleViewSource(i: CommentNode) {
1177     i.state.viewSource = !i.state.viewSource;
1178     i.setState(i.state);
1179   }
1180
1181   handleShowAdvanced(i: CommentNode) {
1182     i.state.showAdvanced = !i.state.showAdvanced;
1183     i.setState(i.state);
1184     setupTippy();
1185   }
1186
1187   get scoreColor() {
1188     if (this.state.my_vote == 1) {
1189       return 'text-info';
1190     } else if (this.state.my_vote == -1) {
1191       return 'text-danger';
1192     } else {
1193       return 'text-muted';
1194     }
1195   }
1196
1197   get pointsTippy(): string {
1198     let points = i18n.t('number_of_points', {
1199       count: this.state.score,
1200     });
1201
1202     let upvotes = i18n.t('number_of_upvotes', {
1203       count: this.state.upvotes,
1204     });
1205
1206     let downvotes = i18n.t('number_of_downvotes', {
1207       count: this.state.downvotes,
1208     });
1209
1210     return `${points} • ${upvotes} • ${downvotes}`;
1211   }
1212 }