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