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