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