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