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