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