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