]> Untitled Git - lemmy.git/blob - ui/src/components/post-listing.tsx
Merge branch 'main' into inbox-refactoring-merge
[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-0 d-inline">
316                   {this.props.showBody && post.url ? (
317                     <a
318                       className="text-body"
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="text-body"
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-success`}>
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-0 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                 <li className="list-inline-item">•</li>
505                 {this.state.upvotes !== this.state.score && (
506                   <>
507                     <span
508                       class="unselectable pointer mr-2"
509                       data-tippy-content={this.pointsTippy}
510                     >
511                       <li className="list-inline-item">
512                         <span className="text-muted">
513                           <svg class="small icon icon-inline mr-1">
514                             <use xlinkHref="#icon-arrow-up"></use>
515                           </svg>
516                           {this.state.upvotes}
517                         </span>
518                       </li>
519                       <li className="list-inline-item">
520                         <span className="text-muted">
521                           <svg class="small icon icon-inline mr-1">
522                             <use xlinkHref="#icon-arrow-down"></use>
523                           </svg>
524                           {this.state.downvotes}
525                         </span>
526                       </li>
527                     </span>
528                     <li className="list-inline-item">•</li>
529                   </>
530                 )}
531                 <li className="list-inline-item">
532                   <Link
533                     className="text-muted"
534                     title={i18n.t('number_of_comments', {
535                       count: post.number_of_comments,
536                     })}
537                     to={`/post/${post.id}`}
538                   >
539                     <svg class="mr-1 icon icon-inline">
540                       <use xlinkHref="#icon-message-square"></use>
541                     </svg>
542                     {post.number_of_comments}
543                   </Link>
544                 </li>
545               </ul>
546               {this.props.post.duplicates && (
547                 <ul class="list-inline mb-1 small text-muted">
548                   <>
549                     <li className="list-inline-item mr-2">
550                       {i18n.t('cross_posted_to')}
551                     </li>
552                     {this.props.post.duplicates.map(post => (
553                       <li className="list-inline-item mr-2">
554                         <Link to={`/post/${post.id}`}>
555                           {post.community_name}
556                         </Link>
557                       </li>
558                     ))}
559                   </>
560                 </ul>
561               )}
562               <ul class="list-inline mb-1 text-muted font-weight-bold">
563                 {UserService.Instance.user && (
564                   <>
565                     {this.props.showBody && (
566                       <>
567                         <li className="list-inline-item">
568                           <button
569                             class="btn btn-link btn-animate text-muted"
570                             onClick={linkEvent(this, this.handleSavePostClick)}
571                             data-tippy-content={
572                               post.saved ? i18n.t('unsave') : i18n.t('save')
573                             }
574                           >
575                             <svg
576                               class={`icon icon-inline ${
577                                 post.saved && 'text-warning'
578                               }`}
579                             >
580                               <use xlinkHref="#icon-star"></use>
581                             </svg>
582                           </button>
583                         </li>
584                         <li className="list-inline-item">
585                           <Link
586                             class="btn btn-link btn-animate text-muted"
587                             to={`/create_post${this.crossPostParams}`}
588                             title={i18n.t('cross_post')}
589                           >
590                             <svg class="icon icon-inline">
591                               <use xlinkHref="#icon-copy"></use>
592                             </svg>
593                           </Link>
594                         </li>
595                       </>
596                     )}
597                     {this.myPost && this.props.showBody && (
598                       <>
599                         <li className="list-inline-item">
600                           <button
601                             class="btn btn-link btn-animate text-muted"
602                             onClick={linkEvent(this, this.handleEditClick)}
603                             data-tippy-content={i18n.t('edit')}
604                           >
605                             <svg class="icon icon-inline">
606                               <use xlinkHref="#icon-edit"></use>
607                             </svg>
608                           </button>
609                         </li>
610                         <li className="list-inline-item">
611                           <button
612                             class="btn btn-link btn-animate text-muted"
613                             onClick={linkEvent(this, this.handleDeleteClick)}
614                             data-tippy-content={
615                               !post.deleted
616                                 ? i18n.t('delete')
617                                 : i18n.t('restore')
618                             }
619                           >
620                             <svg
621                               class={`icon icon-inline ${
622                                 post.deleted && 'text-danger'
623                               }`}
624                             >
625                               <use xlinkHref="#icon-trash"></use>
626                             </svg>
627                           </button>
628                         </li>
629                       </>
630                     )}
631
632                     {!this.state.showAdvanced && this.props.showBody ? (
633                       <li className="list-inline-item">
634                         <button
635                           class="btn btn-link btn-animate text-muted"
636                           onClick={linkEvent(this, this.handleShowAdvanced)}
637                           data-tippy-content={i18n.t('more')}
638                         >
639                           <svg class="icon icon-inline">
640                             <use xlinkHref="#icon-more-vertical"></use>
641                           </svg>
642                         </button>
643                       </li>
644                     ) : (
645                       <>
646                         {this.props.showBody && post.body && (
647                           <li className="list-inline-item">
648                             <button
649                               class="btn btn-link btn-animate text-muted"
650                               onClick={linkEvent(this, this.handleViewSource)}
651                               data-tippy-content={i18n.t('view_source')}
652                             >
653                               <svg
654                                 class={`icon icon-inline ${
655                                   this.state.viewSource && 'text-success'
656                                 }`}
657                               >
658                                 <use xlinkHref="#icon-file-text"></use>
659                               </svg>
660                             </button>
661                           </li>
662                         )}
663                         {this.canModOnSelf && (
664                           <>
665                             <li className="list-inline-item">
666                               <button
667                                 class="btn btn-link btn-animate text-muted"
668                                 onClick={linkEvent(this, this.handleModLock)}
669                                 data-tippy-content={
670                                   post.locked
671                                     ? i18n.t('unlock')
672                                     : i18n.t('lock')
673                                 }
674                               >
675                                 <svg
676                                   class={`icon icon-inline ${
677                                     post.locked && 'text-danger'
678                                   }`}
679                                 >
680                                   <use xlinkHref="#icon-lock"></use>
681                                 </svg>
682                               </button>
683                             </li>
684                             <li className="list-inline-item">
685                               <button
686                                 class="btn btn-link btn-animate text-muted"
687                                 onClick={linkEvent(this, this.handleModSticky)}
688                                 data-tippy-content={
689                                   post.stickied
690                                     ? i18n.t('unsticky')
691                                     : i18n.t('sticky')
692                                 }
693                               >
694                                 <svg
695                                   class={`icon icon-inline ${
696                                     post.stickied && 'text-success'
697                                   }`}
698                                 >
699                                   <use xlinkHref="#icon-pin"></use>
700                                 </svg>
701                               </button>
702                             </li>
703                           </>
704                         )}
705                         {/* Mods can ban from community, and appoint as mods to community */}
706                         {(this.canMod || this.canAdmin) && (
707                           <li className="list-inline-item">
708                             {!post.removed ? (
709                               <span
710                                 class="pointer"
711                                 onClick={linkEvent(
712                                   this,
713                                   this.handleModRemoveShow
714                                 )}
715                               >
716                                 {i18n.t('remove')}
717                               </span>
718                             ) : (
719                               <span
720                                 class="pointer"
721                                 onClick={linkEvent(
722                                   this,
723                                   this.handleModRemoveSubmit
724                                 )}
725                               >
726                                 {i18n.t('restore')}
727                               </span>
728                             )}
729                           </li>
730                         )}
731                         {this.canMod && (
732                           <>
733                             {!this.isMod && (
734                               <li className="list-inline-item">
735                                 {!post.banned_from_community ? (
736                                   <span
737                                     class="pointer"
738                                     onClick={linkEvent(
739                                       this,
740                                       this.handleModBanFromCommunityShow
741                                     )}
742                                   >
743                                     {i18n.t('ban')}
744                                   </span>
745                                 ) : (
746                                   <span
747                                     class="pointer"
748                                     onClick={linkEvent(
749                                       this,
750                                       this.handleModBanFromCommunitySubmit
751                                     )}
752                                   >
753                                     {i18n.t('unban')}
754                                   </span>
755                                 )}
756                               </li>
757                             )}
758                             {!post.banned_from_community && (
759                               <li className="list-inline-item">
760                                 <span
761                                   class="pointer"
762                                   onClick={linkEvent(
763                                     this,
764                                     this.handleAddModToCommunity
765                                   )}
766                                 >
767                                   {this.isMod
768                                     ? i18n.t('remove_as_mod')
769                                     : i18n.t('appoint_as_mod')}
770                                 </span>
771                               </li>
772                             )}
773                           </>
774                         )}
775                         {/* Community creators and admins can transfer community to another mod */}
776                         {(this.amCommunityCreator || this.canAdmin) &&
777                           this.isMod && (
778                             <li className="list-inline-item">
779                               {!this.state.showConfirmTransferCommunity ? (
780                                 <span
781                                   class="pointer"
782                                   onClick={linkEvent(
783                                     this,
784                                     this.handleShowConfirmTransferCommunity
785                                   )}
786                                 >
787                                   {i18n.t('transfer_community')}
788                                 </span>
789                               ) : (
790                                 <>
791                                   <span class="d-inline-block mr-1">
792                                     {i18n.t('are_you_sure')}
793                                   </span>
794                                   <span
795                                     class="pointer d-inline-block mr-1"
796                                     onClick={linkEvent(
797                                       this,
798                                       this.handleTransferCommunity
799                                     )}
800                                   >
801                                     {i18n.t('yes')}
802                                   </span>
803                                   <span
804                                     class="pointer d-inline-block"
805                                     onClick={linkEvent(
806                                       this,
807                                       this
808                                         .handleCancelShowConfirmTransferCommunity
809                                     )}
810                                   >
811                                     {i18n.t('no')}
812                                   </span>
813                                 </>
814                               )}
815                             </li>
816                           )}
817                         {/* Admins can ban from all, and appoint other admins */}
818                         {this.canAdmin && (
819                           <>
820                             {!this.isAdmin && (
821                               <li className="list-inline-item">
822                                 {!post.banned ? (
823                                   <span
824                                     class="pointer"
825                                     onClick={linkEvent(
826                                       this,
827                                       this.handleModBanShow
828                                     )}
829                                   >
830                                     {i18n.t('ban_from_site')}
831                                   </span>
832                                 ) : (
833                                   <span
834                                     class="pointer"
835                                     onClick={linkEvent(
836                                       this,
837                                       this.handleModBanSubmit
838                                     )}
839                                   >
840                                     {i18n.t('unban_from_site')}
841                                   </span>
842                                 )}
843                               </li>
844                             )}
845                             {!post.banned && (
846                               <li className="list-inline-item">
847                                 <span
848                                   class="pointer"
849                                   onClick={linkEvent(this, this.handleAddAdmin)}
850                                 >
851                                   {this.isAdmin
852                                     ? i18n.t('remove_as_admin')
853                                     : i18n.t('appoint_as_admin')}
854                                 </span>
855                               </li>
856                             )}
857                           </>
858                         )}
859                         {/* Site Creator can transfer to another admin */}
860                         {this.amSiteCreator && this.isAdmin && (
861                           <li className="list-inline-item">
862                             {!this.state.showConfirmTransferSite ? (
863                               <span
864                                 class="pointer"
865                                 onClick={linkEvent(
866                                   this,
867                                   this.handleShowConfirmTransferSite
868                                 )}
869                               >
870                                 {i18n.t('transfer_site')}
871                               </span>
872                             ) : (
873                               <>
874                                 <span class="d-inline-block mr-1">
875                                   {i18n.t('are_you_sure')}
876                                 </span>
877                                 <span
878                                   class="pointer d-inline-block mr-1"
879                                   onClick={linkEvent(
880                                     this,
881                                     this.handleTransferSite
882                                   )}
883                                 >
884                                   {i18n.t('yes')}
885                                 </span>
886                                 <span
887                                   class="pointer d-inline-block"
888                                   onClick={linkEvent(
889                                     this,
890                                     this.handleCancelShowConfirmTransferSite
891                                   )}
892                                 >
893                                   {i18n.t('no')}
894                                 </span>
895                               </>
896                             )}
897                           </li>
898                         )}
899                       </>
900                     )}
901                   </>
902                 )}
903               </ul>
904               {this.state.showRemoveDialog && (
905                 <form
906                   class="form-inline"
907                   onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
908                 >
909                   <input
910                     type="text"
911                     class="form-control mr-2"
912                     placeholder={i18n.t('reason')}
913                     value={this.state.removeReason}
914                     onInput={linkEvent(this, this.handleModRemoveReasonChange)}
915                   />
916                   <button type="submit" class="btn btn-secondary">
917                     {i18n.t('remove_post')}
918                   </button>
919                 </form>
920               )}
921               {this.state.showBanDialog && (
922                 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
923                   <div class="form-group row">
924                     <label class="col-form-label" htmlFor="post-listing-reason">
925                       {i18n.t('reason')}
926                     </label>
927                     <input
928                       type="text"
929                       id="post-listing-reason"
930                       class="form-control mr-2"
931                       placeholder={i18n.t('reason')}
932                       value={this.state.banReason}
933                       onInput={linkEvent(this, this.handleModBanReasonChange)}
934                     />
935                   </div>
936                   {/* TODO hold off on expires until later */}
937                   {/* <div class="form-group row"> */}
938                   {/*   <label class="col-form-label">Expires</label> */}
939                   {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
940                   {/* </div> */}
941                   <div class="form-group row">
942                     <button type="submit" class="btn btn-secondary">
943                       {i18n.t('ban')} {post.creator_name}
944                     </button>
945                   </div>
946                 </form>
947               )}
948             </div>
949           </div>
950         </div>
951       </div>
952     );
953   }
954
955   private get myPost(): boolean {
956     return (
957       UserService.Instance.user &&
958       this.props.post.creator_id == UserService.Instance.user.id
959     );
960   }
961
962   get isMod(): boolean {
963     return (
964       this.props.moderators &&
965       isMod(
966         this.props.moderators.map(m => m.user_id),
967         this.props.post.creator_id
968       )
969     );
970   }
971
972   get isAdmin(): boolean {
973     return (
974       this.props.admins &&
975       isMod(
976         this.props.admins.map(a => a.id),
977         this.props.post.creator_id
978       )
979     );
980   }
981
982   get canMod(): boolean {
983     if (this.props.admins && this.props.moderators) {
984       let adminsThenMods = this.props.admins
985         .map(a => a.id)
986         .concat(this.props.moderators.map(m => m.user_id));
987
988       return canMod(
989         UserService.Instance.user,
990         adminsThenMods,
991         this.props.post.creator_id
992       );
993     } else {
994       return false;
995     }
996   }
997
998   get canModOnSelf(): boolean {
999     if (this.props.admins && this.props.moderators) {
1000       let adminsThenMods = this.props.admins
1001         .map(a => a.id)
1002         .concat(this.props.moderators.map(m => m.user_id));
1003
1004       return canMod(
1005         UserService.Instance.user,
1006         adminsThenMods,
1007         this.props.post.creator_id,
1008         true
1009       );
1010     } else {
1011       return false;
1012     }
1013   }
1014
1015   get canAdmin(): boolean {
1016     return (
1017       this.props.admins &&
1018       canMod(
1019         UserService.Instance.user,
1020         this.props.admins.map(a => a.id),
1021         this.props.post.creator_id
1022       )
1023     );
1024   }
1025
1026   get amCommunityCreator(): boolean {
1027     return (
1028       this.props.moderators &&
1029       UserService.Instance.user &&
1030       this.props.post.creator_id != UserService.Instance.user.id &&
1031       UserService.Instance.user.id == this.props.moderators[0].user_id
1032     );
1033   }
1034
1035   get amSiteCreator(): boolean {
1036     return (
1037       this.props.admins &&
1038       UserService.Instance.user &&
1039       this.props.post.creator_id != UserService.Instance.user.id &&
1040       UserService.Instance.user.id == this.props.admins[0].id
1041     );
1042   }
1043
1044   handlePostLike(i: PostListing) {
1045     if (!UserService.Instance.user) {
1046       this.context.router.history.push(`/login`);
1047     }
1048
1049     let new_vote = i.state.my_vote == 1 ? 0 : 1;
1050
1051     if (i.state.my_vote == 1) {
1052       i.state.score--;
1053       i.state.upvotes--;
1054     } else if (i.state.my_vote == -1) {
1055       i.state.downvotes--;
1056       i.state.upvotes++;
1057       i.state.score += 2;
1058     } else {
1059       i.state.upvotes++;
1060       i.state.score++;
1061     }
1062
1063     i.state.my_vote = new_vote;
1064
1065     let form: CreatePostLikeForm = {
1066       post_id: i.props.post.id,
1067       score: i.state.my_vote,
1068     };
1069
1070     WebSocketService.Instance.likePost(form);
1071     i.setState(i.state);
1072     setupTippy();
1073   }
1074
1075   handlePostDisLike(i: PostListing) {
1076     if (!UserService.Instance.user) {
1077       this.context.router.history.push(`/login`);
1078     }
1079
1080     let new_vote = i.state.my_vote == -1 ? 0 : -1;
1081
1082     if (i.state.my_vote == 1) {
1083       i.state.score -= 2;
1084       i.state.upvotes--;
1085       i.state.downvotes++;
1086     } else if (i.state.my_vote == -1) {
1087       i.state.downvotes--;
1088       i.state.score++;
1089     } else {
1090       i.state.downvotes++;
1091       i.state.score--;
1092     }
1093
1094     i.state.my_vote = new_vote;
1095
1096     let form: CreatePostLikeForm = {
1097       post_id: i.props.post.id,
1098       score: i.state.my_vote,
1099     };
1100
1101     WebSocketService.Instance.likePost(form);
1102     i.setState(i.state);
1103     setupTippy();
1104   }
1105
1106   handleEditClick(i: PostListing) {
1107     i.state.showEdit = true;
1108     i.setState(i.state);
1109   }
1110
1111   handleEditCancel() {
1112     this.state.showEdit = false;
1113     this.setState(this.state);
1114   }
1115
1116   // The actual editing is done in the recieve for post
1117   handleEditPost() {
1118     this.state.showEdit = false;
1119     this.setState(this.state);
1120   }
1121
1122   handleDeleteClick(i: PostListing) {
1123     let deleteForm: DeletePostForm = {
1124       edit_id: i.props.post.id,
1125       deleted: !i.props.post.deleted,
1126       auth: null,
1127     };
1128     WebSocketService.Instance.deletePost(deleteForm);
1129   }
1130
1131   handleSavePostClick(i: PostListing) {
1132     let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1133     let form: SavePostForm = {
1134       post_id: i.props.post.id,
1135       save: saved,
1136     };
1137
1138     WebSocketService.Instance.savePost(form);
1139   }
1140
1141   get crossPostParams(): string {
1142     let params = `?title=${this.props.post.name}`;
1143     let post = this.props.post;
1144
1145     if (post.url) {
1146       params += `&url=${post.url}`;
1147     }
1148     if (this.props.post.body) {
1149       params += `&body=${this.props.post.body}`;
1150     }
1151     return params;
1152   }
1153
1154   handleModRemoveShow(i: PostListing) {
1155     i.state.showRemoveDialog = true;
1156     i.setState(i.state);
1157   }
1158
1159   handleModRemoveReasonChange(i: PostListing, event: any) {
1160     i.state.removeReason = event.target.value;
1161     i.setState(i.state);
1162   }
1163
1164   handleModRemoveSubmit(i: PostListing) {
1165     event.preventDefault();
1166     let form: RemovePostForm = {
1167       edit_id: i.props.post.id,
1168       removed: !i.props.post.removed,
1169       reason: i.state.removeReason,
1170       auth: null,
1171     };
1172     WebSocketService.Instance.removePost(form);
1173
1174     i.state.showRemoveDialog = false;
1175     i.setState(i.state);
1176   }
1177
1178   handleModLock(i: PostListing) {
1179     let form: LockPostForm = {
1180       edit_id: i.props.post.id,
1181       locked: !i.props.post.locked,
1182       auth: null,
1183     };
1184     WebSocketService.Instance.lockPost(form);
1185   }
1186
1187   handleModSticky(i: PostListing) {
1188     let form: StickyPostForm = {
1189       edit_id: i.props.post.id,
1190       stickied: !i.props.post.stickied,
1191       auth: null,
1192     };
1193     WebSocketService.Instance.stickyPost(form);
1194   }
1195
1196   handleModBanFromCommunityShow(i: PostListing) {
1197     i.state.showBanDialog = true;
1198     i.state.banType = BanType.Community;
1199     i.setState(i.state);
1200   }
1201
1202   handleModBanShow(i: PostListing) {
1203     i.state.showBanDialog = true;
1204     i.state.banType = BanType.Site;
1205     i.setState(i.state);
1206   }
1207
1208   handleModBanReasonChange(i: PostListing, event: any) {
1209     i.state.banReason = event.target.value;
1210     i.setState(i.state);
1211   }
1212
1213   handleModBanExpiresChange(i: PostListing, event: any) {
1214     i.state.banExpires = event.target.value;
1215     i.setState(i.state);
1216   }
1217
1218   handleModBanFromCommunitySubmit(i: PostListing) {
1219     i.state.banType = BanType.Community;
1220     i.setState(i.state);
1221     i.handleModBanBothSubmit(i);
1222   }
1223
1224   handleModBanSubmit(i: PostListing) {
1225     i.state.banType = BanType.Site;
1226     i.setState(i.state);
1227     i.handleModBanBothSubmit(i);
1228   }
1229
1230   handleModBanBothSubmit(i: PostListing) {
1231     event.preventDefault();
1232
1233     if (i.state.banType == BanType.Community) {
1234       let form: BanFromCommunityForm = {
1235         user_id: i.props.post.creator_id,
1236         community_id: i.props.post.community_id,
1237         ban: !i.props.post.banned_from_community,
1238         reason: i.state.banReason,
1239         expires: getUnixTime(i.state.banExpires),
1240       };
1241       WebSocketService.Instance.banFromCommunity(form);
1242     } else {
1243       let form: BanUserForm = {
1244         user_id: i.props.post.creator_id,
1245         ban: !i.props.post.banned,
1246         reason: i.state.banReason,
1247         expires: getUnixTime(i.state.banExpires),
1248       };
1249       WebSocketService.Instance.banUser(form);
1250     }
1251
1252     i.state.showBanDialog = false;
1253     i.setState(i.state);
1254   }
1255
1256   handleAddModToCommunity(i: PostListing) {
1257     let form: AddModToCommunityForm = {
1258       user_id: i.props.post.creator_id,
1259       community_id: i.props.post.community_id,
1260       added: !i.isMod,
1261     };
1262     WebSocketService.Instance.addModToCommunity(form);
1263     i.setState(i.state);
1264   }
1265
1266   handleAddAdmin(i: PostListing) {
1267     let form: AddAdminForm = {
1268       user_id: i.props.post.creator_id,
1269       added: !i.isAdmin,
1270     };
1271     WebSocketService.Instance.addAdmin(form);
1272     i.setState(i.state);
1273   }
1274
1275   handleShowConfirmTransferCommunity(i: PostListing) {
1276     i.state.showConfirmTransferCommunity = true;
1277     i.setState(i.state);
1278   }
1279
1280   handleCancelShowConfirmTransferCommunity(i: PostListing) {
1281     i.state.showConfirmTransferCommunity = false;
1282     i.setState(i.state);
1283   }
1284
1285   handleTransferCommunity(i: PostListing) {
1286     let form: TransferCommunityForm = {
1287       community_id: i.props.post.community_id,
1288       user_id: i.props.post.creator_id,
1289     };
1290     WebSocketService.Instance.transferCommunity(form);
1291     i.state.showConfirmTransferCommunity = false;
1292     i.setState(i.state);
1293   }
1294
1295   handleShowConfirmTransferSite(i: PostListing) {
1296     i.state.showConfirmTransferSite = true;
1297     i.setState(i.state);
1298   }
1299
1300   handleCancelShowConfirmTransferSite(i: PostListing) {
1301     i.state.showConfirmTransferSite = false;
1302     i.setState(i.state);
1303   }
1304
1305   handleTransferSite(i: PostListing) {
1306     let form: TransferSiteForm = {
1307       user_id: i.props.post.creator_id,
1308     };
1309     WebSocketService.Instance.transferSite(form);
1310     i.state.showConfirmTransferSite = false;
1311     i.setState(i.state);
1312   }
1313
1314   handleImageExpandClick(i: PostListing) {
1315     i.state.imageExpanded = !i.state.imageExpanded;
1316     i.setState(i.state);
1317   }
1318
1319   handleViewSource(i: PostListing) {
1320     i.state.viewSource = !i.state.viewSource;
1321     i.setState(i.state);
1322   }
1323
1324   handleShowAdvanced(i: PostListing) {
1325     i.state.showAdvanced = !i.state.showAdvanced;
1326     i.setState(i.state);
1327     setupTippy();
1328   }
1329
1330   get pointsTippy(): string {
1331     let points = i18n.t('number_of_points', {
1332       count: this.state.score,
1333     });
1334
1335     let upvotes = i18n.t('number_of_upvotes', {
1336       count: this.state.upvotes,
1337     });
1338
1339     let downvotes = i18n.t('number_of_downvotes', {
1340       count: this.state.downvotes,
1341     });
1342
1343     return `${points} • ${upvotes} • ${downvotes}`;
1344   }
1345 }