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