Adding better community and main page routing, with sorting and type in url.

This commit is contained in:
Dessalines 2019-04-28 17:19:04 -07:00
parent 4625aa3060
commit 1769a62aac
6 changed files with 325 additions and 227 deletions

View File

@ -1,11 +1,11 @@
import { Component } from 'inferno';
import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser, UserView } from '../interfaces';
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser, UserView, SortType, Post, GetPostsForm, ListingType, GetPostsResponse, CreatePostLikeResponse } from '../interfaces';
import { WebSocketService } from '../services';
import { PostListings } from './post-listings';
import { Sidebar } from './sidebar';
import { msgOp } from '../utils';
import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils';
interface State {
community: CommunityI;
@ -14,6 +14,9 @@ interface State {
moderators: Array<CommunityUser>;
admins: Array<UserView>;
loading: boolean;
posts: Array<Post>;
sort: SortType;
page: number;
}
export class Community extends Component<any, State> {
@ -38,7 +41,20 @@ export class Community extends Component<any, State> {
admins: [],
communityId: Number(this.props.match.params.id),
communityName: this.props.match.params.name,
loading: true
loading: true,
posts: [],
sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props),
}
getSortTypeFromProps(props: any): SortType {
return (props.match.params.sort) ?
routeSortTypeToEnum(props.match.params.sort) :
SortType.Hot;
}
getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1;
}
constructor(props: any, context: any) {
@ -66,6 +82,16 @@ export class Community extends Component<any, State> {
this.subscription.unsubscribe();
}
// Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP') {
this.state = this.emptyState;
this.state.sort = this.getSortTypeFromProps(nextProps);
this.state.page = this.getPageFromProps(nextProps);
this.fetchPosts();
}
}
render() {
return (
<div class="container">
@ -78,7 +104,9 @@ export class Community extends Component<any, State> {
<small className="ml-2 text-muted font-italic">removed</small>
}
</h5>
{this.state.community && <PostListings communityId={this.state.community.id} />}
{this.selects()}
<PostListings posts={this.state.posts} />
{this.paginator()}
</div>
<div class="col-12 col-md-3">
<Sidebar
@ -93,6 +121,72 @@ export class Community extends Component<any, State> {
)
}
selects() {
return (
<div className="mb-2">
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
<option disabled>Sort Type</option>
<option value={SortType.Hot}>Hot</option>
<option value={SortType.New}>New</option>
<option disabled></option>
<option value={SortType.TopDay}>Top Day</option>
<option value={SortType.TopWeek}>Week</option>
<option value={SortType.TopMonth}>Month</option>
<option value={SortType.TopYear}>Year</option>
<option value={SortType.TopAll}>All</option>
</select>
</div>
)
}
paginator() {
return (
<div class="mt-2">
{this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
}
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
</div>
);
}
nextPage(i: Community) {
i.state.page++;
i.setState(i.state);
i.updateUrl();
i.fetchPosts();
}
prevPage(i: Community) {
i.state.page--;
i.setState(i.state);
i.updateUrl();
i.fetchPosts();
}
handleSortChange(i: Community, event: any) {
i.state.sort = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.updateUrl();
i.fetchPosts();
}
updateUrl() {
let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push(`/f/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`);
}
fetchPosts() {
let getPostsForm: GetPostsForm = {
page: this.state.page,
limit: fetchLimit,
sort: SortType[this.state.sort],
type_: ListingType[ListingType.Community],
community_id: this.state.community.id,
}
WebSocketService.Instance.getPosts(getPostsForm);
}
parseMessage(msg: any) {
console.log(msg);
@ -105,9 +199,9 @@ export class Community extends Component<any, State> {
this.state.community = res.community;
this.state.moderators = res.moderators;
this.state.admins = res.admins;
this.state.loading = false;
document.title = `/f/${this.state.community.name} - Lemmy`;
this.setState(this.state);
this.fetchPosts();
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.state.community = res.community;
@ -117,6 +211,19 @@ export class Community extends Component<any, State> {
this.state.community.subscribed = res.community.subscribed;
this.state.community.number_of_subscribers = res.community.number_of_subscribers;
this.setState(this.state);
} else if (op == UserOperation.GetPosts) {
let res: GetPostsResponse = msg;
this.state.posts = res.posts;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
let found = this.state.posts.find(c => c.id == res.post.id);
found.my_vote = res.post.my_vote;
found.score = res.post.score;
found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes;
this.setState(this.state);
}
}
}

View File

@ -1,24 +0,0 @@
import { Component } from 'inferno';
import { Main } from './main';
import { ListingType } from '../interfaces';
export class Home extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<Main type={this.listType()}/>
)
}
componentDidMount() {
document.title = "Lemmy";
}
listType(): ListingType {
return (this.props.match.path == '/all') ? ListingType.All : ListingType.Subscribed;
}
}

View File

@ -2,16 +2,11 @@ import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, ListingType, SiteResponse } from '../interfaces';
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, ListingType, SiteResponse, GetPostsResponse, CreatePostLikeResponse, Post, GetPostsForm } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings';
import { SiteForm } from './site-form';
import { msgOp, repoUrl, mdToHtml } from '../utils';
interface MainProps {
type: ListingType;
}
import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils';
interface MainState {
subscribedCommunities: Array<CommunityUser>;
@ -19,9 +14,13 @@ interface MainState {
site: GetSiteResponse;
showEditSite: boolean;
loading: boolean;
posts: Array<Post>;
type_: ListingType;
sort: SortType;
page: number;
}
export class Main extends Component<MainProps, MainState> {
export class Main extends Component<any, MainState> {
private subscription: Subscription;
private emptyState: MainState = {
@ -43,7 +42,29 @@ export class Main extends Component<MainProps, MainState> {
banned: [],
},
showEditSite: false,
loading: true
loading: true,
posts: [],
type_: this.getListingTypeFromProps(this.props),
sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props),
}
getListingTypeFromProps(props: any): ListingType {
return (props.match.params.type) ?
routeListingTypeToEnum(props.match.params.type) :
UserService.Instance.user ?
ListingType.Subscribed :
ListingType.All;
}
getSortTypeFromProps(props: any): SortType {
return (props.match.params.sort) ?
routeSortTypeToEnum(props.match.params.sort) :
SortType.Hot;
}
getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1;
}
constructor(props: any, context: any) {
@ -72,37 +93,51 @@ export class Main extends Component<MainProps, MainState> {
WebSocketService.Instance.listCommunities(listCommunitiesForm);
this.handleEditCancel = this.handleEditCancel.bind(this);
this.fetchPosts();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
componentDidMount() {
document.title = "Lemmy";
}
// Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP') {
this.state = this.emptyState;
this.state.type_ = this.getListingTypeFromProps(nextProps);
this.state.sort = this.getSortTypeFromProps(nextProps);
this.state.page = this.getPageFromProps(nextProps);
this.fetchPosts();
}
}
render() {
return (
<div class="container">
<div class="row">
<div class="col-12 col-md-8">
<PostListings type={this.props.type} />
{this.posts()}
</div>
<div class="col-12 col-md-4">
{this.state.loading ?
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div>
{this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div>
<h5>Subscribed forums</h5>
<ul class="list-inline">
{this.state.subscribedCommunities.map(community =>
<li class="list-inline-item"><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li>
)}
</ul>
</div>
}
{this.sidebar()}
</div>
{!this.state.loading &&
<div>
{this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div>
<h5>Subscribed forums</h5>
<ul class="list-inline">
{this.state.subscribedCommunities.map(community =>
<li class="list-inline-item"><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li>
)}
</ul>
</div>
}
{this.sidebar()}
</div>
}
</div>
</div>
@ -138,6 +173,12 @@ export class Main extends Component<MainProps, MainState> {
)
}
updateUrl() {
let typeStr = ListingType[this.state.type_].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push(`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`);
}
siteInfo() {
return (
<div>
@ -185,9 +226,61 @@ export class Main extends Component<MainProps, MainState> {
<p>Suggest new features or report bugs <a href={repoUrl}>here.</a></p>
<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>
</div>
)
}
posts() {
return (
<div>
{this.state.loading ?
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div>
{this.selects()}
<PostListings posts={this.state.posts} showCommunity />
{this.paginator()}
</div>
}
</div>
)
}
selects() {
return (
<div className="mb-2">
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
<option disabled>Sort Type</option>
<option value={SortType.Hot}>Hot</option>
<option value={SortType.New}>New</option>
<option disabled></option>
<option value={SortType.TopDay}>Top Day</option>
<option value={SortType.TopWeek}>Week</option>
<option value={SortType.TopMonth}>Month</option>
<option value={SortType.TopYear}>Year</option>
<option value={SortType.TopAll}>All</option>
</select>
{ UserService.Instance.user &&
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
<option disabled>Type</option>
<option value={ListingType.All}>All</option>
<option value={ListingType.Subscribed}>Subscribed</option>
</select>
}
</div>
)
}
paginator() {
return (
<div class="mt-2">
{this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
}
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
</div>
);
}
get canAdmin(): boolean {
return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
}
@ -202,6 +295,46 @@ export class Main extends Component<MainProps, MainState> {
this.setState(this.state);
}
nextPage(i: Main) {
i.state.page++;
i.setState(i.state);
i.updateUrl();
i.fetchPosts();
}
prevPage(i: Main) {
i.state.page--;
i.setState(i.state);
i.updateUrl();
i.fetchPosts();
}
handleSortChange(i: Main, event: any) {
i.state.sort = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.updateUrl();
i.fetchPosts();
}
handleTypeChange(i: Main, event: any) {
i.state.type_ = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.updateUrl();
i.fetchPosts();
}
fetchPosts() {
let getPostsForm: GetPostsForm = {
page: this.state.page,
limit: fetchLimit,
sort: SortType[this.state.sort],
type_: ListingType[this.state.type_]
}
WebSocketService.Instance.getPosts(getPostsForm);
}
parseMessage(msg: any) {
console.log(msg);
let op: UserOperation = msgOp(msg);
@ -211,12 +344,10 @@ export class Main extends Component<MainProps, MainState> {
} else if (op == UserOperation.GetFollowedCommunities) {
let res: GetFollowedCommunitiesResponse = msg;
this.state.subscribedCommunities = res.communities;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg;
this.state.trendingCommunities = res.communities;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
@ -234,6 +365,19 @@ export class Main extends Component<MainProps, MainState> {
this.state.site.site = res.site;
this.state.showEditSite = false;
this.setState(this.state);
} else if (op == UserOperation.GetPosts) {
let res: GetPostsResponse = msg;
this.state.posts = res.posts;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
let found = this.state.posts.find(c => c.id == res.post.id);
found.my_vote = res.post.my_vote;
found.score = res.post.score;
found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes;
this.setState(this.state);
}
}
}

View File

@ -1,183 +1,28 @@
import { Component, linkEvent } from 'inferno';
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, GetPostsForm, SortType, ListingType, GetPostsResponse, CreatePostLikeResponse, CommunityUser} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { Post } from '../interfaces';
import { PostListing } from './post-listing';
import { msgOp, fetchLimit } from '../utils';
interface PostListingsProps {
type?: ListingType;
communityId?: number;
}
interface PostListingsState {
moderators: Array<CommunityUser>;
posts: Array<Post>;
sortType: SortType;
type_: ListingType;
page: number;
loading: boolean;
showCommunity?: boolean;
}
export class PostListings extends Component<PostListingsProps, PostListingsState> {
private subscription: Subscription;
private emptyState: PostListingsState = {
moderators: [],
posts: [],
sortType: SortType.Hot,
type_: (this.props.type !== undefined && UserService.Instance.user) ? this.props.type :
this.props.communityId
? ListingType.Community
: UserService.Instance.user
? ListingType.Subscribed
: ListingType.All,
page: 1,
loading: true
}
export class PostListings extends Component<PostListingsProps, any> {
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
(msg) => this.parseMessage(msg),
(err) => console.error(err),
() => console.log('complete')
);
this.refetch();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<div>
{this.state.loading ?
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div>
{this.selects()}
{this.state.posts.length > 0
? this.state.posts.map(post =>
<PostListing post={post} showCommunity={!this.props.communityId}/>)
: <div>No Listings. {!this.props.communityId && <span>Subscribe to some <Link to="/communities">forums</Link>.</span>}</div>
}
{this.paginator()}
{this.props.posts.length > 0 ? this.props.posts.map(post =>
<PostListing post={post} showCommunity={this.props.showCommunity} />) :
<div>No Listings. {!this.props.showCommunity && <span>Subscribe to some <Link to="/communities">forums</Link>.</span>}
</div>
}
</div>
)
}
selects() {
return (
<div className="mb-2">
<select value={this.state.sortType} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
<option disabled>Sort Type</option>
<option value={SortType.Hot}>Hot</option>
<option value={SortType.New}>New</option>
<option disabled></option>
<option value={SortType.TopDay}>Top Day</option>
<option value={SortType.TopWeek}>Week</option>
<option value={SortType.TopMonth}>Month</option>
<option value={SortType.TopYear}>Year</option>
<option value={SortType.TopAll}>All</option>
</select>
{!this.props.communityId &&
UserService.Instance.user &&
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
<option disabled>Type</option>
<option value={ListingType.All}>All</option>
<option value={ListingType.Subscribed}>Subscribed</option>
</select>
}
</div>
)
}
paginator() {
return (
<div class="mt-2">
{this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
}
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
</div>
);
}
nextPage(i: PostListings) {
i.state.page++;
i.setState(i.state);
i.refetch();
}
prevPage(i: PostListings) {
i.state.page--;
i.setState(i.state);
i.refetch();
}
handleSortChange(i: PostListings, event: any) {
i.state.sortType = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.refetch();
}
refetch() {
let getPostsForm: GetPostsForm = {
community_id: this.props.communityId,
page: this.state.page,
limit: fetchLimit,
sort: SortType[this.state.sortType],
type_: ListingType[this.state.type_]
}
WebSocketService.Instance.getPosts(getPostsForm);
}
handleTypeChange(i: PostListings, event: any) {
i.state.type_ = Number(event.target.value);
i.state.page = 1;
if (i.state.type_ == ListingType.All) {
i.context.router.history.push('/all');
} else {
i.context.router.history.push('/');
}
i.setState(i.state);
i.refetch();
}
parseMessage(msg: any) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(msg.error);
return;
} else if (op == UserOperation.GetPosts) {
let res: GetPostsResponse = msg;
this.state.posts = res.posts;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
let found = this.state.posts.find(c => c.id == res.post.id);
found.my_vote = res.post.my_vote;
found.score = res.post.score;
found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes;
this.setState(this.state);
}
}
}

View File

@ -1,9 +1,8 @@
import { render, Component } from 'inferno';
import { HashRouter, Route, Switch } from 'inferno-router';
import { HashRouter, BrowserRouter, Route, Switch } from 'inferno-router';
import { Main } from './components/main';
import { Navbar } from './components/navbar';
import { Footer } from './components/footer';
import { Home } from './components/home';
import { Login } from './components/login';
import { CreatePost } from './components/create-post';
import { CreateCommunity } from './components/create-community';
@ -39,14 +38,15 @@ class Index extends Component<any, any> {
<Navbar />
<div class="mt-3 p-0">
<Switch>
<Route exact path="/all" component={Home} />
<Route exact path="/" component={Home} />
<Route path="/home/type/:type/sort/:sort/page/:page" component={Main} />
<Route exact path="/" component={Main} />
<Route path={`/login`} component={Login} />
<Route path={`/create_post`} component={CreatePost} />
<Route path={`/create_community`} component={CreateCommunity} />
<Route path={`/communities`} component={Communities} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
<Route path={`/post/:id`} component={Post} />
<Route path="/f/:name/sort/:sort/page/:page" component={Community} />
<Route path={`/community/:id`} component={Community} />
<Route path={`/f/:name`} component={Community} />
<Route path={`/user/:id`} component={User} />

View File

@ -1,4 +1,4 @@
import { UserOperation, Comment, User } from './interfaces';
import { UserOperation, Comment, User, SortType, ListingType } from './interfaces';
import * as markdown_it from 'markdown-it';
export let repoUrl = 'https://github.com/dessalines/lemmy';
@ -67,3 +67,29 @@ export function isImage(url: string) {
}
export let fetchLimit: number = 20;
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function routeSortTypeToEnum(sort: string): SortType {
if (sort == 'new') {
return SortType.New;
} else if (sort == 'hot') {
return SortType.Hot;
} else if (sort == 'topday') {
return SortType.TopDay;
} else if (sort == 'topweek') {
return SortType.TopWeek;
} else if (sort == 'topmonth') {
return SortType.TopMonth;
} else if (sort == 'topall') {
return SortType.TopAll;
}
}
export function routeListingTypeToEnum(type: string): ListingType {
return ListingType[capitalizeFirstLetter(type)];
}