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