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