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