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