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