]> Untitled Git - lemmy.git/blob - ui/src/components/main.tsx
ed31fff4d70ff194a03efa83ace03c3fd3e72fae
[lemmy.git] / ui / src / components / main.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
5 import {
6   UserOperation,
7   CommunityUser,
8   GetFollowedCommunitiesResponse,
9   ListCommunitiesForm,
10   ListCommunitiesResponse,
11   Community,
12   SortType,
13   GetSiteResponse,
14   ListingType,
15   SiteResponse,
16   GetPostsResponse,
17   PostResponse,
18   Post,
19   GetPostsForm,
20   AddAdminResponse,
21   BanUserResponse,
22   WebSocketJsonResponse,
23 } from '../interfaces';
24 import { WebSocketService, UserService } from '../services';
25 import { PostListings } from './post-listings';
26 import { SortSelect } from './sort-select';
27 import { ListingTypeSelect } from './listing-type-select';
28 import { SiteForm } from './site-form';
29 import {
30   wsJsonToRes,
31   repoUrl,
32   mdToHtml,
33   fetchLimit,
34   routeSortTypeToEnum,
35   routeListingTypeToEnum,
36   pictshareAvatarThumbnail,
37   showAvatars,
38   toast,
39 } from '../utils';
40 import { i18n } from '../i18next';
41 import { T } from 'inferno-i18next';
42
43 interface MainState {
44   subscribedCommunities: Array<CommunityUser>;
45   trendingCommunities: Array<Community>;
46   siteRes: GetSiteResponse;
47   showEditSite: boolean;
48   loading: boolean;
49   posts: Array<Post>;
50   type_: ListingType;
51   sort: SortType;
52   page: number;
53 }
54
55 export class Main extends Component<any, MainState> {
56   private subscription: Subscription;
57   private emptyState: MainState = {
58     subscribedCommunities: [],
59     trendingCommunities: [],
60     siteRes: {
61       site: {
62         id: null,
63         name: null,
64         creator_id: null,
65         creator_name: null,
66         published: null,
67         number_of_users: null,
68         number_of_posts: null,
69         number_of_comments: null,
70         number_of_communities: null,
71         enable_downvotes: null,
72         open_registration: null,
73         enable_nsfw: null,
74       },
75       admins: [],
76       banned: [],
77       online: null,
78     },
79     showEditSite: false,
80     loading: true,
81     posts: [],
82     type_: this.getListingTypeFromProps(this.props),
83     sort: this.getSortTypeFromProps(this.props),
84     page: this.getPageFromProps(this.props),
85   };
86
87   getListingTypeFromProps(props: any): ListingType {
88     return props.match.params.type
89       ? routeListingTypeToEnum(props.match.params.type)
90       : UserService.Instance.user
91       ? UserService.Instance.user.default_listing_type
92       : ListingType.All;
93   }
94
95   getSortTypeFromProps(props: any): SortType {
96     return props.match.params.sort
97       ? routeSortTypeToEnum(props.match.params.sort)
98       : UserService.Instance.user
99       ? UserService.Instance.user.default_sort_type
100       : SortType.Hot;
101   }
102
103   getPageFromProps(props: any): number {
104     return props.match.params.page ? Number(props.match.params.page) : 1;
105   }
106
107   constructor(props: any, context: any) {
108     super(props, context);
109
110     this.state = this.emptyState;
111     this.handleEditCancel = this.handleEditCancel.bind(this);
112     this.handleSortChange = this.handleSortChange.bind(this);
113     this.handleTypeChange = this.handleTypeChange.bind(this);
114
115     this.subscription = WebSocketService.Instance.subject
116       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
117       .subscribe(
118         msg => this.parseMessage(msg),
119         err => console.error(err),
120         () => console.log('complete')
121       );
122
123     WebSocketService.Instance.getSite();
124
125     if (UserService.Instance.user) {
126       WebSocketService.Instance.getFollowedCommunities();
127     }
128
129     let listCommunitiesForm: ListCommunitiesForm = {
130       sort: SortType[SortType.Hot],
131       limit: 6,
132     };
133
134     WebSocketService.Instance.listCommunities(listCommunitiesForm);
135
136     this.fetchPosts();
137   }
138
139   componentWillUnmount() {
140     this.subscription.unsubscribe();
141   }
142
143   // Necessary for back button for some reason
144   componentWillReceiveProps(nextProps: any) {
145     if (
146       nextProps.history.action == 'POP' ||
147       nextProps.history.action == 'PUSH'
148     ) {
149       this.state.type_ = this.getListingTypeFromProps(nextProps);
150       this.state.sort = this.getSortTypeFromProps(nextProps);
151       this.state.page = this.getPageFromProps(nextProps);
152       this.setState(this.state);
153       this.fetchPosts();
154     }
155   }
156
157   render() {
158     return (
159       <div class="container">
160         <div class="row">
161           <main role="main" class="col-12 col-md-8">
162             {this.posts()}
163           </main>
164           <aside class="col-12 col-md-4">{this.my_sidebar()}</aside>
165         </div>
166       </div>
167     );
168   }
169
170   my_sidebar() {
171     return (
172       <div>
173         {!this.state.loading && (
174           <div>
175             <div class="card border-secondary mb-3">
176               <div class="card-body">
177                 {this.trendingCommunities()}
178                 {UserService.Instance.user &&
179                   this.state.subscribedCommunities.length > 0 && (
180                     <div>
181                       <h5>
182                         <T i18nKey="subscribed_to_communities">
183                           #
184                           <Link class="text-white" to="/communities">
185                             #
186                           </Link>
187                         </T>
188                       </h5>
189                       <ul class="list-inline">
190                         {this.state.subscribedCommunities.map(community => (
191                           <li class="list-inline-item">
192                             <Link to={`/c/${community.community_name}`}>
193                               {community.community_name}
194                             </Link>
195                           </li>
196                         ))}
197                       </ul>
198                     </div>
199                   )}
200                 <Link
201                   class="btn btn-sm btn-secondary btn-block"
202                   to="/create_community"
203                 >
204                   {i18n.t('create_a_community')}
205                 </Link>
206               </div>
207             </div>
208             {this.sidebar()}
209             {this.landing()}
210           </div>
211         )}
212       </div>
213     );
214   }
215
216   trendingCommunities() {
217     return (
218       <div>
219         <h5>
220           <T i18nKey="trending_communities">
221             #
222             <Link class="text-white" to="/communities">
223               #
224             </Link>
225           </T>
226         </h5>
227         <ul class="list-inline">
228           {this.state.trendingCommunities.map(community => (
229             <li class="list-inline-item">
230               <Link to={`/c/${community.name}`}>{community.name}</Link>
231             </li>
232           ))}
233         </ul>
234       </div>
235     );
236   }
237
238   sidebar() {
239     return (
240       <div>
241         {!this.state.showEditSite ? (
242           this.siteInfo()
243         ) : (
244           <SiteForm
245             site={this.state.siteRes.site}
246             onCancel={this.handleEditCancel}
247           />
248         )}
249       </div>
250     );
251   }
252
253   updateUrl() {
254     let typeStr = ListingType[this.state.type_].toLowerCase();
255     let sortStr = SortType[this.state.sort].toLowerCase();
256     this.props.history.push(
257       `/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
258     );
259   }
260
261   siteInfo() {
262     return (
263       <div>
264         <div class="card border-secondary mb-3">
265           <div class="card-body">
266             <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
267             {this.canAdmin && (
268               <ul class="list-inline mb-1 text-muted small font-weight-bold">
269                 <li className="list-inline-item">
270                   <span
271                     class="pointer"
272                     onClick={linkEvent(this, this.handleEditClick)}
273                   >
274                     {i18n.t('edit')}
275                   </span>
276                 </li>
277               </ul>
278             )}
279             <ul class="my-2 list-inline">
280               <li className="list-inline-item badge badge-secondary">
281                 {i18n.t('number_online', { count: this.state.siteRes.online })}
282               </li>
283               <li className="list-inline-item badge badge-secondary">
284                 {i18n.t('number_of_users', {
285                   count: this.state.siteRes.site.number_of_users,
286                 })}
287               </li>
288               <li className="list-inline-item badge badge-secondary">
289                 {i18n.t('number_of_communities', {
290                   count: this.state.siteRes.site.number_of_communities,
291                 })}
292               </li>
293               <li className="list-inline-item badge badge-secondary">
294                 {i18n.t('number_of_posts', {
295                   count: this.state.siteRes.site.number_of_posts,
296                 })}
297               </li>
298               <li className="list-inline-item badge badge-secondary">
299                 {i18n.t('number_of_comments', {
300                   count: this.state.siteRes.site.number_of_comments,
301                 })}
302               </li>
303               <li className="list-inline-item">
304                 <Link className="badge badge-secondary" to="/modlog">
305                   {i18n.t('modlog')}
306                 </Link>
307               </li>
308             </ul>
309             <ul class="mt-1 list-inline small mb-0">
310               <li class="list-inline-item">{i18n.t('admins')}:</li>
311               {this.state.siteRes.admins.map(admin => (
312                 <li class="list-inline-item">
313                   <Link class="text-info" to={`/u/${admin.name}`}>
314                     {admin.avatar && showAvatars() && (
315                       <img
316                         height="32"
317                         width="32"
318                         src={pictshareAvatarThumbnail(admin.avatar)}
319                         class="rounded-circle mr-1"
320                       />
321                     )}
322                     <span>{admin.name}</span>
323                   </Link>
324                 </li>
325               ))}
326             </ul>
327           </div>
328         </div>
329         {this.state.siteRes.site.description && (
330           <div class="card border-secondary mb-3">
331             <div class="card-body">
332               <div
333                 className="md-div"
334                 dangerouslySetInnerHTML={mdToHtml(
335                   this.state.siteRes.site.description
336                 )}
337               />
338             </div>
339           </div>
340         )}
341       </div>
342     );
343   }
344
345   landing() {
346     return (
347       <div class="card border-secondary">
348         <div class="card-body">
349           <h5>
350             {i18n.t('powered_by')}
351             <svg class="icon mx-2">
352               <use xlinkHref="#icon-mouse">#</use>
353             </svg>
354             <a href={repoUrl}>
355               Lemmy<sup>beta</sup>
356             </a>
357           </h5>
358           <p class="mb-0">
359             <T i18nKey="landing_0">
360               #
361               <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
362                 #
363               </a>
364               <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
365               <br></br>
366               <code>#</code>
367               <br></br>
368               <b>#</b>
369               <br></br>
370               <a href={repoUrl}>#</a>
371               <br></br>
372               <a href="https://www.rust-lang.org">#</a>
373               <a href="https://actix.rs/">#</a>
374               <a href="https://infernojs.org">#</a>
375               <a href="https://www.typescriptlang.org/">#</a>
376             </T>
377           </p>
378         </div>
379       </div>
380     );
381   }
382
383   posts() {
384     return (
385       <div class="main-content-wrapper">
386         {this.state.loading ? (
387           <h5>
388             <svg class="icon icon-spinner spin">
389               <use xlinkHref="#icon-spinner"></use>
390             </svg>
391           </h5>
392         ) : (
393           <div>
394             {this.selects()}
395             <PostListings posts={this.state.posts} showCommunity />
396             {this.paginator()}
397           </div>
398         )}
399       </div>
400     );
401   }
402
403   selects() {
404     return (
405       <div className="mb-3">
406         <ListingTypeSelect
407           type_={this.state.type_}
408           onChange={this.handleTypeChange}
409         />
410         <span class="mx-2">
411           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
412         </span>
413         {this.state.type_ == ListingType.All && (
414           <a
415             href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
416             target="_blank"
417           >
418             <svg class="icon mx-1 text-muted small">
419               <use xlinkHref="#icon-rss">#</use>
420             </svg>
421           </a>
422         )}
423         {UserService.Instance.user &&
424           this.state.type_ == ListingType.Subscribed && (
425             <a
426               href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
427                 SortType[this.state.sort]
428               }`}
429               target="_blank"
430             >
431               <svg class="icon mx-1 text-muted small">
432                 <use xlinkHref="#icon-rss">#</use>
433               </svg>
434             </a>
435           )}
436       </div>
437     );
438   }
439
440   paginator() {
441     return (
442       <div class="my-2">
443         {this.state.page > 1 && (
444           <button
445             class="btn btn-sm btn-secondary mr-1"
446             onClick={linkEvent(this, this.prevPage)}
447           >
448             {i18n.t('prev')}
449           </button>
450         )}
451         {this.state.posts.length == fetchLimit && (
452           <button
453             class="btn btn-sm btn-secondary"
454             onClick={linkEvent(this, this.nextPage)}
455           >
456             {i18n.t('next')}
457           </button>
458         )}
459       </div>
460     );
461   }
462
463   get canAdmin(): boolean {
464     return (
465       UserService.Instance.user &&
466       this.state.siteRes.admins
467         .map(a => a.id)
468         .includes(UserService.Instance.user.id)
469     );
470   }
471
472   handleEditClick(i: Main) {
473     i.state.showEditSite = true;
474     i.setState(i.state);
475   }
476
477   handleEditCancel() {
478     this.state.showEditSite = false;
479     this.setState(this.state);
480   }
481
482   nextPage(i: Main) {
483     i.state.page++;
484     i.state.loading = true;
485     i.setState(i.state);
486     i.updateUrl();
487     i.fetchPosts();
488     window.scrollTo(0, 0);
489   }
490
491   prevPage(i: Main) {
492     i.state.page--;
493     i.state.loading = true;
494     i.setState(i.state);
495     i.updateUrl();
496     i.fetchPosts();
497     window.scrollTo(0, 0);
498   }
499
500   handleSortChange(val: SortType) {
501     this.state.sort = val;
502     this.state.page = 1;
503     this.state.loading = true;
504     this.setState(this.state);
505     this.updateUrl();
506     this.fetchPosts();
507     window.scrollTo(0, 0);
508   }
509
510   handleTypeChange(val: ListingType) {
511     this.state.type_ = val;
512     this.state.page = 1;
513     this.state.loading = true;
514     this.setState(this.state);
515     this.updateUrl();
516     this.fetchPosts();
517     window.scrollTo(0, 0);
518   }
519
520   fetchPosts() {
521     let getPostsForm: GetPostsForm = {
522       page: this.state.page,
523       limit: fetchLimit,
524       sort: SortType[this.state.sort],
525       type_: ListingType[this.state.type_],
526     };
527     WebSocketService.Instance.getPosts(getPostsForm);
528   }
529
530   parseMessage(msg: WebSocketJsonResponse) {
531     console.log(msg);
532     let res = wsJsonToRes(msg);
533     if (msg.error) {
534       toast(i18n.t(msg.error), 'danger');
535       return;
536     } else if (res.op == UserOperation.GetFollowedCommunities) {
537       let data = res.data as GetFollowedCommunitiesResponse;
538       this.state.subscribedCommunities = data.communities;
539       this.setState(this.state);
540     } else if (res.op == UserOperation.ListCommunities) {
541       let data = res.data as ListCommunitiesResponse;
542       this.state.trendingCommunities = data.communities;
543       this.setState(this.state);
544     } else if (res.op == UserOperation.GetSite) {
545       let data = res.data as GetSiteResponse;
546
547       // This means it hasn't been set up yet
548       if (!data.site) {
549         this.context.router.history.push('/setup');
550       }
551       this.state.siteRes.admins = data.admins;
552       this.state.siteRes.site = data.site;
553       this.state.siteRes.banned = data.banned;
554       this.state.siteRes.online = data.online;
555       this.setState(this.state);
556       document.title = `${WebSocketService.Instance.site.name}`;
557     } else if (res.op == UserOperation.EditSite) {
558       let data = res.data as SiteResponse;
559       this.state.siteRes.site = data.site;
560       this.state.showEditSite = false;
561       this.setState(this.state);
562     } else if (res.op == UserOperation.GetPosts) {
563       let data = res.data as GetPostsResponse;
564       this.state.posts = data.posts;
565       this.state.loading = false;
566       this.setState(this.state);
567     } else if (res.op == UserOperation.CreatePost) {
568       let data = res.data as PostResponse;
569
570       // If you're on subscribed, only push it if you're subscribed.
571       if (this.state.type_ == ListingType.Subscribed) {
572         if (
573           this.state.subscribedCommunities
574             .map(c => c.community_id)
575             .includes(data.post.community_id)
576         ) {
577           this.state.posts.unshift(data.post);
578         }
579       } else {
580         this.state.posts.unshift(data.post);
581       }
582
583       this.setState(this.state);
584     } else if (res.op == UserOperation.EditPost) {
585       let data = res.data as PostResponse;
586       let found = this.state.posts.find(c => c.id == data.post.id);
587
588       found.url = data.post.url;
589       found.name = data.post.name;
590       found.nsfw = data.post.nsfw;
591
592       this.setState(this.state);
593     } else if (res.op == UserOperation.CreatePostLike) {
594       let data = res.data as PostResponse;
595       let found = this.state.posts.find(c => c.id == data.post.id);
596
597       found.score = data.post.score;
598       found.upvotes = data.post.upvotes;
599       found.downvotes = data.post.downvotes;
600       if (data.post.my_vote !== null) {
601         found.my_vote = data.post.my_vote;
602         found.upvoteLoading = false;
603         found.downvoteLoading = false;
604       }
605
606       this.setState(this.state);
607     } else if (res.op == UserOperation.AddAdmin) {
608       let data = res.data as AddAdminResponse;
609       this.state.siteRes.admins = data.admins;
610       this.setState(this.state);
611     } else if (res.op == UserOperation.BanUser) {
612       let data = res.data as BanUserResponse;
613       let found = this.state.siteRes.banned.find(u => (u.id = data.user.id));
614
615       // Remove the banned if its found in the list, and the action is an unban
616       if (found && !data.banned) {
617         this.state.siteRes.banned = this.state.siteRes.banned.filter(
618           i => i.id !== data.user.id
619         );
620       } else {
621         this.state.siteRes.banned.push(data.user);
622       }
623
624       this.state.posts
625         .filter(p => p.creator_id == data.user.id)
626         .forEach(p => (p.banned = data.banned));
627
628       this.setState(this.state);
629     }
630   }
631 }