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