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