]> Untitled Git - lemmy-ui.git/blob - src/shared/components/comment-node.tsx
030515c04100ce47a55431b2882a653e21860d23
[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 className="mr-2" to={`/post/${node.comment.post_id}`}>
206                     {node.comment.post_name}
207                   </Link>
208                 </>
209               )}
210               <button
211                 class="btn btn-sm text-muted"
212                 onClick={linkEvent(this, this.handleCommentCollapse)}
213               >
214                 {this.state.collapsed ? '+' : '—'}
215               </button>
216               {/* This is an expanding spacer for mobile */}
217               <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
218               <a
219                 className={`unselectable pointer ${this.scoreColor}`}
220                 onClick={linkEvent(node, this.handleCommentUpvote)}
221                 data-tippy-content={this.pointsTippy}
222               >
223                 <span class="mr-1 font-weight-bold">{this.state.score}</span>
224               </a>
225               <span className="mr-1">•</span>
226               <span>
227                 <MomentTime data={node.comment} />
228               </span>
229             </div>
230             {/* end of user row */}
231             {this.state.showEdit && (
232               <CommentForm
233                 node={node}
234                 edit
235                 onReplyCancel={this.handleReplyCancel}
236                 disabled={this.props.locked}
237                 focus
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                 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
253                   {this.props.showContext && this.linkBtn}
254                   {this.props.markable && (
255                     <button
256                       class="btn btn-link 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 ${
269                             node.comment.read && 'text-success'
270                           }`}
271                         >
272                           <use xlinkHref="#icon-check"></use>
273                         </svg>
274                       )}
275                     </button>
276                   )}
277                   {UserService.Instance.user && !this.props.viewOnly && (
278                     <>
279                       <button
280                         className={`btn btn-link btn-animate ${
281                           this.state.my_vote == 1 ? 'text-info' : 'text-muted'
282                         }`}
283                         onClick={linkEvent(node, this.handleCommentUpvote)}
284                         data-tippy-content={i18n.t('upvote')}
285                       >
286                         <svg class="icon icon-inline">
287                           <use xlinkHref="#icon-arrow-up1"></use>
288                         </svg>
289                         {this.state.upvotes !== this.state.score && (
290                           <span class="ml-1">{this.state.upvotes}</span>
291                         )}
292                       </button>
293                       {this.props.enableDownvotes && (
294                         <button
295                           className={`btn btn-link btn-animate ${
296                             this.state.my_vote == -1
297                               ? 'text-danger'
298                               : 'text-muted'
299                           }`}
300                           onClick={linkEvent(node, this.handleCommentDownvote)}
301                           data-tippy-content={i18n.t('downvote')}
302                         >
303                           <svg class="icon icon-inline">
304                             <use xlinkHref="#icon-arrow-down1"></use>
305                           </svg>
306                           {this.state.upvotes !== this.state.score && (
307                             <span class="ml-1">{this.state.downvotes}</span>
308                           )}
309                         </button>
310                       )}
311                       <button
312                         class="btn btn-link btn-animate text-muted"
313                         onClick={linkEvent(this, this.handleReplyClick)}
314                         data-tippy-content={i18n.t('reply')}
315                       >
316                         <svg class="icon icon-inline">
317                           <use xlinkHref="#icon-reply1"></use>
318                         </svg>
319                       </button>
320                       {!this.state.showAdvanced ? (
321                         <button
322                           className="btn btn-link btn-animate text-muted"
323                           onClick={linkEvent(this, this.handleShowAdvanced)}
324                           data-tippy-content={i18n.t('more')}
325                         >
326                           <svg class="icon icon-inline">
327                             <use xlinkHref="#icon-more-vertical"></use>
328                           </svg>
329                         </button>
330                       ) : (
331                         <>
332                           {!this.myComment && (
333                             <button class="btn btn-link btn-animate">
334                               <Link
335                                 className="text-muted"
336                                 to={`/create_private_message/recipient/${node.comment.creator_id}`}
337                                 title={i18n.t('message').toLowerCase()}
338                               >
339                                 <svg class="icon">
340                                   <use xlinkHref="#icon-mail"></use>
341                                 </svg>
342                               </Link>
343                             </button>
344                           )}
345                           {!this.props.showContext && this.linkBtn}
346                           <button
347                             class="btn btn-link btn-animate text-muted"
348                             onClick={linkEvent(
349                               this,
350                               this.handleSaveCommentClick
351                             )}
352                             data-tippy-content={
353                               node.comment.saved
354                                 ? i18n.t('unsave')
355                                 : i18n.t('save')
356                             }
357                           >
358                             {this.state.saveLoading ? (
359                               this.loadingIcon
360                             ) : (
361                               <svg
362                                 class={`icon icon-inline ${
363                                   node.comment.saved && 'text-warning'
364                                 }`}
365                               >
366                                 <use xlinkHref="#icon-star"></use>
367                               </svg>
368                             )}
369                           </button>
370                           <button
371                             className="btn btn-link btn-animate text-muted"
372                             onClick={linkEvent(this, this.handleViewSource)}
373                             data-tippy-content={i18n.t('view_source')}
374                           >
375                             <svg
376                               class={`icon icon-inline ${
377                                 this.state.viewSource && 'text-success'
378                               }`}
379                             >
380                               <use xlinkHref="#icon-file-text"></use>
381                             </svg>
382                           </button>
383                           {this.myComment && (
384                             <>
385                               <button
386                                 class="btn btn-link btn-animate text-muted"
387                                 onClick={linkEvent(this, this.handleEditClick)}
388                                 data-tippy-content={i18n.t('edit')}
389                               >
390                                 <svg class="icon icon-inline">
391                                   <use xlinkHref="#icon-edit"></use>
392                                 </svg>
393                               </button>
394                               <button
395                                 class="btn btn-link btn-animate text-muted"
396                                 onClick={linkEvent(
397                                   this,
398                                   this.handleDeleteClick
399                                 )}
400                                 data-tippy-content={
401                                   !node.comment.deleted
402                                     ? i18n.t('delete')
403                                     : i18n.t('restore')
404                                 }
405                               >
406                                 <svg
407                                   class={`icon icon-inline ${
408                                     node.comment.deleted && 'text-danger'
409                                   }`}
410                                 >
411                                   <use xlinkHref="#icon-trash"></use>
412                                 </svg>
413                               </button>
414                             </>
415                           )}
416                           {/* Admins and mods can remove comments */}
417                           {(this.canMod || this.canAdmin) && (
418                             <>
419                               {!node.comment.removed ? (
420                                 <button
421                                   class="btn btn-link btn-animate text-muted"
422                                   onClick={linkEvent(
423                                     this,
424                                     this.handleModRemoveShow
425                                   )}
426                                 >
427                                   {i18n.t('remove')}
428                                 </button>
429                               ) : (
430                                 <button
431                                   class="btn btn-link btn-animate text-muted"
432                                   onClick={linkEvent(
433                                     this,
434                                     this.handleModRemoveSubmit
435                                   )}
436                                 >
437                                   {i18n.t('restore')}
438                                 </button>
439                               )}
440                             </>
441                           )}
442                           {/* Mods can ban from community, and appoint as mods to community */}
443                           {this.canMod && (
444                             <>
445                               {!this.isMod &&
446                                 (!node.comment.banned_from_community ? (
447                                   <button
448                                     class="btn btn-link btn-animate text-muted"
449                                     onClick={linkEvent(
450                                       this,
451                                       this.handleModBanFromCommunityShow
452                                     )}
453                                   >
454                                     {i18n.t('ban')}
455                                   </button>
456                                 ) : (
457                                   <button
458                                     class="btn btn-link btn-animate text-muted"
459                                     onClick={linkEvent(
460                                       this,
461                                       this.handleModBanFromCommunitySubmit
462                                     )}
463                                   >
464                                     {i18n.t('unban')}
465                                   </button>
466                                 ))}
467                               {!node.comment.banned_from_community &&
468                                 node.comment.creator_local &&
469                                 (!this.state.showConfirmAppointAsMod ? (
470                                   <button
471                                     class="btn btn-link btn-animate text-muted"
472                                     onClick={linkEvent(
473                                       this,
474                                       this.handleShowConfirmAppointAsMod
475                                     )}
476                                   >
477                                     {this.isMod
478                                       ? i18n.t('remove_as_mod')
479                                       : i18n.t('appoint_as_mod')}
480                                   </button>
481                                 ) : (
482                                   <>
483                                     <button class="btn btn-link btn-animate text-muted">
484                                       {i18n.t('are_you_sure')}
485                                     </button>
486                                     <button
487                                       class="btn btn-link btn-animate text-muted"
488                                       onClick={linkEvent(
489                                         this,
490                                         this.handleAddModToCommunity
491                                       )}
492                                     >
493                                       {i18n.t('yes')}
494                                     </button>
495                                     <button
496                                       class="btn btn-link btn-animate text-muted"
497                                       onClick={linkEvent(
498                                         this,
499                                         this.handleCancelConfirmAppointAsMod
500                                       )}
501                                     >
502                                       {i18n.t('no')}
503                                     </button>
504                                   </>
505                                 ))}
506                             </>
507                           )}
508                           {/* Community creators and admins can transfer community to another mod */}
509                           {(this.amCommunityCreator || this.canAdmin) &&
510                             this.isMod &&
511                             node.comment.creator_local &&
512                             (!this.state.showConfirmTransferCommunity ? (
513                               <button
514                                 class="btn btn-link btn-animate text-muted"
515                                 onClick={linkEvent(
516                                   this,
517                                   this.handleShowConfirmTransferCommunity
518                                 )}
519                               >
520                                 {i18n.t('transfer_community')}
521                               </button>
522                             ) : (
523                               <>
524                                 <button class="btn btn-link btn-animate text-muted">
525                                   {i18n.t('are_you_sure')}
526                                 </button>
527                                 <button
528                                   class="btn btn-link btn-animate text-muted"
529                                   onClick={linkEvent(
530                                     this,
531                                     this.handleTransferCommunity
532                                   )}
533                                 >
534                                   {i18n.t('yes')}
535                                 </button>
536                                 <button
537                                   class="btn btn-link btn-animate text-muted"
538                                   onClick={linkEvent(
539                                     this,
540                                     this
541                                       .handleCancelShowConfirmTransferCommunity
542                                   )}
543                                 >
544                                   {i18n.t('no')}
545                                 </button>
546                               </>
547                             ))}
548                           {/* Admins can ban from all, and appoint other admins */}
549                           {this.canAdmin && (
550                             <>
551                               {!this.isAdmin &&
552                                 (!node.comment.banned ? (
553                                   <button
554                                     class="btn btn-link btn-animate text-muted"
555                                     onClick={linkEvent(
556                                       this,
557                                       this.handleModBanShow
558                                     )}
559                                   >
560                                     {i18n.t('ban_from_site')}
561                                   </button>
562                                 ) : (
563                                   <button
564                                     class="btn btn-link btn-animate text-muted"
565                                     onClick={linkEvent(
566                                       this,
567                                       this.handleModBanSubmit
568                                     )}
569                                   >
570                                     {i18n.t('unban_from_site')}
571                                   </button>
572                                 ))}
573                               {!node.comment.banned &&
574                                 node.comment.creator_local &&
575                                 (!this.state.showConfirmAppointAsAdmin ? (
576                                   <button
577                                     class="btn btn-link btn-animate text-muted"
578                                     onClick={linkEvent(
579                                       this,
580                                       this.handleShowConfirmAppointAsAdmin
581                                     )}
582                                   >
583                                     {this.isAdmin
584                                       ? i18n.t('remove_as_admin')
585                                       : i18n.t('appoint_as_admin')}
586                                   </button>
587                                 ) : (
588                                   <>
589                                     <button class="btn btn-link btn-animate text-muted">
590                                       {i18n.t('are_you_sure')}
591                                     </button>
592                                     <button
593                                       class="btn btn-link btn-animate text-muted"
594                                       onClick={linkEvent(
595                                         this,
596                                         this.handleAddAdmin
597                                       )}
598                                     >
599                                       {i18n.t('yes')}
600                                     </button>
601                                     <button
602                                       class="btn btn-link btn-animate text-muted"
603                                       onClick={linkEvent(
604                                         this,
605                                         this.handleCancelConfirmAppointAsAdmin
606                                       )}
607                                     >
608                                       {i18n.t('no')}
609                                     </button>
610                                   </>
611                                 ))}
612                             </>
613                           )}
614                           {/* Site Creator can transfer to another admin */}
615                           {this.amSiteCreator &&
616                             this.isAdmin &&
617                             node.comment.creator_local &&
618                             (!this.state.showConfirmTransferSite ? (
619                               <button
620                                 class="btn btn-link btn-animate text-muted"
621                                 onClick={linkEvent(
622                                   this,
623                                   this.handleShowConfirmTransferSite
624                                 )}
625                               >
626                                 {i18n.t('transfer_site')}
627                               </button>
628                             ) : (
629                               <>
630                                 <button class="btn btn-link btn-animate text-muted">
631                                   {i18n.t('are_you_sure')}
632                                 </button>
633                                 <button
634                                   class="btn btn-link btn-animate text-muted"
635                                   onClick={linkEvent(
636                                     this,
637                                     this.handleTransferSite
638                                   )}
639                                 >
640                                   {i18n.t('yes')}
641                                 </button>
642                                 <button
643                                   class="btn btn-link btn-animate text-muted"
644                                   onClick={linkEvent(
645                                     this,
646                                     this.handleCancelShowConfirmTransferSite
647                                   )}
648                                 >
649                                   {i18n.t('no')}
650                                 </button>
651                               </>
652                             ))}
653                         </>
654                       )}
655                     </>
656                   )}
657                 </div>
658                 {/* end of button group */}
659               </div>
660             )}
661           </div>
662         </div>
663         {/* end of details */}
664         {this.state.showRemoveDialog && (
665           <form
666             class="form-inline"
667             onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
668           >
669             <input
670               type="text"
671               class="form-control mr-2"
672               placeholder={i18n.t('reason')}
673               value={this.state.removeReason}
674               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
675             />
676             <button type="submit" class="btn btn-secondary">
677               {i18n.t('remove_comment')}
678             </button>
679           </form>
680         )}
681         {this.state.showBanDialog && (
682           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
683             <div class="form-group row">
684               <label class="col-form-label">{i18n.t('reason')}</label>
685               <input
686                 type="text"
687                 class="form-control mr-2"
688                 placeholder={i18n.t('reason')}
689                 value={this.state.banReason}
690                 onInput={linkEvent(this, this.handleModBanReasonChange)}
691               />
692               <div class="form-group">
693                 <div class="form-check">
694                   <input
695                     class="form-check-input"
696                     id="mod-ban-remove-data"
697                     type="checkbox"
698                     checked={this.state.removeData}
699                     onChange={linkEvent(this, this.handleModRemoveDataChange)}
700                   />
701                   <label class="form-check-label" htmlFor="mod-ban-remove-data">
702                     {i18n.t('remove_posts_comments')}
703                   </label>
704                 </div>
705               </div>
706             </div>
707             {/* TODO hold off on expires until later */}
708             {/* <div class="form-group row"> */}
709             {/*   <label class="col-form-label">Expires</label> */}
710             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
711             {/* </div> */}
712             <div class="form-group row">
713               <button type="submit" class="btn btn-secondary">
714                 {i18n.t('ban')} {node.comment.creator_name}
715               </button>
716             </div>
717           </form>
718         )}
719         {this.state.showReply && (
720           <CommentForm
721             node={node}
722             onReplyCancel={this.handleReplyCancel}
723             disabled={this.props.locked}
724             focus
725           />
726         )}
727         {node.children && !this.state.collapsed && (
728           <CommentNodes
729             nodes={node.children}
730             locked={this.props.locked}
731             moderators={this.props.moderators}
732             admins={this.props.admins}
733             postCreatorId={this.props.postCreatorId}
734             sort={this.props.sort}
735             sortType={this.props.sortType}
736             enableDownvotes={this.props.enableDownvotes}
737           />
738         )}
739         {/* A collapsed clearfix */}
740         {this.state.collapsed && <div class="row col-12"></div>}
741       </div>
742     );
743   }
744
745   get linkBtn() {
746     let node = this.props.node;
747     return (
748       <Link
749         className="btn btn-link btn-animate text-muted"
750         to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
751         title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
752       >
753         <svg class="icon icon-inline">
754           <use xlinkHref="#icon-link"></use>
755         </svg>
756       </Link>
757     );
758   }
759
760   get loadingIcon() {
761     return (
762       <svg class="icon icon-spinner spin">
763         <use xlinkHref="#icon-spinner"></use>
764       </svg>
765     );
766   }
767
768   get myComment(): boolean {
769     return (
770       UserService.Instance.user &&
771       this.props.node.comment.creator_id == UserService.Instance.user.id
772     );
773   }
774
775   get isMod(): boolean {
776     return (
777       this.props.moderators &&
778       isMod(
779         this.props.moderators.map(m => m.user_id),
780         this.props.node.comment.creator_id
781       )
782     );
783   }
784
785   get isAdmin(): boolean {
786     return (
787       this.props.admins &&
788       isMod(
789         this.props.admins.map(a => a.id),
790         this.props.node.comment.creator_id
791       )
792     );
793   }
794
795   get isPostCreator(): boolean {
796     return this.props.node.comment.creator_id == this.props.postCreatorId;
797   }
798
799   get canMod(): boolean {
800     if (this.props.admins && this.props.moderators) {
801       let adminsThenMods = this.props.admins
802         .map(a => a.id)
803         .concat(this.props.moderators.map(m => m.user_id));
804
805       return canMod(
806         UserService.Instance.user,
807         adminsThenMods,
808         this.props.node.comment.creator_id
809       );
810     } else {
811       return false;
812     }
813   }
814
815   get canAdmin(): boolean {
816     return (
817       this.props.admins &&
818       canMod(
819         UserService.Instance.user,
820         this.props.admins.map(a => a.id),
821         this.props.node.comment.creator_id
822       )
823     );
824   }
825
826   get amCommunityCreator(): boolean {
827     return (
828       this.props.moderators &&
829       UserService.Instance.user &&
830       this.props.node.comment.creator_id != UserService.Instance.user.id &&
831       UserService.Instance.user.id == this.props.moderators[0].user_id
832     );
833   }
834
835   get amSiteCreator(): boolean {
836     return (
837       this.props.admins &&
838       UserService.Instance.user &&
839       this.props.node.comment.creator_id != UserService.Instance.user.id &&
840       UserService.Instance.user.id == this.props.admins[0].id
841     );
842   }
843
844   get commentUnlessRemoved(): string {
845     let node = this.props.node;
846     return node.comment.removed
847       ? `*${i18n.t('removed')}*`
848       : node.comment.deleted
849       ? `*${i18n.t('deleted')}*`
850       : node.comment.content;
851   }
852
853   handleReplyClick(i: CommentNode) {
854     i.state.showReply = true;
855     i.setState(i.state);
856   }
857
858   handleEditClick(i: CommentNode) {
859     i.state.showEdit = true;
860     i.setState(i.state);
861   }
862
863   handleDeleteClick(i: CommentNode) {
864     let deleteForm: DeleteCommentForm = {
865       edit_id: i.props.node.comment.id,
866       deleted: !i.props.node.comment.deleted,
867       auth: null,
868     };
869     WebSocketService.Instance.deleteComment(deleteForm);
870   }
871
872   handleSaveCommentClick(i: CommentNode) {
873     let saved =
874       i.props.node.comment.saved == undefined
875         ? true
876         : !i.props.node.comment.saved;
877     let form: SaveCommentForm = {
878       comment_id: i.props.node.comment.id,
879       save: saved,
880     };
881
882     WebSocketService.Instance.saveComment(form);
883
884     i.state.saveLoading = true;
885     i.setState(this.state);
886   }
887
888   handleReplyCancel() {
889     this.state.showReply = false;
890     this.state.showEdit = false;
891     this.setState(this.state);
892   }
893
894   handleCommentUpvote(i: CommentNodeI) {
895     let new_vote = this.state.my_vote == 1 ? 0 : 1;
896
897     if (this.state.my_vote == 1) {
898       this.state.score--;
899       this.state.upvotes--;
900     } else if (this.state.my_vote == -1) {
901       this.state.downvotes--;
902       this.state.upvotes++;
903       this.state.score += 2;
904     } else {
905       this.state.upvotes++;
906       this.state.score++;
907     }
908
909     this.state.my_vote = new_vote;
910
911     let form: CommentLikeForm = {
912       comment_id: i.comment.id,
913       score: this.state.my_vote,
914     };
915
916     WebSocketService.Instance.likeComment(form);
917     this.setState(this.state);
918     setupTippy();
919   }
920
921   handleCommentDownvote(i: CommentNodeI) {
922     let new_vote = this.state.my_vote == -1 ? 0 : -1;
923
924     if (this.state.my_vote == 1) {
925       this.state.score -= 2;
926       this.state.upvotes--;
927       this.state.downvotes++;
928     } else if (this.state.my_vote == -1) {
929       this.state.downvotes--;
930       this.state.score++;
931     } else {
932       this.state.downvotes++;
933       this.state.score--;
934     }
935
936     this.state.my_vote = new_vote;
937
938     let form: CommentLikeForm = {
939       comment_id: i.comment.id,
940       score: this.state.my_vote,
941     };
942
943     WebSocketService.Instance.likeComment(form);
944     this.setState(this.state);
945     setupTippy();
946   }
947
948   handleModRemoveShow(i: CommentNode) {
949     i.state.showRemoveDialog = true;
950     i.setState(i.state);
951   }
952
953   handleModRemoveReasonChange(i: CommentNode, event: any) {
954     i.state.removeReason = event.target.value;
955     i.setState(i.state);
956   }
957
958   handleModRemoveDataChange(i: CommentNode, event: any) {
959     i.state.removeData = event.target.checked;
960     i.setState(i.state);
961   }
962
963   handleModRemoveSubmit(i: CommentNode) {
964     event.preventDefault();
965     let form: RemoveCommentForm = {
966       edit_id: i.props.node.comment.id,
967       removed: !i.props.node.comment.removed,
968       reason: i.state.removeReason,
969       auth: null,
970     };
971     WebSocketService.Instance.removeComment(form);
972
973     i.state.showRemoveDialog = false;
974     i.setState(i.state);
975   }
976
977   handleMarkRead(i: CommentNode) {
978     // if it has a user_mention_id field, then its a mention
979     if (i.props.node.comment.user_mention_id) {
980       let form: MarkUserMentionAsReadForm = {
981         user_mention_id: i.props.node.comment.user_mention_id,
982         read: !i.props.node.comment.read,
983       };
984       WebSocketService.Instance.markUserMentionAsRead(form);
985     } else {
986       let form: MarkCommentAsReadForm = {
987         edit_id: i.props.node.comment.id,
988         read: !i.props.node.comment.read,
989         auth: null,
990       };
991       WebSocketService.Instance.markCommentAsRead(form);
992     }
993
994     i.state.readLoading = true;
995     i.setState(this.state);
996   }
997
998   handleModBanFromCommunityShow(i: CommentNode) {
999     i.state.showBanDialog = !i.state.showBanDialog;
1000     i.state.banType = BanType.Community;
1001     i.setState(i.state);
1002   }
1003
1004   handleModBanShow(i: CommentNode) {
1005     i.state.showBanDialog = !i.state.showBanDialog;
1006     i.state.banType = BanType.Site;
1007     i.setState(i.state);
1008   }
1009
1010   handleModBanReasonChange(i: CommentNode, event: any) {
1011     i.state.banReason = event.target.value;
1012     i.setState(i.state);
1013   }
1014
1015   handleModBanExpiresChange(i: CommentNode, event: any) {
1016     i.state.banExpires = event.target.value;
1017     i.setState(i.state);
1018   }
1019
1020   handleModBanFromCommunitySubmit(i: CommentNode) {
1021     i.state.banType = BanType.Community;
1022     i.setState(i.state);
1023     i.handleModBanBothSubmit(i);
1024   }
1025
1026   handleModBanSubmit(i: CommentNode) {
1027     i.state.banType = BanType.Site;
1028     i.setState(i.state);
1029     i.handleModBanBothSubmit(i);
1030   }
1031
1032   handleModBanBothSubmit(i: CommentNode) {
1033     event.preventDefault();
1034
1035     if (i.state.banType == BanType.Community) {
1036       // If its an unban, restore all their data
1037       let ban = !i.props.node.comment.banned_from_community;
1038       if (ban == false) {
1039         i.state.removeData = false;
1040       }
1041       let form: BanFromCommunityForm = {
1042         user_id: i.props.node.comment.creator_id,
1043         community_id: i.props.node.comment.community_id,
1044         ban,
1045         remove_data: i.state.removeData,
1046         reason: i.state.banReason,
1047         expires: getUnixTime(i.state.banExpires),
1048       };
1049       WebSocketService.Instance.banFromCommunity(form);
1050     } else {
1051       // If its an unban, restore all their data
1052       let ban = !i.props.node.comment.banned;
1053       if (ban == false) {
1054         i.state.removeData = false;
1055       }
1056       let form: BanUserForm = {
1057         user_id: i.props.node.comment.creator_id,
1058         ban,
1059         remove_data: i.state.removeData,
1060         reason: i.state.banReason,
1061         expires: getUnixTime(i.state.banExpires),
1062       };
1063       WebSocketService.Instance.banUser(form);
1064     }
1065
1066     i.state.showBanDialog = false;
1067     i.setState(i.state);
1068   }
1069
1070   handleShowConfirmAppointAsMod(i: CommentNode) {
1071     i.state.showConfirmAppointAsMod = true;
1072     i.setState(i.state);
1073   }
1074
1075   handleCancelConfirmAppointAsMod(i: CommentNode) {
1076     i.state.showConfirmAppointAsMod = false;
1077     i.setState(i.state);
1078   }
1079
1080   handleAddModToCommunity(i: CommentNode) {
1081     let form: AddModToCommunityForm = {
1082       user_id: i.props.node.comment.creator_id,
1083       community_id: i.props.node.comment.community_id,
1084       added: !i.isMod,
1085     };
1086     WebSocketService.Instance.addModToCommunity(form);
1087     i.state.showConfirmAppointAsMod = false;
1088     i.setState(i.state);
1089   }
1090
1091   handleShowConfirmAppointAsAdmin(i: CommentNode) {
1092     i.state.showConfirmAppointAsAdmin = true;
1093     i.setState(i.state);
1094   }
1095
1096   handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1097     i.state.showConfirmAppointAsAdmin = false;
1098     i.setState(i.state);
1099   }
1100
1101   handleAddAdmin(i: CommentNode) {
1102     let form: AddAdminForm = {
1103       user_id: i.props.node.comment.creator_id,
1104       added: !i.isAdmin,
1105     };
1106     WebSocketService.Instance.addAdmin(form);
1107     i.state.showConfirmAppointAsAdmin = false;
1108     i.setState(i.state);
1109   }
1110
1111   handleShowConfirmTransferCommunity(i: CommentNode) {
1112     i.state.showConfirmTransferCommunity = true;
1113     i.setState(i.state);
1114   }
1115
1116   handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1117     i.state.showConfirmTransferCommunity = false;
1118     i.setState(i.state);
1119   }
1120
1121   handleTransferCommunity(i: CommentNode) {
1122     let form: TransferCommunityForm = {
1123       community_id: i.props.node.comment.community_id,
1124       user_id: i.props.node.comment.creator_id,
1125     };
1126     WebSocketService.Instance.transferCommunity(form);
1127     i.state.showConfirmTransferCommunity = false;
1128     i.setState(i.state);
1129   }
1130
1131   handleShowConfirmTransferSite(i: CommentNode) {
1132     i.state.showConfirmTransferSite = true;
1133     i.setState(i.state);
1134   }
1135
1136   handleCancelShowConfirmTransferSite(i: CommentNode) {
1137     i.state.showConfirmTransferSite = false;
1138     i.setState(i.state);
1139   }
1140
1141   handleTransferSite(i: CommentNode) {
1142     let form: TransferSiteForm = {
1143       user_id: i.props.node.comment.creator_id,
1144     };
1145     WebSocketService.Instance.transferSite(form);
1146     i.state.showConfirmTransferSite = false;
1147     i.setState(i.state);
1148   }
1149
1150   get isCommentNew(): boolean {
1151     let now = moment.utc().subtract(10, 'minutes');
1152     let then = moment.utc(this.props.node.comment.published);
1153     return now.isBefore(then);
1154   }
1155
1156   handleCommentCollapse(i: CommentNode) {
1157     i.state.collapsed = !i.state.collapsed;
1158     i.setState(i.state);
1159   }
1160
1161   handleViewSource(i: CommentNode) {
1162     i.state.viewSource = !i.state.viewSource;
1163     i.setState(i.state);
1164   }
1165
1166   handleShowAdvanced(i: CommentNode) {
1167     i.state.showAdvanced = !i.state.showAdvanced;
1168     i.setState(i.state);
1169     setupTippy();
1170   }
1171
1172   get scoreColor() {
1173     if (this.state.my_vote == 1) {
1174       return 'text-info';
1175     } else if (this.state.my_vote == -1) {
1176       return 'text-danger';
1177     } else {
1178       return 'text-muted';
1179     }
1180   }
1181
1182   get pointsTippy(): string {
1183     let points = i18n.t('number_of_points', {
1184       count: this.state.score,
1185     });
1186
1187     let upvotes = i18n.t('number_of_upvotes', {
1188       count: this.state.upvotes,
1189     });
1190
1191     let downvotes = i18n.t('number_of_downvotes', {
1192       count: this.state.downvotes,
1193     });
1194
1195     return `${points} • ${upvotes} • ${downvotes}`;
1196   }
1197 }