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