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