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