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