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