]> Untitled Git - lemmy.git/blob - ui/src/components/post-listing.tsx
Changing comment action button size. Adding comment dividers. Fixes #604
[lemmy.git] / ui / src / components / post-listing.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { WebSocketService, UserService } from '../services';
4 import {
5   Post,
6   CreatePostLikeForm,
7   PostForm as PostFormI,
8   SavePostForm,
9   CommunityUser,
10   UserView,
11   BanType,
12   BanFromCommunityForm,
13   BanUserForm,
14   AddModToCommunityForm,
15   AddAdminForm,
16   TransferSiteForm,
17   TransferCommunityForm,
18 } from '../interfaces';
19 import { MomentTime } from './moment-time';
20 import { PostForm } from './post-form';
21 import { IFramelyCard } from './iframely-card';
22 import {
23   mdToHtml,
24   canMod,
25   isMod,
26   isImage,
27   isVideo,
28   getUnixTime,
29   pictshareAvatarThumbnail,
30   showAvatars,
31   pictshareImage,
32   setupTippy,
33 } from '../utils';
34 import { i18n } from '../i18next';
35
36 interface PostListingState {
37   showEdit: boolean;
38   showRemoveDialog: boolean;
39   removeReason: string;
40   showBanDialog: boolean;
41   banReason: string;
42   banExpires: string;
43   banType: BanType;
44   showConfirmTransferSite: boolean;
45   showConfirmTransferCommunity: boolean;
46   imageExpanded: boolean;
47   viewSource: boolean;
48   showAdvanced: boolean;
49   my_vote: number;
50   score: number;
51   upvotes: number;
52   downvotes: number;
53 }
54
55 interface PostListingProps {
56   post: Post;
57   showCommunity?: boolean;
58   showBody?: boolean;
59   moderators?: Array<CommunityUser>;
60   admins?: Array<UserView>;
61 }
62
63 export class PostListing extends Component<PostListingProps, PostListingState> {
64   private emptyState: PostListingState = {
65     showEdit: false,
66     showRemoveDialog: false,
67     removeReason: null,
68     showBanDialog: false,
69     banReason: null,
70     banExpires: null,
71     banType: BanType.Community,
72     showConfirmTransferSite: false,
73     showConfirmTransferCommunity: false,
74     imageExpanded: false,
75     viewSource: false,
76     showAdvanced: false,
77     my_vote: this.props.post.my_vote,
78     score: this.props.post.score,
79     upvotes: this.props.post.upvotes,
80     downvotes: this.props.post.downvotes,
81   };
82
83   constructor(props: any, context: any) {
84     super(props, context);
85
86     this.state = this.emptyState;
87     this.handlePostLike = this.handlePostLike.bind(this);
88     this.handlePostDisLike = this.handlePostDisLike.bind(this);
89     this.handleEditPost = this.handleEditPost.bind(this);
90     this.handleEditCancel = this.handleEditCancel.bind(this);
91   }
92
93   componentWillReceiveProps(nextProps: PostListingProps) {
94     this.state.my_vote = nextProps.post.my_vote;
95     this.state.upvotes = nextProps.post.upvotes;
96     this.state.downvotes = nextProps.post.downvotes;
97     this.state.score = nextProps.post.score;
98     this.setState(this.state);
99   }
100
101   render() {
102     return (
103       <div class="">
104         {!this.state.showEdit ? (
105           <>
106             {this.listing()}
107             {this.body()}
108           </>
109         ) : (
110           <div class="col-12">
111             <PostForm
112               post={this.props.post}
113               onEdit={this.handleEditPost}
114               onCancel={this.handleEditCancel}
115             />
116           </div>
117         )}
118       </div>
119     );
120   }
121
122   body() {
123     return (
124       <div class="row">
125         <div class="col-12">
126           {this.props.post.url &&
127             this.props.showBody &&
128             this.props.post.embed_title && (
129               <IFramelyCard post={this.props.post} />
130             )}
131           {this.props.showBody && this.props.post.body && (
132             <>
133               {this.state.viewSource ? (
134                 <pre>{this.props.post.body}</pre>
135               ) : (
136                 <div
137                   className="md-div"
138                   dangerouslySetInnerHTML={mdToHtml(this.props.post.body)}
139                 />
140               )}
141             </>
142           )}
143         </div>
144       </div>
145     );
146   }
147
148   imgThumb(src: string) {
149     let post = this.props.post;
150     return (
151       <img
152         className={`img-fluid thumbnail rounded ${(post.nsfw ||
153           post.community_nsfw) &&
154           'img-blur'}`}
155         src={src}
156       />
157     );
158   }
159
160   getImage(thumbnail: boolean = false) {
161     let post = this.props.post;
162     if (isImage(post.url)) {
163       if (post.url.includes('pictshare')) {
164         return pictshareImage(post.url, thumbnail);
165       } else {
166         return post.url;
167       }
168     } else if (post.thumbnail_url) {
169       return pictshareImage(post.thumbnail_url, thumbnail);
170     }
171   }
172
173   thumbnail() {
174     let post = this.props.post;
175
176     if (isImage(post.url)) {
177       return (
178         <span
179           class="text-body pointer"
180           data-tippy-content={i18n.t('expand_here')}
181           onClick={linkEvent(this, this.handleImageExpandClick)}
182         >
183           {this.imgThumb(this.getImage(true))}
184           <svg class="icon mini-overlay">
185             <use xlinkHref="#icon-image"></use>
186           </svg>
187         </span>
188       );
189     } else if (post.thumbnail_url) {
190       return (
191         <a
192           className="text-body"
193           href={post.url}
194           target="_blank"
195           title={post.url}
196         >
197           {this.imgThumb(this.getImage(true))}
198           <svg class="icon mini-overlay">
199             <use xlinkHref="#icon-external-link"></use>
200           </svg>
201         </a>
202       );
203     } else if (post.url) {
204       if (isVideo(post.url)) {
205         return (
206           <div class="embed-responsive embed-responsive-16by9">
207             <video
208               playsinline
209               muted
210               loop
211               controls
212               class="embed-responsive-item"
213             >
214               <source src={post.url} type="video/mp4" />
215             </video>
216           </div>
217         );
218       } else {
219         return (
220           <a
221             className="text-body"
222             href={post.url}
223             target="_blank"
224             title={post.url}
225           >
226             <svg class="icon thumbnail">
227               <use xlinkHref="#icon-external-link"></use>
228             </svg>
229           </a>
230         );
231       }
232     } else {
233       return (
234         <Link
235           className="text-body"
236           to={`/post/${post.id}`}
237           title={i18n.t('comments')}
238         >
239           <svg class="icon thumbnail">
240             <use xlinkHref="#icon-message-square"></use>
241           </svg>
242         </Link>
243       );
244     }
245   }
246
247   listing() {
248     let post = this.props.post;
249     return (
250       <div class="row">
251         <div className={`vote-bar col-1 pr-0 small text-center`}>
252           <button
253             className={`btn-animate btn btn-link p-0 ${
254               this.state.my_vote == 1 ? 'text-info' : 'text-muted'
255             }`}
256             onClick={linkEvent(this, this.handlePostLike)}
257             data-tippy-content={i18n.t('upvote')}
258           >
259             <svg class="icon upvote">
260               <use xlinkHref="#icon-arrow-up"></use>
261             </svg>
262           </button>
263           <div
264             class={`unselectable pointer font-weight-bold text-muted px-1`}
265             data-tippy-content={this.pointsTippy}
266           >
267             {this.state.score}
268           </div>
269           {WebSocketService.Instance.site.enable_downvotes && (
270             <button
271               className={`btn-animate btn btn-link p-0 ${
272                 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
273               }`}
274               onClick={linkEvent(this, this.handlePostDisLike)}
275               data-tippy-content={i18n.t('downvote')}
276             >
277               <svg class="icon downvote">
278                 <use xlinkHref="#icon-arrow-down"></use>
279               </svg>
280             </button>
281           )}
282         </div>
283         {!this.state.imageExpanded && (
284           <div class="col-3 col-sm-2 pr-0 mt-1">
285             <div class="position-relative">{this.thumbnail()}</div>
286           </div>
287         )}
288         <div
289           class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
290         >
291           <div class="row">
292             <div className="col-12">
293               <div className="post-title">
294                 <h5 className="mb-0 d-inline">
295                   {this.props.showBody && post.url ? (
296                     <a
297                       className="text-body"
298                       href={post.url}
299                       target="_blank"
300                       title={post.url}
301                     >
302                       {post.name}
303                     </a>
304                   ) : (
305                     <Link
306                       className="text-body"
307                       to={`/post/${post.id}`}
308                       title={i18n.t('comments')}
309                     >
310                       {post.name}
311                     </Link>
312                   )}
313                 </h5>
314                 {post.url &&
315                   !(new URL(post.url).hostname == window.location.hostname) && (
316                     <small class="d-inline-block">
317                       <a
318                         className="ml-2 text-muted font-italic"
319                         href={post.url}
320                         target="_blank"
321                         title={post.url}
322                       >
323                         {new URL(post.url).hostname}
324                         <svg class="ml-1 icon icon-inline">
325                           <use xlinkHref="#icon-external-link"></use>
326                         </svg>
327                       </a>
328                     </small>
329                   )}
330                 {(isImage(post.url) || this.props.post.thumbnail_url) && (
331                   <>
332                     {!this.state.imageExpanded ? (
333                       <span
334                         class="text-monospace unselectable pointer ml-2 text-muted small"
335                         data-tippy-content={i18n.t('expand_here')}
336                         onClick={linkEvent(this, this.handleImageExpandClick)}
337                       >
338                         <svg class="icon icon-inline">
339                           <use xlinkHref="#icon-plus-square"></use>
340                         </svg>
341                       </span>
342                     ) : (
343                       <span>
344                         <span
345                           class="text-monospace unselectable pointer ml-2 text-muted small"
346                           onClick={linkEvent(this, this.handleImageExpandClick)}
347                         >
348                           <svg class="icon icon-inline">
349                             <use xlinkHref="#icon-minus-square"></use>
350                           </svg>
351                         </span>
352                         <div>
353                           <span
354                             class="pointer"
355                             onClick={linkEvent(
356                               this,
357                               this.handleImageExpandClick
358                             )}
359                           >
360                             <img
361                               class="img-fluid img-expanded"
362                               src={this.getImage()}
363                             />
364                           </span>
365                         </div>
366                       </span>
367                     )}
368                   </>
369                 )}
370                 {post.removed && (
371                   <small className="ml-2 text-muted font-italic">
372                     {i18n.t('removed')}
373                   </small>
374                 )}
375                 {post.deleted && (
376                   <small
377                     className="unselectable pointer ml-2 text-muted font-italic"
378                     data-tippy-content={i18n.t('deleted')}
379                   >
380                     <svg class={`icon icon-inline text-danger`}>
381                       <use xlinkHref="#icon-trash"></use>
382                     </svg>
383                   </small>
384                 )}
385                 {post.locked && (
386                   <small
387                     className="unselectable pointer ml-2 text-muted font-italic"
388                     data-tippy-content={i18n.t('locked')}
389                   >
390                     <svg class={`icon icon-inline text-danger`}>
391                       <use xlinkHref="#icon-lock"></use>
392                     </svg>
393                   </small>
394                 )}
395                 {post.stickied && (
396                   <small
397                     className="unselectable pointer ml-2 text-muted font-italic"
398                     data-tippy-content={i18n.t('stickied')}
399                   >
400                     <svg class={`icon icon-inline text-success`}>
401                       <use xlinkHref="#icon-pin"></use>
402                     </svg>
403                   </small>
404                 )}
405                 {post.nsfw && (
406                   <small className="ml-2 text-muted font-italic">
407                     {i18n.t('nsfw')}
408                   </small>
409                 )}
410               </div>
411             </div>
412           </div>
413           <div class="row">
414             <div className="details col-12">
415               <ul class="list-inline mb-0 text-muted small">
416                 <li className="list-inline-item">
417                   <span>{i18n.t('by')} </span>
418                   <Link
419                     className="text-body font-weight-bold"
420                     to={`/u/${post.creator_name}`}
421                   >
422                     {post.creator_avatar && showAvatars() && (
423                       <img
424                         height="32"
425                         width="32"
426                         src={pictshareAvatarThumbnail(post.creator_avatar)}
427                         class="rounded-circle mr-1"
428                       />
429                     )}
430                     <span>{post.creator_name}</span>
431                   </Link>
432                   {this.isMod && (
433                     <span className="mx-1 badge badge-light">
434                       {i18n.t('mod')}
435                     </span>
436                   )}
437                   {this.isAdmin && (
438                     <span className="mx-1 badge badge-light">
439                       {i18n.t('admin')}
440                     </span>
441                   )}
442                   {(post.banned_from_community || post.banned) && (
443                     <span className="mx-1 badge badge-danger">
444                       {i18n.t('banned')}
445                     </span>
446                   )}
447                   {this.props.showCommunity && (
448                     <span>
449                       <span> {i18n.t('to')} </span>
450                       <Link to={`/c/${post.community_name}`}>
451                         {post.community_name}
452                       </Link>
453                     </span>
454                   )}
455                 </li>
456                 <li className="list-inline-item">•</li>
457                 <li className="list-inline-item">
458                   <span>
459                     <MomentTime data={post} />
460                   </span>
461                 </li>
462                 <li className="list-inline-item">•</li>
463                 {this.state.upvotes !== this.state.score && (
464                   <>
465                     <span
466                       class="unselectable pointer mr-2"
467                       data-tippy-content={this.pointsTippy}
468                     >
469                       <li className="list-inline-item">
470                         <span className="text-muted">
471                           <svg class="small icon icon-inline mr-1">
472                             <use xlinkHref="#icon-arrow-up"></use>
473                           </svg>
474                           {this.state.upvotes}
475                         </span>
476                       </li>
477                       <li className="list-inline-item">
478                         <span className="text-muted">
479                           <svg class="small icon icon-inline mr-1">
480                             <use xlinkHref="#icon-arrow-down"></use>
481                           </svg>
482                           {this.state.downvotes}
483                         </span>
484                       </li>
485                     </span>
486                     <li className="list-inline-item">•</li>
487                   </>
488                 )}
489                 <li className="list-inline-item">
490                   <Link
491                     className="text-muted"
492                     title={i18n.t('number_of_comments', {
493                       count: post.number_of_comments,
494                     })}
495                     to={`/post/${post.id}`}
496                   >
497                     <svg class="mr-1 icon icon-inline">
498                       <use xlinkHref="#icon-message-square"></use>
499                     </svg>
500                     {post.number_of_comments}
501                   </Link>
502                 </li>
503               </ul>
504               {this.props.post.duplicates && (
505                 <ul class="list-inline mb-1 small text-muted">
506                   <>
507                     <li className="list-inline-item mr-2">
508                       {i18n.t('cross_posted_to')}
509                     </li>
510                     {this.props.post.duplicates.map(post => (
511                       <li className="list-inline-item mr-2">
512                         <Link to={`/post/${post.id}`}>
513                           {post.community_name}
514                         </Link>
515                       </li>
516                     ))}
517                   </>
518                 </ul>
519               )}
520               <ul class="list-inline mb-1 text-muted font-weight-bold">
521                 {UserService.Instance.user && (
522                   <>
523                     {this.props.showBody && (
524                       <>
525                         <li className="list-inline-item">
526                           <button
527                             class="btn btn-link btn-animate btn-sm text-muted"
528                             onClick={linkEvent(this, this.handleSavePostClick)}
529                             data-tippy-content={
530                               post.saved ? i18n.t('unsave') : i18n.t('save')
531                             }
532                           >
533                             <svg
534                               class={`icon icon-inline ${post.saved &&
535                                 'text-warning'}`}
536                             >
537                               <use xlinkHref="#icon-star"></use>
538                             </svg>
539                           </button>
540                         </li>
541                         <li className="list-inline-item">
542                           <Link
543                             class="btn btn-link btn-animate btn-sm text-muted"
544                             to={`/create_post${this.crossPostParams}`}
545                             title={i18n.t('cross_post')}
546                           >
547                             <svg class="icon icon-inline">
548                               <use xlinkHref="#icon-copy"></use>
549                             </svg>
550                           </Link>
551                         </li>
552                       </>
553                     )}
554                     {this.myPost && this.props.showBody && (
555                       <>
556                         <li className="list-inline-item">
557                           <button
558                             class="btn btn-link btn-animate btn-sm text-muted"
559                             onClick={linkEvent(this, this.handleEditClick)}
560                             data-tippy-content={i18n.t('edit')}
561                           >
562                             <svg class="icon icon-inline">
563                               <use xlinkHref="#icon-edit"></use>
564                             </svg>
565                           </button>
566                         </li>
567                         <li className="list-inline-item">
568                           <button
569                             class="btn btn-link btn-animate btn-sm text-muted"
570                             onClick={linkEvent(this, this.handleDeleteClick)}
571                             data-tippy-content={
572                               !post.deleted
573                                 ? i18n.t('delete')
574                                 : i18n.t('restore')
575                             }
576                           >
577                             <svg
578                               class={`icon icon-inline ${post.deleted &&
579                                 'text-danger'}`}
580                             >
581                               <use xlinkHref="#icon-trash"></use>
582                             </svg>
583                           </button>
584                         </li>
585                       </>
586                     )}
587
588                     {!this.state.showAdvanced && this.props.showBody ? (
589                       <li className="list-inline-item">
590                         <button
591                           class="btn btn-link btn-animate btn-sm text-muted"
592                           onClick={linkEvent(this, this.handleShowAdvanced)}
593                           data-tippy-content={i18n.t('more')}
594                         >
595                           <svg class="icon icon-inline">
596                             <use xlinkHref="#icon-more-vertical"></use>
597                           </svg>
598                         </button>
599                       </li>
600                     ) : (
601                       <>
602                         {this.props.showBody && post.body && (
603                           <li className="list-inline-item">
604                             <button
605                               class="btn btn-link btn-animate btn-sm text-muted"
606                               onClick={linkEvent(this, this.handleViewSource)}
607                               data-tippy-content={i18n.t('view_source')}
608                             >
609                               <svg
610                                 class={`icon icon-inline ${this.state
611                                   .viewSource && 'text-success'}`}
612                               >
613                                 <use xlinkHref="#icon-file-text"></use>
614                               </svg>
615                             </button>
616                           </li>
617                         )}
618                         {this.canModOnSelf && (
619                           <>
620                             <li className="list-inline-item">
621                               <button
622                                 class="btn btn-link btn-animate btn-sm text-muted"
623                                 onClick={linkEvent(this, this.handleModLock)}
624                                 data-tippy-content={
625                                   post.locked
626                                     ? i18n.t('unlock')
627                                     : i18n.t('lock')
628                                 }
629                               >
630                                 <svg
631                                   class={`icon icon-inline ${post.locked &&
632                                     'text-danger'}`}
633                                 >
634                                   <use xlinkHref="#icon-lock"></use>
635                                 </svg>
636                               </button>
637                             </li>
638                             <li className="list-inline-item">
639                               <button
640                                 class="btn btn-link btn-animate btn-sm text-muted"
641                                 onClick={linkEvent(this, this.handleModSticky)}
642                                 data-tippy-content={
643                                   post.stickied
644                                     ? i18n.t('unsticky')
645                                     : i18n.t('sticky')
646                                 }
647                               >
648                                 <svg
649                                   class={`icon icon-inline ${post.stickied &&
650                                     'text-success'}`}
651                                 >
652                                   <use xlinkHref="#icon-pin"></use>
653                                 </svg>
654                               </button>
655                             </li>
656                           </>
657                         )}
658                         {/* Mods can ban from community, and appoint as mods to community */}
659                         {(this.canMod || this.canAdmin) && (
660                           <li className="list-inline-item">
661                             {!post.removed ? (
662                               <span
663                                 class="pointer"
664                                 onClick={linkEvent(
665                                   this,
666                                   this.handleModRemoveShow
667                                 )}
668                               >
669                                 {i18n.t('remove')}
670                               </span>
671                             ) : (
672                               <span
673                                 class="pointer"
674                                 onClick={linkEvent(
675                                   this,
676                                   this.handleModRemoveSubmit
677                                 )}
678                               >
679                                 {i18n.t('restore')}
680                               </span>
681                             )}
682                           </li>
683                         )}
684                         {this.canMod && (
685                           <>
686                             {!this.isMod && (
687                               <li className="list-inline-item">
688                                 {!post.banned_from_community ? (
689                                   <span
690                                     class="pointer"
691                                     onClick={linkEvent(
692                                       this,
693                                       this.handleModBanFromCommunityShow
694                                     )}
695                                   >
696                                     {i18n.t('ban')}
697                                   </span>
698                                 ) : (
699                                   <span
700                                     class="pointer"
701                                     onClick={linkEvent(
702                                       this,
703                                       this.handleModBanFromCommunitySubmit
704                                     )}
705                                   >
706                                     {i18n.t('unban')}
707                                   </span>
708                                 )}
709                               </li>
710                             )}
711                             {!post.banned_from_community && (
712                               <li className="list-inline-item">
713                                 <span
714                                   class="pointer"
715                                   onClick={linkEvent(
716                                     this,
717                                     this.handleAddModToCommunity
718                                   )}
719                                 >
720                                   {this.isMod
721                                     ? i18n.t('remove_as_mod')
722                                     : i18n.t('appoint_as_mod')}
723                                 </span>
724                               </li>
725                             )}
726                           </>
727                         )}
728                         {/* Community creators and admins can transfer community to another mod */}
729                         {(this.amCommunityCreator || this.canAdmin) &&
730                           this.isMod && (
731                             <li className="list-inline-item">
732                               {!this.state.showConfirmTransferCommunity ? (
733                                 <span
734                                   class="pointer"
735                                   onClick={linkEvent(
736                                     this,
737                                     this.handleShowConfirmTransferCommunity
738                                   )}
739                                 >
740                                   {i18n.t('transfer_community')}
741                                 </span>
742                               ) : (
743                                 <>
744                                   <span class="d-inline-block mr-1">
745                                     {i18n.t('are_you_sure')}
746                                   </span>
747                                   <span
748                                     class="pointer d-inline-block mr-1"
749                                     onClick={linkEvent(
750                                       this,
751                                       this.handleTransferCommunity
752                                     )}
753                                   >
754                                     {i18n.t('yes')}
755                                   </span>
756                                   <span
757                                     class="pointer d-inline-block"
758                                     onClick={linkEvent(
759                                       this,
760                                       this
761                                         .handleCancelShowConfirmTransferCommunity
762                                     )}
763                                   >
764                                     {i18n.t('no')}
765                                   </span>
766                                 </>
767                               )}
768                             </li>
769                           )}
770                         {/* Admins can ban from all, and appoint other admins */}
771                         {this.canAdmin && (
772                           <>
773                             {!this.isAdmin && (
774                               <li className="list-inline-item">
775                                 {!post.banned ? (
776                                   <span
777                                     class="pointer"
778                                     onClick={linkEvent(
779                                       this,
780                                       this.handleModBanShow
781                                     )}
782                                   >
783                                     {i18n.t('ban_from_site')}
784                                   </span>
785                                 ) : (
786                                   <span
787                                     class="pointer"
788                                     onClick={linkEvent(
789                                       this,
790                                       this.handleModBanSubmit
791                                     )}
792                                   >
793                                     {i18n.t('unban_from_site')}
794                                   </span>
795                                 )}
796                               </li>
797                             )}
798                             {!post.banned && (
799                               <li className="list-inline-item">
800                                 <span
801                                   class="pointer"
802                                   onClick={linkEvent(this, this.handleAddAdmin)}
803                                 >
804                                   {this.isAdmin
805                                     ? i18n.t('remove_as_admin')
806                                     : i18n.t('appoint_as_admin')}
807                                 </span>
808                               </li>
809                             )}
810                           </>
811                         )}
812                         {/* Site Creator can transfer to another admin */}
813                         {this.amSiteCreator && this.isAdmin && (
814                           <li className="list-inline-item">
815                             {!this.state.showConfirmTransferSite ? (
816                               <span
817                                 class="pointer"
818                                 onClick={linkEvent(
819                                   this,
820                                   this.handleShowConfirmTransferSite
821                                 )}
822                               >
823                                 {i18n.t('transfer_site')}
824                               </span>
825                             ) : (
826                               <>
827                                 <span class="d-inline-block mr-1">
828                                   {i18n.t('are_you_sure')}
829                                 </span>
830                                 <span
831                                   class="pointer d-inline-block mr-1"
832                                   onClick={linkEvent(
833                                     this,
834                                     this.handleTransferSite
835                                   )}
836                                 >
837                                   {i18n.t('yes')}
838                                 </span>
839                                 <span
840                                   class="pointer d-inline-block"
841                                   onClick={linkEvent(
842                                     this,
843                                     this.handleCancelShowConfirmTransferSite
844                                   )}
845                                 >
846                                   {i18n.t('no')}
847                                 </span>
848                               </>
849                             )}
850                           </li>
851                         )}
852                       </>
853                     )}
854                   </>
855                 )}
856               </ul>
857               {this.state.showRemoveDialog && (
858                 <form
859                   class="form-inline"
860                   onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
861                 >
862                   <input
863                     type="text"
864                     class="form-control mr-2"
865                     placeholder={i18n.t('reason')}
866                     value={this.state.removeReason}
867                     onInput={linkEvent(this, this.handleModRemoveReasonChange)}
868                   />
869                   <button type="submit" class="btn btn-secondary">
870                     {i18n.t('remove_post')}
871                   </button>
872                 </form>
873               )}
874               {this.state.showBanDialog && (
875                 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
876                   <div class="form-group row">
877                     <label class="col-form-label" htmlFor="post-listing-reason">
878                       {i18n.t('reason')}
879                     </label>
880                     <input
881                       type="text"
882                       id="post-listing-reason"
883                       class="form-control mr-2"
884                       placeholder={i18n.t('reason')}
885                       value={this.state.banReason}
886                       onInput={linkEvent(this, this.handleModBanReasonChange)}
887                     />
888                   </div>
889                   {/* TODO hold off on expires until later */}
890                   {/* <div class="form-group row"> */}
891                   {/*   <label class="col-form-label">Expires</label> */}
892                   {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
893                   {/* </div> */}
894                   <div class="form-group row">
895                     <button type="submit" class="btn btn-secondary">
896                       {i18n.t('ban')} {post.creator_name}
897                     </button>
898                   </div>
899                 </form>
900               )}
901             </div>
902           </div>
903         </div>
904       </div>
905     );
906   }
907
908   private get myPost(): boolean {
909     return (
910       UserService.Instance.user &&
911       this.props.post.creator_id == UserService.Instance.user.id
912     );
913   }
914
915   get isMod(): boolean {
916     return (
917       this.props.moderators &&
918       isMod(
919         this.props.moderators.map(m => m.user_id),
920         this.props.post.creator_id
921       )
922     );
923   }
924
925   get isAdmin(): boolean {
926     return (
927       this.props.admins &&
928       isMod(
929         this.props.admins.map(a => a.id),
930         this.props.post.creator_id
931       )
932     );
933   }
934
935   get canMod(): boolean {
936     if (this.props.admins && this.props.moderators) {
937       let adminsThenMods = this.props.admins
938         .map(a => a.id)
939         .concat(this.props.moderators.map(m => m.user_id));
940
941       return canMod(
942         UserService.Instance.user,
943         adminsThenMods,
944         this.props.post.creator_id
945       );
946     } else {
947       return false;
948     }
949   }
950
951   get canModOnSelf(): boolean {
952     if (this.props.admins && this.props.moderators) {
953       let adminsThenMods = this.props.admins
954         .map(a => a.id)
955         .concat(this.props.moderators.map(m => m.user_id));
956
957       return canMod(
958         UserService.Instance.user,
959         adminsThenMods,
960         this.props.post.creator_id,
961         true
962       );
963     } else {
964       return false;
965     }
966   }
967
968   get canAdmin(): boolean {
969     return (
970       this.props.admins &&
971       canMod(
972         UserService.Instance.user,
973         this.props.admins.map(a => a.id),
974         this.props.post.creator_id
975       )
976     );
977   }
978
979   get amCommunityCreator(): boolean {
980     return (
981       this.props.moderators &&
982       UserService.Instance.user &&
983       this.props.post.creator_id != UserService.Instance.user.id &&
984       UserService.Instance.user.id == this.props.moderators[0].user_id
985     );
986   }
987
988   get amSiteCreator(): boolean {
989     return (
990       this.props.admins &&
991       UserService.Instance.user &&
992       this.props.post.creator_id != UserService.Instance.user.id &&
993       UserService.Instance.user.id == this.props.admins[0].id
994     );
995   }
996
997   handlePostLike(i: PostListing) {
998     let new_vote = i.state.my_vote == 1 ? 0 : 1;
999
1000     if (i.state.my_vote == 1) {
1001       i.state.score--;
1002       i.state.upvotes--;
1003     } else if (i.state.my_vote == -1) {
1004       i.state.downvotes--;
1005       i.state.upvotes++;
1006       i.state.score += 2;
1007     } else {
1008       i.state.upvotes++;
1009       i.state.score++;
1010     }
1011
1012     i.state.my_vote = new_vote;
1013
1014     let form: CreatePostLikeForm = {
1015       post_id: i.props.post.id,
1016       score: i.state.my_vote,
1017     };
1018
1019     WebSocketService.Instance.likePost(form);
1020     i.setState(i.state);
1021     setupTippy();
1022   }
1023
1024   handlePostDisLike(i: PostListing) {
1025     let new_vote = i.state.my_vote == -1 ? 0 : -1;
1026
1027     if (i.state.my_vote == 1) {
1028       i.state.score -= 2;
1029       i.state.upvotes--;
1030       i.state.downvotes++;
1031     } else if (i.state.my_vote == -1) {
1032       i.state.downvotes--;
1033       i.state.score++;
1034     } else {
1035       i.state.downvotes++;
1036       i.state.score--;
1037     }
1038
1039     i.state.my_vote = new_vote;
1040
1041     let form: CreatePostLikeForm = {
1042       post_id: i.props.post.id,
1043       score: i.state.my_vote,
1044     };
1045
1046     WebSocketService.Instance.likePost(form);
1047     i.setState(i.state);
1048     setupTippy();
1049   }
1050
1051   handleEditClick(i: PostListing) {
1052     i.state.showEdit = true;
1053     i.setState(i.state);
1054   }
1055
1056   handleEditCancel() {
1057     this.state.showEdit = false;
1058     this.setState(this.state);
1059   }
1060
1061   // The actual editing is done in the recieve for post
1062   handleEditPost() {
1063     this.state.showEdit = false;
1064     this.setState(this.state);
1065   }
1066
1067   handleDeleteClick(i: PostListing) {
1068     let deleteForm: PostFormI = {
1069       body: i.props.post.body,
1070       community_id: i.props.post.community_id,
1071       name: i.props.post.name,
1072       url: i.props.post.url,
1073       edit_id: i.props.post.id,
1074       creator_id: i.props.post.creator_id,
1075       deleted: !i.props.post.deleted,
1076       nsfw: i.props.post.nsfw,
1077       auth: null,
1078     };
1079     WebSocketService.Instance.editPost(deleteForm);
1080   }
1081
1082   handleSavePostClick(i: PostListing) {
1083     let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1084     let form: SavePostForm = {
1085       post_id: i.props.post.id,
1086       save: saved,
1087     };
1088
1089     WebSocketService.Instance.savePost(form);
1090   }
1091
1092   get crossPostParams(): string {
1093     let params = `?title=${this.props.post.name}`;
1094     let post = this.props.post;
1095
1096     if (post.url) {
1097       params += `&url=${post.url}`;
1098     }
1099     if (this.props.post.body) {
1100       params += `&body=${this.props.post.body}`;
1101     }
1102     return params;
1103   }
1104
1105   handleModRemoveShow(i: PostListing) {
1106     i.state.showRemoveDialog = true;
1107     i.setState(i.state);
1108   }
1109
1110   handleModRemoveReasonChange(i: PostListing, event: any) {
1111     i.state.removeReason = event.target.value;
1112     i.setState(i.state);
1113   }
1114
1115   handleModRemoveSubmit(i: PostListing) {
1116     event.preventDefault();
1117     let form: PostFormI = {
1118       name: i.props.post.name,
1119       community_id: i.props.post.community_id,
1120       edit_id: i.props.post.id,
1121       creator_id: i.props.post.creator_id,
1122       removed: !i.props.post.removed,
1123       reason: i.state.removeReason,
1124       nsfw: i.props.post.nsfw,
1125       auth: null,
1126     };
1127     WebSocketService.Instance.editPost(form);
1128
1129     i.state.showRemoveDialog = false;
1130     i.setState(i.state);
1131   }
1132
1133   handleModLock(i: PostListing) {
1134     let form: PostFormI = {
1135       name: i.props.post.name,
1136       community_id: i.props.post.community_id,
1137       edit_id: i.props.post.id,
1138       creator_id: i.props.post.creator_id,
1139       nsfw: i.props.post.nsfw,
1140       locked: !i.props.post.locked,
1141       auth: null,
1142     };
1143     WebSocketService.Instance.editPost(form);
1144   }
1145
1146   handleModSticky(i: PostListing) {
1147     let form: PostFormI = {
1148       name: i.props.post.name,
1149       community_id: i.props.post.community_id,
1150       edit_id: i.props.post.id,
1151       creator_id: i.props.post.creator_id,
1152       nsfw: i.props.post.nsfw,
1153       stickied: !i.props.post.stickied,
1154       auth: null,
1155     };
1156     WebSocketService.Instance.editPost(form);
1157   }
1158
1159   handleModBanFromCommunityShow(i: PostListing) {
1160     i.state.showBanDialog = true;
1161     i.state.banType = BanType.Community;
1162     i.setState(i.state);
1163   }
1164
1165   handleModBanShow(i: PostListing) {
1166     i.state.showBanDialog = true;
1167     i.state.banType = BanType.Site;
1168     i.setState(i.state);
1169   }
1170
1171   handleModBanReasonChange(i: PostListing, event: any) {
1172     i.state.banReason = event.target.value;
1173     i.setState(i.state);
1174   }
1175
1176   handleModBanExpiresChange(i: PostListing, event: any) {
1177     i.state.banExpires = event.target.value;
1178     i.setState(i.state);
1179   }
1180
1181   handleModBanFromCommunitySubmit(i: PostListing) {
1182     i.state.banType = BanType.Community;
1183     i.setState(i.state);
1184     i.handleModBanBothSubmit(i);
1185   }
1186
1187   handleModBanSubmit(i: PostListing) {
1188     i.state.banType = BanType.Site;
1189     i.setState(i.state);
1190     i.handleModBanBothSubmit(i);
1191   }
1192
1193   handleModBanBothSubmit(i: PostListing) {
1194     event.preventDefault();
1195
1196     if (i.state.banType == BanType.Community) {
1197       let form: BanFromCommunityForm = {
1198         user_id: i.props.post.creator_id,
1199         community_id: i.props.post.community_id,
1200         ban: !i.props.post.banned_from_community,
1201         reason: i.state.banReason,
1202         expires: getUnixTime(i.state.banExpires),
1203       };
1204       WebSocketService.Instance.banFromCommunity(form);
1205     } else {
1206       let form: BanUserForm = {
1207         user_id: i.props.post.creator_id,
1208         ban: !i.props.post.banned,
1209         reason: i.state.banReason,
1210         expires: getUnixTime(i.state.banExpires),
1211       };
1212       WebSocketService.Instance.banUser(form);
1213     }
1214
1215     i.state.showBanDialog = false;
1216     i.setState(i.state);
1217   }
1218
1219   handleAddModToCommunity(i: PostListing) {
1220     let form: AddModToCommunityForm = {
1221       user_id: i.props.post.creator_id,
1222       community_id: i.props.post.community_id,
1223       added: !i.isMod,
1224     };
1225     WebSocketService.Instance.addModToCommunity(form);
1226     i.setState(i.state);
1227   }
1228
1229   handleAddAdmin(i: PostListing) {
1230     let form: AddAdminForm = {
1231       user_id: i.props.post.creator_id,
1232       added: !i.isAdmin,
1233     };
1234     WebSocketService.Instance.addAdmin(form);
1235     i.setState(i.state);
1236   }
1237
1238   handleShowConfirmTransferCommunity(i: PostListing) {
1239     i.state.showConfirmTransferCommunity = true;
1240     i.setState(i.state);
1241   }
1242
1243   handleCancelShowConfirmTransferCommunity(i: PostListing) {
1244     i.state.showConfirmTransferCommunity = false;
1245     i.setState(i.state);
1246   }
1247
1248   handleTransferCommunity(i: PostListing) {
1249     let form: TransferCommunityForm = {
1250       community_id: i.props.post.community_id,
1251       user_id: i.props.post.creator_id,
1252     };
1253     WebSocketService.Instance.transferCommunity(form);
1254     i.state.showConfirmTransferCommunity = false;
1255     i.setState(i.state);
1256   }
1257
1258   handleShowConfirmTransferSite(i: PostListing) {
1259     i.state.showConfirmTransferSite = true;
1260     i.setState(i.state);
1261   }
1262
1263   handleCancelShowConfirmTransferSite(i: PostListing) {
1264     i.state.showConfirmTransferSite = false;
1265     i.setState(i.state);
1266   }
1267
1268   handleTransferSite(i: PostListing) {
1269     let form: TransferSiteForm = {
1270       user_id: i.props.post.creator_id,
1271     };
1272     WebSocketService.Instance.transferSite(form);
1273     i.state.showConfirmTransferSite = false;
1274     i.setState(i.state);
1275   }
1276
1277   handleImageExpandClick(i: PostListing) {
1278     i.state.imageExpanded = !i.state.imageExpanded;
1279     i.setState(i.state);
1280   }
1281
1282   handleViewSource(i: PostListing) {
1283     i.state.viewSource = !i.state.viewSource;
1284     i.setState(i.state);
1285   }
1286
1287   handleShowAdvanced(i: PostListing) {
1288     i.state.showAdvanced = !i.state.showAdvanced;
1289     i.setState(i.state);
1290     setupTippy();
1291   }
1292
1293   get pointsTippy(): string {
1294     let points = i18n.t('number_of_points', {
1295       count: this.state.score,
1296     });
1297
1298     let upvotes = i18n.t('number_of_upvotes', {
1299       count: this.state.upvotes,
1300     });
1301
1302     let downvotes = i18n.t('number_of_downvotes', {
1303       count: this.state.downvotes,
1304     });
1305
1306     return `${points} • ${upvotes} • ${downvotes}`;
1307   }
1308 }