diff --git a/server/migrations/2019-04-03-155205_create_community_view/up.sql b/server/migrations/2019-04-03-155205_create_community_view/up.sql index f2f4a7664..7c6087428 100644 --- a/server/migrations/2019-04-03-155205_create_community_view/up.sql +++ b/server/migrations/2019-04-03-155205_create_community_view/up.sql @@ -1,11 +1,31 @@ create view community_view as -select *, -(select name from user_ u where c.creator_id = u.id) as creator_name, -(select name from category ct where c.category_id = ct.id) as category_name, -(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers, -(select count(*) from post p where p.community_id = c.id) as number_of_posts, -(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments -from community c; +with all_community as +( + select *, + (select name from user_ u where c.creator_id = u.id) as creator_name, + (select name from category ct where c.category_id = ct.id) as category_name, + (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers, + (select count(*) from post p where p.community_id = c.id) as number_of_posts, + (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments + from community c +) + +select +ac.*, +u.id as user_id, +cf.id::boolean as subscribed +from user_ u +cross join all_community ac +left join community_follower cf on u.id = cf.user_id and ac.id = cf.community_id + +union all + +select +ac.*, +null as user_id, +null as subscribed +from all_community ac +; create view community_moderator_view as select *, diff --git a/server/src/actions/community_view.rs b/server/src/actions/community_view.rs index eafda161d..7eb07a162 100644 --- a/server/src/actions/community_view.rs +++ b/server/src/actions/community_view.rs @@ -18,6 +18,8 @@ table! { number_of_subscribers -> BigInt, number_of_posts -> BigInt, number_of_comments -> BigInt, + user_id -> Nullable, + subscribed -> Nullable, } } @@ -58,18 +60,43 @@ pub struct CommunityView { pub category_name: String, pub number_of_subscribers: i64, pub number_of_posts: i64, - pub number_of_comments: i64 + pub number_of_comments: i64, + pub user_id: Option, + pub subscribed: Option, } impl CommunityView { - pub fn read(conn: &PgConnection, from_community_id: i32) -> Result { + pub fn read(conn: &PgConnection, from_community_id: i32, from_user_id: Option) -> Result { use actions::community_view::community_view::dsl::*; - community_view.find(from_community_id).first::(conn) + + let mut query = community_view.into_boxed(); + + query = query.filter(id.eq(from_community_id)); + + // 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)); + } else { + query = query.filter(user_id.is_null()); + }; + + query.first::(conn) } - pub fn list_all(conn: &PgConnection) -> Result, Error> { + pub fn list_all(conn: &PgConnection, from_user_id: Option) -> Result, Error> { use actions::community_view::community_view::dsl::*; - community_view.load::(conn) + let mut query = community_view.into_boxed(); + + // 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)) + .order_by((subscribed.desc(), number_of_subscribers.desc())); + } else { + query = query.filter(user_id.is_null()) + .order_by(number_of_subscribers.desc()); + }; + + query.load::(conn) } } diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs index f53a9f0c6..c48c651e3 100644 --- a/server/src/actions/post_view.rs +++ b/server/src/actions/post_view.rs @@ -113,7 +113,7 @@ impl PostView { query = query.filter(user_id.eq(from_user_id)); } else { query = query.filter(user_id.is_null()); - } + }; query.first::(conn) } diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index a0d129354..fe7cd0e66 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 + Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity } #[derive(Serialize, Deserialize)] @@ -109,7 +109,9 @@ pub struct CommunityResponse { } #[derive(Serialize, Deserialize)] -pub struct ListCommunities; +pub struct ListCommunities { + auth: Option +} #[derive(Serialize, Deserialize)] pub struct ListCommunitiesResponse { @@ -174,7 +176,8 @@ pub struct GetPostsResponse { #[derive(Serialize, Deserialize)] pub struct GetCommunity { - id: i32 + id: i32, + auth: Option } #[derive(Serialize, Deserialize)] @@ -251,6 +254,13 @@ pub struct EditCommunity { auth: String } +#[derive(Serialize, Deserialize)] +pub struct FollowCommunity { + community_id: i32, + follow: bool, + auth: String +} + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive pub struct ChatServer { @@ -389,7 +399,7 @@ impl Handler for ChatServer { create_community.perform(self, msg.id) }, UserOperation::ListCommunities => { - let list_communities: ListCommunities = ListCommunities; + let list_communities: ListCommunities = serde_json::from_str(&data.to_string()).unwrap(); list_communities.perform(self, msg.id) }, UserOperation::ListCategories => { @@ -436,6 +446,10 @@ impl Handler for ChatServer { let edit_community: EditCommunity = serde_json::from_str(&data.to_string()).unwrap(); edit_community.perform(self, msg.id) }, + UserOperation::FollowCommunity => { + let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap(); + follow_community.perform(self, msg.id) + }, _ => { let e = ErrorMessage { op: "Unknown".to_string(), @@ -599,7 +613,7 @@ impl Perform for CreateCommunity { } }; - let community_view = CommunityView::read(&conn, inserted_community.id).unwrap(); + let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id)).unwrap(); serde_json::to_string( &CommunityResponse { @@ -620,7 +634,20 @@ impl Perform for ListCommunities { let conn = establish_connection(); - let communities: Vec = CommunityView::list_all(&conn).unwrap(); + let user_id: Option = match &self.auth { + Some(auth) => { + match Claims::decode(&auth) { + Ok(claims) => { + let user_id = claims.claims.id; + Some(user_id) + } + Err(_e) => None + } + } + None => None + }; + + let communities: Vec = CommunityView::list_all(&conn, user_id).unwrap(); // Return the jwt serde_json::to_string( @@ -767,7 +794,7 @@ impl Perform for GetPost { let comments = CommentView::list(&conn, self.id, user_id).unwrap(); - let community = CommunityView::read(&conn, post_view.community_id).unwrap(); + let community = CommunityView::read(&conn, post_view.community_id, user_id).unwrap(); let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id).unwrap(); @@ -794,7 +821,20 @@ impl Perform for GetCommunity { let conn = establish_connection(); - let community_view = match CommunityView::read(&conn, self.id) { + let user_id: Option = match &self.auth { + Some(auth) => { + match Claims::decode(&auth) { + Ok(claims) => { + let user_id = claims.claims.id; + Some(user_id) + } + Err(_e) => None + } + } + None => None + }; + + let community_view = match CommunityView::read(&conn, self.id, user_id) { Ok(community) => community, Err(_e) => { return self.error("Couldn't find Community"); @@ -917,7 +957,7 @@ impl Perform for EditComment { // Verify its the creator let orig_comment = Comment::read(&conn, self.edit_id).unwrap(); if user_id != orig_comment.creator_id { - return self.error("Incorrect creator."); + return self.error("Incorrect creator."); } let comment_form = CommentForm { @@ -1158,7 +1198,7 @@ impl Perform for EditPost { // Verify its the creator let orig_post = Post::read(&conn, self.edit_id).unwrap(); if user_id != orig_post.creator_id { - return self.error("Incorrect creator."); + return self.error("Incorrect creator."); } let post_form = PostForm { @@ -1227,7 +1267,7 @@ impl Perform for EditCommunity { let moderator_view = CommunityModeratorView::for_community(&conn, self.edit_id).unwrap(); let mod_ids: Vec = moderator_view.into_iter().map(|m| m.user_id).collect(); if !mod_ids.contains(&user_id) { - return self.error("Incorrect creator."); + return self.error("Incorrect creator."); }; let community_form = CommunityForm { @@ -1246,7 +1286,7 @@ impl Perform for EditCommunity { } }; - let community_view = CommunityView::read(&conn, self.edit_id).unwrap(); + let community_view = CommunityView::read(&conn, self.edit_id, Some(user_id)).unwrap(); // Do the subscriber stuff here // let mut community_sent = post_view.clone(); @@ -1273,6 +1313,61 @@ impl Perform for EditCommunity { community_out } } + + +impl Perform for FollowCommunity { + fn op_type(&self) -> UserOperation { + UserOperation::FollowCommunity + } + + 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 community_follower_form = CommunityFollowerForm { + community_id: self.community_id, + user_id: user_id + }; + + if self.follow { + + match CommunityFollower::follow(&conn, &community_follower_form) { + Ok(user) => user, + Err(_e) => { + return self.error("Community follower already exists."); + } + }; + } else { + match CommunityFollower::ignore(&conn, &community_follower_form) { + Ok(user) => user, + Err(_e) => { + return self.error("Community follower already exists."); + } + }; + } + + let community_view = CommunityView::read(&conn, self.community_id, Some(user_id)).unwrap(); + + serde_json::to_string( + &CommunityResponse { + op: self.op_type().to_string(), + community: community_view + } + ) + .unwrap() + } +} + + // impl Handler for ChatServer { // type Result = MessageResult; diff --git a/ui/src/components/communities.tsx b/ui/src/components/communities.tsx index 80953aaa3..e8158a365 100644 --- a/ui/src/components/communities.tsx +++ b/ui/src/components/communities.tsx @@ -2,7 +2,7 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; -import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, ListCommunitiesResponse } from '../interfaces'; +import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { msgOp, hotRank,mdToHtml } from '../utils'; @@ -29,6 +29,7 @@ export class Communities extends Component { () => console.log('complete') ); WebSocketService.Instance.listCommunities(); + } componentDidMount() { @@ -50,6 +51,7 @@ export class Communities extends Component { Subscribers Posts Comments + @@ -61,6 +63,12 @@ export class Communities extends Component { {community.number_of_subscribers} {community.number_of_posts} {community.number_of_comments} + + {community.subscribed + ? + : + } + )} @@ -70,8 +78,23 @@ export class Communities extends Component { ); } + handleUnsubscribe(communityId: number) { + let form: FollowCommunityForm = { + community_id: communityId, + follow: false + }; + WebSocketService.Instance.followCommunity(form); + } + handleSubscribe(communityId: number) { + let form: FollowCommunityForm = { + community_id: communityId, + follow: true + }; + WebSocketService.Instance.followCommunity(form); + } + parseMessage(msg: any) { console.log(msg); let op: UserOperation = msgOp(msg); @@ -83,6 +106,12 @@ export class Communities extends Component { this.state.communities = res.communities; this.state.communities.sort((a, b) => b.number_of_subscribers - a.number_of_subscribers); this.setState(this.state); + } else if (op == UserOperation.FollowCommunity) { + let res: CommunityResponse = msg; + let found = this.state.communities.find(c => c.id == res.community.id); + found.subscribed = res.community.subscribed; + found.number_of_subscribers = res.community.number_of_subscribers; + this.setState(this.state); } } } diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index d5f75b45e..726055ba7 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -147,6 +147,11 @@ export class Community extends Component { let res: CommunityResponse = msg; this.state.community = res.community; this.setState(this.state); + } else if (op == UserOperation.FollowCommunity) { + let res: CommunityResponse = msg; + this.state.community.subscribed = res.community.subscribed; + this.state.community.number_of_subscribers = res.community.number_of_subscribers; + this.setState(this.state); } } } diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 0075e9dff..2a870c4dc 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -229,6 +229,11 @@ export class Post extends Component { this.state.post.community_id = res.community.id; this.state.post.community_name = res.community.name; this.setState(this.state); + } else if (op == UserOperation.FollowCommunity) { + let res: CommunityResponse = msg; + this.state.community.subscribed = res.community.subscribed; + this.state.community.number_of_subscribers = res.community.number_of_subscribers; + this.setState(this.state); } } diff --git a/ui/src/components/sidebar.tsx b/ui/src/components/sidebar.tsx index 3f11749c8..ad3eeccc3 100644 --- a/ui/src/components/sidebar.tsx +++ b/ui/src/components/sidebar.tsx @@ -1,6 +1,6 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; -import { Community, CommunityUser } from '../interfaces'; +import { Community, CommunityUser, FollowCommunityForm } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { mdToHtml } from '../utils'; import { CommunityForm } from './community-form'; @@ -61,7 +61,12 @@ export class Sidebar extends Component {
  • {community.number_of_posts} Posts
  • {community.number_of_comments} Comments
  • -
    +
    + {community.subscribed + ? + : + } +
    {community.description &&

    @@ -96,6 +101,22 @@ export class Sidebar extends Component { handleDeleteClick(i: Sidebar, event) { } + handleUnsubscribe(communityId: number) { + let form: FollowCommunityForm = { + community_id: communityId, + follow: false + }; + WebSocketService.Instance.followCommunity(form); + } + + handleSubscribe(communityId: number) { + let form: FollowCommunityForm = { + community_id: communityId, + follow: true + }; + WebSocketService.Instance.followCommunity(form); + } + private get amCreator(): boolean { return UserService.Instance.loggedIn && this.props.community.creator_id == UserService.Instance.user.id; } diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 0505a3989..f8007cbaf 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 + Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity } export interface User { @@ -18,6 +18,8 @@ export interface CommunityUser { } export interface Community { + user_id?: number; + subscribed?: boolean; id: number; name: string; title: string; @@ -171,6 +173,12 @@ export interface Category { name: string; } +export interface FollowCommunityForm { + community_id: number; + follow: boolean; + auth?: string; +} + export interface LoginForm { username_or_email: string; password: string; diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index d89d0128c..c8cc95570 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -1,5 +1,5 @@ import { wsUri } from '../env'; -import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm } from '../interfaces'; +import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm } from '../interfaces'; import { webSocket } from 'rxjs/webSocket'; import { Subject } from 'rxjs'; import { retryWhen, delay, take } from 'rxjs/operators'; @@ -42,8 +42,14 @@ export class WebSocketService { this.subject.next(this.wsSendWrapper(UserOperation.EditCommunity, communityForm)); } + public followCommunity(followCommunityForm: FollowCommunityForm) { + this.setAuth(followCommunityForm); + this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm)); + } + public listCommunities() { - this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, undefined)); + let data = {auth: UserService.Instance.auth }; + this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, data)); } public listCategories() { @@ -61,7 +67,8 @@ export class WebSocketService { } public getCommunity(communityId: number) { - this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, {id: communityId})); + let data = {id: communityId, auth: UserService.Instance.auth }; + this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data)); } public createComment(commentForm: CommentForm) {