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