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