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