From 38fa7969f95f49f20074ad8b982d8d92923071ad Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 5 Apr 2019 12:14:54 -0700 Subject: [PATCH] Adding a front page / fetching subscribed forums. - Adding subscribed to post view. Fixes #25 --- server/Cargo.lock | 2 + .../2019-03-30-212058_create_post_view/up.sql | 8 +- server/src/actions/community_view.rs | 22 +++ server/src/actions/post_view.rs | 9 + server/src/websocket_server/server.rs | 50 +++++- ui/src/components/communities.tsx | 4 +- ui/src/components/community.tsx | 70 +------- ui/src/components/home.tsx | 5 +- ui/src/components/main.tsx | 85 +++++++++ ui/src/components/post-listing.tsx | 8 +- ui/src/components/post-listings.tsx | 167 ++++++++++++++++++ ui/src/index.html | 2 +- ui/src/interfaces.ts | 7 +- ui/src/services/WebSocketService.ts | 5 + 14 files changed, 362 insertions(+), 82 deletions(-) create mode 100644 ui/src/components/main.tsx create mode 100644 ui/src/components/post-listings.tsx diff --git a/server/Cargo.lock b/server/Cargo.lock index 21594ccf0..0527eae7e 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. [[package]] name = "activitypub" version = "0.1.4" diff --git a/server/migrations/2019-03-30-212058_create_post_view/up.sql b/server/migrations/2019-03-30-212058_create_post_view/up.sql index c1848631e..79084a47e 100644 --- a/server/migrations/2019-03-30-212058_create_post_view/up.sql +++ b/server/migrations/2019-03-30-212058_create_post_view/up.sql @@ -14,7 +14,7 @@ with all_post as ( select 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 count(*) from comment where comment.post_id = p.id) as number_of_comments, coalesce(sum(pl.score), 0) as score, @@ -29,7 +29,8 @@ with all_post as select ap.*, 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 cross join all_post ap left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id @@ -39,7 +40,8 @@ union all select ap.*, null as user_id, -null as my_vote +null as my_vote, +null as subscribed from all_post ap ; diff --git a/server/src/actions/community_view.rs b/server/src/actions/community_view.rs index 7eb07a162..185484bf0 100644 --- a/server/src/actions/community_view.rs +++ b/server/src/actions/community_view.rs @@ -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, Error> { + use actions::community_view::community_follower_view::dsl::*; + community_follower_view.filter(community_id.eq(from_community_id)).load::(conn) + } + + pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result, Error> { + use actions::community_view::community_follower_view::dsl::*; + community_follower_view.filter(user_id.eq(from_user_id)).load::(conn) + } +} diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs index c48c651e3..b41a77aea 100644 --- a/server/src/actions/post_view.rs +++ b/server/src/actions/post_view.rs @@ -33,6 +33,7 @@ table! { hot_rank -> Int4, user_id -> Nullable, my_vote -> Nullable, + subscribed -> Nullable, } } @@ -57,6 +58,7 @@ pub struct PostView { pub hot_rank: i32, pub user_id: Option, pub my_vote: Option, + pub subscribed: Option, } impl PostView { @@ -71,6 +73,13 @@ impl PostView { 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 if let Some(from_user_id) = from_user_id { query = query.filter(user_id.eq(from_user_id)); diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index fe7cd0e66..6aae4f2fd 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -22,7 +22,7 @@ use actions::community_view::*; #[derive(EnumString,ToString,Debug)] 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)] @@ -261,6 +261,18 @@ pub struct FollowCommunity { auth: String } +#[derive(Serialize, Deserialize)] +pub struct GetFollowedCommunities { + auth: String +} + +#[derive(Serialize, Deserialize)] +pub struct GetFollowedCommunitiesResponse { + op: String, + communities: Vec +} + + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive pub struct ChatServer { @@ -450,6 +462,10 @@ impl Handler for ChatServer { let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap(); 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 { op: "Unknown".to_string(), @@ -1081,8 +1097,6 @@ impl Perform for GetPosts { let conn = establish_connection(); - println!("{:?}", self.auth); - let user_id: Option = match &self.auth { Some(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::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 for ChatServer { diff --git a/ui/src/components/communities.tsx b/ui/src/components/communities.tsx index e8158a365..c3cde1774 100644 --- a/ui/src/components/communities.tsx +++ b/ui/src/components/communities.tsx @@ -42,8 +42,8 @@ export class Communities extends Component {

Communities

- - +
+ diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 726055ba7..0d6d353df 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -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 { WebSocketService, UserService } from '../services'; import { MomentTime } from './moment-time'; -import { PostListing } from './post-listing'; +import { PostListings } from './post-listings'; import { Sidebar } from './sidebar'; import { msgOp, mdToHtml } from '../utils'; interface State { community: CommunityI; + communityId: number; moderators: Array; - posts: Array; - sortType: ListingSortType; } export class Community extends Component { @@ -34,8 +33,7 @@ export class Community extends Component { published: null }, moderators: [], - posts: [], - sortType: ListingSortType.Hot, + communityId: Number(this.props.match.params.id) } constructor(props, context) { @@ -51,16 +49,7 @@ export class Community extends Component { () => console.log('complete') ); - let communityId = Number(this.props.match.params.id); - WebSocketService.Instance.getCommunity(communityId); - - let getPostsForm: GetPostsForm = { - community_id: communityId, - limit: 10, - sort: ListingSortType[ListingSortType.Hot], - type_: ListingType[ListingType.Community] - } - WebSocketService.Instance.getPosts(getPostsForm); + WebSocketService.Instance.getCommunity(this.state.communityId); } componentWillUnmount() { @@ -73,12 +62,7 @@ export class Community extends Component {

/f/{this.state.community.name}

-
{this.selects()}
- {this.state.posts.length > 0 - ? this.state.posts.map(post => - ) - :
no listings
- } +
@@ -88,37 +72,6 @@ export class Community extends Component { ) } - selects() { - return ( -
- -
- ) - - } - - 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) { console.log(msg); @@ -131,18 +84,6 @@ export class Community extends Component { this.state.community = res.community; this.state.moderators = res.moderators; 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) { let res: CommunityResponse = msg; this.state.community = res.community; @@ -156,4 +97,3 @@ export class Community extends Component { } } - diff --git a/ui/src/components/home.tsx b/ui/src/components/home.tsx index 07cb94f5a..356534f75 100644 --- a/ui/src/components/home.tsx +++ b/ui/src/components/home.tsx @@ -1,13 +1,12 @@ import { Component } from 'inferno'; import { repoUrl } from '../utils'; +import { Main } from './main'; export class Home extends Component { render() { return ( -
- hola this is me. -
+
) } diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx new file mode 100644 index 000000000..b7b0a5622 --- /dev/null +++ b/ui/src/components/main.tsx @@ -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; +} + +export class Main extends Component { + + 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 ( +
+
+
+ +
+
+

A Landing message

+ {UserService.Instance.loggedIn && +
+
+

Subscribed forums

+
    + {this.state.subscribedCommunities.map(community => +
  • {community.community_name}
  • + )} +
+
+ } +
+
+
+ ) + } + + + 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); + } + } +} + diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 516baad32..c5052efb4 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -59,8 +59,8 @@ export class PostListing extends Component {
{post.url - ?

- {post.name} + ?
+

{post.name}

{(new URL(post.url)).hostname} { !this.state.iframeExpanded ? + @@ -72,7 +72,7 @@ export class PostListing extends Component {
} -

+
:

{post.name}

}
@@ -80,7 +80,7 @@ export class PostListing extends Component {
  • by - {post.creator_name} + {post.creator_name} {this.props.showCommunity && to diff --git a/ui/src/components/post-listings.tsx b/ui/src/components/post-listings.tsx new file mode 100644 index 000000000..fcc41cf5b --- /dev/null +++ b/ui/src/components/post-listings.tsx @@ -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; + posts: Array; + sortType: ListingSortType; + type_: ListingType; +} + +export class PostListings extends Component { + + 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 ( +
    +
    {this.selects()}
    + {this.state.posts.length > 0 + ? this.state.posts.map(post => + ) + :
    No Listings
    + } +
    + ) + } + + selects() { + return ( +
    + + {!this.props.communityId && + UserService.Instance.loggedIn && + + + } +
    + ) + + } + + 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); + } + } +} + + diff --git a/ui/src/index.html b/ui/src/index.html index 8b6fccf24..2b79ac1ce 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -10,7 +10,7 @@ - + diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index f8007cbaf..6d314c621 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -1,5 +1,5 @@ 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 { @@ -179,6 +179,11 @@ export interface FollowCommunityForm { auth?: string; } +export interface GetFollowedCommunitiesResponse { + op: string; + communities: Array; +} + export interface LoginForm { username_or_email: string; password: string; diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index c8cc95570..79f6750ab 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -52,6 +52,11 @@ export class WebSocketService { 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() { this.subject.next(this.wsSendWrapper(UserOperation.ListCategories, undefined)); }
Name Title