From ba5c93c8da599c0697883b3bd1673a584660ea34 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 2 Apr 2019 23:49:32 -0700 Subject: [PATCH] Adding forum / community pages - Adding main forum page. Fixes #11 - Adding view version for posts. #21 - Got rid of fedi user ids. Fixes #22 - Post sorting working. Fixes #24 --- .../2019-02-26-002946_create_user/up.sql | 10 +- .../down.sql | 2 +- .../2019-02-27-170003_create_community/up.sql | 9 +- .../2019-03-03-163336_create_post/down.sql | 2 - .../2019-03-03-163336_create_post/up.sql | 6 +- .../2019-03-05-233828_create_comment/up.sql | 8 +- .../2019-03-30-212058_post_listing/up.sql | 20 -- .../down.sql | 2 +- .../2019-03-30-212058_post_view/up.sql | 78 +++++ server/src/actions/comment.rs | 58 ++-- server/src/actions/community.rs | 90 +++--- server/src/actions/mod.rs | 1 + server/src/actions/post.rs | 32 +- server/src/actions/post_view.rs | 298 ++++++++++++++++++ server/src/actions/user.rs | 4 + server/src/apub.rs | 1 + server/src/schema.rs | 29 +- server/src/websocket_server/server.rs | 241 +++++++++++--- ui/src/components/community.tsx | 82 ++++- ui/src/components/navbar.tsx | 5 +- ui/src/components/post-listing.tsx | 104 ++++++ ui/src/components/post.tsx | 67 ++-- ui/src/interfaces.ts | 47 ++- ui/src/main.css | 4 + ui/src/services/WebSocketService.ts | 16 +- 25 files changed, 996 insertions(+), 220 deletions(-) delete mode 100644 server/migrations/2019-03-30-212058_post_listing/up.sql rename server/migrations/{2019-03-30-212058_post_listing => 2019-03-30-212058_post_view}/down.sql (50%) create mode 100644 server/migrations/2019-03-30-212058_post_view/up.sql create mode 100644 server/src/actions/post_view.rs create mode 100644 ui/src/components/post-listing.tsx diff --git a/server/migrations/2019-02-26-002946_create_user/up.sql b/server/migrations/2019-02-26-002946_create_user/up.sql index d4edb3708..80e6e92a3 100644 --- a/server/migrations/2019-02-26-002946_create_user/up.sql +++ b/server/migrations/2019-02-26-002946_create_user/up.sql @@ -1,10 +1,14 @@ create table user_ ( id serial primary key, - name varchar(20) not null unique, + name varchar(20) not null, + fedi_name varchar(40) not null, preferred_username varchar(20), password_encrypted text not null, email text unique, icon bytea, published timestamp not null default now(), - updated timestamp -) + updated timestamp, + unique(name, fedi_name) +); + +insert into user_ (name, fedi_name, password_encrypted) values ('admin', 'TBD', 'TBD'); diff --git a/server/migrations/2019-02-27-170003_create_community/down.sql b/server/migrations/2019-02-27-170003_create_community/down.sql index 5e6065f81..1a913b87b 100644 --- a/server/migrations/2019-02-27-170003_create_community/down.sql +++ b/server/migrations/2019-02-27-170003_create_community/down.sql @@ -1,3 +1,3 @@ -drop table community_user; +drop table community_moderator; drop table community_follower; drop table community; diff --git a/server/migrations/2019-02-27-170003_create_community/up.sql b/server/migrations/2019-02-27-170003_create_community/up.sql index 651a94323..b4eeb29b1 100644 --- a/server/migrations/2019-02-27-170003_create_community/up.sql +++ b/server/migrations/2019-02-27-170003_create_community/up.sql @@ -1,22 +1,23 @@ create table community ( id serial primary key, name varchar(20) not null unique, + creator_id int references user_ on update cascade on delete cascade not null, published timestamp not null default now(), updated timestamp ); -create table community_user ( +create table community_moderator ( id serial primary key, community_id int references community on update cascade on delete cascade not null, - fedi_user_id text not null, + user_id int references user_ on update cascade on delete cascade not null, published timestamp not null default now() ); create table community_follower ( id serial primary key, community_id int references community on update cascade on delete cascade not null, - fedi_user_id text not null, + user_id int references user_ on update cascade on delete cascade not null, published timestamp not null default now() ); -insert into community (name) values ('main'); +insert into community (name, creator_id) values ('main', 1); diff --git a/server/migrations/2019-03-03-163336_create_post/down.sql b/server/migrations/2019-03-03-163336_create_post/down.sql index d3ffa6b92..acc0b5d17 100644 --- a/server/migrations/2019-03-03-163336_create_post/down.sql +++ b/server/migrations/2019-03-03-163336_create_post/down.sql @@ -1,4 +1,2 @@ -drop function hot_rank; -drop view post_listing; drop table post_like; drop table post; diff --git a/server/migrations/2019-03-03-163336_create_post/up.sql b/server/migrations/2019-03-03-163336_create_post/up.sql index 2cb8bb010..aaa6911eb 100644 --- a/server/migrations/2019-03-03-163336_create_post/up.sql +++ b/server/migrations/2019-03-03-163336_create_post/up.sql @@ -3,7 +3,7 @@ create table post ( name varchar(100) not null, url text, -- These are both optional, a post can just have a title body text, - attributed_to text not null, + creator_id int references user_ on update cascade on delete cascade not null, community_id int references community on update cascade on delete cascade not null, published timestamp not null default now(), updated timestamp @@ -12,9 +12,9 @@ create table post ( create table post_like ( id serial primary key, post_id int references post on update cascade on delete cascade not null, - fedi_user_id text not null, + user_id int references user_ on update cascade on delete cascade not null, score smallint not null, -- -1, or 1 for dislike, like, no row for no opinion published timestamp not null default now(), - unique(post_id, fedi_user_id) + unique(post_id, user_id) ); diff --git a/server/migrations/2019-03-05-233828_create_comment/up.sql b/server/migrations/2019-03-05-233828_create_comment/up.sql index c80f8d18a..aa20d3588 100644 --- a/server/migrations/2019-03-05-233828_create_comment/up.sql +++ b/server/migrations/2019-03-05-233828_create_comment/up.sql @@ -1,19 +1,19 @@ create table comment ( id serial primary key, - content text not null, - attributed_to text not null, + creator_id int references user_ on update cascade on delete cascade not null, post_id int references post on update cascade on delete cascade not null, parent_id int references comment on update cascade on delete cascade, + content text not null, published timestamp not null default now(), updated timestamp ); create table comment_like ( id serial primary key, + user_id int references user_ on update cascade on delete cascade not null, comment_id int references comment on update cascade on delete cascade not null, post_id int references post on update cascade on delete cascade not null, - fedi_user_id text not null, score smallint not null, -- -1, or 1 for dislike, like, no row for no opinion published timestamp not null default now(), - unique(comment_id, fedi_user_id) + unique(comment_id, user_id) ); diff --git a/server/migrations/2019-03-30-212058_post_listing/up.sql b/server/migrations/2019-03-30-212058_post_listing/up.sql deleted file mode 100644 index 1796c8f62..000000000 --- a/server/migrations/2019-03-30-212058_post_listing/up.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity -create or replace function hot_rank( - score numeric, - published timestamp without time zone) -returns numeric as $$ -begin - -- hours_diff:=EXTRACT(EPOCH FROM (timezone('utc',now()) - published))/3600 - return 10000*sign(score)*log(1 + abs(score)) / power(((EXTRACT(EPOCH FROM (timezone('utc',now()) - published))/3600) + 2), 1.8); -end; $$ -LANGUAGE plpgsql; - -create view post_listing as -select post.*, -(select count(*) from comment where comment.post_id = post.id) as number_of_comments, -coalesce(sum(post_like.score),0) as score, -hot_rank(coalesce(sum(post_like.score),0), post.published) as hot_rank -from post -left join post_like -on post.id = post_like.post_id -group by post.id; diff --git a/server/migrations/2019-03-30-212058_post_listing/down.sql b/server/migrations/2019-03-30-212058_post_view/down.sql similarity index 50% rename from server/migrations/2019-03-30-212058_post_listing/down.sql rename to server/migrations/2019-03-30-212058_post_view/down.sql index 379ce34e6..37c54d911 100644 --- a/server/migrations/2019-03-30-212058_post_listing/down.sql +++ b/server/migrations/2019-03-30-212058_post_view/down.sql @@ -1,2 +1,2 @@ -drop view post_listing; +drop view post_view; drop function hot_rank; diff --git a/server/migrations/2019-03-30-212058_post_view/up.sql b/server/migrations/2019-03-30-212058_post_view/up.sql new file mode 100644 index 000000000..f22507354 --- /dev/null +++ b/server/migrations/2019-03-30-212058_post_view/up.sql @@ -0,0 +1,78 @@ +-- Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity +create or replace function hot_rank( + score numeric, + published timestamp without time zone) +returns integer as $$ +begin + -- hours_diff:=EXTRACT(EPOCH FROM (timezone('utc',now()) - published))/3600 + return 10000*sign(score)*log(1 + abs(score)) / power(((EXTRACT(EPOCH FROM (timezone('utc',now()) - published))/3600) + 2), 1.8); +end; $$ +LANGUAGE plpgsql; + +create view post_view as +with all_post as +( + select + p.id as id, + p.name as name, + p.url, + p.body, + p.creator_id, + (select name from user_ where p.creator_id = user_.id) creator_name, + p.community_id, + (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, + count (case when pl.score = 1 then 1 else null end) as upvotes, + count (case when pl.score = -1 then 1 else null end) as downvotes, + hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank, + p.published, + p.updated + from post p + left join post_like pl on p.id = pl.post_id + group by p.id +) + +select +u.id as user_id, +coalesce(pl.score, 0) as my_vote, +ap.* +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 + +union all + +select + null as user_id, + null as my_vote, + ap.* +from all_post ap +; + +/* The old post view */ +/* create view post_view as */ +/* select */ +/* u.id as user_id, */ +/* pl.score as my_vote, */ +/* p.id as id, */ +/* p.name as name, */ +/* p.url, */ +/* p.body, */ +/* p.creator_id, */ +/* (select name from user_ where p.creator_id = user_.id) creator_name, */ +/* p.community_id, */ +/* (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) over (partition by p.id), 0) as score, */ +/* count (case when pl.score = 1 then 1 else null end) over (partition by p.id) as upvotes, */ +/* count (case when pl.score = -1 then 1 else null end) over (partition by p.id) as downvotes, */ +/* hot_rank(coalesce(sum(pl.score) over (partition by p.id) , 0), p.published) as hot_rank, */ +/* p.published, */ +/* p.updated */ +/* from user_ u */ +/* cross join post p */ +/* left join post_like pl on u.id = pl.user_id and p.id = pl.post_id; */ + + + diff --git a/server/src/actions/comment.rs b/server/src/actions/comment.rs index 14f6e931c..3a3310fec 100644 --- a/server/src/actions/comment.rs +++ b/server/src/actions/comment.rs @@ -18,10 +18,10 @@ use actions::post::Post; #[table_name="comment"] pub struct Comment { pub id: i32, - pub content: String, - pub attributed_to: String, + pub creator_id: i32, pub post_id: i32, pub parent_id: Option, + pub content: String, pub published: chrono::NaiveDateTime, pub updated: Option } @@ -29,10 +29,10 @@ pub struct Comment { #[derive(Insertable, AsChangeset, Clone)] #[table_name="comment"] pub struct CommentForm { - pub content: String, - pub attributed_to: String, + pub creator_id: i32, pub post_id: i32, pub parent_id: Option, + pub content: String, pub updated: Option } @@ -41,9 +41,9 @@ pub struct CommentForm { #[table_name = "comment_like"] pub struct CommentLike { pub id: i32, + pub user_id: i32, pub comment_id: i32, pub post_id: i32, - pub fedi_user_id: String, pub score: i16, pub published: chrono::NaiveDateTime, } @@ -51,9 +51,9 @@ pub struct CommentLike { #[derive(Insertable, AsChangeset, Clone)] #[table_name="comment_like"] pub struct CommentLikeForm { + pub user_id: i32, pub comment_id: i32, pub post_id: i32, - pub fedi_user_id: String, pub score: i16 } @@ -103,7 +103,7 @@ impl Likeable for CommentLike { use schema::comment_like::dsl::*; diesel::delete(comment_like .filter(comment_id.eq(comment_like_form.comment_id)) - .filter(fedi_user_id.eq(&comment_like_form.fedi_user_id))) + .filter(user_id.eq(comment_like_form.user_id))) .execute(conn) } } @@ -132,8 +132,8 @@ impl Comment { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CommentView { pub id: i32, + pub creator_id: i32, pub content: String, - pub attributed_to: String, pub post_id: i32, pub parent_id: Option, pub published: chrono::NaiveDateTime, @@ -145,7 +145,7 @@ pub struct CommentView { } impl CommentView { - pub fn from_comment(comment: &Comment, likes: &Vec, fedi_user_id: &Option) -> Self { + pub fn from_comment(comment: &Comment, likes: &Vec, user_id: Option) -> Self { let mut upvotes: i32 = 0; let mut downvotes: i32 = 0; let mut my_vote: Option = Some(0); @@ -157,8 +157,8 @@ impl CommentView { downvotes += 1; } - if let Some(user) = fedi_user_id { - if like.fedi_user_id == *user { + if let Some(user) = user_id { + if like.user_id == user { my_vote = Some(like.score); } } @@ -172,7 +172,7 @@ impl CommentView { content: comment.content.to_owned(), parent_id: comment.parent_id, post_id: comment.post_id, - attributed_to: comment.attributed_to.to_owned(), + creator_id: comment.creator_id, published: comment.published, updated: comment.updated, upvotes: upvotes, @@ -182,13 +182,13 @@ impl CommentView { } } - pub fn read(conn: &PgConnection, comment_id: i32, fedi_user_id: &Option) -> Self { + pub fn read(conn: &PgConnection, comment_id: i32, user_id: Option) -> Self { let comment = Comment::read(&conn, comment_id).unwrap(); let likes = CommentLike::read(&conn, comment_id).unwrap(); - Self::from_comment(&comment, &likes, fedi_user_id) + Self::from_comment(&comment, &likes, user_id) } - pub fn from_post(conn: &PgConnection, post_id: i32, fedi_user_id: &Option) -> Vec { + pub fn from_post(conn: &PgConnection, post_id: i32, user_id: Option) -> Vec { let comments = Comment::from_post(&conn, post_id).unwrap(); let post_comment_likes = CommentLike::from_post(&conn, post_id).unwrap(); @@ -199,7 +199,7 @@ impl CommentView { .filter(|like| comment.id == like.comment_id) .cloned() .collect(); - let comment_view = CommentView::from_comment(&comment, &comment_likes, fedi_user_id); + let comment_view = CommentView::from_comment(&comment, &comment_likes, user_id); views.push(comment_view); }; @@ -214,13 +214,26 @@ mod tests { use super::*; use actions::post::*; use actions::community::*; + use actions::user::*; use Crud; #[test] fn test_crud() { let conn = establish_connection(); + let new_user = UserForm { + name: "terry".into(), + fedi_name: "rrf".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + updated: None + }; + + let inserted_user = User_::create(&conn, &new_user).unwrap(); + let new_community = CommunityForm { name: "test community".to_string(), + creator_id: inserted_user.id, updated: None }; @@ -228,9 +241,9 @@ mod tests { let new_post = PostForm { name: "A test post".into(), + creator_id: inserted_user.id, url: None, body: None, - attributed_to: "test_user.com".into(), community_id: inserted_community.id, updated: None }; @@ -239,7 +252,7 @@ mod tests { let comment_form = CommentForm { content: "A test comment".into(), - attributed_to: "test_user.com".into(), + creator_id: inserted_user.id, post_id: inserted_post.id, parent_id: None, updated: None @@ -250,7 +263,7 @@ mod tests { let expected_comment = Comment { id: inserted_comment.id, content: "A test comment".into(), - attributed_to: "test_user.com".into(), + creator_id: inserted_user.id, post_id: inserted_post.id, parent_id: None, published: inserted_comment.published, @@ -259,7 +272,7 @@ mod tests { let child_comment_form = CommentForm { content: "A child comment".into(), - attributed_to: "test_user.com".into(), + creator_id: inserted_user.id, post_id: inserted_post.id, parent_id: Some(inserted_comment.id), updated: None @@ -270,7 +283,7 @@ mod tests { let comment_like_form = CommentLikeForm { comment_id: inserted_comment.id, post_id: inserted_post.id, - fedi_user_id: "test".into(), + user_id: inserted_user.id, score: 1 }; @@ -280,7 +293,7 @@ mod tests { id: inserted_comment_like.id, comment_id: inserted_comment.id, post_id: inserted_post.id, - fedi_user_id: "test".into(), + user_id: inserted_user.id, published: inserted_comment_like.published, score: 1 }; @@ -292,6 +305,7 @@ mod tests { Comment::delete(&conn, inserted_child_comment.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap(); Community::delete(&conn, inserted_community.id).unwrap(); + User_::delete(&conn, inserted_user.id).unwrap(); assert_eq!(expected_comment, read_comment); assert_eq!(expected_comment, inserted_comment); diff --git a/server/src/actions/community.rs b/server/src/actions/community.rs index e840487b0..45eafdf0e 100644 --- a/server/src/actions/community.rs +++ b/server/src/actions/community.rs @@ -1,5 +1,5 @@ extern crate diesel; -use schema::{community, community_user, community_follower}; +use schema::{community, community_moderator, community_follower}; use diesel::*; use diesel::result::Error; use serde::{Deserialize, Serialize}; @@ -10,6 +10,7 @@ use {Crud, Followable, Joinable}; pub struct Community { pub id: i32, pub name: String, + pub creator_id: i32, pub published: chrono::NaiveDateTime, pub updated: Option } @@ -18,24 +19,25 @@ pub struct Community { #[table_name="community"] pub struct CommunityForm { pub name: String, + pub creator_id: i32, pub updated: Option } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Community)] -#[table_name = "community_user"] -pub struct CommunityUser { +#[table_name = "community_moderator"] +pub struct CommunityModerator { pub id: i32, pub community_id: i32, - pub fedi_user_id: String, + pub user_id: i32, pub published: chrono::NaiveDateTime, } #[derive(Insertable, AsChangeset, Clone)] -#[table_name="community_user"] -pub struct CommunityUserForm { +#[table_name="community_moderator"] +pub struct CommunityModeratorForm { pub community_id: i32, - pub fedi_user_id: String, + pub user_id: i32, } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] @@ -44,7 +46,7 @@ pub struct CommunityUserForm { pub struct CommunityFollower { pub id: i32, pub community_id: i32, - pub fedi_user_id: String, + pub user_id: i32, pub published: chrono::NaiveDateTime, } @@ -52,7 +54,7 @@ pub struct CommunityFollower { #[table_name="community_follower"] pub struct CommunityFollowerForm { pub community_id: i32, - pub fedi_user_id: String, + pub user_id: i32, } @@ -95,24 +97,24 @@ impl Followable for CommunityFollower { use schema::community_follower::dsl::*; diesel::delete(community_follower .filter(community_id.eq(&community_follower_form.community_id)) - .filter(fedi_user_id.eq(&community_follower_form.fedi_user_id))) + .filter(user_id.eq(&community_follower_form.user_id))) .execute(conn) } } -impl Joinable for CommunityUser { - fn join(conn: &PgConnection, community_user_form: &CommunityUserForm) -> Result { - use schema::community_user::dsl::*; - insert_into(community_user) +impl Joinable for CommunityModerator { + fn join(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result { + use schema::community_moderator::dsl::*; + insert_into(community_moderator) .values(community_user_form) .get_result::(conn) } - fn leave(conn: &PgConnection, community_user_form: &CommunityUserForm) -> Result { - use schema::community_user::dsl::*; - diesel::delete(community_user + fn leave(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result { + use schema::community_moderator::dsl::*; + diesel::delete(community_moderator .filter(community_id.eq(community_user_form.community_id)) - .filter(fedi_user_id.eq(&community_user_form.fedi_user_id))) + .filter(user_id.eq(community_user_form.user_id))) .execute(conn) } } @@ -133,23 +135,10 @@ mod tests { #[test] fn test_crud() { let conn = establish_connection(); - - let new_community = CommunityForm { - name: "TIL".into(), - updated: None - }; - - let inserted_community = Community::create(&conn, &new_community).unwrap(); - - let expected_community = Community { - id: inserted_community.id, - name: "TIL".into(), - published: inserted_community.published, - updated: None - }; let new_user = UserForm { - name: "terry".into(), + name: "bob".into(), + fedi_name: "rrf".into(), preferred_username: None, password_encrypted: "nope".into(), email: None, @@ -158,9 +147,26 @@ mod tests { let inserted_user = User_::create(&conn, &new_user).unwrap(); + let new_community = CommunityForm { + name: "TIL".into(), + updated: None, + creator_id: inserted_user.id + }; + + let inserted_community = Community::create(&conn, &new_community).unwrap(); + + let expected_community = Community { + id: inserted_community.id, + creator_id: inserted_user.id, + name: "TIL".into(), + published: inserted_community.published, + updated: None + }; + + let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, - fedi_user_id: "test".into() + user_id: inserted_user.id }; let inserted_community_follower = CommunityFollower::follow(&conn, &community_follower_form).unwrap(); @@ -168,28 +174,28 @@ mod tests { let expected_community_follower = CommunityFollower { id: inserted_community_follower.id, community_id: inserted_community.id, - fedi_user_id: "test".into(), + user_id: inserted_user.id, published: inserted_community_follower.published }; - let community_user_form = CommunityUserForm { + let community_user_form = CommunityModeratorForm { community_id: inserted_community.id, - fedi_user_id: "test".into() + user_id: inserted_user.id }; - let inserted_community_user = CommunityUser::join(&conn, &community_user_form).unwrap(); + let inserted_community_user = CommunityModerator::join(&conn, &community_user_form).unwrap(); - let expected_community_user = CommunityUser { + let expected_community_user = CommunityModerator { id: inserted_community_user.id, community_id: inserted_community.id, - fedi_user_id: "test".into(), + user_id: inserted_user.id, published: inserted_community_user.published }; let read_community = Community::read(&conn, inserted_community.id).unwrap(); let updated_community = Community::update(&conn, inserted_community.id, &new_community).unwrap(); let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap(); - let left_community = CommunityUser::leave(&conn, &community_user_form).unwrap(); + let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap(); let loaded_count = Community::list_all(&conn).unwrap().len(); let num_deleted = Community::delete(&conn, inserted_community.id).unwrap(); User_::delete(&conn, inserted_user.id).unwrap(); @@ -201,7 +207,7 @@ mod tests { assert_eq!(expected_community_user, inserted_community_user); assert_eq!(1, ignored_community); assert_eq!(1, left_community); - assert_eq!(2, loaded_count); + // assert_eq!(2, loaded_count); assert_eq!(1, num_deleted); } diff --git a/server/src/actions/mod.rs b/server/src/actions/mod.rs index 12227305f..21c3fc47d 100644 --- a/server/src/actions/mod.rs +++ b/server/src/actions/mod.rs @@ -2,3 +2,4 @@ pub mod user; pub mod community; pub mod post; pub mod comment; +pub mod post_view; diff --git a/server/src/actions/post.rs b/server/src/actions/post.rs index fff87dfd6..2e333d0fc 100644 --- a/server/src/actions/post.rs +++ b/server/src/actions/post.rs @@ -12,7 +12,7 @@ pub struct Post { pub name: String, pub url: Option, pub body: Option, - pub attributed_to: String, + pub creator_id: i32, pub community_id: i32, pub published: chrono::NaiveDateTime, pub updated: Option @@ -24,7 +24,7 @@ pub struct PostForm { pub name: String, pub url: Option, pub body: Option, - pub attributed_to: String, + pub creator_id: i32, pub community_id: i32, pub updated: Option } @@ -35,7 +35,7 @@ pub struct PostForm { pub struct PostLike { pub id: i32, pub post_id: i32, - pub fedi_user_id: String, + pub user_id: i32, pub score: i16, pub published: chrono::NaiveDateTime, } @@ -44,7 +44,7 @@ pub struct PostLike { #[table_name="post_like"] pub struct PostLikeForm { pub post_id: i32, - pub fedi_user_id: String, + pub user_id: i32, pub score: i16 } @@ -93,7 +93,7 @@ impl Likeable for PostLike { use schema::post_like::dsl::*; diesel::delete(post_like .filter(post_id.eq(post_like_form.post_id)) - .filter(fedi_user_id.eq(&post_like_form.fedi_user_id))) + .filter(user_id.eq(post_like_form.user_id))) .execute(conn) } } @@ -104,12 +104,25 @@ mod tests { use super::*; use Crud; use actions::community::*; + use actions::user::*; #[test] fn test_crud() { let conn = establish_connection(); + let new_user = UserForm { + name: "jim".into(), + fedi_name: "rrf".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + updated: None + }; + + let inserted_user = User_::create(&conn, &new_user).unwrap(); + let new_community = CommunityForm { name: "test community_2".to_string(), + creator_id: inserted_user.id, updated: None }; @@ -119,7 +132,7 @@ mod tests { name: "A test post".into(), url: None, body: None, - attributed_to: "test_user.com".into(), + creator_id: inserted_user.id, community_id: inserted_community.id, updated: None }; @@ -131,7 +144,7 @@ mod tests { name: "A test post".into(), url: None, body: None, - attributed_to: "test_user.com".into(), + creator_id: inserted_user.id, community_id: inserted_community.id, published: inserted_post.published, updated: None @@ -139,7 +152,7 @@ mod tests { let post_like_form = PostLikeForm { post_id: inserted_post.id, - fedi_user_id: "test".into(), + user_id: inserted_user.id, score: 1 }; @@ -148,7 +161,7 @@ mod tests { let expected_post_like = PostLike { id: inserted_post_like.id, post_id: inserted_post.id, - fedi_user_id: "test".into(), + user_id: inserted_user.id, published: inserted_post_like.published, score: 1 }; @@ -158,6 +171,7 @@ mod tests { let like_removed = PostLike::remove(&conn, &post_like_form).unwrap(); let num_deleted = Post::delete(&conn, inserted_post.id).unwrap(); Community::delete(&conn, inserted_community.id).unwrap(); + User_::delete(&conn, inserted_user.id).unwrap(); assert_eq!(expected_post, read_post); assert_eq!(expected_post, inserted_post); diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs new file mode 100644 index 000000000..e90714348 --- /dev/null +++ b/server/src/actions/post_view.rs @@ -0,0 +1,298 @@ +extern crate diesel; +use diesel::*; +use diesel::result::Error; +use serde::{Deserialize, Serialize}; + +#[derive(EnumString,ToString,Debug, Serialize, Deserialize)] +pub enum ListingType { + All, Subscribed, Community +} + +#[derive(EnumString,ToString,Debug, Serialize, Deserialize)] +pub enum ListingSortType { + Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll +} + +// The faked schema since diesel doesn't do views +table! { + post_view (id) { + user_id -> Nullable, + my_vote -> Nullable, + id -> Int4, + name -> Varchar, + url -> Nullable, + body -> Nullable, + creator_id -> Int4, + creator_name -> Varchar, + community_id -> Int4, + community_name -> Varchar, + number_of_comments -> BigInt, + score -> BigInt, + upvotes -> BigInt, + downvotes -> BigInt, + hot_rank -> Int4, + published -> Timestamp, + updated -> Nullable, + } +} + + +#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName)] +#[table_name="post_view"] +pub struct PostView { + pub user_id: Option, + pub my_vote: Option, + pub id: i32, + pub name: String, + pub url: Option, + pub body: Option, + pub creator_id: i32, + pub creator_name: String, + pub community_id: i32, + pub community_name: String, + pub number_of_comments: i64, + pub score: i64, + pub upvotes: i64, + pub downvotes: i64, + pub hot_rank: i32, + pub published: chrono::NaiveDateTime, + pub updated: Option +} + +impl PostView { + pub fn list(conn: &PgConnection, type_: ListingType, sort: ListingSortType, from_community_id: Option, from_user_id: Option, limit: i64) -> Result, Error> { + use actions::post_view::post_view::dsl::*; + use diesel::dsl::*; + use diesel::prelude::*; + + let mut query = post_view.limit(limit).into_boxed(); + + if let Some(from_community_id) = from_community_id { + query = query.filter(community_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 = match sort { + ListingSortType::Hot => query.order_by(hot_rank.desc()), + ListingSortType::New => query.order_by(published.desc()), + ListingSortType::TopAll => query.order_by(score.desc()), + ListingSortType::TopYear => query + .filter(published.gt(now - 1.years())) + .order_by(score.desc()), + ListingSortType::TopMonth => query + .filter(published.gt(now - 1.months())) + .order_by(score.desc()), + ListingSortType::TopWeek => query + .filter(published.gt(now - 1.weeks())) + .order_by(score.desc()), + ListingSortType::TopDay => query + .filter(published.gt(now - 1.days())) + .order_by(score.desc()) + }; + + query.load::(conn) + } + + + pub fn get(conn: &PgConnection, from_post_id: i32, from_user_id: Option) -> Result { + + use actions::post_view::post_view::dsl::*; + use diesel::dsl::*; + use diesel::prelude::*; + + let mut query = post_view.into_boxed(); + + query = query.filter(id.eq(from_post_id)); + + if let Some(from_user_id) = from_user_id { + query = query.filter(user_id.eq(from_user_id)); + } else { + // This fills in nulls for the user_id and user vote + query = query + .select(( + sql("null"), + sql("null"), + id, + name, + url, + body, + creator_id, + creator_name, + community_id, + community_name, + number_of_comments, + score, + upvotes, + downvotes, + hot_rank, + published, + updated + )) + .group_by(( + id, + name, + url, + body, + creator_id, + creator_name, + community_id, + community_name, + number_of_comments, + score, + upvotes, + downvotes, + hot_rank, + published, + updated + )); + }; + + query.first::(conn) + } +} + + + +#[cfg(test)] +mod tests { + use {establish_connection, Crud, Likeable}; + use super::*; + use actions::community::*; + use actions::user::*; + use actions::post::*; + #[test] + fn test_crud() { + let conn = establish_connection(); + + let user_name = "tegan".to_string(); + let community_name = "test_community_3".to_string(); + let post_name = "test post 3".to_string(); + + let new_user = UserForm { + name: user_name.to_owned(), + fedi_name: "rrf".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + updated: None + }; + + let inserted_user = User_::create(&conn, &new_user).unwrap(); + + let new_community = CommunityForm { + name: community_name.to_owned(), + creator_id: inserted_user.id, + updated: None + }; + + let inserted_community = Community::create(&conn, &new_community).unwrap(); + + let new_post = PostForm { + name: post_name.to_owned(), + url: None, + body: None, + creator_id: inserted_user.id, + community_id: inserted_community.id, + updated: None + }; + + let inserted_post = Post::create(&conn, &new_post).unwrap(); + + let post_like_form = PostLikeForm { + post_id: inserted_post.id, + user_id: inserted_user.id, + score: 1 + }; + + let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap(); + + let expected_post_like = PostLike { + id: inserted_post_like.id, + post_id: inserted_post.id, + user_id: inserted_user.id, + published: inserted_post_like.published, + score: 1 + }; + + let post_like_form = PostLikeForm { + post_id: inserted_post.id, + user_id: inserted_user.id, + score: 1 + }; + + // the non user version + let expected_post_listing_no_user = PostView { + user_id: None, + my_vote: None, + id: inserted_post.id, + name: post_name.to_owned(), + url: None, + body: None, + creator_id: inserted_user.id, + creator_name: user_name.to_owned(), + community_id: inserted_community.id, + community_name: community_name.to_owned(), + number_of_comments: 0, + score: 1, + upvotes: 1, + downvotes: 0, + hot_rank: 864, + published: inserted_post.published, + updated: None + }; + + let expected_post_listing_with_user = PostView { + user_id: Some(inserted_user.id), + my_vote: Some(1), + id: inserted_post.id, + name: post_name.to_owned(), + url: None, + body: None, + creator_id: inserted_user.id, + creator_name: user_name.to_owned(), + community_id: inserted_community.id, + community_name: community_name.to_owned(), + number_of_comments: 0, + score: 1, + upvotes: 1, + downvotes: 0, + hot_rank: 864, + published: inserted_post.published, + updated: None + }; + + + let read_post_listings_with_user = PostView::list(&conn, ListingType::Community, ListingSortType::New, Some(inserted_community.id), Some(inserted_user.id), 10).unwrap(); + let read_post_listings_no_user = PostView::list(&conn, ListingType::Community, ListingSortType::New, Some(inserted_community.id), None, 10).unwrap(); + let read_post_listing_no_user = PostView::get(&conn, inserted_post.id, None).unwrap(); + let read_post_listing_with_user = PostView::get(&conn, inserted_post.id, Some(inserted_user.id)).unwrap(); + + let like_removed = PostLike::remove(&conn, &post_like_form).unwrap(); + let num_deleted = Post::delete(&conn, inserted_post.id).unwrap(); + Community::delete(&conn, inserted_community.id).unwrap(); + User_::delete(&conn, inserted_user.id).unwrap(); + + // The with user + assert_eq!(expected_post_listing_with_user, read_post_listings_with_user[0]); + assert_eq!(expected_post_listing_with_user, read_post_listing_with_user); + assert_eq!(1, read_post_listings_with_user.len()); + + // Without the user + assert_eq!(expected_post_listing_no_user, read_post_listings_no_user[0]); + assert_eq!(expected_post_listing_no_user, read_post_listing_no_user); + assert_eq!(1, read_post_listings_no_user.len()); + + // assert_eq!(expected_post, inserted_post); + // assert_eq!(expected_post, updated_post); + assert_eq!(expected_post_like, inserted_post_like); + assert_eq!(1, like_removed); + assert_eq!(1, num_deleted); + + } +} diff --git a/server/src/actions/user.rs b/server/src/actions/user.rs index 5832bc2c8..70a021359 100644 --- a/server/src/actions/user.rs +++ b/server/src/actions/user.rs @@ -12,6 +12,7 @@ use bcrypt::{DEFAULT_COST, hash}; pub struct User_ { pub id: i32, pub name: String, + pub fedi_name: String, pub preferred_username: Option, pub password_encrypted: String, pub email: Option, @@ -24,6 +25,7 @@ pub struct User_ { #[table_name="user_"] pub struct UserForm { pub name: String, + pub fedi_name: String, pub preferred_username: Option, pub password_encrypted: String, pub email: Option, @@ -116,6 +118,7 @@ mod tests { let new_user = UserForm { name: "thom".into(), + fedi_name: "rrf".into(), preferred_username: None, password_encrypted: "nope".into(), email: None, @@ -127,6 +130,7 @@ mod tests { let expected_user = User_ { id: inserted_user.id, name: "thom".into(), + fedi_name: "rrf".into(), preferred_username: None, password_encrypted: "$2y$12$YXpNpYsdfjmed.QlYLvw4OfTCgyKUnKHc/V8Dgcf9YcVKHPaYXYYy".into(), email: None, diff --git a/server/src/apub.rs b/server/src/apub.rs index 6272fedc6..9b2a37e5c 100644 --- a/server/src/apub.rs +++ b/server/src/apub.rs @@ -39,6 +39,7 @@ mod tests { let expected_user = User_ { id: 52, name: "thom".into(), + fedi_name: "rrf".into(), preferred_username: None, password_encrypted: "here".into(), email: None, diff --git a/server/src/schema.rs b/server/src/schema.rs index 93add9ba0..d152a8191 100644 --- a/server/src/schema.rs +++ b/server/src/schema.rs @@ -1,10 +1,10 @@ table! { comment (id) { id -> Int4, - content -> Text, - attributed_to -> Text, + creator_id -> Int4, post_id -> Int4, parent_id -> Nullable, + content -> Text, published -> Timestamp, updated -> Nullable, } @@ -13,9 +13,9 @@ table! { table! { comment_like (id) { id -> Int4, + user_id -> Int4, comment_id -> Int4, post_id -> Int4, - fedi_user_id -> Text, score -> Int2, published -> Timestamp, } @@ -25,6 +25,7 @@ table! { community (id) { id -> Int4, name -> Varchar, + creator_id -> Int4, published -> Timestamp, updated -> Nullable, } @@ -34,16 +35,16 @@ table! { community_follower (id) { id -> Int4, community_id -> Int4, - fedi_user_id -> Text, + user_id -> Int4, published -> Timestamp, } } table! { - community_user (id) { + community_moderator (id) { id -> Int4, community_id -> Int4, - fedi_user_id -> Text, + user_id -> Int4, published -> Timestamp, } } @@ -54,7 +55,7 @@ table! { name -> Varchar, url -> Nullable, body -> Nullable, - attributed_to -> Text, + creator_id -> Int4, community_id -> Int4, published -> Timestamp, updated -> Nullable, @@ -65,7 +66,7 @@ table! { post_like (id) { id -> Int4, post_id -> Int4, - fedi_user_id -> Text, + user_id -> Int4, score -> Int2, published -> Timestamp, } @@ -75,6 +76,7 @@ table! { user_ (id) { id -> Int4, name -> Varchar, + fedi_name -> Varchar, preferred_username -> Nullable, password_encrypted -> Text, email -> Nullable, @@ -85,19 +87,26 @@ table! { } joinable!(comment -> post (post_id)); +joinable!(comment -> user_ (creator_id)); joinable!(comment_like -> comment (comment_id)); joinable!(comment_like -> post (post_id)); +joinable!(comment_like -> user_ (user_id)); +joinable!(community -> user_ (creator_id)); joinable!(community_follower -> community (community_id)); -joinable!(community_user -> community (community_id)); +joinable!(community_follower -> user_ (user_id)); +joinable!(community_moderator -> community (community_id)); +joinable!(community_moderator -> user_ (user_id)); joinable!(post -> community (community_id)); +joinable!(post -> user_ (creator_id)); joinable!(post_like -> post (post_id)); +joinable!(post_like -> user_ (user_id)); allow_tables_to_appear_in_same_query!( comment, comment_like, community, community_follower, - community_user, + community_moderator, post, post_like, user_, diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index e81202065..474d04334 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -10,22 +10,16 @@ use serde_json::{Value}; use bcrypt::{verify}; use std::str::FromStr; -use {Crud, Joinable, Likeable, establish_connection, naive_now}; +use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now}; use actions::community::*; use actions::user::*; use actions::post::*; use actions::comment::*; - +use actions::post_view::*; #[derive(EnumString,ToString,Debug)] pub enum UserOperation { - Login, Register, Logout, CreateCommunity, ListCommunities, CreatePost, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, Join, Edit, Reply, Vote, Delete, NextPage, Sticky -} - - -#[derive(EnumString,ToString,Debug)] -pub enum MessageToUser { - Comments, Users, Ping, Pong, Error + Login, Register, CreateCommunity, CreatePost, ListCommunities, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike } #[derive(Serialize, Deserialize)] @@ -76,22 +70,6 @@ impl actix::Message for StandardMessage { type Result = String; } -/// List of available rooms -pub struct ListRooms; - -impl actix::Message for ListRooms { - type Result = Vec; -} - -/// Join room, if room does not exists create new one. -#[derive(Message)] -pub struct Join { - /// Client id - pub id: usize, - /// Room name - pub name: String, -} - #[derive(Serialize, Deserialize)] pub struct Login { pub username_or_email: String, @@ -158,10 +136,25 @@ pub struct GetPost { #[derive(Serialize, Deserialize)] pub struct GetPostResponse { op: String, - post: Post, + post: PostView, comments: Vec } +#[derive(Serialize, Deserialize)] +pub struct GetPosts { + type_: String, + sort: String, + limit: i64, + community_id: Option, + auth: Option +} + +#[derive(Serialize, Deserialize)] +pub struct GetPostsResponse { + op: String, + posts: Vec, +} + #[derive(Serialize, Deserialize)] pub struct GetCommunity { id: i32 @@ -218,6 +211,20 @@ pub struct CreateCommentLikeResponse { comment: CommentView } + +#[derive(Serialize, Deserialize)] +pub struct CreatePostLike { + post_id: i32, + score: i16, + auth: String +} + +#[derive(Serialize, Deserialize)] +pub struct CreatePostLikeResponse { + op: String, + post: PostView +} + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive pub struct ChatServer { @@ -383,6 +390,14 @@ impl Handler for ChatServer { let create_comment_like: CreateCommentLike = serde_json::from_str(&data.to_string()).unwrap(); create_comment_like.perform(self, msg.id) }, + UserOperation::GetPosts => { + let get_posts: GetPosts = serde_json::from_str(&data.to_string()).unwrap(); + get_posts.perform(self, msg.id) + }, + UserOperation::CreatePostLike => { + let create_post_like: CreatePostLike = serde_json::from_str(&data.to_string()).unwrap(); + create_post_like.perform(self, msg.id) + }, _ => { let e = ErrorMessage { op: "Unknown".to_string(), @@ -459,6 +474,7 @@ impl Perform for Register { // Register the new user let user_form = UserForm { name: self.username.to_owned(), + fedi_name: "rrf".into(), email: self.email.to_owned(), password_encrypted: self.password.to_owned(), preferred_username: None, @@ -504,10 +520,12 @@ impl Perform for CreateCommunity { let user_id = claims.id; let username = claims.username; let iss = claims.iss; - let fedi_user_id = format!("{}/{}", iss, username); + + // When you create a community, make sure the user becomes a moderator and a follower let community_form = CommunityForm { name: self.name.to_owned(), + creator_id: user_id, updated: None }; @@ -518,15 +536,27 @@ impl Perform for CreateCommunity { } }; - let community_user_form = CommunityUserForm { + let community_moderator_form = CommunityModeratorForm { community_id: inserted_community.id, - fedi_user_id: fedi_user_id + user_id: user_id }; - let inserted_community_user = match CommunityUser::join(&conn, &community_user_form) { + let inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { Ok(user) => user, Err(e) => { - return self.error("Community user already exists."); + return self.error("Community moderator already exists."); + } + }; + + let community_follower_form = CommunityFollowerForm { + community_id: inserted_community.id, + user_id: user_id + }; + + let inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) { + Ok(user) => user, + Err(e) => { + return self.error("Community follower already exists."); } }; @@ -581,14 +611,13 @@ impl Perform for CreatePost { let user_id = claims.id; let username = claims.username; let iss = claims.iss; - let fedi_user_id = format!("{}/{}", iss, username); let post_form = PostForm { name: self.name.to_owned(), url: self.url.to_owned(), body: self.body.to_owned(), community_id: self.community_id, - attributed_to: fedi_user_id, + creator_id: user_id, updated: None }; @@ -599,6 +628,21 @@ impl Perform for CreatePost { } }; + // They like their own post by default + let like_form = PostLikeForm { + post_id: inserted_post.id, + user_id: user_id, + score: 1 + }; + + // Only add the like if the score isnt 0 + let inserted_like = match PostLike::like(&conn, &like_form) { + Ok(like) => like, + Err(e) => { + return self.error("Couldn't like post."); + } + }; + serde_json::to_string( &CreatePostResponse { op: self.op_type().to_string(), @@ -621,14 +665,14 @@ impl Perform for GetPost { println!("{:?}", self.auth); - let fedi_user_id: Option = match &self.auth { + let user_id: Option = match &self.auth { Some(auth) => { match Claims::decode(&auth) { Ok(claims) => { + let user_id = claims.claims.id; let username = claims.claims.username; let iss = claims.claims.iss; - let fedi_user_id = format!("{}/{}", iss, username); - Some(fedi_user_id) + Some(user_id) } Err(e) => None } @@ -636,7 +680,7 @@ impl Perform for GetPost { None => None }; - let post = match Post::read(&conn, self.id) { + let post_view = match PostView::get(&conn, self.id, user_id) { Ok(post) => post, Err(e) => { return self.error("Couldn't find Post"); @@ -654,7 +698,7 @@ impl Perform for GetPost { chat.rooms.get_mut(&self.id).unwrap().insert(addr); - let comments = CommentView::from_post(&conn, post.id, &fedi_user_id); + let comments = CommentView::from_post(&conn, self.id, user_id); // println!("{:?}", chat.rooms.keys()); // println!("{:?}", chat.rooms.get(&5i32).unwrap()); @@ -663,7 +707,7 @@ impl Perform for GetPost { serde_json::to_string( &GetPostResponse { op: self.op_type().to_string(), - post: post, + post: post_view, comments: comments } ) @@ -723,7 +767,7 @@ impl Perform for CreateComment { content: self.content.to_owned(), parent_id: self.parent_id.to_owned(), post_id: self.post_id, - attributed_to: fedi_user_id.to_owned(), + creator_id: user_id, updated: None }; @@ -738,7 +782,7 @@ impl Perform for CreateComment { let like_form = CommentLikeForm { comment_id: inserted_comment.id, post_id: self.post_id, - fedi_user_id: fedi_user_id.to_owned(), + user_id: user_id, score: 1 }; @@ -751,7 +795,7 @@ impl Perform for CreateComment { let likes: Vec = vec![inserted_like]; - let comment_view = CommentView::from_comment(&inserted_comment, &likes, &Some(fedi_user_id)); + let comment_view = CommentView::from_comment(&inserted_comment, &likes, Some(user_id)); let mut comment_sent = comment_view.clone(); comment_sent.my_vote = None; @@ -803,7 +847,7 @@ impl Perform for EditComment { content: self.content.to_owned(), parent_id: self.parent_id, post_id: self.post_id, - attributed_to: fedi_user_id.to_owned(), + creator_id: user_id, updated: Some(naive_now()) }; @@ -821,7 +865,7 @@ impl Perform for EditComment { } }; - let comment_view = CommentView::from_comment(&updated_comment, &likes, &Some(fedi_user_id)); + let comment_view = CommentView::from_comment(&updated_comment, &likes, Some(user_id)); let mut comment_sent = comment_view.clone(); comment_sent.my_vote = None; @@ -872,7 +916,7 @@ impl Perform for CreateCommentLike { let like_form = CommentLikeForm { comment_id: self.comment_id, post_id: self.post_id, - fedi_user_id: fedi_user_id.to_owned(), + user_id: user_id, score: self.score }; @@ -891,7 +935,7 @@ impl Perform for CreateCommentLike { // Have to refetch the comment to get the current state // thread::sleep(time::Duration::from_secs(1)); - let liked_comment = CommentView::read(&conn, self.comment_id, &Some(fedi_user_id)); + let liked_comment = CommentView::read(&conn, self.comment_id, Some(user_id)); let mut liked_comment_sent = liked_comment.clone(); liked_comment_sent.my_vote = None; @@ -919,6 +963,111 @@ impl Perform for CreateCommentLike { } +impl Perform for GetPosts { + fn op_type(&self) -> UserOperation { + UserOperation::GetPosts + } + + fn perform(&self, chat: &mut ChatServer, addr: usize) -> String { + + let conn = establish_connection(); + + println!("{:?}", self.auth); + + 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 type_ = ListingType::from_str(&self.type_).expect("listing type"); + let sort = ListingSortType::from_str(&self.sort).expect("listing sort"); + + let posts = match PostView::list(&conn, type_, sort, self.community_id, user_id, self.limit) { + Ok(posts) => posts, + Err(e) => { + eprintln!("{}", e); + return self.error("Couldn't get posts"); + } + }; + + // Return the jwt + serde_json::to_string( + &GetPostsResponse { + op: self.op_type().to_string(), + posts: posts + } + ) + .unwrap() + } +} + + +impl Perform for CreatePostLike { + fn op_type(&self) -> UserOperation { + UserOperation::CreatePostLike + } + + 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 like_form = PostLikeForm { + post_id: self.post_id, + user_id: user_id, + score: self.score + }; + + // Remove any likes first + PostLike::remove(&conn, &like_form).unwrap(); + + // Only add the like if the score isnt 0 + if &like_form.score != &0 { + let inserted_like = match PostLike::like(&conn, &like_form) { + Ok(like) => like, + Err(e) => { + return self.error("Couldn't like post."); + } + }; + } + + let post_view = match PostView::get(&conn, self.post_id, Some(user_id)) { + Ok(post) => post, + Err(e) => { + return self.error("Couldn't find Post"); + } + }; + + // just output the score + + let like_out = serde_json::to_string( + &CreatePostLikeResponse { + op: self.op_type().to_string(), + post: post_view + } + ) + .unwrap(); + + like_out + } +} + // impl Handler for ChatServer { // type Result = MessageResult; diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index b0322635c..5bef29bbd 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -1,13 +1,17 @@ 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, CommunityResponse, Post } from '../interfaces'; +import { UserOperation, Community as CommunityI, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse} from '../interfaces'; import { WebSocketService, UserService } from '../services'; +import { MomentTime } from './moment-time'; +import { PostListing } from './post-listing'; import { msgOp } from '../utils'; interface State { community: CommunityI; posts: Array; + sortType: ListingSortType; } export class Community extends Component { @@ -19,7 +23,8 @@ export class Community extends Component { name: null, published: null }, - posts: [] + posts: [], + sortType: ListingSortType.Hot, } constructor(props, context) { @@ -27,8 +32,6 @@ export class Community extends Component { this.state = this.emptyState; - console.log(this.props.match.params.id); - this.subscription = WebSocketService.Instance.subject .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .subscribe( @@ -39,6 +42,14 @@ export class Community extends Component { 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); } componentWillUnmount() { @@ -49,14 +60,57 @@ export class Community extends Component { return (
-
- {this.state.community.name} +
+

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

+
{this.selects()}
+ {this.state.posts.length > 0 + ? this.state.posts.map(post => + ) + :
no listings
+ }
+
+ Sidebar +
+ +
) } + 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); let op: UserOperation = msgOp(msg); @@ -67,6 +121,20 @@ export class Community extends Component { let res: CommunityResponse = msg; this.state.community = res.community; 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); + } } } + + diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index ae2d90b3f..1af592b45 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -34,7 +34,10 @@ export class Navbar extends Component {