]> 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
11 interface MainState {
12   subscribedCommunities: Array<CommunityUser>;
13   trendingCommunities: Array<Community>;
14   site: GetSiteResponse;
15   showEditSite: boolean;
16   loading: boolean;
17   posts: Array<Post>;
18   type_: ListingType;
19   sort: SortType;
20   page: number;
21 }
22
23 export class Main extends Component<any, MainState> {
24
25   private subscription: Subscription;
26   private emptyState: MainState = {
27     subscribedCommunities: [],
28     trendingCommunities: [],
29     site: {
30       op: null,
31       site: {
32         id: null,
33         name: null,
34         creator_id: null,
35         creator_name: null,
36         published: null,
37         number_of_users: null,
38         number_of_posts: null,
39         number_of_comments: null,
40       },
41       admins: [],
42       banned: [],
43     },
44     showEditSite: false,
45     loading: true,
46     posts: [],
47     type_: this.getListingTypeFromProps(this.props),
48     sort: this.getSortTypeFromProps(this.props),
49     page: this.getPageFromProps(this.props),
50   }
51
52   getListingTypeFromProps(props: any): ListingType {
53     return (props.match.params.type) ? 
54       routeListingTypeToEnum(props.match.params.type) : 
55       UserService.Instance.user ? 
56       ListingType.Subscribed : 
57       ListingType.All;
58   }
59
60   getSortTypeFromProps(props: any): SortType {
61     return (props.match.params.sort) ? 
62       routeSortTypeToEnum(props.match.params.sort) : 
63       SortType.Hot;
64   }
65
66   getPageFromProps(props: any): number {
67     return (props.match.params.page) ? Number(props.match.params.page) : 1;
68   }
69
70   constructor(props: any, context: any) {
71     super(props, context);
72
73     this.state = this.emptyState;
74
75     this.subscription = WebSocketService.Instance.subject
76     .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
77     .subscribe(
78       (msg) => this.parseMessage(msg),
79         (err) => console.error(err),
80         () => console.log('complete')
81     );
82
83     WebSocketService.Instance.getSite();
84
85     if (UserService.Instance.user) {
86       WebSocketService.Instance.getFollowedCommunities();
87     }
88
89     let listCommunitiesForm: ListCommunitiesForm = {
90       sort: SortType[SortType.Hot],
91       limit: 6
92     }
93
94     WebSocketService.Instance.listCommunities(listCommunitiesForm);
95
96     this.fetchPosts();
97   }
98
99   componentWillUnmount() {
100     this.subscription.unsubscribe();
101   }
102
103   componentDidMount() {
104     document.title = "Lemmy";
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.state.loading &&
127               <div>
128                 {this.trendingCommunities()}
129                 {UserService.Instance.user && this.state.subscribedCommunities.length > 0 && 
130                   <div>
131                     <h5>Subscribed <Link class="text-white" to="/communities">communities</Link></h5> 
132                     <ul class="list-inline"> 
133                       {this.state.subscribedCommunities.map(community =>
134                         <li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
135                       )}
136                     </ul>
137                   </div>
138                 }
139                 <Link class="btn btn-sm btn-secondary btn-block mb-3" 
140                   to="/create_community">Create a Community</Link>
141                 {this.sidebar()}
142               </div>
143             }
144           </div>
145         </div>
146       </div>
147     )
148   }
149
150   trendingCommunities() {
151     return (
152       <div>
153         <h5>Trending <Link class="text-white" to="/communities">communities</Link></h5> 
154         <ul class="list-inline"> 
155           {this.state.trendingCommunities.map(community =>
156             <li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
157           )}
158         </ul>
159       </div>
160     )
161   }
162
163   sidebar() {
164     return (
165       <div>
166         {!this.state.showEditSite ?
167           this.siteInfo() :
168           <SiteForm
169             site={this.state.site.site} 
170             onCancel={this.handleEditCancel} 
171           />
172         }
173         {this.landing()}
174       </div>
175     )
176   }
177
178   updateUrl() {
179     let typeStr = ListingType[this.state.type_].toLowerCase();
180     let sortStr = SortType[this.state.sort].toLowerCase();
181     this.props.history.push(`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`);
182   }
183
184   siteInfo() {
185     return (
186       <div>
187         <h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
188         {this.canAdmin && 
189           <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
190             <li className="list-inline-item">
191               <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
192             </li>
193           </ul>
194         }
195         <ul class="my-2 list-inline">
196           <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_users} Users</li>
197           <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li>
198           <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_comments} Comments</li>
199           <li className="list-inline-item"><Link className="badge badge-light" to="/modlog">Modlog</Link></li>
200         </ul>
201         <ul class="my-1 list-inline small"> 
202           <li class="list-inline-item">admins: </li>
203           {this.state.site.admins.map(admin =>
204             <li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li>
205           )}
206         </ul>
207         {this.state.site.site.description && 
208           <div>
209             <hr />
210             <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
211             <hr />
212           </div>
213         }
214       </div>
215     )
216   }
217
218   landing() {
219     return (
220       <div>
221         <h5>Powered by  
222           <svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
223           <a href={repoUrl}>Lemmy<sup>Beta</sup></a>
224         </h5>
225         <p>Lemmy is a <a href="https://en.wikipedia.org/wiki/Link_aggregation">link aggregator</a> / reddit alternative, intended to work in the <a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a>.</p>
226         <p>Its self-hostable, has live-updating comment threads, and is tiny (<code>~80kB</code>). Federation into the ActivityPub network is on the roadmap.</p>
227         <p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p>
228         <p>Suggest new features or report bugs <a href={repoUrl}>here.</a></p>
229         <p>Made with <a href="https://www.rust-lang.org">Rust</a>, <a href="https://actix.rs/">Actix</a>, <a href="https://www.infernojs.org">Inferno</a>, <a href="https://www.typescriptlang.org/">Typescript</a>.</p>
230       </div>
231     )
232   }
233
234   posts() {
235     return (
236       <div>
237         {this.state.loading ? 
238         <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
239         <div>
240           {this.selects()}
241           <PostListings posts={this.state.posts} showCommunity />
242           {this.paginator()}
243         </div>
244         }
245       </div>
246     )
247   }
248
249   selects() {
250     return (
251       <div className="mb-2">
252         <div class="btn-group btn-group-toggle">
253           <label className={`btn btn-sm btn-secondary 
254             ${this.state.type_ == ListingType.Subscribed && 'active'}
255             ${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
256             `}>
257             <input type="radio" 
258               value={ListingType.Subscribed}
259               checked={this.state.type_ == ListingType.Subscribed}
260               onChange={linkEvent(this, this.handleTypeChange)}
261               disabled={UserService.Instance.user == undefined}
262             />
263             Subscribed
264           </label>
265           <label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
266             <input type="radio" 
267               value={ListingType.All}
268               checked={this.state.type_ == ListingType.All}
269               onChange={linkEvent(this, this.handleTypeChange)}
270             /> 
271             All
272           </label>
273         </div>
274         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto">
275           <option disabled>Sort Type</option>
276           <option value={SortType.Hot}>Hot</option>
277           <option value={SortType.New}>New</option>
278           <option disabled>──────────</option>
279           <option value={SortType.TopDay}>Top Day</option>
280           <option value={SortType.TopWeek}>Week</option>
281           <option value={SortType.TopMonth}>Month</option>
282           <option value={SortType.TopYear}>Year</option>
283           <option value={SortType.TopAll}>All</option>
284         </select>
285       </div>
286     )
287   }
288
289   paginator() {
290     return (
291       <div class="mt-2">
292         {this.state.page > 1 && 
293           <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
294         }
295         <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
296       </div>
297     );
298   }
299
300   get canAdmin(): boolean {
301     return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
302   }
303
304   handleEditClick(i: Main) {
305     i.state.showEditSite = true;
306     i.setState(i.state);
307   }
308
309   handleEditCancel() {
310     this.state.showEditSite = false;
311     this.setState(this.state);
312   }
313
314   nextPage(i: Main) { 
315     i.state.page++;
316     i.setState(i.state);
317     i.updateUrl();
318     i.fetchPosts();
319   }
320
321   prevPage(i: Main) { 
322     i.state.page--;
323     i.setState(i.state);
324     i.updateUrl();
325     i.fetchPosts();
326   }
327
328   handleSortChange(i: Main, event: any) {
329     i.state.sort = Number(event.target.value);
330     i.state.page = 1;
331     i.setState(i.state);
332     i.updateUrl();
333     i.fetchPosts();
334   }
335
336   handleTypeChange(i: Main, event: any) {
337     i.state.type_ = Number(event.target.value);
338     i.state.page = 1;
339     i.setState(i.state);
340     i.updateUrl();
341     i.fetchPosts();
342   }
343
344   fetchPosts() {
345     let getPostsForm: GetPostsForm = {
346       page: this.state.page,
347       limit: fetchLimit,
348       sort: SortType[this.state.sort],
349       type_: ListingType[this.state.type_]
350     }
351     WebSocketService.Instance.getPosts(getPostsForm);
352   }
353
354   parseMessage(msg: any) {
355     console.log(msg);
356     let op: UserOperation = msgOp(msg);
357     if (msg.error) {
358       alert(msg.error);
359       return;
360     } else if (op == UserOperation.GetFollowedCommunities) {
361       let res: GetFollowedCommunitiesResponse = msg;
362       this.state.subscribedCommunities = res.communities;
363       this.setState(this.state);
364     } else if (op == UserOperation.ListCommunities) {
365       let res: ListCommunitiesResponse = msg;
366       this.state.trendingCommunities = res.communities;
367       this.setState(this.state);
368     } else if (op == UserOperation.GetSite) {
369       let res: GetSiteResponse = msg;
370
371       // This means it hasn't been set up yet
372       if (!res.site) {
373         this.context.router.history.push("/setup");
374       }
375       this.state.site.admins = res.admins;
376       this.state.site.site = res.site;
377       this.state.site.banned = res.banned;
378       this.setState(this.state);
379     } else if (op == UserOperation.EditSite) {
380       let res: SiteResponse = msg;
381       this.state.site.site = res.site;
382       this.state.showEditSite = false;
383       this.setState(this.state);
384     } else if (op == UserOperation.GetPosts) {
385       let res: GetPostsResponse = msg;
386       this.state.posts = res.posts;
387       this.state.loading = false;
388       window.scrollTo(0,0);
389       this.setState(this.state);
390     } else if (op == UserOperation.CreatePostLike) {
391       let res: CreatePostLikeResponse = msg;
392       let found = this.state.posts.find(c => c.id == res.post.id);
393       found.my_vote = res.post.my_vote;
394       found.score = res.post.score;
395       found.upvotes = res.post.upvotes;
396       found.downvotes = res.post.downvotes;
397       this.setState(this.state);
398     }
399   }
400 }
401