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