]> Untitled Git - lemmy.git/blob - ui/src/components/comment-node.tsx
Front end federation names and links for users, posts, and communities.
[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 ${
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 mb-1 mt-1 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.handleSaveCommentClick)}
303                         data-tippy-content={
304                           node.comment.saved ? i18n.t('unsave') : i18n.t('save')
305                         }
306                       >
307                         {this.state.saveLoading ? (
308                           this.loadingIcon
309                         ) : (
310                           <svg
311                             class={`icon icon-inline ${
312                               node.comment.saved && 'text-warning'
313                             }`}
314                           >
315                             <use xlinkHref="#icon-star"></use>
316                           </svg>
317                         )}
318                       </button>
319                       <button
320                         class="btn btn-link btn-animate text-muted"
321                         onClick={linkEvent(this, this.handleReplyClick)}
322                         data-tippy-content={i18n.t('reply')}
323                       >
324                         <svg class="icon icon-inline">
325                           <use xlinkHref="#icon-reply1"></use>
326                         </svg>
327                       </button>
328                       {!this.state.showAdvanced ? (
329                         <button
330                           className="btn btn-link btn-animate text-muted"
331                           onClick={linkEvent(this, this.handleShowAdvanced)}
332                           data-tippy-content={i18n.t('more')}
333                         >
334                           <svg class="icon icon-inline">
335                             <use xlinkHref="#icon-more-vertical"></use>
336                           </svg>
337                         </button>
338                       ) : (
339                         <>
340                           {!this.myComment && (
341                             <button class="btn btn-link btn-animate">
342                               <Link
343                                 class="text-muted"
344                                 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
345                                 title={i18n.t('message').toLowerCase()}
346                               >
347                                 <svg class="icon">
348                                   <use xlinkHref="#icon-mail"></use>
349                                 </svg>
350                               </Link>
351                             </button>
352                           )}
353                           {!this.props.showContext && this.linkBtn}
354                           <button
355                             className="btn btn-link btn-animate text-muted"
356                             onClick={linkEvent(this, this.handleViewSource)}
357                             data-tippy-content={i18n.t('view_source')}
358                           >
359                             <svg
360                               class={`icon icon-inline ${
361                                 this.state.viewSource && 'text-success'
362                               }`}
363                             >
364                               <use xlinkHref="#icon-file-text"></use>
365                             </svg>
366                           </button>
367                           {this.myComment && (
368                             <>
369                               <button
370                                 class="btn btn-link btn-animate text-muted"
371                                 onClick={linkEvent(this, this.handleEditClick)}
372                                 data-tippy-content={i18n.t('edit')}
373                               >
374                                 <svg class="icon icon-inline">
375                                   <use xlinkHref="#icon-edit"></use>
376                                 </svg>
377                               </button>
378                               <button
379                                 class="btn btn-link btn-animate text-muted"
380                                 onClick={linkEvent(
381                                   this,
382                                   this.handleDeleteClick
383                                 )}
384                                 data-tippy-content={
385                                   !node.comment.deleted
386                                     ? i18n.t('delete')
387                                     : i18n.t('restore')
388                                 }
389                               >
390                                 <svg
391                                   class={`icon icon-inline ${
392                                     node.comment.deleted && 'text-danger'
393                                   }`}
394                                 >
395                                   <use xlinkHref="#icon-trash"></use>
396                                 </svg>
397                               </button>
398                             </>
399                           )}
400                           {/* Admins and mods can remove comments */}
401                           {(this.canMod || this.canAdmin) && (
402                             <>
403                               {!node.comment.removed ? (
404                                 <button
405                                   class="btn btn-link btn-animate text-muted"
406                                   onClick={linkEvent(
407                                     this,
408                                     this.handleModRemoveShow
409                                   )}
410                                 >
411                                   {i18n.t('remove')}
412                                 </button>
413                               ) : (
414                                 <button
415                                   class="btn btn-link btn-animate text-muted"
416                                   onClick={linkEvent(
417                                     this,
418                                     this.handleModRemoveSubmit
419                                   )}
420                                 >
421                                   {i18n.t('restore')}
422                                 </button>
423                               )}
424                             </>
425                           )}
426                           {/* Mods can ban from community, and appoint as mods to community */}
427                           {this.canMod && (
428                             <>
429                               {!this.isMod &&
430                                 (!node.comment.banned_from_community ? (
431                                   <button
432                                     class="btn btn-link btn-animate text-muted"
433                                     onClick={linkEvent(
434                                       this,
435                                       this.handleModBanFromCommunityShow
436                                     )}
437                                   >
438                                     {i18n.t('ban')}
439                                   </button>
440                                 ) : (
441                                   <button
442                                     class="btn btn-link btn-animate text-muted"
443                                     onClick={linkEvent(
444                                       this,
445                                       this.handleModBanFromCommunitySubmit
446                                     )}
447                                   >
448                                     {i18n.t('unban')}
449                                   </button>
450                                 ))}
451                               {!node.comment.banned_from_community &&
452                                 (!this.state.showConfirmAppointAsMod ? (
453                                   <button
454                                     class="btn btn-link btn-animate text-muted"
455                                     onClick={linkEvent(
456                                       this,
457                                       this.handleShowConfirmAppointAsMod
458                                     )}
459                                   >
460                                     {this.isMod
461                                       ? i18n.t('remove_as_mod')
462                                       : i18n.t('appoint_as_mod')}
463                                   </button>
464                                 ) : (
465                                   <>
466                                     <button class="btn btn-link btn-animate text-muted">
467                                       {i18n.t('are_you_sure')}
468                                     </button>
469                                     <button
470                                       class="btn btn-link btn-animate text-muted"
471                                       onClick={linkEvent(
472                                         this,
473                                         this.handleAddModToCommunity
474                                       )}
475                                     >
476                                       {i18n.t('yes')}
477                                     </button>
478                                     <button
479                                       class="btn btn-link btn-animate text-muted"
480                                       onClick={linkEvent(
481                                         this,
482                                         this.handleCancelConfirmAppointAsMod
483                                       )}
484                                     >
485                                       {i18n.t('no')}
486                                     </button>
487                                   </>
488                                 ))}
489                             </>
490                           )}
491                           {/* Community creators and admins can transfer community to another mod */}
492                           {(this.amCommunityCreator || this.canAdmin) &&
493                             this.isMod &&
494                             (!this.state.showConfirmTransferCommunity ? (
495                               <button
496                                 class="btn btn-link btn-animate text-muted"
497                                 onClick={linkEvent(
498                                   this,
499                                   this.handleShowConfirmTransferCommunity
500                                 )}
501                               >
502                                 {i18n.t('transfer_community')}
503                               </button>
504                             ) : (
505                               <>
506                                 <button class="btn btn-link btn-animate text-muted">
507                                   {i18n.t('are_you_sure')}
508                                 </button>
509                                 <button
510                                   class="btn btn-link btn-animate text-muted"
511                                   onClick={linkEvent(
512                                     this,
513                                     this.handleTransferCommunity
514                                   )}
515                                 >
516                                   {i18n.t('yes')}
517                                 </button>
518                                 <button
519                                   class="btn btn-link btn-animate text-muted"
520                                   onClick={linkEvent(
521                                     this,
522                                     this
523                                       .handleCancelShowConfirmTransferCommunity
524                                   )}
525                                 >
526                                   {i18n.t('no')}
527                                 </button>
528                               </>
529                             ))}
530                           {/* Admins can ban from all, and appoint other admins */}
531                           {this.canAdmin && (
532                             <>
533                               {!this.isAdmin &&
534                                 (!node.comment.banned ? (
535                                   <button
536                                     class="btn btn-link btn-animate text-muted"
537                                     onClick={linkEvent(
538                                       this,
539                                       this.handleModBanShow
540                                     )}
541                                   >
542                                     {i18n.t('ban_from_site')}
543                                   </button>
544                                 ) : (
545                                   <button
546                                     class="btn btn-link btn-animate text-muted"
547                                     onClick={linkEvent(
548                                       this,
549                                       this.handleModBanSubmit
550                                     )}
551                                   >
552                                     {i18n.t('unban_from_site')}
553                                   </button>
554                                 ))}
555                               {!node.comment.banned &&
556                                 (!this.state.showConfirmAppointAsAdmin ? (
557                                   <button
558                                     class="btn btn-link btn-animate text-muted"
559                                     onClick={linkEvent(
560                                       this,
561                                       this.handleShowConfirmAppointAsAdmin
562                                     )}
563                                   >
564                                     {this.isAdmin
565                                       ? i18n.t('remove_as_admin')
566                                       : i18n.t('appoint_as_admin')}
567                                   </button>
568                                 ) : (
569                                   <>
570                                     <button class="btn btn-link btn-animate text-muted">
571                                       {i18n.t('are_you_sure')}
572                                     </button>
573                                     <button
574                                       class="btn btn-link btn-animate text-muted"
575                                       onClick={linkEvent(
576                                         this,
577                                         this.handleAddAdmin
578                                       )}
579                                     >
580                                       {i18n.t('yes')}
581                                     </button>
582                                     <button
583                                       class="btn btn-link btn-animate text-muted"
584                                       onClick={linkEvent(
585                                         this,
586                                         this.handleCancelConfirmAppointAsAdmin
587                                       )}
588                                     >
589                                       {i18n.t('no')}
590                                     </button>
591                                   </>
592                                 ))}
593                             </>
594                           )}
595                           {/* Site Creator can transfer to another admin */}
596                           {this.amSiteCreator &&
597                             this.isAdmin &&
598                             (!this.state.showConfirmTransferSite ? (
599                               <button
600                                 class="btn btn-link btn-animate text-muted"
601                                 onClick={linkEvent(
602                                   this,
603                                   this.handleShowConfirmTransferSite
604                                 )}
605                               >
606                                 {i18n.t('transfer_site')}
607                               </button>
608                             ) : (
609                               <>
610                                 <button class="btn btn-link btn-animate text-muted">
611                                   {i18n.t('are_you_sure')}
612                                 </button>
613                                 <button
614                                   class="btn btn-link btn-animate text-muted"
615                                   onClick={linkEvent(
616                                     this,
617                                     this.handleTransferSite
618                                   )}
619                                 >
620                                   {i18n.t('yes')}
621                                 </button>
622                                 <button
623                                   class="btn btn-link btn-animate text-muted"
624                                   onClick={linkEvent(
625                                     this,
626                                     this.handleCancelShowConfirmTransferSite
627                                   )}
628                                 >
629                                   {i18n.t('no')}
630                                 </button>
631                               </>
632                             ))}
633                         </>
634                       )}
635                     </>
636                   )}
637                 </div>
638                 {/* end of button group */}
639               </div>
640             )}
641           </div>
642         </div>
643         {/* end of details */}
644         {this.state.showRemoveDialog && (
645           <form
646             class="form-inline"
647             onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
648           >
649             <input
650               type="text"
651               class="form-control mr-2"
652               placeholder={i18n.t('reason')}
653               value={this.state.removeReason}
654               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
655             />
656             <button type="submit" class="btn btn-secondary">
657               {i18n.t('remove_comment')}
658             </button>
659           </form>
660         )}
661         {this.state.showBanDialog && (
662           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
663             <div class="form-group row">
664               <label class="col-form-label">{i18n.t('reason')}</label>
665               <input
666                 type="text"
667                 class="form-control mr-2"
668                 placeholder={i18n.t('reason')}
669                 value={this.state.banReason}
670                 onInput={linkEvent(this, this.handleModBanReasonChange)}
671               />
672             </div>
673             {/* TODO hold off on expires until later */}
674             {/* <div class="form-group row"> */}
675             {/*   <label class="col-form-label">Expires</label> */}
676             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
677             {/* </div> */}
678             <div class="form-group row">
679               <button type="submit" class="btn btn-secondary">
680                 {i18n.t('ban')} {node.comment.creator_name}
681               </button>
682             </div>
683           </form>
684         )}
685         {this.state.showReply && (
686           <CommentForm
687             node={node}
688             onReplyCancel={this.handleReplyCancel}
689             disabled={this.props.locked}
690           />
691         )}
692         {node.children && !this.state.collapsed && (
693           <CommentNodes
694             nodes={node.children}
695             locked={this.props.locked}
696             moderators={this.props.moderators}
697             admins={this.props.admins}
698             postCreatorId={this.props.postCreatorId}
699             sort={this.props.sort}
700             sortType={this.props.sortType}
701           />
702         )}
703         {/* A collapsed clearfix */}
704         {this.state.collapsed && <div class="row col-12"></div>}
705       </div>
706     );
707   }
708
709   get linkBtn() {
710     let node = this.props.node;
711     return (
712       <button className="btn btn-link btn-animate">
713         <Link
714           class="text-muted"
715           to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
716           title={
717             this.props.showContext ? i18n.t('show_context') : i18n.t('link')
718           }
719         >
720           <svg class="icon icon-inline">
721             <use xlinkHref="#icon-link"></use>
722           </svg>
723         </Link>
724       </button>
725     );
726   }
727
728   get loadingIcon() {
729     return (
730       <svg class="icon icon-spinner spin">
731         <use xlinkHref="#icon-spinner"></use>
732       </svg>
733     );
734   }
735
736   get myComment(): boolean {
737     return (
738       UserService.Instance.user &&
739       this.props.node.comment.creator_id == UserService.Instance.user.id
740     );
741   }
742
743   get isMod(): boolean {
744     return (
745       this.props.moderators &&
746       isMod(
747         this.props.moderators.map(m => m.user_id),
748         this.props.node.comment.creator_id
749       )
750     );
751   }
752
753   get isAdmin(): boolean {
754     return (
755       this.props.admins &&
756       isMod(
757         this.props.admins.map(a => a.id),
758         this.props.node.comment.creator_id
759       )
760     );
761   }
762
763   get isPostCreator(): boolean {
764     return this.props.node.comment.creator_id == this.props.postCreatorId;
765   }
766
767   get canMod(): boolean {
768     if (this.props.admins && this.props.moderators) {
769       let adminsThenMods = this.props.admins
770         .map(a => a.id)
771         .concat(this.props.moderators.map(m => m.user_id));
772
773       return canMod(
774         UserService.Instance.user,
775         adminsThenMods,
776         this.props.node.comment.creator_id
777       );
778     } else {
779       return false;
780     }
781   }
782
783   get canAdmin(): boolean {
784     return (
785       this.props.admins &&
786       canMod(
787         UserService.Instance.user,
788         this.props.admins.map(a => a.id),
789         this.props.node.comment.creator_id
790       )
791     );
792   }
793
794   get amCommunityCreator(): boolean {
795     return (
796       this.props.moderators &&
797       UserService.Instance.user &&
798       this.props.node.comment.creator_id != UserService.Instance.user.id &&
799       UserService.Instance.user.id == this.props.moderators[0].user_id
800     );
801   }
802
803   get amSiteCreator(): boolean {
804     return (
805       this.props.admins &&
806       UserService.Instance.user &&
807       this.props.node.comment.creator_id != UserService.Instance.user.id &&
808       UserService.Instance.user.id == this.props.admins[0].id
809     );
810   }
811
812   get commentUnlessRemoved(): string {
813     let node = this.props.node;
814     return node.comment.removed
815       ? `*${i18n.t('removed')}*`
816       : node.comment.deleted
817       ? `*${i18n.t('deleted')}*`
818       : node.comment.content;
819   }
820
821   handleReplyClick(i: CommentNode) {
822     i.state.showReply = true;
823     i.setState(i.state);
824   }
825
826   handleEditClick(i: CommentNode) {
827     i.state.showEdit = true;
828     i.setState(i.state);
829   }
830
831   handleDeleteClick(i: CommentNode) {
832     let deleteForm: CommentFormI = {
833       content: i.props.node.comment.content,
834       edit_id: i.props.node.comment.id,
835       creator_id: i.props.node.comment.creator_id,
836       post_id: i.props.node.comment.post_id,
837       parent_id: i.props.node.comment.parent_id,
838       deleted: !i.props.node.comment.deleted,
839       auth: null,
840     };
841     WebSocketService.Instance.editComment(deleteForm);
842   }
843
844   handleSaveCommentClick(i: CommentNode) {
845     let saved =
846       i.props.node.comment.saved == undefined
847         ? true
848         : !i.props.node.comment.saved;
849     let form: SaveCommentForm = {
850       comment_id: i.props.node.comment.id,
851       save: saved,
852     };
853
854     WebSocketService.Instance.saveComment(form);
855
856     i.state.saveLoading = true;
857     i.setState(this.state);
858   }
859
860   handleReplyCancel() {
861     this.state.showReply = false;
862     this.state.showEdit = false;
863     this.setState(this.state);
864   }
865
866   handleCommentUpvote(i: CommentNodeI) {
867     let new_vote = this.state.my_vote == 1 ? 0 : 1;
868
869     if (this.state.my_vote == 1) {
870       this.state.score--;
871       this.state.upvotes--;
872     } else if (this.state.my_vote == -1) {
873       this.state.downvotes--;
874       this.state.upvotes++;
875       this.state.score += 2;
876     } else {
877       this.state.upvotes++;
878       this.state.score++;
879     }
880
881     this.state.my_vote = new_vote;
882
883     let form: CommentLikeForm = {
884       comment_id: i.comment.id,
885       post_id: i.comment.post_id,
886       score: this.state.my_vote,
887     };
888
889     WebSocketService.Instance.likeComment(form);
890     this.setState(this.state);
891     setupTippy();
892   }
893
894   handleCommentDownvote(i: CommentNodeI) {
895     let new_vote = this.state.my_vote == -1 ? 0 : -1;
896
897     if (this.state.my_vote == 1) {
898       this.state.score -= 2;
899       this.state.upvotes--;
900       this.state.downvotes++;
901     } else if (this.state.my_vote == -1) {
902       this.state.downvotes--;
903       this.state.score++;
904     } else {
905       this.state.downvotes++;
906       this.state.score--;
907     }
908
909     this.state.my_vote = new_vote;
910
911     let form: CommentLikeForm = {
912       comment_id: i.comment.id,
913       post_id: i.comment.post_id,
914       score: this.state.my_vote,
915     };
916
917     WebSocketService.Instance.likeComment(form);
918     this.setState(this.state);
919     setupTippy();
920   }
921
922   handleModRemoveShow(i: CommentNode) {
923     i.state.showRemoveDialog = true;
924     i.setState(i.state);
925   }
926
927   handleModRemoveReasonChange(i: CommentNode, event: any) {
928     i.state.removeReason = event.target.value;
929     i.setState(i.state);
930   }
931
932   handleModRemoveSubmit(i: CommentNode) {
933     event.preventDefault();
934     let form: CommentFormI = {
935       content: i.props.node.comment.content,
936       edit_id: i.props.node.comment.id,
937       creator_id: i.props.node.comment.creator_id,
938       post_id: i.props.node.comment.post_id,
939       parent_id: i.props.node.comment.parent_id,
940       removed: !i.props.node.comment.removed,
941       reason: i.state.removeReason,
942       auth: null,
943     };
944     WebSocketService.Instance.editComment(form);
945
946     i.state.showRemoveDialog = false;
947     i.setState(i.state);
948   }
949
950   handleMarkRead(i: CommentNode) {
951     // if it has a user_mention_id field, then its a mention
952     if (i.props.node.comment.user_mention_id) {
953       let form: EditUserMentionForm = {
954         user_mention_id: i.props.node.comment.user_mention_id,
955         read: !i.props.node.comment.read,
956       };
957       WebSocketService.Instance.editUserMention(form);
958     } else {
959       let form: CommentFormI = {
960         content: i.props.node.comment.content,
961         edit_id: i.props.node.comment.id,
962         creator_id: i.props.node.comment.creator_id,
963         post_id: i.props.node.comment.post_id,
964         parent_id: i.props.node.comment.parent_id,
965         read: !i.props.node.comment.read,
966         auth: null,
967       };
968       WebSocketService.Instance.editComment(form);
969     }
970
971     i.state.readLoading = true;
972     i.setState(this.state);
973   }
974
975   handleModBanFromCommunityShow(i: CommentNode) {
976     i.state.showBanDialog = !i.state.showBanDialog;
977     i.state.banType = BanType.Community;
978     i.setState(i.state);
979   }
980
981   handleModBanShow(i: CommentNode) {
982     i.state.showBanDialog = !i.state.showBanDialog;
983     i.state.banType = BanType.Site;
984     i.setState(i.state);
985   }
986
987   handleModBanReasonChange(i: CommentNode, event: any) {
988     i.state.banReason = event.target.value;
989     i.setState(i.state);
990   }
991
992   handleModBanExpiresChange(i: CommentNode, event: any) {
993     i.state.banExpires = event.target.value;
994     i.setState(i.state);
995   }
996
997   handleModBanFromCommunitySubmit(i: CommentNode) {
998     i.state.banType = BanType.Community;
999     i.setState(i.state);
1000     i.handleModBanBothSubmit(i);
1001   }
1002
1003   handleModBanSubmit(i: CommentNode) {
1004     i.state.banType = BanType.Site;
1005     i.setState(i.state);
1006     i.handleModBanBothSubmit(i);
1007   }
1008
1009   handleModBanBothSubmit(i: CommentNode) {
1010     event.preventDefault();
1011
1012     if (i.state.banType == BanType.Community) {
1013       let form: BanFromCommunityForm = {
1014         user_id: i.props.node.comment.creator_id,
1015         community_id: i.props.node.comment.community_id,
1016         ban: !i.props.node.comment.banned_from_community,
1017         reason: i.state.banReason,
1018         expires: getUnixTime(i.state.banExpires),
1019       };
1020       WebSocketService.Instance.banFromCommunity(form);
1021     } else {
1022       let form: BanUserForm = {
1023         user_id: i.props.node.comment.creator_id,
1024         ban: !i.props.node.comment.banned,
1025         reason: i.state.banReason,
1026         expires: getUnixTime(i.state.banExpires),
1027       };
1028       WebSocketService.Instance.banUser(form);
1029     }
1030
1031     i.state.showBanDialog = false;
1032     i.setState(i.state);
1033   }
1034
1035   handleShowConfirmAppointAsMod(i: CommentNode) {
1036     i.state.showConfirmAppointAsMod = true;
1037     i.setState(i.state);
1038   }
1039
1040   handleCancelConfirmAppointAsMod(i: CommentNode) {
1041     i.state.showConfirmAppointAsMod = false;
1042     i.setState(i.state);
1043   }
1044
1045   handleAddModToCommunity(i: CommentNode) {
1046     let form: AddModToCommunityForm = {
1047       user_id: i.props.node.comment.creator_id,
1048       community_id: i.props.node.comment.community_id,
1049       added: !i.isMod,
1050     };
1051     WebSocketService.Instance.addModToCommunity(form);
1052     i.state.showConfirmAppointAsMod = false;
1053     i.setState(i.state);
1054   }
1055
1056   handleShowConfirmAppointAsAdmin(i: CommentNode) {
1057     i.state.showConfirmAppointAsAdmin = true;
1058     i.setState(i.state);
1059   }
1060
1061   handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1062     i.state.showConfirmAppointAsAdmin = false;
1063     i.setState(i.state);
1064   }
1065
1066   handleAddAdmin(i: CommentNode) {
1067     let form: AddAdminForm = {
1068       user_id: i.props.node.comment.creator_id,
1069       added: !i.isAdmin,
1070     };
1071     WebSocketService.Instance.addAdmin(form);
1072     i.state.showConfirmAppointAsAdmin = false;
1073     i.setState(i.state);
1074   }
1075
1076   handleShowConfirmTransferCommunity(i: CommentNode) {
1077     i.state.showConfirmTransferCommunity = true;
1078     i.setState(i.state);
1079   }
1080
1081   handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1082     i.state.showConfirmTransferCommunity = false;
1083     i.setState(i.state);
1084   }
1085
1086   handleTransferCommunity(i: CommentNode) {
1087     let form: TransferCommunityForm = {
1088       community_id: i.props.node.comment.community_id,
1089       user_id: i.props.node.comment.creator_id,
1090     };
1091     WebSocketService.Instance.transferCommunity(form);
1092     i.state.showConfirmTransferCommunity = false;
1093     i.setState(i.state);
1094   }
1095
1096   handleShowConfirmTransferSite(i: CommentNode) {
1097     i.state.showConfirmTransferSite = true;
1098     i.setState(i.state);
1099   }
1100
1101   handleCancelShowConfirmTransferSite(i: CommentNode) {
1102     i.state.showConfirmTransferSite = false;
1103     i.setState(i.state);
1104   }
1105
1106   handleTransferSite(i: CommentNode) {
1107     let form: TransferSiteForm = {
1108       user_id: i.props.node.comment.creator_id,
1109     };
1110     WebSocketService.Instance.transferSite(form);
1111     i.state.showConfirmTransferSite = false;
1112     i.setState(i.state);
1113   }
1114
1115   get isCommentNew(): boolean {
1116     let now = moment.utc().subtract(10, 'minutes');
1117     let then = moment.utc(this.props.node.comment.published);
1118     return now.isBefore(then);
1119   }
1120
1121   handleCommentCollapse(i: CommentNode) {
1122     i.state.collapsed = !i.state.collapsed;
1123     i.setState(i.state);
1124   }
1125
1126   handleViewSource(i: CommentNode) {
1127     i.state.viewSource = !i.state.viewSource;
1128     i.setState(i.state);
1129   }
1130
1131   handleShowAdvanced(i: CommentNode) {
1132     i.state.showAdvanced = !i.state.showAdvanced;
1133     i.setState(i.state);
1134     setupTippy();
1135   }
1136
1137   get scoreColor() {
1138     if (this.state.my_vote == 1) {
1139       return 'text-info';
1140     } else if (this.state.my_vote == -1) {
1141       return 'text-danger';
1142     } else {
1143       return 'text-muted';
1144     }
1145   }
1146
1147   get pointsTippy(): string {
1148     let points = i18n.t('number_of_points', {
1149       count: this.state.score,
1150     });
1151
1152     let upvotes = i18n.t('number_of_upvotes', {
1153       count: this.state.upvotes,
1154     });
1155
1156     let downvotes = i18n.t('number_of_downvotes', {
1157       count: this.state.downvotes,
1158     });
1159
1160     return `${points} • ${upvotes} • ${downvotes}`;
1161   }
1162 }