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