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