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