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