]> Untitled Git - lemmy-ui.git/blob - src/shared/components/main.tsx
Partly functioning fuse-box, but moving te webpack now.
[lemmy-ui.git] / src / shared / components / main.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Helmet } from 'inferno-helmet';
3 import { Link } from 'inferno-router';
4 import { Subscription } from 'rxjs';
5 import { retryWhen, delay, take } from 'rxjs/operators';
6 import {
7   UserOperation,
8   CommunityUser,
9   GetFollowedCommunitiesResponse,
10   ListCommunitiesForm,
11   ListCommunitiesResponse,
12   Community,
13   SortType,
14   GetSiteResponse,
15   ListingType,
16   SiteResponse,
17   GetPostsResponse,
18   PostResponse,
19   Post,
20   GetPostsForm,
21   Comment,
22   GetCommentsForm,
23   GetCommentsResponse,
24   CommentResponse,
25   AddAdminResponse,
26   BanUserResponse,
27   WebSocketJsonResponse,
28 } from 'lemmy-js-client';
29 import { DataType } from '../interfaces';
30 import { WebSocketService, UserService } from '../services';
31 import { PostListings } from './post-listings';
32 import { CommentNodes } from './comment-nodes';
33 import { SortSelect } from './sort-select';
34 import { ListingTypeSelect } from './listing-type-select';
35 import { DataTypeSelect } from './data-type-select';
36 import { SiteForm } from './site-form';
37 import { UserListing } from './user-listing';
38 import { CommunityLink } from './community-link';
39 import { BannerIconHeader } from './banner-icon-header';
40 import {
41   wsJsonToRes,
42   repoUrl,
43   mdToHtml,
44   fetchLimit,
45   toast,
46   getListingTypeFromProps,
47   getPageFromProps,
48   getSortTypeFromProps,
49   getDataTypeFromProps,
50   editCommentRes,
51   saveCommentRes,
52   createCommentLikeRes,
53   createPostLikeFindRes,
54   editPostFindRes,
55   commentsToFlatNodes,
56   setupTippy,
57   favIconUrl,
58   notifyPost,
59   isBrowser,
60 } from '../utils';
61 import { i18n } from '../i18next';
62 import { T } from 'inferno-i18next';
63
64 interface MainState {
65   subscribedCommunities: CommunityUser[];
66   trendingCommunities: Community[];
67   siteRes: GetSiteResponse;
68   showEditSite: boolean;
69   loading: boolean;
70   posts: Post[];
71   comments: Comment[];
72   listingType: ListingType;
73   dataType: DataType;
74   sort: SortType;
75   page: number;
76 }
77
78 interface MainProps {
79   listingType: ListingType;
80   dataType: DataType;
81   sort: SortType;
82   page: number;
83 }
84
85 interface UrlParams {
86   listingType?: ListingType;
87   dataType?: string;
88   sort?: SortType;
89   page?: number;
90 }
91
92 export class Main extends Component<any, MainState> {
93   private subscription: Subscription;
94   private emptyState: MainState = {
95     subscribedCommunities: [],
96     trendingCommunities: [],
97     siteRes: {
98       site: {
99         id: null,
100         name: null,
101         creator_id: null,
102         creator_name: null,
103         published: null,
104         number_of_users: null,
105         number_of_posts: null,
106         number_of_comments: null,
107         number_of_communities: null,
108         enable_downvotes: null,
109         open_registration: null,
110         enable_nsfw: null,
111         icon: null,
112         banner: null,
113         creator_preferred_username: null,
114       },
115       admins: [],
116       banned: [],
117       online: null,
118       version: null,
119       federated_instances: null,
120     },
121     showEditSite: false,
122     loading: true,
123     posts: [],
124     comments: [],
125     listingType: getListingTypeFromProps(this.props),
126     dataType: getDataTypeFromProps(this.props),
127     sort: getSortTypeFromProps(this.props),
128     page: getPageFromProps(this.props),
129   };
130
131   constructor(props: any, context: any) {
132     super(props, context);
133
134     this.state = this.emptyState;
135     this.handleEditCancel = this.handleEditCancel.bind(this);
136     this.handleSortChange = this.handleSortChange.bind(this);
137     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
138     this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
139
140     if (isBrowser()) {
141       // TODO
142       /* this.subscription = WebSocketService.Instance.subject */
143       /* .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) */
144       /* .subscribe( */
145       /*   msg => this.parseMessage(msg), */
146       /*     err => console.error(err), */
147       /*     () => console.log('complete') */
148       /* ); */
149       /* WebSocketService.Instance.getSite(); */
150       /* if (UserService.Instance.user) { */
151       /*   WebSocketService.Instance.getFollowedCommunities(); */
152       /* } */
153       /* let listCommunitiesForm: ListCommunitiesForm = { */
154       /*   sort: SortType.Hot, */
155       /*   limit: 6, */
156       /* }; */
157       /* WebSocketService.Instance.listCommunities(listCommunitiesForm); */
158       /* this.fetchData(); */
159     }
160   }
161
162   componentWillUnmount() {
163     this.subscription.unsubscribe();
164   }
165
166   /* static getDerivedStateFromProps(props: any): MainProps { */
167   /*   return { */
168   /*     listingType: getListingTypeFromProps(props), */
169   /*     dataType: getDataTypeFromProps(props), */
170   /*     sort: getSortTypeFromProps(props), */
171   /*     page: getPageFromProps(props), */
172   /*   }; */
173   /* } */
174
175   /* componentDidUpdate(_: any, lastState: MainState) { */
176   /*   if ( */
177   /*     lastState.listingType !== this.state.listingType || */
178   /*     lastState.dataType !== this.state.dataType || */
179   /*     lastState.sort !== this.state.sort || */
180   /*     lastState.page !== this.state.page */
181   /*   ) { */
182   /*     this.setState({ loading: true }); */
183   /*     this.fetchData(); */
184   /*   } */
185   /* } */
186
187   get documentTitle(): string {
188     if (this.state.siteRes.site.name) {
189       return `${this.state.siteRes.site.name}`;
190     } else {
191       return 'Lemmy';
192     }
193   }
194
195   get favIcon(): string {
196     return this.state.siteRes.site.icon
197       ? this.state.siteRes.site.icon
198       : favIconUrl;
199   }
200
201   render() {
202     return (
203       <div class="container">
204         <h1 className={`text-warning`}>u stink main</h1>
205         <Helmet title={this.documentTitle}>
206           <link
207             id="favicon"
208             rel="icon"
209             type="image/x-icon"
210             href={this.favIcon}
211           />
212         </Helmet>
213         <div class="row">
214           <main role="main" class="col-12 col-md-8">
215             {this.posts()}
216           </main>
217           <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
218         </div>
219       </div>
220     );
221   }
222
223   mySidebar() {
224     return (
225       <div>
226         {!this.state.loading && (
227           <div>
228             <div class="card bg-transparent border-secondary mb-3">
229               <div class="card-header bg-transparent border-secondary">
230                 <div class="mb-2">
231                   {this.siteName()}
232                   {this.adminButtons()}
233                 </div>
234                 <BannerIconHeader banner={this.state.siteRes.site.banner} />
235               </div>
236               <div class="card-body">
237                 {this.trendingCommunities()}
238                 {this.createCommunityButton()}
239                 {/*
240                 {this.subscribedCommunities()}
241                 */}
242               </div>
243             </div>
244
245             <div class="card bg-transparent border-secondary mb-3">
246               <div class="card-body">{this.sidebar()}</div>
247             </div>
248
249             <div class="card bg-transparent border-secondary">
250               <div class="card-body">{this.landing()}</div>
251             </div>
252           </div>
253         )}
254       </div>
255     );
256   }
257
258   createCommunityButton() {
259     return (
260       <Link class="btn btn-secondary btn-block" to="/create_community">
261         {i18n.t('create_a_community')}
262       </Link>
263     );
264   }
265
266   trendingCommunities() {
267     return (
268       <div>
269         <h5>
270           <T i18nKey="trending_communities">
271             #
272             <Link class="text-body" to="/communities">
273               #
274             </Link>
275           </T>
276         </h5>
277         <ul class="list-inline">
278           {this.state.trendingCommunities.map(community => (
279             <li class="list-inline-item">
280               <CommunityLink community={community} />
281             </li>
282           ))}
283         </ul>
284       </div>
285     );
286   }
287
288   subscribedCommunities() {
289     return (
290       UserService.Instance.user &&
291       this.state.subscribedCommunities.length > 0 && (
292         <div>
293           <h5>
294             <T i18nKey="subscribed_to_communities">
295               #
296               <Link class="text-body" to="/communities">
297                 #
298               </Link>
299             </T>
300           </h5>
301           <ul class="list-inline">
302             {this.state.subscribedCommunities.map(community => (
303               <li class="list-inline-item">
304                 <CommunityLink
305                   community={{
306                     name: community.community_name,
307                     id: community.community_id,
308                     local: community.community_local,
309                     actor_id: community.community_actor_id,
310                     icon: community.community_icon,
311                   }}
312                 />
313               </li>
314             ))}
315           </ul>
316         </div>
317       )
318     );
319   }
320
321   sidebar() {
322     return (
323       <div>
324         {!this.state.showEditSite ? (
325           this.siteInfo()
326         ) : (
327           <SiteForm
328             site={this.state.siteRes.site}
329             onCancel={this.handleEditCancel}
330           />
331         )}
332       </div>
333     );
334   }
335
336   updateUrl(paramUpdates: UrlParams) {
337     const listingTypeStr = paramUpdates.listingType || this.state.listingType;
338     const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
339     const sortStr = paramUpdates.sort || this.state.sort;
340     const page = paramUpdates.page || this.state.page;
341     this.props.history.push(
342       `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
343     );
344   }
345
346   siteInfo() {
347     return (
348       <div>
349         {this.state.siteRes.site.description && this.siteDescription()}
350         {this.badges()}
351         {this.admins()}
352       </div>
353     );
354   }
355
356   siteName() {
357     return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
358   }
359
360   admins() {
361     return (
362       <ul class="mt-1 list-inline small mb-0">
363         <li class="list-inline-item">{i18n.t('admins')}:</li>
364         {this.state.siteRes.admins.map(admin => (
365           <li class="list-inline-item">
366             <UserListing
367               user={{
368                 name: admin.name,
369                 preferred_username: admin.preferred_username,
370                 avatar: admin.avatar,
371                 local: admin.local,
372                 actor_id: admin.actor_id,
373                 id: admin.id,
374               }}
375             />
376           </li>
377         ))}
378       </ul>
379     );
380   }
381
382   badges() {
383     return (
384       <ul class="my-2 list-inline">
385         <li className="list-inline-item badge badge-light">
386           {i18n.t('number_online', { count: this.state.siteRes.online })}
387         </li>
388         <li className="list-inline-item badge badge-light">
389           {i18n.t('number_of_users', {
390             count: this.state.siteRes.site.number_of_users,
391           })}
392         </li>
393         <li className="list-inline-item badge badge-light">
394           {i18n.t('number_of_communities', {
395             count: this.state.siteRes.site.number_of_communities,
396           })}
397         </li>
398         <li className="list-inline-item badge badge-light">
399           {i18n.t('number_of_posts', {
400             count: this.state.siteRes.site.number_of_posts,
401           })}
402         </li>
403         <li className="list-inline-item badge badge-light">
404           {i18n.t('number_of_comments', {
405             count: this.state.siteRes.site.number_of_comments,
406           })}
407         </li>
408         <li className="list-inline-item">
409           <Link className="badge badge-light" to="/modlog">
410             {i18n.t('modlog')}
411           </Link>
412         </li>
413       </ul>
414     );
415   }
416
417   adminButtons() {
418     return (
419       this.canAdmin && (
420         <ul class="list-inline mb-1 text-muted font-weight-bold">
421           <li className="list-inline-item-action">
422             <span
423               class="pointer"
424               onClick={linkEvent(this, this.handleEditClick)}
425               data-tippy-content={i18n.t('edit')}
426             >
427               <svg class="icon icon-inline">
428                 <use xlinkHref="#icon-edit"></use>
429               </svg>
430             </span>
431           </li>
432         </ul>
433       )
434     );
435   }
436
437   siteDescription() {
438     return (
439       <div
440         className="md-div"
441         dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
442       />
443     );
444   }
445
446   landing() {
447     return (
448       <>
449         <h5>
450           {i18n.t('powered_by')}
451           <svg class="icon mx-2">
452             <use xlinkHref="#icon-mouse">#</use>
453           </svg>
454           <a href={repoUrl}>
455             Lemmy<sup>beta</sup>
456           </a>
457         </h5>
458         <p class="mb-0">
459           <T i18nKey="landing_0">
460             #
461             <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
462               #
463             </a>
464             <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
465             <br class="big"></br>
466             <code>#</code>
467             <br></br>
468             <b>#</b>
469             <br class="big"></br>
470             <a href={repoUrl}>#</a>
471             <br class="big"></br>
472             <a href="https://www.rust-lang.org">#</a>
473             <a href="https://actix.rs/">#</a>
474             <a href="https://infernojs.org">#</a>
475             <a href="https://www.typescriptlang.org/">#</a>
476             <br class="big"></br>
477             <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
478               #
479             </a>
480           </T>
481         </p>
482       </>
483     );
484   }
485
486   posts() {
487     return (
488       <div class="main-content-wrapper">
489         {this.state.loading ? (
490           <h5>
491             <svg class="icon icon-spinner spin">
492               <use xlinkHref="#icon-spinner"></use>
493             </svg>
494           </h5>
495         ) : (
496           <div>
497             {this.selects()}
498             {this.listings()}
499             {this.paginator()}
500           </div>
501         )}
502       </div>
503     );
504   }
505
506   listings() {
507     return this.state.dataType == DataType.Post ? (
508       <PostListings
509         posts={this.state.posts}
510         showCommunity
511         removeDuplicates
512         sort={this.state.sort}
513         enableDownvotes={this.state.siteRes.site.enable_downvotes}
514         enableNsfw={this.state.siteRes.site.enable_nsfw}
515       />
516     ) : (
517       <CommentNodes
518         nodes={commentsToFlatNodes(this.state.comments)}
519         noIndent
520         showCommunity
521         sortType={this.state.sort}
522         showContext
523         enableDownvotes={this.state.siteRes.site.enable_downvotes}
524       />
525     );
526   }
527
528   selects() {
529     return (
530       <div className="mb-3">
531         <span class="mr-3">
532           <DataTypeSelect
533             type_={this.state.dataType}
534             onChange={this.handleDataTypeChange}
535           />
536         </span>
537         <span class="mr-3">
538           <ListingTypeSelect
539             type_={this.state.listingType}
540             onChange={this.handleListingTypeChange}
541           />
542         </span>
543         <span class="mr-2">
544           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
545         </span>
546         {this.state.listingType == ListingType.All && (
547           <a
548             href={`/feeds/all.xml?sort=${this.state.sort}`}
549             target="_blank"
550             rel="noopener"
551             title="RSS"
552           >
553             <svg class="icon text-muted small">
554               <use xlinkHref="#icon-rss">#</use>
555             </svg>
556           </a>
557         )}
558         {UserService.Instance.user &&
559           this.state.listingType == ListingType.Subscribed && (
560             <a
561               href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
562               target="_blank"
563               title="RSS"
564               rel="noopener"
565             >
566               <svg class="icon text-muted small">
567                 <use xlinkHref="#icon-rss">#</use>
568               </svg>
569             </a>
570           )}
571       </div>
572     );
573   }
574
575   paginator() {
576     return (
577       <div class="my-2">
578         {this.state.page > 1 && (
579           <button
580             class="btn btn-secondary mr-1"
581             onClick={linkEvent(this, this.prevPage)}
582           >
583             {i18n.t('prev')}
584           </button>
585         )}
586         {this.state.posts.length > 0 && (
587           <button
588             class="btn btn-secondary"
589             onClick={linkEvent(this, this.nextPage)}
590           >
591             {i18n.t('next')}
592           </button>
593         )}
594       </div>
595     );
596   }
597
598   get canAdmin(): boolean {
599     return (
600       UserService.Instance.user &&
601       this.state.siteRes.admins
602         .map(a => a.id)
603         .includes(UserService.Instance.user.id)
604     );
605   }
606
607   handleEditClick(i: Main) {
608     i.state.showEditSite = true;
609     i.setState(i.state);
610   }
611
612   handleEditCancel() {
613     this.state.showEditSite = false;
614     this.setState(this.state);
615   }
616
617   nextPage(i: Main) {
618     i.updateUrl({ page: i.state.page + 1 });
619     window.scrollTo(0, 0);
620   }
621
622   prevPage(i: Main) {
623     i.updateUrl({ page: i.state.page - 1 });
624     window.scrollTo(0, 0);
625   }
626
627   handleSortChange(val: SortType) {
628     this.updateUrl({ sort: val, page: 1 });
629     window.scrollTo(0, 0);
630   }
631
632   handleListingTypeChange(val: ListingType) {
633     this.updateUrl({ listingType: val, page: 1 });
634     window.scrollTo(0, 0);
635   }
636
637   handleDataTypeChange(val: DataType) {
638     this.updateUrl({ dataType: DataType[val], page: 1 });
639     window.scrollTo(0, 0);
640   }
641
642   fetchData() {
643     if (this.state.dataType == DataType.Post) {
644       let getPostsForm: GetPostsForm = {
645         page: this.state.page,
646         limit: fetchLimit,
647         sort: this.state.sort,
648         type_: this.state.listingType,
649       };
650       WebSocketService.Instance.getPosts(getPostsForm);
651     } else {
652       let getCommentsForm: GetCommentsForm = {
653         page: this.state.page,
654         limit: fetchLimit,
655         sort: this.state.sort,
656         type_: this.state.listingType,
657       };
658       WebSocketService.Instance.getComments(getCommentsForm);
659     }
660   }
661
662   parseMessage(msg: WebSocketJsonResponse) {
663     console.log(msg);
664     let res = wsJsonToRes(msg);
665     if (msg.error) {
666       toast(i18n.t(msg.error), 'danger');
667       return;
668     } else if (msg.reconnect) {
669       this.fetchData();
670     } else if (res.op == UserOperation.GetFollowedCommunities) {
671       let data = res.data as GetFollowedCommunitiesResponse;
672       this.state.subscribedCommunities = data.communities;
673       this.setState(this.state);
674     } else if (res.op == UserOperation.ListCommunities) {
675       let data = res.data as ListCommunitiesResponse;
676       this.state.trendingCommunities = data.communities;
677       this.setState(this.state);
678     } else if (res.op == UserOperation.GetSite) {
679       let data = res.data as GetSiteResponse;
680
681       // This means it hasn't been set up yet
682       if (!data.site) {
683         this.context.router.history.push('/setup');
684       }
685       this.state.siteRes.admins = data.admins;
686       this.state.siteRes.site = data.site;
687       this.state.siteRes.banned = data.banned;
688       this.state.siteRes.online = data.online;
689       this.setState(this.state);
690     } else if (res.op == UserOperation.EditSite) {
691       let data = res.data as SiteResponse;
692       this.state.siteRes.site = data.site;
693       this.state.showEditSite = false;
694       this.setState(this.state);
695       toast(i18n.t('site_saved'));
696     } else if (res.op == UserOperation.GetPosts) {
697       let data = res.data as GetPostsResponse;
698       this.state.posts = data.posts;
699       this.state.loading = false;
700       this.setState(this.state);
701       setupTippy();
702     } else if (res.op == UserOperation.CreatePost) {
703       let data = res.data as PostResponse;
704
705       // If you're on subscribed, only push it if you're subscribed.
706       if (this.state.listingType == ListingType.Subscribed) {
707         if (
708           this.state.subscribedCommunities
709             .map(c => c.community_id)
710             .includes(data.post.community_id)
711         ) {
712           this.state.posts.unshift(data.post);
713           notifyPost(data.post, this.context.router);
714         }
715       } else {
716         // NSFW posts
717         let nsfw = data.post.nsfw || data.post.community_nsfw;
718
719         // Don't push the post if its nsfw, and don't have that setting on
720         if (
721           !nsfw ||
722           (nsfw &&
723             UserService.Instance.user &&
724             UserService.Instance.user.show_nsfw)
725         ) {
726           this.state.posts.unshift(data.post);
727           notifyPost(data.post, this.context.router);
728         }
729       }
730       this.setState(this.state);
731     } else if (res.op == UserOperation.EditPost) {
732       let data = res.data as PostResponse;
733       editPostFindRes(data, this.state.posts);
734       this.setState(this.state);
735     } else if (res.op == UserOperation.CreatePostLike) {
736       let data = res.data as PostResponse;
737       createPostLikeFindRes(data, this.state.posts);
738       this.setState(this.state);
739     } else if (res.op == UserOperation.AddAdmin) {
740       let data = res.data as AddAdminResponse;
741       this.state.siteRes.admins = data.admins;
742       this.setState(this.state);
743     } else if (res.op == UserOperation.BanUser) {
744       let data = res.data as BanUserResponse;
745       let found = this.state.siteRes.banned.find(u => (u.id = data.user.id));
746
747       // Remove the banned if its found in the list, and the action is an unban
748       if (found && !data.banned) {
749         this.state.siteRes.banned = this.state.siteRes.banned.filter(
750           i => i.id !== data.user.id
751         );
752       } else {
753         this.state.siteRes.banned.push(data.user);
754       }
755
756       this.state.posts
757         .filter(p => p.creator_id == data.user.id)
758         .forEach(p => (p.banned = data.banned));
759
760       this.setState(this.state);
761     } else if (res.op == UserOperation.GetComments) {
762       let data = res.data as GetCommentsResponse;
763       this.state.comments = data.comments;
764       this.state.loading = false;
765       this.setState(this.state);
766     } else if (
767       res.op == UserOperation.EditComment ||
768       res.op == UserOperation.DeleteComment ||
769       res.op == UserOperation.RemoveComment
770     ) {
771       let data = res.data as CommentResponse;
772       editCommentRes(data, this.state.comments);
773       this.setState(this.state);
774     } else if (res.op == UserOperation.CreateComment) {
775       let data = res.data as CommentResponse;
776
777       // Necessary since it might be a user reply
778       if (data.recipient_ids.length == 0) {
779         // If you're on subscribed, only push it if you're subscribed.
780         if (this.state.listingType == ListingType.Subscribed) {
781           if (
782             this.state.subscribedCommunities
783               .map(c => c.community_id)
784               .includes(data.comment.community_id)
785           ) {
786             this.state.comments.unshift(data.comment);
787           }
788         } else {
789           this.state.comments.unshift(data.comment);
790         }
791         this.setState(this.state);
792       }
793     } else if (res.op == UserOperation.SaveComment) {
794       let data = res.data as CommentResponse;
795       saveCommentRes(data, this.state.comments);
796       this.setState(this.state);
797     } else if (res.op == UserOperation.CreateCommentLike) {
798       let data = res.data as CommentResponse;
799       createCommentLikeRes(data, this.state.comments);
800       this.setState(this.state);
801     }
802   }
803 }