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