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