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