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