Merge branch 'dev'

This commit is contained in:
Dessalines 2019-04-28 19:07:11 -07:00
commit 2d86dad1a6
17 changed files with 403 additions and 259 deletions

View file

@ -45,7 +45,7 @@ export class Communities extends Component<any, CommunitiesState> {
} }
componentDidMount() { componentDidMount() {
document.title = "Forums - Lemmy"; document.title = "Communities - Lemmy";
let table = document.querySelector('#community_table'); let table = document.querySelector('#community_table');
Sortable.initTable(table); Sortable.initTable(table);
} }
@ -56,7 +56,7 @@ export class Communities extends Component<any, CommunitiesState> {
{this.state.loading ? {this.state.loading ?
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div> <div>
<h5>Forums</h5> <h5>Communities</h5>
<div class="table-responsive"> <div class="table-responsive">
<table id="community_table" class="table table-sm table-hover"> <table id="community_table" class="table table-sm table-hover">
<thead class="pointer"> <thead class="pointer">
@ -73,7 +73,7 @@ export class Communities extends Component<any, CommunitiesState> {
<tbody> <tbody>
{this.state.communities.map(community => {this.state.communities.map(community =>
<tr> <tr>
<td><Link to={`/f/${community.name}`}>{community.name}</Link></td> <td><Link to={`/c/${community.name}`}>{community.name}</Link></td>
<td>{community.title}</td> <td>{community.title}</td>
<td>{community.category_name}</td> <td>{community.category_name}</td>
<td class="text-right d-none d-md-table-cell">{community.number_of_subscribers}</td> <td class="text-right d-none d-md-table-cell">{community.number_of_subscribers}</td>

View file

@ -120,10 +120,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
if (i.props.community) { if (i.props.community) {
WebSocketService.Instance.editCommunity(i.state.communityForm); WebSocketService.Instance.editCommunity(i.state.communityForm);
} else { } else {
WebSocketService.Instance.createCommunity(i.state.communityForm);
setTimeout(function(){
WebSocketService.Instance.createCommunity(i.state.communityForm);
}, 10000);
} }
i.setState(i.state); i.setState(i.state);
} }

View file

@ -1,11 +1,11 @@
import { Component } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; 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 { WebSocketService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { Sidebar } from './sidebar'; import { Sidebar } from './sidebar';
import { msgOp } from '../utils'; import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils';
interface State { interface State {
community: CommunityI; community: CommunityI;
@ -14,6 +14,9 @@ interface State {
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>; admins: Array<UserView>;
loading: boolean; loading: boolean;
posts: Array<Post>;
sort: SortType;
page: number;
} }
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
@ -38,7 +41,20 @@ export class Community extends Component<any, State> {
admins: [], admins: [],
communityId: Number(this.props.match.params.id), communityId: Number(this.props.match.params.id),
communityName: this.props.match.params.name, 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) { constructor(props: any, context: any) {
@ -66,6 +82,16 @@ export class Community extends Component<any, State> {
this.subscription.unsubscribe(); 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() { render() {
return ( return (
<div class="container"> <div class="container">
@ -78,7 +104,9 @@ export class Community extends Component<any, State> {
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic">removed</small>
} }
</h5> </h5>
{this.state.community && <PostListings communityId={this.state.community.id} />} {this.selects()}
<PostListings posts={this.state.posts} />
{this.paginator()}
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<Sidebar <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(`/c/${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) { parseMessage(msg: any) {
console.log(msg); console.log(msg);
@ -105,9 +199,9 @@ export class Community extends Component<any, State> {
this.state.community = res.community; this.state.community = res.community;
this.state.moderators = res.moderators; this.state.moderators = res.moderators;
this.state.admins = res.admins; this.state.admins = res.admins;
this.state.loading = false; document.title = `/c/${this.state.community.name} - Lemmy`;
document.title = `/f/${this.state.community.name} - Lemmy`;
this.setState(this.state); this.setState(this.state);
this.fetchPosts();
} else if (op == UserOperation.EditCommunity) { } else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg; let res: CommunityResponse = msg;
this.state.community = res.community; 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.subscribed = res.community.subscribed;
this.state.community.number_of_subscribers = res.community.number_of_subscribers; this.state.community.number_of_subscribers = res.community.number_of_subscribers;
this.setState(this.state); 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

@ -10,7 +10,7 @@ export class CreateCommunity extends Component<any, any> {
} }
componentDidMount() { componentDidMount() {
document.title = "Create Forum - Lemmy"; document.title = "Create Community - Lemmy";
} }
render() { render() {
@ -18,7 +18,7 @@ export class CreateCommunity extends Component<any, any> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 mb-4"> <div class="col-12 col-lg-6 mb-4">
<h5>Create Forum</h5> <h5>Create Community</h5>
<CommunityForm onCreate={this.handleCommunityCreate}/> <CommunityForm onCreate={this.handleCommunityCreate}/>
</div> </div>
</div> </div>
@ -27,7 +27,7 @@ export class CreateCommunity extends Component<any, any> {
} }
handleCommunityCreate(community: Community) { handleCommunityCreate(community: Community) {
this.props.history.push(`/f/${community.name}`); this.props.history.push(`/c/${community.name}`);
} }
} }

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 { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; 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 { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { SiteForm } from './site-form'; import { SiteForm } from './site-form';
import { msgOp, repoUrl, mdToHtml } from '../utils'; import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils';
interface MainProps {
type: ListingType;
}
interface MainState { interface MainState {
subscribedCommunities: Array<CommunityUser>; subscribedCommunities: Array<CommunityUser>;
@ -19,9 +14,13 @@ interface MainState {
site: GetSiteResponse; site: GetSiteResponse;
showEditSite: boolean; showEditSite: boolean;
loading: 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 subscription: Subscription;
private emptyState: MainState = { private emptyState: MainState = {
@ -43,7 +42,29 @@ export class Main extends Component<MainProps, MainState> {
banned: [], banned: [],
}, },
showEditSite: false, 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) { constructor(props: any, context: any) {
@ -72,37 +93,51 @@ export class Main extends Component<MainProps, MainState> {
WebSocketService.Instance.listCommunities(listCommunitiesForm); WebSocketService.Instance.listCommunities(listCommunitiesForm);
this.handleEditCancel = this.handleEditCancel.bind(this); this.fetchPosts();
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); 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() { render() {
return ( return (
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<PostListings type={this.props.type} /> {this.posts()}
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
{this.state.loading ? {!this.state.loading &&
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <div>
<div> {this.trendingCommunities()}
{this.trendingCommunities()} {UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 && <div>
<div> <h5>Subscribed communities</h5>
<h5>Subscribed forums</h5> <ul class="list-inline">
<ul class="list-inline"> {this.state.subscribedCommunities.map(community =>
{this.state.subscribedCommunities.map(community => <li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
<li class="list-inline-item"><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li> )}
)} </ul>
</ul> </div>
</div> }
} {this.sidebar()}
{this.sidebar()} </div>
</div>
} }
</div> </div>
</div> </div>
@ -113,10 +148,10 @@ export class Main extends Component<MainProps, MainState> {
trendingCommunities() { trendingCommunities() {
return ( return (
<div> <div>
<h5>Trending <Link class="text-white" to="/communities">forums</Link></h5> <h5>Trending <Link class="text-white" to="/communities">communities</Link></h5>
<ul class="list-inline"> <ul class="list-inline">
{this.state.trendingCommunities.map(community => {this.state.trendingCommunities.map(community =>
<li class="list-inline-item"><Link to={`/f/${community.name}`}>{community.name}</Link></li> <li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
)} )}
</ul> </ul>
</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() { siteInfo() {
return ( return (
<div> <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>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> <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> </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 { get canAdmin(): boolean {
return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id); 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); 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) { parseMessage(msg: any) {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
@ -211,12 +344,10 @@ export class Main extends Component<MainProps, MainState> {
} else if (op == UserOperation.GetFollowedCommunities) { } else if (op == UserOperation.GetFollowedCommunities) {
let res: GetFollowedCommunitiesResponse = msg; let res: GetFollowedCommunitiesResponse = msg;
this.state.subscribedCommunities = res.communities; this.state.subscribedCommunities = res.communities;
this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg; let res: ListCommunitiesResponse = msg;
this.state.trendingCommunities = res.communities; this.state.trendingCommunities = res.communities;
this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetSite) { } else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg; let res: GetSiteResponse = msg;
@ -234,6 +365,19 @@ export class Main extends Component<MainProps, MainState> {
this.state.site.site = res.site; this.state.site.site = res.site;
this.state.showEditSite = false; this.state.showEditSite = false;
this.setState(this.state); 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

@ -110,7 +110,7 @@ export class Modlog extends Component<any, ModlogState> {
{i.type_ == 'removed_communities' && {i.type_ == 'removed_communities' &&
<> <>
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'} {(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'}
<span> Community <Link to={`/f/${(i.data as ModRemoveCommunity).community_name}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span> <span> Community <Link to={`/c/${(i.data as ModRemoveCommunity).community_name}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span>
<div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div> <div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
<div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div> <div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div>
</> </>
@ -120,7 +120,7 @@ export class Modlog extends Component<any, ModlogState> {
<span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span> <span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span>
<span><Link to={`/u/${(i.data as ModBanFromCommunity).other_user_name}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span> <span><Link to={`/u/${(i.data as ModBanFromCommunity).other_user_name}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
<span> from the community </span> <span> from the community </span>
<span><Link to={`/f/${(i.data as ModBanFromCommunity).community_name}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span> <span><Link to={`/c/${(i.data as ModBanFromCommunity).community_name}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span>
<div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div> <div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
<div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div> <div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div>
</> </>
@ -130,7 +130,7 @@ export class Modlog extends Component<any, ModlogState> {
<span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span> <span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
<span><Link to={`/u/${(i.data as ModAddCommunity).other_user_name}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span> <span><Link to={`/u/${(i.data as ModAddCommunity).other_user_name}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
<span> as a mod to the community </span> <span> as a mod to the community </span>
<span><Link to={`/f/${(i.data as ModAddCommunity).community_name}`}>{(i.data as ModAddCommunity).community_name}</Link></span> <span><Link to={`/c/${(i.data as ModAddCommunity).community_name}`}>{(i.data as ModAddCommunity).community_name}</Link></span>
</> </>
} }
{i.type_ == 'banned' && {i.type_ == 'banned' &&
@ -165,7 +165,7 @@ export class Modlog extends Component<any, ModlogState> {
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div> <div>
<h5> <h5>
{this.state.communityName && <Link className="text-white" to={`/f/${this.state.communityName}`}>/f/{this.state.communityName} </Link>} {this.state.communityName && <Link className="text-white" to={`/c/${this.state.communityName}`}>/c/{this.state.communityName} </Link>}
<span>Modlog</span> <span>Modlog</span>
</h5> </h5>
<div class="table-responsive"> <div class="table-responsive">

View file

@ -63,17 +63,17 @@ export class Navbar extends Component<any, NavbarState> {
navbar() { navbar() {
return ( return (
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3"> <nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3">
<a title={version} class="navbar-brand" href="#"> <Link title={version} class="navbar-brand" to="/">
<svg class="icon mr-2"><use xlinkHref="#icon-mouse"></use></svg> <svg class="icon mr-2 mouse-icon"><use xlinkHref="#icon-mouse"></use></svg>
Lemmy Lemmy
</a> </Link>
<button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}> <button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}> <div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/communities">Forums</Link> <Link class="nav-link" to="/communities">Communities</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/search">Search</Link> <Link class="nav-link" to="/search">Search</Link>
@ -82,7 +82,7 @@ export class Navbar extends Component<any, NavbarState> {
<Link class="nav-link" to="/create_post">Create Post</Link> <Link class="nav-link" to="/create_post">Create Post</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/create_community">Create Forum</Link> <Link class="nav-link" to="/create_community">Create Community</Link>
</li> </li>
</ul> </ul>
<ul class="navbar-nav ml-auto mr-2"> <ul class="navbar-nav ml-auto mr-2">

View file

@ -99,7 +99,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{/* Cant change a community from an edit */} {/* Cant change a community from an edit */}
{!this.props.post && {!this.props.post &&
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Forum</label> <label class="col-sm-2 col-form-label">Community</label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}> <select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
{this.state.communities.map(community => {this.state.communities.map(community =>

View file

@ -118,7 +118,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{this.props.showCommunity && {this.props.showCommunity &&
<span> <span>
<span> to </span> <span> to </span>
<Link to={`/f/${post.community_name}`}>{post.community_name}</Link> <Link to={`/c/${post.community_name}`}>{post.community_name}</Link>
</span> </span>
} }
</li> </li>

View file

@ -1,183 +1,28 @@
import { Component, linkEvent } from 'inferno'; import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Post } from '../interfaces';
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, GetPostsForm, SortType, ListingType, GetPostsResponse, CreatePostLikeResponse, CommunityUser} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { msgOp, fetchLimit } from '../utils';
interface PostListingsProps { interface PostListingsProps {
type?: ListingType;
communityId?: number;
}
interface PostListingsState {
moderators: Array<CommunityUser>;
posts: Array<Post>; posts: Array<Post>;
sortType: SortType; showCommunity?: boolean;
type_: ListingType;
page: number;
loading: boolean;
} }
export class PostListings extends Component<PostListingsProps, PostListingsState> { export class PostListings extends Component<PostListingsProps, any> {
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
}
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); 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() { render() {
return ( return (
<div> <div>
{this.state.loading ? {this.props.posts.length > 0 ? this.props.posts.map(post =>
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <PostListing post={post} showCommunity={this.props.showCommunity} />) :
<div> <div>No posts. {this.props.showCommunity !== undefined && <span>Subscribe to some <Link to="/communities">communities</Link>.</span>}
{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()}
</div> </div>
} }
</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

@ -20,6 +20,7 @@ export class Setup extends Component<any, State> {
username: undefined, username: undefined,
password: undefined, password: undefined,
password_verify: undefined, password_verify: undefined,
spam_timeri: 3000,
admin: true, admin: true,
}, },
doneRegisteringUser: false, doneRegisteringUser: false,

View file

@ -57,7 +57,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic">removed</small>
} }
</h5> </h5>
<Link className="text-muted" to={`/f/${community.name}`}>/f/{community.name}</Link> <Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link>
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{this.canMod && {this.canMod &&
<> <>

View file

@ -4,7 +4,7 @@ import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces'; import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp, fetchLimit } from '../utils'; import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
@ -25,6 +25,7 @@ interface UserState {
view: View; view: View;
sort: SortType; sort: SortType;
page: number; page: number;
loading: boolean;
} }
export class User extends Component<any, UserState> { export class User extends Component<any, UserState> {
@ -47,9 +48,10 @@ export class User extends Component<any, UserState> {
moderates: [], moderates: [],
comments: [], comments: [],
posts: [], posts: [],
view: View.Overview, loading: true,
sort: SortType.New, view: this.getViewFromProps(this.props),
page: 1, sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props),
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -71,13 +73,42 @@ export class User extends Component<any, UserState> {
this.refetch(); this.refetch();
} }
getViewFromProps(props: any): View {
return (props.match.params.view) ?
View[capitalizeFirstLetter(props.match.params.view)] :
View.Overview;
}
getSortTypeFromProps(props: any): SortType {
return (props.match.params.sort) ?
routeSortTypeToEnum(props.match.params.sort) :
SortType.New;
}
getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1;
}
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP') {
this.state = this.emptyState;
this.state.view = this.getViewFromProps(nextProps);
this.state.sort = this.getSortTypeFromProps(nextProps);
this.state.page = this.getPageFromProps(nextProps);
this.refetch();
}
}
render() { render() {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ?
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div class="row"> <div class="row">
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<h5>/u/{this.state.user.name}</h5> <h5>/u/{this.state.user.name}</h5>
@ -102,6 +133,7 @@ export class User extends Component<any, UserState> {
{this.follows()} {this.follows()}
</div> </div>
</div> </div>
}
</div> </div>
) )
} }
@ -209,7 +241,7 @@ export class User extends Component<any, UserState> {
<h5>Moderates</h5> <h5>Moderates</h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
{this.state.moderates.map(community => {this.state.moderates.map(community =>
<li><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li> <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
)} )}
</ul> </ul>
</div> </div>
@ -227,7 +259,7 @@ export class User extends Component<any, UserState> {
<h5>Subscribed</h5> <h5>Subscribed</h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
{this.state.follows.map(community => {this.state.follows.map(community =>
<li><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li> <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
)} )}
</ul> </ul>
</div> </div>
@ -247,15 +279,23 @@ export class User extends Component<any, UserState> {
); );
} }
updateUrl() {
let viewStr = View[this.state.view].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push(`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`);
}
nextPage(i: User) { nextPage(i: User) {
i.state.page++; i.state.page++;
i.setState(i.state); i.setState(i.state);
i.updateUrl();
i.refetch(); i.refetch();
} }
prevPage(i: User) { prevPage(i: User) {
i.state.page--; i.state.page--;
i.setState(i.state); i.setState(i.state);
i.updateUrl();
i.refetch(); i.refetch();
} }
@ -275,6 +315,7 @@ export class User extends Component<any, UserState> {
i.state.sort = Number(event.target.value); i.state.sort = Number(event.target.value);
i.state.page = 1; i.state.page = 1;
i.setState(i.state); i.setState(i.state);
i.updateUrl();
i.refetch(); i.refetch();
} }
@ -282,6 +323,7 @@ export class User extends Component<any, UserState> {
i.state.view = Number(event.target.value); i.state.view = Number(event.target.value);
i.state.page = 1; i.state.page = 1;
i.setState(i.state); i.setState(i.state);
i.updateUrl();
i.refetch(); i.refetch();
} }
@ -298,6 +340,7 @@ export class User extends Component<any, UserState> {
this.state.follows = res.follows; this.state.follows = res.follows;
this.state.moderates = res.moderates; this.state.moderates = res.moderates;
this.state.posts = res.posts; this.state.posts = res.posts;
this.state.loading = false;
document.title = `/u/${this.state.user.name} - Lemmy`; document.title = `/u/${this.state.user.name} - Lemmy`;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.EditComment) { } else if (op == UserOperation.EditComment) {

View file

@ -87,6 +87,10 @@ blockquote {
margin-top: 6px; margin-top: 6px;
} }
.mouse-icon {
margin-top: -4px;
}
.new-comments { .new-comments {
max-height: 100vh; max-height: 100vh;
overflow: hidden; overflow: hidden;

View file

@ -1,9 +1,8 @@
import { render, Component } from 'inferno'; 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 { Navbar } from './components/navbar';
import { Footer } from './components/footer'; import { Footer } from './components/footer';
import { Home } from './components/home';
import { Login } from './components/login'; import { Login } from './components/login';
import { CreatePost } from './components/create-post'; import { CreatePost } from './components/create-post';
import { CreateCommunity } from './components/create-community'; import { CreateCommunity } from './components/create-community';
@ -39,16 +38,18 @@ class Index extends Component<any, any> {
<Navbar /> <Navbar />
<div class="mt-3 p-0"> <div class="mt-3 p-0">
<Switch> <Switch>
<Route exact path="/all" component={Home} /> <Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} />
<Route exact path="/" component={Home} /> <Route exact path={`/`} component={Main} />
<Route path={`/login`} component={Login} /> <Route path={`/login`} component={Login} />
<Route path={`/create_post`} component={CreatePost} /> <Route path={`/create_post`} component={CreatePost} />
<Route path={`/create_community`} component={CreateCommunity} /> <Route path={`/create_community`} component={CreateCommunity} />
<Route path={`/communities`} component={Communities} /> <Route path={`/communities`} component={Communities} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} /> <Route path={`/post/:id/comment/:comment_id`} component={Post} />
<Route path={`/post/:id`} component={Post} /> <Route path={`/post/:id`} component={Post} />
<Route path={`/c/:name/sort/:sort/page/:page`} component={Community} />
<Route path={`/community/:id`} component={Community} /> <Route path={`/community/:id`} component={Community} />
<Route path={`/f/:name`} component={Community} /> <Route path={`/c/:name`} component={Community} />
<Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} />
<Route path={`/user/:id`} component={User} /> <Route path={`/user/:id`} component={User} />
<Route path={`/u/:username`} component={User} /> <Route path={`/u/:username`} component={User} />
<Route path={`/inbox`} component={Inbox} /> <Route path={`/inbox`} component={Inbox} />

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'; import * as markdown_it from 'markdown-it';
export let repoUrl = 'https://github.com/dessalines/lemmy'; export let repoUrl = 'https://github.com/dessalines/lemmy';
@ -67,3 +67,29 @@ export function isImage(url: string) {
} }
export let fetchLimit: number = 20; 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)];
}