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