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