]> Untitled Git - lemmy.git/blob - ui/src/components/main.tsx
Spanish translations
[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 { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, ListingType, SiteResponse, GetPostsResponse, CreatePostLikeResponse, Post, GetPostsForm } from '../interfaces';
6 import { WebSocketService, UserService } from '../services';
7 import { PostListings } from './post-listings';
8 import { SiteForm } from './site-form';
9 import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum, postRefetchSeconds } from '../utils';
10 import { i18n } from '../i18next';
11 import { T } from 'inferno-i18next';
12
13 interface MainState {
14   subscribedCommunities: Array<CommunityUser>;
15   trendingCommunities: Array<Community>;
16   site: GetSiteResponse;
17   showEditSite: boolean;
18   loading: boolean;
19   posts: Array<Post>;
20   type_: ListingType;
21   sort: SortType;
22   page: number;
23 }
24
25 export class Main extends Component<any, MainState> {
26
27   private subscription: Subscription;
28   private postFetcher: any;
29   private emptyState: MainState = {
30     subscribedCommunities: [],
31     trendingCommunities: [],
32     site: {
33       op: null,
34       site: {
35         id: null,
36         name: null,
37         creator_id: null,
38         creator_name: null,
39         published: null,
40         number_of_users: null,
41         number_of_posts: null,
42         number_of_comments: null,
43         number_of_communities: null,
44       },
45       admins: [],
46       banned: [],
47       online: null,
48     },
49     showEditSite: false,
50     loading: true,
51     posts: [],
52     type_: this.getListingTypeFromProps(this.props),
53     sort: this.getSortTypeFromProps(this.props),
54     page: this.getPageFromProps(this.props),
55   }
56
57   getListingTypeFromProps(props: any): ListingType {
58     return (props.match.params.type) ? 
59       routeListingTypeToEnum(props.match.params.type) : 
60       UserService.Instance.user ? 
61       ListingType.Subscribed : 
62       ListingType.All;
63   }
64
65   getSortTypeFromProps(props: any): SortType {
66     return (props.match.params.sort) ? 
67       routeSortTypeToEnum(props.match.params.sort) : 
68       SortType.Hot;
69   }
70
71   getPageFromProps(props: any): number {
72     return (props.match.params.page) ? Number(props.match.params.page) : 1;
73   }
74
75   constructor(props: any, context: any) {
76     super(props, context);
77
78     this.state = this.emptyState;
79     this.handleEditCancel = this.handleEditCancel.bind(this);
80
81     this.subscription = WebSocketService.Instance.subject
82     .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
83     .subscribe(
84       (msg) => this.parseMessage(msg),
85         (err) => console.error(err),
86         () => console.log('complete')
87     );
88
89     WebSocketService.Instance.getSite();
90
91     if (UserService.Instance.user) {
92       WebSocketService.Instance.getFollowedCommunities();
93     }
94
95     let listCommunitiesForm: ListCommunitiesForm = {
96       sort: SortType[SortType.Hot],
97       limit: 6
98     }
99
100     WebSocketService.Instance.listCommunities(listCommunitiesForm);
101
102     this.keepFetchingPosts();
103   }
104
105   componentWillUnmount() {
106     this.subscription.unsubscribe();
107     clearInterval(this.postFetcher);
108   }
109
110   // Necessary for back button for some reason
111   componentWillReceiveProps(nextProps: any) {
112     if (nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH') {
113       this.state.type_ = this.getListingTypeFromProps(nextProps);
114       this.state.sort = this.getSortTypeFromProps(nextProps);
115       this.state.page = this.getPageFromProps(nextProps);
116       this.setState(this.state);
117       this.fetchPosts();
118     }
119   }
120
121   render() {
122     return (
123       <div class="container">
124         <div class="row">
125           <div class="col-12 col-md-8">
126             {this.posts()}
127           </div>
128           <div class="col-12 col-md-4">
129             {this.my_sidebar()}
130           </div>
131         </div>
132       </div>
133     )
134   }
135     
136   my_sidebar() {
137     return(
138       <div>
139         {!this.state.loading &&
140           <div>
141             <div class="card border-secondary mb-3">
142               <div class="card-body">
143                 {this.trendingCommunities()}
144                 {UserService.Instance.user && this.state.subscribedCommunities.length > 0 && 
145                   <div>
146                     <h5>
147                       <T i18nKey="subscribed_to_communities">#<Link class="text-white" to="/communities">#</Link></T>
148                     </h5> 
149                     <ul class="list-inline"> 
150                       {this.state.subscribedCommunities.map(community =>
151                         <li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
152                       )}
153                     </ul>
154                   </div>
155                 }
156                 <Link class="btn btn-sm btn-secondary btn-block" 
157                   to="/create_community">
158                   <T i18nKey="create_a_community">#</T>
159                 </Link>
160               </div>
161             </div>
162             {this.sidebar()}
163             {this.landing()}
164           </div>
165         }
166       </div>
167     )
168   }
169
170   trendingCommunities() {
171     return (
172       <div>
173         <h5>
174           <T i18nKey="trending_communities">#<Link class="text-white" to="/communities">#</Link></T>
175         </h5>
176         <ul class="list-inline"> 
177           {this.state.trendingCommunities.map(community =>
178             <li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
179           )}
180         </ul>
181       </div>
182     )
183   }
184
185   sidebar() {
186     return (
187       <div>
188         {!this.state.showEditSite ?
189           this.siteInfo() :
190           <SiteForm
191             site={this.state.site.site} 
192             onCancel={this.handleEditCancel} 
193           />
194         }
195       </div>
196     )
197   }
198
199   updateUrl() {
200     let typeStr = ListingType[this.state.type_].toLowerCase();
201     let sortStr = SortType[this.state.sort].toLowerCase();
202     this.props.history.push(`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`);
203   }
204
205   siteInfo() {
206     return (
207       <div>
208         <div class="card border-secondary mb-3">
209           <div class="card-body">
210             <h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
211             {this.canAdmin && 
212               <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
213                 <li className="list-inline-item">
214                   <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>
215                     <T i18nKey="edit">#</T>
216                   </span>
217                 </li>
218               </ul>
219             }
220             <ul class="my-2 list-inline">
221               <li className="list-inline-item badge badge-secondary">
222                 <T i18nKey="number_online" interpolation={{count: this.state.site.online}}>#</T>
223               </li>
224               <li className="list-inline-item badge badge-secondary">
225                 <T i18nKey="number_of_users" interpolation={{count: this.state.site.site.number_of_users}}>#</T>
226               </li>
227               <li className="list-inline-item badge badge-secondary">
228                 <T i18nKey="number_of_communities" interpolation={{count: this.state.site.site.number_of_communities}}>#</T>
229               </li>
230               <li className="list-inline-item badge badge-secondary">
231                 <T i18nKey="number_of_posts" interpolation={{count: this.state.site.site.number_of_posts}}>#</T>
232               </li>
233               <li className="list-inline-item badge badge-secondary">
234                 <T i18nKey="number_of_comments" interpolation={{count: this.state.site.site.number_of_comments}}>#</T>
235               </li>
236               <li className="list-inline-item">
237                 <Link className="badge badge-secondary" to="/modlog">
238                   <T i18nKey="modlog">#</T>
239                 </Link>
240               </li>
241             </ul>
242             <ul class="mt-1 list-inline small mb-0"> 
243               <li class="list-inline-item">
244                 <T i18nKey="admins" class="d-inline">#</T>:
245                 </li>
246                 {this.state.site.admins.map(admin =>
247                   <li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li>
248                 )}
249               </ul>
250             </div>
251           </div>
252           {this.state.site.site.description && 
253             <div class="card border-secondary mb-3">
254               <div class="card-body">
255                 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
256               </div>
257             </div>
258           }
259         </div>
260     )
261   }
262
263   landing() {
264     return (
265       <div class="card border-secondary">
266         <div class="card-body">
267           <h5>
268             <T i18nKey="powered_by" class="d-inline">#</T>
269             <svg class="icon mx-2"><use xlinkHref="#icon-mouse">#</use></svg>
270             <a href={repoUrl}>Lemmy<sup>beta</sup></a>
271           </h5>
272           <p class="mb-0">
273             <T i18nKey="landing_0">#<a href="https://en.wikipedia.org/wiki/Social_network_aggregation">#</a><a href="https://en.wikipedia.org/wiki/Fediverse">#</a><br></br><code>#</code><br></br><b>#</b><br></br><a href={repoUrl}>#</a><br></br><a href="https://www.rust-lang.org">#</a><a href="https://actix.rs/">#</a><a href="https://infernojs.org">#</a><a href="https://www.typescriptlang.org/">#</a>
274           </T>
275         </p>
276       </div>
277     </div>
278     )
279   }
280
281   posts() {
282     return (
283       <div>
284         {this.state.loading ? 
285         <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
286         <div>
287           {this.selects()}
288           <PostListings posts={this.state.posts} showCommunity />
289           {this.paginator()}
290         </div>
291         }
292       </div>
293     )
294   }
295
296   selects() {
297     return (
298       <div className="mb-3">
299         <div class="btn-group btn-group-toggle">
300           <label className={`btn btn-sm btn-secondary 
301             ${this.state.type_ == ListingType.Subscribed && 'active'}
302             ${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
303             `}>
304             <input type="radio" 
305               value={ListingType.Subscribed}
306               checked={this.state.type_ == ListingType.Subscribed}
307               onChange={linkEvent(this, this.handleTypeChange)}
308               disabled={UserService.Instance.user == undefined}
309             />
310             {i18n.t('subscribed')}
311           </label>
312           <label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
313             <input type="radio" 
314               value={ListingType.All}
315               checked={this.state.type_ == ListingType.All}
316               onChange={linkEvent(this, this.handleTypeChange)}
317             /> 
318             {i18n.t('all')}
319           </label>
320         </div>
321         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto">
322           <option disabled><T i18nKey="sort_type">#</T></option>
323           <option value={SortType.Hot}><T i18nKey="hot">#</T></option>
324           <option value={SortType.New}><T i18nKey="new">#</T></option>
325           <option disabled>─────</option>
326           <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
327           <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
328           <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
329           <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
330           <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
331         </select>
332       </div>
333     )
334   }
335
336   paginator() {
337     return (
338       <div class="my-2">
339         {this.state.page > 1 && 
340           <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
341         }
342         <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
343       </div>
344     );
345   }
346
347   get canAdmin(): boolean {
348     return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
349   }
350
351   handleEditClick(i: Main) {
352     i.state.showEditSite = true;
353     i.setState(i.state);
354   }
355
356   handleEditCancel() {
357     this.state.showEditSite = false;
358     this.setState(this.state);
359   }
360
361   nextPage(i: Main) { 
362     i.state.page++;
363     i.state.loading = true;
364     i.setState(i.state);
365     i.updateUrl();
366     i.fetchPosts();
367     window.scrollTo(0,0);
368   }
369
370   prevPage(i: Main) { 
371     i.state.page--;
372     i.state.loading = true;
373     i.setState(i.state);
374     i.updateUrl();
375     i.fetchPosts();
376     window.scrollTo(0,0);
377   }
378
379   handleSortChange(i: Main, event: any) {
380     i.state.sort = Number(event.target.value);
381     i.state.page = 1;
382     i.state.loading = true;
383     i.setState(i.state);
384     i.updateUrl();
385     i.fetchPosts();
386     window.scrollTo(0,0);
387   }
388
389   handleTypeChange(i: Main, event: any) {
390     i.state.type_ = Number(event.target.value);
391     i.state.page = 1;
392     i.state.loading = true;
393     i.setState(i.state);
394     i.updateUrl();
395     i.fetchPosts();
396     window.scrollTo(0,0);
397   }
398
399   keepFetchingPosts() {
400     this.fetchPosts();
401     this.postFetcher = setInterval(() => this.fetchPosts(), postRefetchSeconds);
402   }
403
404   fetchPosts() {
405     let getPostsForm: GetPostsForm = {
406       page: this.state.page,
407       limit: fetchLimit,
408       sort: SortType[this.state.sort],
409       type_: ListingType[this.state.type_]
410     }
411     WebSocketService.Instance.getPosts(getPostsForm);
412   }
413
414   parseMessage(msg: any) {
415     console.log(msg);
416     let op: UserOperation = msgOp(msg);
417     if (msg.error) {
418       alert(i18n.t(msg.error));
419       return;
420     } else if (op == UserOperation.GetFollowedCommunities) {
421       let res: GetFollowedCommunitiesResponse = msg;
422       this.state.subscribedCommunities = res.communities;
423       this.setState(this.state);
424     } else if (op == UserOperation.ListCommunities) {
425       let res: ListCommunitiesResponse = msg;
426       this.state.trendingCommunities = res.communities;
427       this.setState(this.state);
428     } else if (op == UserOperation.GetSite) {
429       let res: GetSiteResponse = msg;
430
431       // This means it hasn't been set up yet
432       if (!res.site) {
433         this.context.router.history.push("/setup");
434       }
435       this.state.site.admins = res.admins;
436       this.state.site.site = res.site;
437       this.state.site.banned = res.banned;
438       this.state.site.online = res.online;
439       this.setState(this.state);
440       document.title = `${WebSocketService.Instance.site.name}`;
441
442     } else if (op == UserOperation.EditSite) {
443       let res: SiteResponse = msg;
444       this.state.site.site = res.site;
445       this.state.showEditSite = false;
446       this.setState(this.state);
447     } else if (op == UserOperation.GetPosts) {
448       let res: GetPostsResponse = msg;
449       this.state.posts = res.posts;
450       this.state.loading = false;
451       this.setState(this.state);
452     } else if (op == UserOperation.CreatePostLike) {
453       let res: CreatePostLikeResponse = msg;
454       let found = this.state.posts.find(c => c.id == res.post.id);
455       found.my_vote = res.post.my_vote;
456       found.score = res.post.score;
457       found.upvotes = res.post.upvotes;
458       found.downvotes = res.post.downvotes;
459       this.setState(this.state);
460     }
461   }
462 }
463