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';
12 subscribedCommunities: Array<CommunityUser>;
13 trendingCommunities: Array<Community>;
14 site: GetSiteResponse;
15 showEditSite: boolean;
23 export class Main extends Component<any, MainState> {
25 private subscription: Subscription;
26 private emptyState: MainState = {
27 subscribedCommunities: [],
28 trendingCommunities: [],
37 number_of_users: null,
38 number_of_posts: null,
39 number_of_comments: null,
47 type_: this.getListingTypeFromProps(this.props),
48 sort: this.getSortTypeFromProps(this.props),
49 page: this.getPageFromProps(this.props),
52 getListingTypeFromProps(props: any): ListingType {
53 return (props.match.params.type) ?
54 routeListingTypeToEnum(props.match.params.type) :
55 UserService.Instance.user ?
56 ListingType.Subscribed :
60 getSortTypeFromProps(props: any): SortType {
61 return (props.match.params.sort) ?
62 routeSortTypeToEnum(props.match.params.sort) :
66 getPageFromProps(props: any): number {
67 return (props.match.params.page) ? Number(props.match.params.page) : 1;
70 constructor(props: any, context: any) {
71 super(props, context);
73 this.state = this.emptyState;
75 this.subscription = WebSocketService.Instance.subject
76 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
78 (msg) => this.parseMessage(msg),
79 (err) => console.error(err),
80 () => console.log('complete')
83 WebSocketService.Instance.getSite();
85 if (UserService.Instance.user) {
86 WebSocketService.Instance.getFollowedCommunities();
89 let listCommunitiesForm: ListCommunitiesForm = {
90 sort: SortType[SortType.Hot],
94 WebSocketService.Instance.listCommunities(listCommunitiesForm);
99 componentWillUnmount() {
100 this.subscription.unsubscribe();
103 componentDidMount() {
104 document.title = "Lemmy";
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);
120 <div class="container">
122 <div class="col-12 col-md-8">
125 <div class="col-12 col-md-4">
126 {!this.state.loading &&
128 {this.trendingCommunities()}
129 {UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
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>
139 <Link class="btn btn-sm btn-secondary btn-block mb-3"
140 to="/create_community">Create a Community</Link>
150 trendingCommunities() {
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>
166 {!this.state.showEditSite ?
169 site={this.state.site.site}
170 onCancel={this.handleEditCancel}
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}`);
187 <h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
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>
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>
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>
207 {this.state.site.site.description &&
210 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
222 <svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
223 <a href={repoUrl}>Lemmy<sup>Beta</sup></a>
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>
237 {this.state.loading ?
238 <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
241 <PostListings posts={this.state.posts} showCommunity />
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'}
258 value={ListingType.Subscribed}
259 checked={this.state.type_ == ListingType.Subscribed}
260 onChange={linkEvent(this, this.handleTypeChange)}
261 disabled={UserService.Instance.user == undefined}
265 <label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
267 value={ListingType.All}
268 checked={this.state.type_ == ListingType.All}
269 onChange={linkEvent(this, this.handleTypeChange)}
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>
292 {this.state.page > 1 &&
293 <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
295 <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
300 get canAdmin(): boolean {
301 return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
304 handleEditClick(i: Main) {
305 i.state.showEditSite = true;
310 this.state.showEditSite = false;
311 this.setState(this.state);
328 handleSortChange(i: Main, event: any) {
329 i.state.sort = Number(event.target.value);
336 handleTypeChange(i: Main, event: any) {
337 i.state.type_ = Number(event.target.value);
345 let getPostsForm: GetPostsForm = {
346 page: this.state.page,
348 sort: SortType[this.state.sort],
349 type_: ListingType[this.state.type_]
351 WebSocketService.Instance.getPosts(getPostsForm);
354 parseMessage(msg: any) {
356 let op: UserOperation = msgOp(msg);
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;
371 // This means it hasn't been set up yet
373 this.context.router.history.push("/setup");
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);