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