Adding a front page / fetching subscribed forums.
- Adding subscribed to post view. Fixes #25
This commit is contained in:
parent
4b6b456446
commit
1c95f4efcd
14 changed files with 362 additions and 82 deletions
2
server/Cargo.lock
generated
2
server/Cargo.lock
generated
|
@ -1,3 +1,5 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "activitypub"
|
name = "activitypub"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
|
@ -14,7 +14,7 @@ with all_post as
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
p.*,
|
p.*,
|
||||||
(select name from user_ where p.creator_id = user_.id) creator_name,
|
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||||
(select name from community where p.community_id = community.id) as community_name,
|
(select name from community where p.community_id = community.id) as community_name,
|
||||||
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||||
coalesce(sum(pl.score), 0) as score,
|
coalesce(sum(pl.score), 0) as score,
|
||||||
|
@ -29,7 +29,8 @@ with all_post as
|
||||||
select
|
select
|
||||||
ap.*,
|
ap.*,
|
||||||
u.id as user_id,
|
u.id as user_id,
|
||||||
coalesce(pl.score, 0) as my_vote
|
coalesce(pl.score, 0) as my_vote,
|
||||||
|
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed
|
||||||
from user_ u
|
from user_ u
|
||||||
cross join all_post ap
|
cross join all_post ap
|
||||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||||
|
@ -39,7 +40,8 @@ union all
|
||||||
select
|
select
|
||||||
ap.*,
|
ap.*,
|
||||||
null as user_id,
|
null as user_id,
|
||||||
null as my_vote
|
null as my_vote,
|
||||||
|
null as subscribed
|
||||||
from all_post ap
|
from all_post ap
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
@ -124,3 +124,25 @@ impl CommunityModeratorView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
|
||||||
|
#[table_name="community_follower_view"]
|
||||||
|
pub struct CommunityFollowerView {
|
||||||
|
pub id: i32,
|
||||||
|
pub community_id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub user_name : String,
|
||||||
|
pub community_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommunityFollowerView {
|
||||||
|
pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
|
||||||
|
use actions::community_view::community_follower_view::dsl::*;
|
||||||
|
community_follower_view.filter(community_id.eq(from_community_id)).load::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
|
||||||
|
use actions::community_view::community_follower_view::dsl::*;
|
||||||
|
community_follower_view.filter(user_id.eq(from_user_id)).load::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ table! {
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
subscribed -> Nullable<Bool>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ pub struct PostView {
|
||||||
pub hot_rank: i32,
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
|
pub subscribed: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostView {
|
impl PostView {
|
||||||
|
@ -71,6 +73,13 @@ impl PostView {
|
||||||
query = query.filter(community_id.eq(from_community_id));
|
query = query.filter(community_id.eq(from_community_id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
match type_ {
|
||||||
|
ListingType::Subscribed => {
|
||||||
|
query = query.filter(subscribed.eq(true));
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
// The view lets you pass a null user_id, if you're not logged in
|
// The view lets you pass a null user_id, if you're not logged in
|
||||||
if let Some(from_user_id) = from_user_id {
|
if let Some(from_user_id) = from_user_id {
|
||||||
query = query.filter(user_id.eq(from_user_id));
|
query = query.filter(user_id.eq(from_user_id));
|
||||||
|
|
|
@ -22,7 +22,7 @@ use actions::community_view::*;
|
||||||
|
|
||||||
#[derive(EnumString,ToString,Debug)]
|
#[derive(EnumString,ToString,Debug)]
|
||||||
pub enum UserOperation {
|
pub enum UserOperation {
|
||||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity
|
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -261,6 +261,18 @@ pub struct FollowCommunity {
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetFollowedCommunities {
|
||||||
|
auth: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetFollowedCommunitiesResponse {
|
||||||
|
op: String,
|
||||||
|
communities: Vec<CommunityFollowerView>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
||||||
/// session. implementation is super primitive
|
/// session. implementation is super primitive
|
||||||
pub struct ChatServer {
|
pub struct ChatServer {
|
||||||
|
@ -450,6 +462,10 @@ impl Handler<StandardMessage> for ChatServer {
|
||||||
let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap();
|
let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap();
|
||||||
follow_community.perform(self, msg.id)
|
follow_community.perform(self, msg.id)
|
||||||
},
|
},
|
||||||
|
UserOperation::GetFollowedCommunities => {
|
||||||
|
let followed_communities: GetFollowedCommunities = serde_json::from_str(&data.to_string()).unwrap();
|
||||||
|
followed_communities.perform(self, msg.id)
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
let e = ErrorMessage {
|
let e = ErrorMessage {
|
||||||
op: "Unknown".to_string(),
|
op: "Unknown".to_string(),
|
||||||
|
@ -1081,8 +1097,6 @@ impl Perform for GetPosts {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
println!("{:?}", self.auth);
|
|
||||||
|
|
||||||
let user_id: Option<i32> = match &self.auth {
|
let user_id: Option<i32> = match &self.auth {
|
||||||
Some(auth) => {
|
Some(auth) => {
|
||||||
match Claims::decode(&auth) {
|
match Claims::decode(&auth) {
|
||||||
|
@ -1367,6 +1381,36 @@ impl Perform for FollowCommunity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Perform for GetFollowedCommunities {
|
||||||
|
fn op_type(&self) -> UserOperation {
|
||||||
|
UserOperation::GetFollowedCommunities
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
|
||||||
|
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&self.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => {
|
||||||
|
return self.error("Not logged in.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let communities: Vec<CommunityFollowerView> = CommunityFollowerView::for_user(&conn, user_id).unwrap();
|
||||||
|
|
||||||
|
// Return the jwt
|
||||||
|
serde_json::to_string(
|
||||||
|
&GetFollowedCommunitiesResponse {
|
||||||
|
op: self.op_type().to_string(),
|
||||||
|
communities: communities
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// impl Handler<Login> for ChatServer {
|
// impl Handler<Login> for ChatServer {
|
||||||
|
|
||||||
|
|
|
@ -42,8 +42,8 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<h4>Communities</h4>
|
<h4>Communities</h4>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="community_table" class="table table-sm table-hover" data-sortable>
|
<table id="community_table" class="table table-sm table-hover">
|
||||||
<thead>
|
<thead class="pointer">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
|
|
|
@ -5,15 +5,14 @@ import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
|
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListings } from './post-listings';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { msgOp, mdToHtml } from '../utils';
|
import { msgOp, mdToHtml } from '../utils';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
community: CommunityI;
|
community: CommunityI;
|
||||||
|
communityId: number;
|
||||||
moderators: Array<CommunityUser>;
|
moderators: Array<CommunityUser>;
|
||||||
posts: Array<Post>;
|
|
||||||
sortType: ListingSortType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Community extends Component<any, State> {
|
export class Community extends Component<any, State> {
|
||||||
|
@ -34,8 +33,7 @@ export class Community extends Component<any, State> {
|
||||||
published: null
|
published: null
|
||||||
},
|
},
|
||||||
moderators: [],
|
moderators: [],
|
||||||
posts: [],
|
communityId: Number(this.props.match.params.id)
|
||||||
sortType: ListingSortType.Hot,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
|
@ -51,16 +49,7 @@ export class Community extends Component<any, State> {
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
let communityId = Number(this.props.match.params.id);
|
WebSocketService.Instance.getCommunity(this.state.communityId);
|
||||||
WebSocketService.Instance.getCommunity(communityId);
|
|
||||||
|
|
||||||
let getPostsForm: GetPostsForm = {
|
|
||||||
community_id: communityId,
|
|
||||||
limit: 10,
|
|
||||||
sort: ListingSortType[ListingSortType.Hot],
|
|
||||||
type_: ListingType[ListingType.Community]
|
|
||||||
}
|
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -73,12 +62,7 @@ export class Community extends Component<any, State> {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-9">
|
<div class="col-12 col-lg-9">
|
||||||
<h4>/f/{this.state.community.name}</h4>
|
<h4>/f/{this.state.community.name}</h4>
|
||||||
<div>{this.selects()}</div>
|
<PostListings communityId={this.state.communityId} />
|
||||||
{this.state.posts.length > 0
|
|
||||||
? this.state.posts.map(post =>
|
|
||||||
<PostListing post={post} />)
|
|
||||||
: <div>no listings</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-3">
|
<div class="col-12 col-lg-3">
|
||||||
<Sidebar community={this.state.community} moderators={this.state.moderators} />
|
<Sidebar community={this.state.community} moderators={this.state.moderators} />
|
||||||
|
@ -88,37 +72,6 @@ export class Community extends Component<any, State> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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={ListingSortType.Hot}>Hot</option>
|
|
||||||
<option value={ListingSortType.New}>New</option>
|
|
||||||
<option disabled>──────────</option>
|
|
||||||
<option value={ListingSortType.TopDay}>Top Day</option>
|
|
||||||
<option value={ListingSortType.TopWeek}>Week</option>
|
|
||||||
<option value={ListingSortType.TopMonth}>Month</option>
|
|
||||||
<option value={ListingSortType.TopYear}>Year</option>
|
|
||||||
<option value={ListingSortType.TopAll}>All</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSortChange(i: Community, event) {
|
|
||||||
i.state.sortType = Number(event.target.value);
|
|
||||||
i.setState(i.state);
|
|
||||||
|
|
||||||
let getPostsForm: GetPostsForm = {
|
|
||||||
community_id: i.state.community.id,
|
|
||||||
limit: 10,
|
|
||||||
sort: ListingSortType[i.state.sortType],
|
|
||||||
type_: ListingType[ListingType.Community]
|
|
||||||
}
|
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
|
@ -131,18 +84,6 @@ 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.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.GetPosts) {
|
|
||||||
let res: GetPostsResponse = msg;
|
|
||||||
this.state.posts = res.posts;
|
|
||||||
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);
|
|
||||||
} 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;
|
||||||
|
@ -156,4 +97,3 @@ export class Community extends Component<any, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
import { repoUrl } from '../utils';
|
import { repoUrl } from '../utils';
|
||||||
|
import { Main } from './main';
|
||||||
|
|
||||||
export class Home extends Component<any, any> {
|
export class Home extends Component<any, any> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<Main />
|
||||||
hola this is me.
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
85
ui/src/components/main.tsx
Normal file
85
ui/src/components/main.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Link } from 'inferno-router';
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser, GetFollowedCommunitiesResponse } from '../interfaces';
|
||||||
|
import { WebSocketService, UserService } from '../services';
|
||||||
|
import { MomentTime } from './moment-time';
|
||||||
|
import { PostListings } from './post-listings';
|
||||||
|
import { Sidebar } from './sidebar';
|
||||||
|
import { msgOp, mdToHtml } from '../utils';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
subscribedCommunities: Array<CommunityUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Main extends Component<any, State> {
|
||||||
|
|
||||||
|
private subscription: Subscription;
|
||||||
|
private emptyState: State = {
|
||||||
|
subscribedCommunities: []
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(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')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (UserService.Instance.loggedIn) {
|
||||||
|
WebSocketService.Instance.getFollowedCommunities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-9">
|
||||||
|
<PostListings />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-3">
|
||||||
|
<h4>A Landing message</h4>
|
||||||
|
{UserService.Instance.loggedIn &&
|
||||||
|
<div>
|
||||||
|
<hr />
|
||||||
|
<h4>Subscribed forums</h4>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
{this.state.subscribedCommunities.map(community =>
|
||||||
|
<li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
parseMessage(msg: any) {
|
||||||
|
console.log(msg);
|
||||||
|
let op: UserOperation = msgOp(msg);
|
||||||
|
if (msg.error) {
|
||||||
|
alert(msg.error);
|
||||||
|
return;
|
||||||
|
} else if (op == UserOperation.GetFollowedCommunities) {
|
||||||
|
let res: GetFollowedCommunitiesResponse = msg;
|
||||||
|
this.state.subscribedCommunities = res.communities;
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -59,8 +59,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
{post.url
|
{post.url
|
||||||
? <h4 className="mb-0">
|
? <div className="mb-0">
|
||||||
<a className="text-white" href={post.url}>{post.name}</a>
|
<h4 className="d-inline"><a className="text-white" href={post.url}>{post.name}</a></h4>
|
||||||
<small><a className="ml-2 text-muted font-italic" href={post.url}>{(new URL(post.url)).hostname}</a></small>
|
<small><a className="ml-2 text-muted font-italic" href={post.url}>{(new URL(post.url)).hostname}</a></small>
|
||||||
{ !this.state.iframeExpanded
|
{ !this.state.iframeExpanded
|
||||||
? <span class="pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
|
? <span class="pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
|
||||||
|
@ -72,7 +72,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</h4>
|
</div>
|
||||||
: <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link></h4>
|
: <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link></h4>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,7 +80,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<ul class="list-inline mb-0 text-muted small">
|
<ul class="list-inline mb-0 text-muted small">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>by </span>
|
<span>by </span>
|
||||||
<a href={post.creator_id.toString()}>{post.creator_name}</a>
|
<Link to={`/user/${post.creator_id}`}>{post.creator_name}</Link>
|
||||||
{this.props.showCommunity &&
|
{this.props.showCommunity &&
|
||||||
<span>
|
<span>
|
||||||
<span> to </span>
|
<span> to </span>
|
||||||
|
|
167
ui/src/components/post-listings.tsx
Normal file
167
ui/src/components/post-listings.tsx
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Link } from 'inferno-router';
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
|
||||||
|
import { WebSocketService, UserService } from '../services';
|
||||||
|
import { MomentTime } from './moment-time';
|
||||||
|
import { PostListing } from './post-listing';
|
||||||
|
import { Sidebar } from './sidebar';
|
||||||
|
import { msgOp, mdToHtml } from '../utils';
|
||||||
|
|
||||||
|
|
||||||
|
interface PostListingsProps {
|
||||||
|
communityId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostListingsState {
|
||||||
|
community: CommunityI;
|
||||||
|
moderators: Array<CommunityUser>;
|
||||||
|
posts: Array<Post>;
|
||||||
|
sortType: ListingSortType;
|
||||||
|
type_: ListingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PostListings extends Component<PostListingsProps, PostListingsState> {
|
||||||
|
|
||||||
|
private subscription: Subscription;
|
||||||
|
private emptyState: PostListingsState = {
|
||||||
|
community: {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
title: null,
|
||||||
|
category_id: null,
|
||||||
|
category_name: null,
|
||||||
|
creator_id: null,
|
||||||
|
creator_name: null,
|
||||||
|
number_of_subscribers: null,
|
||||||
|
number_of_posts: null,
|
||||||
|
number_of_comments: null,
|
||||||
|
published: null
|
||||||
|
},
|
||||||
|
moderators: [],
|
||||||
|
posts: [],
|
||||||
|
sortType: ListingSortType.Hot,
|
||||||
|
type_: this.props.communityId
|
||||||
|
? ListingType.Community
|
||||||
|
: UserService.Instance.loggedIn
|
||||||
|
? ListingType.Subscribed
|
||||||
|
: ListingType.All
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(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')
|
||||||
|
);
|
||||||
|
|
||||||
|
let getPostsForm: GetPostsForm = {
|
||||||
|
type_: ListingType[this.state.type_],
|
||||||
|
community_id: this.props.communityId,
|
||||||
|
limit: 10,
|
||||||
|
sort: ListingSortType[ListingSortType.Hot],
|
||||||
|
}
|
||||||
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{this.selects()}</div>
|
||||||
|
{this.state.posts.length > 0
|
||||||
|
? this.state.posts.map(post =>
|
||||||
|
<PostListing post={post} showCommunity={!this.props.communityId}/>)
|
||||||
|
: <div>No Listings</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={ListingSortType.Hot}>Hot</option>
|
||||||
|
<option value={ListingSortType.New}>New</option>
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
<option value={ListingSortType.TopDay}>Top Day</option>
|
||||||
|
<option value={ListingSortType.TopWeek}>Week</option>
|
||||||
|
<option value={ListingSortType.TopMonth}>Month</option>
|
||||||
|
<option value={ListingSortType.TopYear}>Year</option>
|
||||||
|
<option value={ListingSortType.TopAll}>All</option>
|
||||||
|
</select>
|
||||||
|
{!this.props.communityId &&
|
||||||
|
UserService.Instance.loggedIn &&
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSortChange(i: PostListings, event) {
|
||||||
|
i.state.sortType = Number(event.target.value);
|
||||||
|
i.setState(i.state);
|
||||||
|
|
||||||
|
let getPostsForm: GetPostsForm = {
|
||||||
|
community_id: i.state.community.id,
|
||||||
|
limit: 10,
|
||||||
|
sort: ListingSortType[i.state.sortType],
|
||||||
|
type_: ListingType[ListingType.Community]
|
||||||
|
}
|
||||||
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTypeChange(i: PostListings, event) {
|
||||||
|
i.state.type_ = Number(event.target.value);
|
||||||
|
i.setState(i.state);
|
||||||
|
|
||||||
|
let getPostsForm: GetPostsForm = {
|
||||||
|
limit: 10,
|
||||||
|
sort: ListingSortType[i.state.sortType],
|
||||||
|
type_: ListingType[i.state.type_]
|
||||||
|
}
|
||||||
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css">
|
<link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
|
||||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,700,800" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,700,800" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/css/sortable-theme-minimal.min.css" />
|
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/css/sortable-theme-minimal.min.css" /> -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export enum UserOperation {
|
export enum UserOperation {
|
||||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity
|
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
|
@ -179,6 +179,11 @@ export interface FollowCommunityForm {
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetFollowedCommunitiesResponse {
|
||||||
|
op: string;
|
||||||
|
communities: Array<CommunityUser>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginForm {
|
export interface LoginForm {
|
||||||
username_or_email: string;
|
username_or_email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|
|
@ -52,6 +52,11 @@ export class WebSocketService {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, data));
|
this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFollowedCommunities() {
|
||||||
|
let data = {auth: UserService.Instance.auth };
|
||||||
|
this.subject.next(this.wsSendWrapper(UserOperation.GetFollowedCommunities, data));
|
||||||
|
}
|
||||||
|
|
||||||
public listCategories() {
|
public listCategories() {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.ListCategories, undefined));
|
this.subject.next(this.wsSendWrapper(UserOperation.ListCategories, undefined));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue