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';
14 subscribedCommunities: Array<CommunityUser>;
15 trendingCommunities: Array<Community>;
16 site: GetSiteResponse;
17 showEditSite: boolean;
25 export class Main extends Component<any, MainState> {
27 private subscription: Subscription;
28 private postFetcher: any;
29 private emptyState: MainState = {
30 subscribedCommunities: [],
31 trendingCommunities: [],
40 number_of_users: null,
41 number_of_posts: null,
42 number_of_comments: null,
43 number_of_communities: null,
52 type_: this.getListingTypeFromProps(this.props),
53 sort: this.getSortTypeFromProps(this.props),
54 page: this.getPageFromProps(this.props),
57 getListingTypeFromProps(props: any): ListingType {
58 return (props.match.params.type) ?
59 routeListingTypeToEnum(props.match.params.type) :
60 UserService.Instance.user ?
61 ListingType.Subscribed :
65 getSortTypeFromProps(props: any): SortType {
66 return (props.match.params.sort) ?
67 routeSortTypeToEnum(props.match.params.sort) :
71 getPageFromProps(props: any): number {
72 return (props.match.params.page) ? Number(props.match.params.page) : 1;
75 constructor(props: any, context: any) {
76 super(props, context);
78 this.state = this.emptyState;
79 this.handleEditCancel = this.handleEditCancel.bind(this);
81 this.subscription = WebSocketService.Instance.subject
82 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
84 (msg) => this.parseMessage(msg),
85 (err) => console.error(err),
86 () => console.log('complete')
89 WebSocketService.Instance.getSite();
91 if (UserService.Instance.user) {
92 WebSocketService.Instance.getFollowedCommunities();
95 let listCommunitiesForm: ListCommunitiesForm = {
96 sort: SortType[SortType.Hot],
100 WebSocketService.Instance.listCommunities(listCommunitiesForm);
102 this.keepFetchingPosts();
105 componentWillUnmount() {
106 this.subscription.unsubscribe();
107 clearInterval(this.postFetcher);
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);
123 <div class="container">
125 <div class="col-12 col-md-8">
128 <div class="col-12 col-md-4">
139 {!this.state.loading &&
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 &&
147 <T i18nKey="subscribed_to_communities">#<Link class="text-white" to="/communities">#</Link></T>
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>
156 <Link class="btn btn-sm btn-secondary btn-block"
157 to="/create_community">
158 <T i18nKey="create_a_community">#</T>
170 trendingCommunities() {
174 <T i18nKey="trending_communities">#<Link class="text-white" to="/communities">#</Link></T>
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>
188 {!this.state.showEditSite ?
191 site={this.state.site.site}
192 onCancel={this.handleEditCancel}
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}`);
208 <div class="card border-secondary mb-3">
209 <div class="card-body">
210 <h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
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>
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>
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>
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>
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>
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>
236 <li className="list-inline-item">
237 <Link className="badge badge-secondary" to="/modlog">
238 <T i18nKey="modlog">#</T>
242 <ul class="mt-1 list-inline small mb-0">
243 <li class="list-inline-item">
244 <T i18nKey="admins" class="d-inline">#</T>:
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>
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)} />
265 <div class="card border-secondary">
266 <div class="card-body">
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>
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>
284 {this.state.loading ?
285 <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
288 <PostListings posts={this.state.posts} showCommunity />
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'}
305 value={ListingType.Subscribed}
306 checked={this.state.type_ == ListingType.Subscribed}
307 onChange={linkEvent(this, this.handleTypeChange)}
308 disabled={UserService.Instance.user == undefined}
310 {i18n.t('subscribed')}
312 <label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
314 value={ListingType.All}
315 checked={this.state.type_ == ListingType.All}
316 onChange={linkEvent(this, this.handleTypeChange)}
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>
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>
342 <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
347 get canAdmin(): boolean {
348 return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
351 handleEditClick(i: Main) {
352 i.state.showEditSite = true;
357 this.state.showEditSite = false;
358 this.setState(this.state);
363 i.state.loading = true;
367 window.scrollTo(0,0);
372 i.state.loading = true;
376 window.scrollTo(0,0);
379 handleSortChange(i: Main, event: any) {
380 i.state.sort = Number(event.target.value);
382 i.state.loading = true;
386 window.scrollTo(0,0);
389 handleTypeChange(i: Main, event: any) {
390 i.state.type_ = Number(event.target.value);
392 i.state.loading = true;
396 window.scrollTo(0,0);
399 keepFetchingPosts() {
401 this.postFetcher = setInterval(() => this.fetchPosts(), postRefetchSeconds);
405 let getPostsForm: GetPostsForm = {
406 page: this.state.page,
408 sort: SortType[this.state.sort],
409 type_: ListingType[this.state.type_]
411 WebSocketService.Instance.getPosts(getPostsForm);
414 parseMessage(msg: any) {
416 let op: UserOperation = msgOp(msg);
418 alert(i18n.t(msg.error));
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;
431 // This means it hasn't been set up yet
433 this.context.router.history.push("/setup");
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}`;
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);