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