From 9afadfb9c4c5db1796848ec4af9756fe03d51ee3 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 19 Apr 2019 21:06:25 -0700 Subject: [PATCH 1/3] Saving replies, the actual fixes will be in the merge to dev. --- .../2019-02-27-170003_create_community/up.sql | 2 +- .../2019-03-03-163336_create_post/down.sql | 2 + .../2019-03-03-163336_create_post/up.sql | 19 +- .../2019-03-05-233828_create_comment/down.sql | 1 + .../2019-03-05-233828_create_comment/up.sql | 11 +- .../2019-03-30-212058_create_post_view/up.sql | 6 +- .../up.sql | 7 +- .../up.sql | 7 +- .../2019-04-11-144915_create_mod_views/up.sql | 4 +- server/src/actions/comment.rs | 110 +++++-- server/src/actions/comment_view.rs | 47 ++- server/src/actions/community.rs | 8 +- server/src/actions/community_view.rs | 6 +- server/src/actions/moderator.rs | 6 +- server/src/actions/post.rs | 163 ++++++++-- server/src/actions/post_view.rs | 49 ++- server/src/lib.rs | 10 + server/src/schema.rs | 45 ++- server/src/websocket_server/server.rs | 293 ++++++++++++++---- ui/src/components/comment-node.tsx | 231 ++++++++++---- ui/src/components/comment-nodes.tsx | 7 +- ui/src/components/communities.tsx | 4 +- ui/src/components/community.tsx | 6 +- ui/src/components/create-community.tsx | 2 +- ui/src/components/create-post.tsx | 2 +- ui/src/components/login.tsx | 4 +- ui/src/components/main.tsx | 12 +- ui/src/components/modlog.tsx | 40 ++- ui/src/components/navbar.tsx | 26 +- ui/src/components/post-listing.tsx | 35 ++- ui/src/components/post-listings.tsx | 2 +- ui/src/components/post.tsx | 50 ++- ui/src/components/setup.tsx | 2 +- ui/src/components/sidebar.tsx | 4 +- ui/src/components/site-form.tsx | 2 +- ui/src/components/user.tsx | 14 +- ui/src/index.html | 2 - ui/src/index.tsx | 3 +- ui/src/interfaces.ts | 74 +++-- ui/src/main.css | 87 ------ ui/src/services/WebSocketService.ts | 23 +- ui/src/utils.ts | 21 +- 42 files changed, 1017 insertions(+), 432 deletions(-) delete mode 100644 ui/src/main.css 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 2d6856b3..363f99f2 100644 --- a/server/migrations/2019-02-27-170003_create_community/up.sql +++ b/server/migrations/2019-02-27-170003_create_community/up.sql @@ -38,7 +38,7 @@ create table community ( description text, category_id int references category on update cascade on delete cascade not null, creator_id int references user_ on update cascade on delete cascade not null, - removed boolean default false, + removed boolean default false not null, published timestamp not null default now(), updated timestamp ); 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 acc0b5d1..a671c2e7 100644 --- a/server/migrations/2019-03-03-163336_create_post/down.sql +++ b/server/migrations/2019-03-03-163336_create_post/down.sql @@ -1,2 +1,4 @@ +drop table post_read; +drop table post_saved; 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 c3b7c0b8..90737812 100644 --- a/server/migrations/2019-03-03-163336_create_post/up.sql +++ b/server/migrations/2019-03-03-163336_create_post/up.sql @@ -5,8 +5,8 @@ create table post ( body text, 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, - removed boolean default false, - locked boolean default false, + removed boolean default false not null, + locked boolean default false not null, published timestamp not null default now(), updated timestamp ); @@ -20,3 +20,18 @@ create table post_like ( unique(post_id, user_id) ); +create table post_saved ( + id serial primary key, + post_id int references post on update cascade on delete cascade not null, + user_id int references user_ on update cascade on delete cascade not null, + published timestamp not null default now(), + unique(post_id, user_id) +); + +create table post_read ( + id serial primary key, + post_id int references post on update cascade on delete cascade not null, + user_id int references user_ on update cascade on delete cascade not null, + published timestamp not null default now(), + unique(post_id, user_id) +); diff --git a/server/migrations/2019-03-05-233828_create_comment/down.sql b/server/migrations/2019-03-05-233828_create_comment/down.sql index 5b92a44c..80fe0b1f 100644 --- a/server/migrations/2019-03-05-233828_create_comment/down.sql +++ b/server/migrations/2019-03-05-233828_create_comment/down.sql @@ -1,2 +1,3 @@ +drop table comment_saved; drop table comment_like; drop table comment; 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 214d50a6..4b754ece 100644 --- a/server/migrations/2019-03-05-233828_create_comment/up.sql +++ b/server/migrations/2019-03-05-233828_create_comment/up.sql @@ -4,7 +4,8 @@ create table comment ( 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, - removed boolean default false, + removed boolean default false not null, + read boolean default false not null, published timestamp not null default now(), updated timestamp ); @@ -18,3 +19,11 @@ create table comment_like ( published timestamp not null default now(), unique(comment_id, user_id) ); + +create table comment_saved ( + id serial primary key, + comment_id int references comment on update cascade on delete cascade not null, + user_id int references user_ on update cascade on delete cascade not null, + published timestamp not null default now(), + unique(comment_id, user_id) +); 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 ecf3280a..2f71b6fb 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 @@ -31,7 +31,8 @@ ap.*, u.id as user_id, 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, -u.admin or (select cm.id::bool from community_moderator cm where u.id = cm.user_id and cm.community_id = ap.community_id) as am_mod +(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read, +(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved 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 @@ -43,6 +44,7 @@ ap.*, null as user_id, null as my_vote, null as subscribed, -null as am_mod +null as read, +null as saved from all_post ap ; 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 1b73af51..7d38dbfa 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 @@ -13,19 +13,16 @@ with all_community as select ac.*, u.id as user_id, -cf.id::boolean as subscribed, -u.admin or (select cm.id::bool from community_moderator cm where u.id = cm.user_id and cm.community_id = ac.id) as am_mod +(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) 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, -null as am_mod +null as subscribed from all_community ac ; diff --git a/server/migrations/2019-04-03-155309_create_comment_view/up.sql b/server/migrations/2019-04-03-155309_create_comment_view/up.sql index a73b6182..a78e3ac3 100644 --- a/server/migrations/2019-04-03-155309_create_comment_view/up.sql +++ b/server/migrations/2019-04-03-155309_create_comment_view/up.sql @@ -4,7 +4,8 @@ with all_comment as select c.*, (select community_id from post p where p.id = c.post_id), - (select cb.id::bool from community_user_ban cb where c.creator_id = cb.user_id) as banned, + (select u.banned from user_ u where c.creator_id = u.id) as banned, + (select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community, (select name from user_ where c.creator_id = user_.id) as creator_name, coalesce(sum(cl.score), 0) as score, count (case when cl.score = 1 then 1 else null end) as upvotes, @@ -18,7 +19,7 @@ select ac.*, u.id as user_id, coalesce(cl.score, 0) as my_vote, -u.admin or (select cm.id::bool from community_moderator cm, post p where u.id = cm.user_id and ac.post_id = p.id and p.community_id = cm.community_id) as am_mod +(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved from user_ u cross join all_comment ac left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id @@ -29,6 +30,6 @@ select ac.*, null as user_id, null as my_vote, - null as am_mod + null as saved from all_comment ac ; diff --git a/server/migrations/2019-04-11-144915_create_mod_views/up.sql b/server/migrations/2019-04-11-144915_create_mod_views/up.sql index 908028d0..70a33e46 100644 --- a/server/migrations/2019-04-11-144915_create_mod_views/up.sql +++ b/server/migrations/2019-04-11-144915_create_mod_views/up.sql @@ -43,8 +43,7 @@ create view mod_ban_view as select mb.*, (select name from user_ u where mb.mod_user_id = u.id) as mod_user_name, (select name from user_ u where mb.other_user_id = u.id) as other_user_name -from mod_ban_from_community mb; - +from mod_ban mb; create view mod_add_community_view as select ma.*, @@ -53,7 +52,6 @@ select ma.*, (select name from community c where ma.community_id = c.id) as community_name from mod_add_community ma; - create view mod_add_view as select ma.*, (select name from user_ u where ma.mod_user_id = u.id) as mod_user_name, diff --git a/server/src/actions/comment.rs b/server/src/actions/comment.rs index f6eee5f1..c3aa0107 100644 --- a/server/src/actions/comment.rs +++ b/server/src/actions/comment.rs @@ -1,9 +1,9 @@ extern crate diesel; -use schema::{comment, comment_like}; +use schema::{comment, comment_like, comment_saved}; use diesel::*; use diesel::result::Error; use serde::{Deserialize, Serialize}; -use {Crud, Likeable}; +use {Crud, Likeable, Saveable}; use actions::post::Post; // WITH RECURSIVE MyTree AS ( @@ -22,7 +22,8 @@ pub struct Comment { pub post_id: i32, pub parent_id: Option, pub content: String, - pub removed: Option, + pub removed: bool, + pub read: bool, pub published: chrono::NaiveDateTime, pub updated: Option } @@ -38,27 +39,6 @@ pub struct CommentForm { pub updated: Option } -#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)] -#[belongs_to(Comment)] -#[table_name = "comment_like"] -pub struct CommentLike { - pub id: i32, - pub user_id: i32, - pub comment_id: i32, - pub post_id: i32, - pub score: i16, - pub published: chrono::NaiveDateTime, -} - -#[derive(Insertable, AsChangeset, Clone)] -#[table_name="comment_like"] -pub struct CommentLikeForm { - pub user_id: i32, - pub comment_id: i32, - pub post_id: i32, - pub score: i16 -} - impl Crud for Comment { fn read(conn: &PgConnection, comment_id: i32) -> Result { use schema::comment::dsl::*; @@ -87,6 +67,27 @@ impl Crud for Comment { } } +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)] +#[belongs_to(Comment)] +#[table_name = "comment_like"] +pub struct CommentLike { + pub id: i32, + pub user_id: i32, + pub comment_id: i32, + pub post_id: i32, + pub score: i16, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name="comment_like"] +pub struct CommentLikeForm { + pub user_id: i32, + pub comment_id: i32, + pub post_id: i32, + pub score: i16 +} + impl Likeable for CommentLike { fn read(conn: &PgConnection, comment_id_from: i32) -> Result, Error> { use schema::comment_like::dsl::*; @@ -119,6 +120,39 @@ impl CommentLike { } } +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] +#[belongs_to(Comment)] +#[table_name = "comment_saved"] +pub struct CommentSaved { + pub id: i32, + pub comment_id: i32, + pub user_id: i32, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name="comment_saved"] +pub struct CommentSavedForm { + pub comment_id: i32, + pub user_id: i32, +} + +impl Saveable for CommentSaved { + fn save(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result { + use schema::comment_saved::dsl::*; + insert_into(comment_saved) + .values(comment_saved_form) + .get_result::(conn) + } + fn unsave(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result { + use schema::comment_saved::dsl::*; + diesel::delete(comment_saved + .filter(comment_id.eq(comment_saved_form.comment_id)) + .filter(user_id.eq(comment_saved_form.user_id))) + .execute(conn) + } +} + #[cfg(test)] mod tests { use establish_connection; @@ -150,7 +184,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: None, + removed: false, updated: None }; @@ -162,8 +196,8 @@ mod tests { url: None, body: None, community_id: inserted_community.id, - removed: None, - locked: None, + removed: false, + locked: false, updated: None }; @@ -185,7 +219,8 @@ mod tests { content: "A test comment".into(), creator_id: inserted_user.id, post_id: inserted_post.id, - removed: Some(false), + removed: false, + read: false, parent_id: None, published: inserted_comment.published, updated: None @@ -202,6 +237,7 @@ mod tests { let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); + // Comment Like let comment_like_form = CommentLikeForm { comment_id: inserted_comment.id, post_id: inserted_post.id, @@ -220,9 +256,25 @@ mod tests { score: 1 }; + // Comment Saved + let comment_saved_form = CommentSavedForm { + comment_id: inserted_comment.id, + user_id: inserted_user.id, + }; + + let inserted_comment_saved = CommentSaved::save(&conn, &comment_saved_form).unwrap(); + + let expected_comment_saved = CommentSaved { + id: inserted_comment_saved.id, + comment_id: inserted_comment.id, + user_id: inserted_user.id, + published: inserted_comment_saved.published, + }; + let read_comment = Comment::read(&conn, inserted_comment.id).unwrap(); let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap(); let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap(); + let saved_removed = CommentSaved::unsave(&conn, &comment_saved_form).unwrap(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); Comment::delete(&conn, inserted_child_comment.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap(); @@ -233,8 +285,10 @@ mod tests { assert_eq!(expected_comment, inserted_comment); assert_eq!(expected_comment, updated_comment); assert_eq!(expected_comment_like, inserted_comment_like); + assert_eq!(expected_comment_saved, inserted_comment_saved); assert_eq!(expected_comment.id, inserted_child_comment.parent_id.unwrap()); assert_eq!(1, like_removed); + assert_eq!(1, saved_removed); assert_eq!(1, num_deleted); } diff --git a/server/src/actions/comment_view.rs b/server/src/actions/comment_view.rs index 0848ee1c..36043716 100644 --- a/server/src/actions/comment_view.rs +++ b/server/src/actions/comment_view.rs @@ -13,18 +13,20 @@ table! { post_id -> Int4, parent_id -> Nullable, content -> Text, - removed -> Nullable, + removed -> Bool, + read -> Bool, published -> Timestamp, updated -> Nullable, community_id -> Int4, - banned -> Nullable, + banned -> Bool, + banned_from_community -> Bool, creator_name -> Varchar, score -> BigInt, upvotes -> BigInt, downvotes -> BigInt, user_id -> Nullable, my_vote -> Nullable, - am_mod -> Nullable, + saved -> Nullable, } } @@ -36,18 +38,20 @@ pub struct CommentView { pub post_id: i32, pub parent_id: Option, pub content: String, - pub removed: Option, + pub removed: bool, + pub read: bool, pub published: chrono::NaiveDateTime, pub updated: Option, pub community_id: i32, - pub banned: Option, + pub banned: bool, + pub banned_from_community: bool, pub creator_name: String, pub score: i64, pub upvotes: i64, pub downvotes: i64, pub user_id: Option, pub my_vote: Option, - pub am_mod: Option, + pub saved: Option, } impl CommentView { @@ -57,6 +61,7 @@ impl CommentView { for_post_id: Option, for_creator_id: Option, my_user_id: Option, + saved_only: bool, page: Option, limit: Option, ) -> Result, Error> { @@ -81,6 +86,10 @@ impl CommentView { if let Some(for_post_id) = for_post_id { query = query.filter(post_id.eq(for_post_id)); }; + + if saved_only { + query = query.filter(saved.eq(true)); + } query = match sort { // SortType::Hot => query.order_by(hot_rank.desc()), @@ -159,7 +168,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: None, + removed: false, updated: None }; @@ -171,8 +180,8 @@ mod tests { url: None, body: None, community_id: inserted_community.id, - removed: None, - locked: None, + removed: false, + locked: false, updated: None }; @@ -205,8 +214,10 @@ mod tests { post_id: inserted_post.id, community_id: inserted_community.id, parent_id: None, - removed: Some(false), - banned: None, + removed: false, + read: false, + banned: false, + banned_from_community: false, published: inserted_comment.published, updated: None, creator_name: inserted_user.name.to_owned(), @@ -215,7 +226,7 @@ mod tests { upvotes: 1, user_id: None, my_vote: None, - am_mod: None, + saved: None, }; let expected_comment_view_with_user = CommentView { @@ -225,8 +236,10 @@ mod tests { post_id: inserted_post.id, community_id: inserted_community.id, parent_id: None, - removed: Some(false), - banned: None, + removed: false, + read: false, + banned: false, + banned_from_community: false, published: inserted_comment.published, updated: None, creator_name: inserted_user.name.to_owned(), @@ -235,11 +248,11 @@ mod tests { upvotes: 1, user_id: Some(inserted_user.id), my_vote: Some(1), - am_mod: None, + saved: None, }; - let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, None, None).unwrap(); - let read_comment_views_with_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, Some(inserted_user.id), None, None).unwrap(); + let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, false, None, None).unwrap(); + let read_comment_views_with_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, Some(inserted_user.id), false, None, None).unwrap(); let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap(); diff --git a/server/src/actions/community.rs b/server/src/actions/community.rs index ac331934..594518ba 100644 --- a/server/src/actions/community.rs +++ b/server/src/actions/community.rs @@ -14,7 +14,7 @@ pub struct Community { pub description: Option, pub category_id: i32, pub creator_id: i32, - pub removed: Option, + pub removed: bool, pub published: chrono::NaiveDateTime, pub updated: Option } @@ -27,7 +27,7 @@ pub struct CommunityForm { pub description: Option, pub category_id: i32, pub creator_id: i32, - pub removed: Option, + pub removed: bool, pub updated: Option } @@ -236,7 +236,7 @@ mod tests { title: "nada".to_owned(), description: None, category_id: 1, - removed: None, + removed: false, updated: None, }; @@ -249,7 +249,7 @@ mod tests { title: "nada".to_owned(), description: None, category_id: 1, - removed: Some(false), + removed: false, published: inserted_community.published, updated: None }; diff --git a/server/src/actions/community_view.rs b/server/src/actions/community_view.rs index 4db97491..8966ee15 100644 --- a/server/src/actions/community_view.rs +++ b/server/src/actions/community_view.rs @@ -12,7 +12,7 @@ table! { description -> Nullable, category_id -> Int4, creator_id -> Int4, - removed -> Nullable, + removed -> Bool, published -> Timestamp, updated -> Nullable, creator_name -> Varchar, @@ -22,7 +22,6 @@ table! { number_of_comments -> BigInt, user_id -> Nullable, subscribed -> Nullable, - am_mod -> Nullable, } } @@ -83,7 +82,7 @@ pub struct CommunityView { pub description: Option, pub category_id: i32, pub creator_id: i32, - pub removed: Option, + pub removed: bool, pub published: chrono::NaiveDateTime, pub updated: Option, pub creator_name: String, @@ -93,7 +92,6 @@ pub struct CommunityView { pub number_of_comments: i64, pub user_id: Option, pub subscribed: Option, - pub am_mod: Option, } impl CommunityView { diff --git a/server/src/actions/moderator.rs b/server/src/actions/moderator.rs index a97b2120..e0d885ce 100644 --- a/server/src/actions/moderator.rs +++ b/server/src/actions/moderator.rs @@ -441,7 +441,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: None, + removed: false, updated: None }; @@ -453,8 +453,8 @@ mod tests { body: None, creator_id: inserted_user.id, community_id: inserted_community.id, - removed: None, - locked: None, + removed: false, + locked: false, updated: None }; diff --git a/server/src/actions/post.rs b/server/src/actions/post.rs index 468b3a9b..0fd0e5c5 100644 --- a/server/src/actions/post.rs +++ b/server/src/actions/post.rs @@ -1,9 +1,9 @@ extern crate diesel; -use schema::{post, post_like}; +use schema::{post, post_like, post_saved, post_read}; use diesel::*; use diesel::result::Error; use serde::{Deserialize, Serialize}; -use {Crud, Likeable}; +use {Crud, Likeable, Saveable, Readable}; #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[table_name="post"] @@ -14,8 +14,8 @@ pub struct Post { pub body: Option, pub creator_id: i32, pub community_id: i32, - pub removed: Option, - pub locked: Option, + pub removed: bool, + pub locked: bool, pub published: chrono::NaiveDateTime, pub updated: Option } @@ -28,30 +28,11 @@ pub struct PostForm { pub body: Option, pub creator_id: i32, pub community_id: i32, - pub removed: Option, - pub locked: Option, + pub removed: bool, + pub locked: bool, pub updated: Option } -#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] -#[belongs_to(Post)] -#[table_name = "post_like"] -pub struct PostLike { - pub id: i32, - pub post_id: i32, - pub user_id: i32, - pub score: i16, - pub published: chrono::NaiveDateTime, -} - -#[derive(Insertable, AsChangeset, Clone)] -#[table_name="post_like"] -pub struct PostLikeForm { - pub post_id: i32, - pub user_id: i32, - pub score: i16 -} - impl Crud for Post { fn read(conn: &PgConnection, post_id: i32) -> Result { use schema::post::dsl::*; @@ -80,6 +61,25 @@ impl Crud for Post { } } +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] +#[belongs_to(Post)] +#[table_name = "post_like"] +pub struct PostLike { + pub id: i32, + pub post_id: i32, + pub user_id: i32, + pub score: i16, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name="post_like"] +pub struct PostLikeForm { + pub post_id: i32, + pub user_id: i32, + pub score: i16 +} + impl Likeable for PostLike { fn read(conn: &PgConnection, post_id_from: i32) -> Result, Error> { use schema::post_like::dsl::*; @@ -102,6 +102,72 @@ impl Likeable for PostLike { } } +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] +#[belongs_to(Post)] +#[table_name = "post_saved"] +pub struct PostSaved { + pub id: i32, + pub post_id: i32, + pub user_id: i32, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name="post_saved"] +pub struct PostSavedForm { + pub post_id: i32, + pub user_id: i32, +} + +impl Saveable for PostSaved { + fn save(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result { + use schema::post_saved::dsl::*; + insert_into(post_saved) + .values(post_saved_form) + .get_result::(conn) + } + fn unsave(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result { + use schema::post_saved::dsl::*; + diesel::delete(post_saved + .filter(post_id.eq(post_saved_form.post_id)) + .filter(user_id.eq(post_saved_form.user_id))) + .execute(conn) + } +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] +#[belongs_to(Post)] +#[table_name = "post_read"] +pub struct PostRead { + pub id: i32, + pub post_id: i32, + pub user_id: i32, + pub published: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name="post_read"] +pub struct PostReadForm { + pub post_id: i32, + pub user_id: i32, +} + +impl Readable for PostRead { + fn mark_as_read(conn: &PgConnection, post_read_form: &PostReadForm) -> Result { + use schema::post_read::dsl::*; + insert_into(post_read) + .values(post_read_form) + .get_result::(conn) + } + fn mark_as_unread(conn: &PgConnection, post_read_form: &PostReadForm) -> Result { + use schema::post_read::dsl::*; + diesel::delete(post_read + .filter(post_id.eq(post_read_form.post_id)) + .filter(user_id.eq(post_read_form.user_id))) + .execute(conn) + } +} + #[cfg(test)] mod tests { use establish_connection; @@ -132,7 +198,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: None, + removed: false, updated: None }; @@ -144,8 +210,8 @@ mod tests { body: None, creator_id: inserted_user.id, community_id: inserted_community.id, - removed: None, - locked: None, + removed: false, + locked: false, updated: None }; @@ -159,11 +225,12 @@ mod tests { creator_id: inserted_user.id, community_id: inserted_community.id, published: inserted_post.published, - removed: Some(false), - locked: Some(false), + removed: false, + locked: false, updated: None }; + // Post Like let post_like_form = PostLikeForm { post_id: inserted_post.id, user_id: inserted_user.id, @@ -179,10 +246,42 @@ mod tests { published: inserted_post_like.published, score: 1 }; + + // Post Save + let post_saved_form = PostSavedForm { + post_id: inserted_post.id, + user_id: inserted_user.id, + }; + + let inserted_post_saved = PostSaved::save(&conn, &post_saved_form).unwrap(); + + let expected_post_saved = PostSaved { + id: inserted_post_saved.id, + post_id: inserted_post.id, + user_id: inserted_user.id, + published: inserted_post_saved.published, + }; + + // Post Read + let post_read_form = PostReadForm { + post_id: inserted_post.id, + user_id: inserted_user.id, + }; + + let inserted_post_read = PostRead::mark_as_read(&conn, &post_read_form).unwrap(); + + let expected_post_read = PostRead { + id: inserted_post_read.id, + post_id: inserted_post.id, + user_id: inserted_user.id, + published: inserted_post_read.published, + }; let read_post = Post::read(&conn, inserted_post.id).unwrap(); let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap(); let like_removed = PostLike::remove(&conn, &post_like_form).unwrap(); + let saved_removed = PostSaved::unsave(&conn, &post_saved_form).unwrap(); + let read_removed = PostRead::mark_as_unread(&conn, &post_read_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(); @@ -191,7 +290,11 @@ mod tests { assert_eq!(expected_post, inserted_post); assert_eq!(expected_post, updated_post); assert_eq!(expected_post_like, inserted_post_like); + assert_eq!(expected_post_saved, inserted_post_saved); + assert_eq!(expected_post_read, inserted_post_read); assert_eq!(1, like_removed); + assert_eq!(1, saved_removed); + assert_eq!(1, read_removed); assert_eq!(1, num_deleted); } diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs index 7ab490aa..78fcef63 100644 --- a/server/src/actions/post_view.rs +++ b/server/src/actions/post_view.rs @@ -19,8 +19,8 @@ table! { body -> Nullable, creator_id -> Int4, community_id -> Int4, - removed -> Nullable, - locked -> Nullable, + removed -> Bool, + locked -> Bool, published -> Timestamp, updated -> Nullable, creator_name -> Varchar, @@ -33,7 +33,8 @@ table! { user_id -> Nullable, my_vote -> Nullable, subscribed -> Nullable, - am_mod -> Nullable, + read -> Nullable, + saved -> Nullable, } } @@ -47,8 +48,8 @@ pub struct PostView { pub body: Option, pub creator_id: i32, pub community_id: i32, - pub removed: Option, - pub locked: Option, + pub removed: bool, + pub locked: bool, pub published: chrono::NaiveDateTime, pub updated: Option, pub creator_name: String, @@ -61,7 +62,8 @@ pub struct PostView { pub user_id: Option, pub my_vote: Option, pub subscribed: Option, - pub am_mod: Option, + pub read: Option, + pub saved: Option, } impl PostView { @@ -71,6 +73,8 @@ impl PostView { for_community_id: Option, for_creator_id: Option, my_user_id: Option, + saved_only: bool, + unread_only: bool, page: Option, limit: Option, ) -> Result, Error> { @@ -88,6 +92,15 @@ impl PostView { query = query.filter(creator_id.eq(for_creator_id)); }; + // TODO these are wrong, bc they'll only show saved for your logged in user, not theirs + if saved_only { + query = query.filter(saved.eq(true)); + }; + + if unread_only { + query = query.filter(read.eq(false)); + }; + match type_ { PostListingType::Subscribed => { query = query.filter(subscribed.eq(true)); @@ -187,7 +200,7 @@ mod tests { description: None, creator_id: inserted_user.id, category_id: 1, - removed: None, + removed: false, updated: None }; @@ -199,8 +212,8 @@ mod tests { body: None, creator_id: inserted_user.id, community_id: inserted_community.id, - removed: None, - locked: None, + removed: false, + locked: false, updated: None }; @@ -239,8 +252,8 @@ mod tests { creator_id: inserted_user.id, creator_name: user_name.to_owned(), community_id: inserted_community.id, - removed: Some(false), - locked: Some(false), + removed: false, + locked: false, community_name: community_name.to_owned(), number_of_comments: 0, score: 1, @@ -250,7 +263,8 @@ mod tests { published: inserted_post.published, updated: None, subscribed: None, - am_mod: None, + read: None, + saved: None, }; let expected_post_listing_with_user = PostView { @@ -260,8 +274,8 @@ mod tests { name: post_name.to_owned(), url: None, body: None, - removed: Some(false), - locked: Some(false), + removed: false, + locked: false, creator_id: inserted_user.id, creator_name: user_name.to_owned(), community_id: inserted_community.id, @@ -274,12 +288,13 @@ mod tests { published: inserted_post.published, updated: None, subscribed: None, - am_mod: None, + read: None, + saved: None, }; - let read_post_listings_with_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, Some(inserted_user.id), None, None).unwrap(); - let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, None, None, None).unwrap(); + let read_post_listings_with_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, Some(inserted_user.id), false, false, None, None).unwrap(); + let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, None, false, false, None, None).unwrap(); let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap(); let read_post_listing_with_user = PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap(); diff --git a/server/src/lib.rs b/server/src/lib.rs index 3390dbdc..31c1af7c 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -55,6 +55,16 @@ pub trait Bannable { fn unban(conn: &PgConnection, form: &T) -> Result where Self: Sized; } +pub trait Saveable { + fn save(conn: &PgConnection, form: &T) -> Result where Self: Sized; + fn unsave(conn: &PgConnection, form: &T) -> Result where Self: Sized; +} + +pub trait Readable { + fn mark_as_read(conn: &PgConnection, form: &T) -> Result where Self: Sized; + fn mark_as_unread(conn: &PgConnection, form: &T) -> Result where Self: Sized; +} + pub fn establish_connection() -> PgConnection { let db_url = Settings::get().db_url; PgConnection::establish(&db_url) diff --git a/server/src/schema.rs b/server/src/schema.rs index f431610a..65c2ae55 100644 --- a/server/src/schema.rs +++ b/server/src/schema.rs @@ -12,7 +12,8 @@ table! { post_id -> Int4, parent_id -> Nullable, content -> Text, - removed -> Nullable, + removed -> Bool, + read -> Bool, published -> Timestamp, updated -> Nullable, } @@ -29,6 +30,15 @@ table! { } } +table! { + comment_saved (id) { + id -> Int4, + comment_id -> Int4, + user_id -> Int4, + published -> Timestamp, + } +} + table! { community (id) { id -> Int4, @@ -37,7 +47,7 @@ table! { description -> Nullable, category_id -> Int4, creator_id -> Int4, - removed -> Nullable, + removed -> Bool, published -> Timestamp, updated -> Nullable, } @@ -168,8 +178,8 @@ table! { body -> Nullable, creator_id -> Int4, community_id -> Int4, - removed -> Nullable, - locked -> Nullable, + removed -> Bool, + locked -> Bool, published -> Timestamp, updated -> Nullable, } @@ -185,6 +195,24 @@ table! { } } +table! { + post_read (id) { + id -> Int4, + post_id -> Int4, + user_id -> Int4, + published -> Timestamp, + } +} + +table! { + post_saved (id) { + id -> Int4, + post_id -> Int4, + user_id -> Int4, + published -> Timestamp, + } +} + table! { site (id) { id -> Int4, @@ -225,6 +253,8 @@ joinable!(comment -> user_ (creator_id)); joinable!(comment_like -> comment (comment_id)); joinable!(comment_like -> post (post_id)); joinable!(comment_like -> user_ (user_id)); +joinable!(comment_saved -> comment (comment_id)); +joinable!(comment_saved -> user_ (user_id)); joinable!(community -> category (category_id)); joinable!(community -> user_ (creator_id)); joinable!(community_follower -> community (community_id)); @@ -247,6 +277,10 @@ joinable!(post -> community (community_id)); joinable!(post -> user_ (creator_id)); joinable!(post_like -> post (post_id)); joinable!(post_like -> user_ (user_id)); +joinable!(post_read -> post (post_id)); +joinable!(post_read -> user_ (user_id)); +joinable!(post_saved -> post (post_id)); +joinable!(post_saved -> user_ (user_id)); joinable!(site -> user_ (creator_id)); joinable!(user_ban -> user_ (user_id)); @@ -254,6 +288,7 @@ allow_tables_to_appear_in_same_query!( category, comment, comment_like, + comment_saved, community, community_follower, community_moderator, @@ -268,6 +303,8 @@ allow_tables_to_appear_in_same_query!( mod_remove_post, post, post_like, + post_read, + post_saved, site, user_, user_ban, diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index a4c5b620..d1f72109 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -11,7 +11,7 @@ use bcrypt::{verify}; use std::str::FromStr; use diesel::PgConnection; -use {Crud, Joinable, Likeable, Followable, Bannable, establish_connection, naive_now, naive_from_unix, SortType, has_slurs, remove_slurs}; +use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, has_slurs, remove_slurs}; use actions::community::*; use actions::user::*; use actions::post::*; @@ -26,7 +26,7 @@ use actions::moderator::*; #[derive(EnumString,ToString,Debug)] pub enum UserOperation { - Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser + Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser } #[derive(Serialize, Deserialize)] @@ -164,7 +164,8 @@ pub struct GetPostResponse { post: PostView, comments: Vec, community: CommunityView, - moderators: Vec + moderators: Vec, + admins: Vec, } #[derive(Serialize, Deserialize)] @@ -217,6 +218,13 @@ pub struct EditComment { auth: String } +#[derive(Serialize, Deserialize)] +pub struct SaveComment { + comment_id: i32, + save: bool, + auth: String +} + #[derive(Serialize, Deserialize)] pub struct CommentResponse { op: String, @@ -253,9 +261,16 @@ pub struct EditPost { name: String, url: Option, body: Option, - removed: Option, + removed: bool, + locked: bool, reason: Option, - locked: Option, + auth: String +} + +#[derive(Serialize, Deserialize)] +pub struct SavePost { + post_id: i32, + save: bool, auth: String } @@ -266,7 +281,7 @@ pub struct EditCommunity { title: String, description: Option, category_id: i32, - removed: Option, + removed: bool, reason: Option, expires: Option, auth: String @@ -297,7 +312,7 @@ pub struct GetUserDetails { page: Option, limit: Option, community_id: Option, - auth: Option + saved_only: bool, } #[derive(Serialize, Deserialize)] @@ -308,8 +323,6 @@ pub struct GetUserDetailsResponse { moderates: Vec, comments: Vec, posts: Vec, - saved_posts: Vec, - saved_comments: Vec, } #[derive(Serialize, Deserialize)] @@ -468,6 +481,8 @@ impl ChatServer { Some(community_id), None, None, + false, + false, None, Some(9999)) .unwrap(); @@ -491,7 +506,6 @@ impl Handler for ChatServer { type Result = usize; fn handle(&mut self, msg: Connect, _: &mut Context) -> Self::Result { - println!("Someone joined"); // notify all users in same room // self.send_room_message(&"Main".to_owned(), "Someone joined", 0); @@ -513,7 +527,6 @@ impl Handler for ChatServer { type Result = (); fn handle(&mut self, msg: Disconnect, _: &mut Context) { - println!("Someone disconnected"); // let mut rooms: Vec = Vec::new(); @@ -586,6 +599,10 @@ impl Handler for ChatServer { let edit_comment: EditComment = serde_json::from_str(data).unwrap(); edit_comment.perform(self, msg.id) }, + UserOperation::SaveComment => { + let save_post: SaveComment = serde_json::from_str(data).unwrap(); + save_post.perform(self, msg.id) + }, UserOperation::CreateCommentLike => { let create_comment_like: CreateCommentLike = serde_json::from_str(data).unwrap(); create_comment_like.perform(self, msg.id) @@ -602,6 +619,10 @@ impl Handler for ChatServer { let edit_post: EditPost = serde_json::from_str(data).unwrap(); edit_post.perform(self, msg.id) }, + UserOperation::SavePost => { + let save_post: SavePost = serde_json::from_str(data).unwrap(); + save_post.perform(self, msg.id) + }, UserOperation::EditCommunity => { let edit_community: EditCommunity = serde_json::from_str(data).unwrap(); edit_community.perform(self, msg.id) @@ -745,11 +766,11 @@ impl Perform for Register { } }; - // If its an admin, add them as a mod to main + // If its an admin, add them as a mod and follower to main if self.admin { let community_moderator_form = CommunityModeratorForm { community_id: 1, - user_id: inserted_user.id + user_id: inserted_user.id, }; let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { @@ -758,6 +779,18 @@ impl Perform for Register { return self.error("Community moderator already exists."); } }; + + let community_follower_form = CommunityFollowerForm { + community_id: 1, + user_id: inserted_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."); + } + }; } @@ -797,6 +830,11 @@ impl Perform for CreateCommunity { let user_id = claims.id; + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } + // When you create a community, make sure the user becomes a moderator and a follower let community_form = CommunityForm { @@ -805,7 +843,7 @@ impl Perform for CreateCommunity { description: self.description.to_owned(), category_id: self.category_id, creator_id: user_id, - removed: None, + removed: false, updated: None, }; @@ -934,19 +972,24 @@ impl Perform for CreatePost { let user_id = claims.id; - // Check for a ban + // Check for a community ban if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() { return self.error("You have been banned from this community"); } + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } + let post_form = PostForm { name: self.name.to_owned(), url: self.url.to_owned(), body: self.body.to_owned(), community_id: self.community_id, creator_id: user_id, - removed: None, - locked: None, + removed: false, + locked: false, updated: None }; @@ -1031,12 +1074,14 @@ impl Perform for GetPost { chat.rooms.get_mut(&self.id).unwrap().insert(addr); - let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, user_id, None, Some(9999)).unwrap(); + let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, user_id, false, None, Some(9999)).unwrap(); let community = CommunityView::read(&conn, post_view.community_id, user_id).unwrap(); let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id).unwrap(); + let admins = UserView::admins(&conn).unwrap(); + // Return the jwt serde_json::to_string( &GetPostResponse { @@ -1044,7 +1089,8 @@ impl Perform for GetPost { post: post_view, comments: comments, community: community, - moderators: moderators + moderators: moderators, + admins: admins, } ) .unwrap() @@ -1117,11 +1163,16 @@ impl Perform for CreateComment { let user_id = claims.id; - // Check for a ban + // Check for a community ban let post = Post::read(&conn, self.post_id).unwrap(); if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { return self.error("You have been banned from this community"); } + + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } let content_slurs_removed = remove_slurs(&self.content.to_owned()); @@ -1202,24 +1253,38 @@ impl Perform for EditComment { let user_id = claims.id; - - // Verify its the creator or a mod + // Verify its the creator or a mod, or an admin let orig_comment = CommentView::read(&conn, self.edit_id, None).unwrap(); - let mut editors: Vec = CommunityModeratorView::for_community(&conn, orig_comment.community_id) + let mut editors: Vec = vec![self.creator_id]; + editors.append( + &mut CommunityModeratorView::for_community(&conn, orig_comment.community_id) .unwrap() .into_iter() .map(|m| m.user_id) - .collect(); - editors.push(self.creator_id); + .collect() + ); + editors.append( + &mut UserView::admins(&conn) + .unwrap() + .into_iter() + .map(|a| a.id) + .collect() + ); + if !editors.contains(&user_id) { return self.error("Not allowed to edit comment."); } - // Check for a ban + // Check for a community ban if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() { return self.error("You have been banned from this community"); } + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } + let content_slurs_removed = remove_slurs(&self.content.to_owned()); let comment_form = CommentForm { @@ -1278,6 +1343,60 @@ impl Perform for EditComment { } } +impl Perform for SaveComment { + fn op_type(&self) -> UserOperation { + UserOperation::SaveComment + } + + 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 comment_saved_form = CommentSavedForm { + comment_id: self.comment_id, + user_id: user_id, + }; + + if self.save { + match CommentSaved::save(&conn, &comment_saved_form) { + Ok(comment) => comment, + Err(_e) => { + return self.error("Couldnt do comment save"); + } + }; + } else { + match CommentSaved::unsave(&conn, &comment_saved_form) { + Ok(comment) => comment, + Err(_e) => { + return self.error("Couldnt do comment save"); + } + }; + } + + let comment_view = CommentView::read(&conn, self.comment_id, Some(user_id)).unwrap(); + + let comment_out = serde_json::to_string( + &CommentResponse { + op: self.op_type().to_string(), + comment: comment_view + } + ) + .unwrap(); + + comment_out + } +} + + impl Perform for CreateCommentLike { fn op_type(&self) -> UserOperation { UserOperation::CreateCommentLike @@ -1296,12 +1415,17 @@ impl Perform for CreateCommentLike { let user_id = claims.id; - // Check for a ban + // Check for a community ban let post = Post::read(&conn, self.post_id).unwrap(); if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { return self.error("You have been banned from this community"); } + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } + let like_form = CommentLikeForm { comment_id: self.comment_id, post_id: self.post_id, @@ -1377,7 +1501,7 @@ impl Perform for GetPosts { let type_ = PostListingType::from_str(&self.type_).expect("listing type"); let sort = SortType::from_str(&self.sort).expect("listing sort"); - let posts = match PostView::list(&conn, type_, &sort, self.community_id, None, user_id, self.page, self.limit) { + let posts = match PostView::list(&conn, type_, &sort, self.community_id, None, user_id, false, false, self.page, self.limit) { Ok(posts) => posts, Err(_e) => { return self.error("Couldn't get posts"); @@ -1414,12 +1538,17 @@ impl Perform for CreatePostLike { let user_id = claims.id; - // Check for a ban + // Check for a community ban let post = Post::read(&conn, self.post_id).unwrap(); if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { return self.error("You have been banned from this community"); } + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } + let like_form = PostLikeForm { post_id: self.post_id, user_id: user_id, @@ -1494,11 +1623,16 @@ impl Perform for EditPost { return self.error("Not allowed to edit comment."); } - // Check for a ban + // Check for a community ban if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() { return self.error("You have been banned from this community"); } + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } + let post_form = PostForm { name: self.name.to_owned(), url: self.url.to_owned(), @@ -1518,21 +1652,21 @@ impl Perform for EditPost { }; // Mod tables - if let Some(removed) = self.removed.to_owned() { + if self.removed { let form = ModRemovePostForm { mod_user_id: user_id, post_id: self.edit_id, - removed: Some(removed), + removed: Some(self.removed), reason: self.reason.to_owned(), }; ModRemovePost::create(&conn, &form).unwrap(); } - if let Some(locked) = self.locked.to_owned() { + if self.locked { let form = ModLockPostForm { mod_user_id: user_id, post_id: self.edit_id, - locked: Some(locked), + locked: Some(self.locked), }; ModLockPost::create(&conn, &form).unwrap(); } @@ -1564,6 +1698,59 @@ impl Perform for EditPost { } } +impl Perform for SavePost { + fn op_type(&self) -> UserOperation { + UserOperation::SavePost + } + + 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 post_saved_form = PostSavedForm { + post_id: self.post_id, + user_id: user_id, + }; + + if self.save { + match PostSaved::save(&conn, &post_saved_form) { + Ok(post) => post, + Err(_e) => { + return self.error("Couldnt do post save"); + } + }; + } else { + match PostSaved::unsave(&conn, &post_saved_form) { + Ok(post) => post, + Err(_e) => { + return self.error("Couldnt do post save"); + } + }; + } + + let post_view = PostView::read(&conn, self.post_id, Some(user_id)).unwrap(); + + let post_out = serde_json::to_string( + &PostResponse { + op: self.op_type().to_string(), + post: post_view + } + ) + .unwrap(); + + post_out + } +} + impl Perform for EditCommunity { fn op_type(&self) -> UserOperation { UserOperation::EditCommunity @@ -1586,6 +1773,11 @@ impl Perform for EditCommunity { let user_id = claims.id; + // Check for a site ban + if UserView::read(&conn, user_id).unwrap().banned { + return self.error("You have been banned from the site"); + } + // Verify its a mod 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(); @@ -1611,7 +1803,7 @@ impl Perform for EditCommunity { }; // Mod tables - if let Some(removed) = self.removed.to_owned() { + if self.removed { let expires = match self.expires { Some(time) => Some(naive_from_unix(time)), None => None @@ -1619,7 +1811,7 @@ impl Perform for EditCommunity { let form = ModRemoveCommunityForm { mod_user_id: user_id, community_id: self.edit_id, - removed: Some(removed), + removed: Some(self.removed), reason: self.reason.to_owned(), expires: expires }; @@ -1750,26 +1942,21 @@ impl Perform for GetUserDetails { let conn = establish_connection(); - 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 - }; - - //TODO add save let sort = SortType::from_str(&self.sort).expect("listing sort"); let user_view = UserView::read(&conn, self.user_id).unwrap(); - let posts = PostView::list(&conn, PostListingType::All, &sort, self.community_id, Some(self.user_id), user_id, self.page, self.limit).unwrap(); - let comments = CommentView::list(&conn, &sort, None, Some(self.user_id), user_id, self.page, self.limit).unwrap(); + let posts = if self.saved_only { + PostView::list(&conn, PostListingType::All, &sort, self.community_id, None, Some(self.user_id), self.saved_only, false, self.page, self.limit).unwrap() + } else { + PostView::list(&conn, PostListingType::All, &sort, self.community_id, Some(self.user_id), None, self.saved_only, false, self.page, self.limit).unwrap() + }; + let comments = if self.saved_only { + CommentView::list(&conn, &sort, None, None, Some(self.user_id), self.saved_only, self.page, self.limit).unwrap() + } else { + CommentView::list(&conn, &sort, None, Some(self.user_id), None, self.saved_only, self.page, self.limit).unwrap() + }; + let follows = CommunityFollowerView::for_user(&conn, self.user_id).unwrap(); let moderates = CommunityModeratorView::for_user(&conn, self.user_id).unwrap(); @@ -1782,8 +1969,6 @@ impl Perform for GetUserDetails { moderates: moderates, comments: comments, posts: posts, - saved_posts: Vec::new(), - saved_comments: Vec::new(), } ) .unwrap() diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index dcfb18a9..c1fc059b 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -1,12 +1,14 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; -import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, BanFromCommunityForm, CommunityUser, AddModToCommunityForm } from '../interfaces'; +import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm } from '../interfaces'; import { WebSocketService, UserService } from '../services'; -import { mdToHtml, getUnixTime } from '../utils'; +import { mdToHtml, getUnixTime, canMod, isMod } from '../utils'; import { MomentTime } from './moment-time'; import { CommentForm } from './comment-form'; import { CommentNodes } from './comment-nodes'; +enum BanType {Community, Site}; + interface CommentNodeState { showReply: boolean; showEdit: boolean; @@ -15,6 +17,7 @@ interface CommentNodeState { showBanDialog: boolean; banReason: string; banExpires: string; + banType: BanType; } interface CommentNodeProps { @@ -23,6 +26,7 @@ interface CommentNodeProps { viewOnly?: boolean; locked?: boolean; moderators: Array; + admins: Array; } export class CommentNode extends Component { @@ -35,6 +39,7 @@ export class CommentNode extends Component { showBanDialog: false, banReason: null, banExpires: null, + banType: BanType.Community } constructor(props: any, context: any) { @@ -60,6 +65,12 @@ export class CommentNode extends Component {
  • {node.comment.creator_name}
  • + {this.isMod && +
  • mod
  • + } + {this.isAdmin && +
  • admin
  • + }
  • ( +{node.comment.upvotes} @@ -77,47 +88,70 @@ export class CommentNode extends Component {
      - {!this.props.viewOnly && - + {UserService.Instance.user && !this.props.viewOnly && + <>
    • reply
    • +
    • + {node.comment.saved ? 'unsave' : 'save'} +
    • {this.myComment && <> -
    • - edit -
    • -
    • - delete -
    • - +
    • + edit +
    • +
    • + delete +
    • + } - {this.canMod && - <> + {/* Admins and mods can remove comments */} + {this.canMod &&
    • {!this.props.node.comment.removed ? remove : restore }
    • - {!this.isMod && - <> + } + {/* Mods can ban from community, and appoint as mods to community */} + {this.canMod && + <> + {!this.isMod &&
    • - {!this.props.node.comment.banned ? - ban : - unban + {!this.props.node.comment.banned_from_community ? + ban : + unban }
    • - - } - {!this.props.node.comment.banned && -
    • - {`${this.isMod ? 'remove' : 'appoint'} as mod`} -
    • - } - + } + {!this.props.node.comment.banned_from_community && +
    • + {`${this.isMod ? 'remove' : 'appoint'} as mod`} +
    • + } + } -
      + {/* Admins can ban from all, and appoint other admins */} + {this.canAdmin && + <> + {!this.isAdmin && +
    • + {!this.props.node.comment.banned ? + ban from site : + unban from site + } +
    • + } + {!this.props.node.comment.banned && +
    • + {`${this.isAdmin ? 'remove' : 'appoint'} as admin`} +
    • + } + + } + }
    • link @@ -133,22 +167,35 @@ export class CommentNode extends Component { } {this.state.showBanDialog && -
      -
      - - -
      -
      - - -
      -
      - -
      -
      +
      +
      + + +
      +
      + + +
      +
      + +
      +
      + } + {this.state.showReply && + + } + {this.props.node.children && + } - {this.state.showReply && } - {this.props.node.children && }
    ) } @@ -158,27 +205,22 @@ export class CommentNode extends Component { } get canMod(): boolean { + let adminsThenMods = this.props.admins.map(a => a.id) + .concat(this.props.moderators.map(m => m.user_id)); - // You can do moderator actions only on the mods added after you. - if (UserService.Instance.user) { - let modIds = this.props.moderators.map(m => m.user_id); - let yourIndex = modIds.findIndex(id => id == UserService.Instance.user.id); - if (yourIndex == -1) { - return false; - } else { - console.log(modIds); - modIds = modIds.slice(0, yourIndex+1); // +1 cause you cant mod yourself - console.log(modIds); - return !modIds.includes(this.props.node.comment.creator_id); - } - } else { - return false; - } - + return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id); } get isMod(): boolean { - return this.props.moderators.map(m => m.user_id).includes(this.props.node.comment.creator_id); + return isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id); + } + + get isAdmin(): boolean { + return isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id); + } + + get canAdmin(): boolean { + return canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id); } handleReplyClick(i: CommentNode) { @@ -193,16 +235,27 @@ export class CommentNode extends Component { handleDeleteClick(i: CommentNode) { let deleteForm: CommentFormI = { - content: "*deleted*", + content: '*deleted*', edit_id: i.props.node.comment.id, creator_id: i.props.node.comment.creator_id, post_id: i.props.node.comment.post_id, parent_id: i.props.node.comment.parent_id, + removed: i.props.node.comment.removed, auth: null }; WebSocketService.Instance.editComment(deleteForm); } + handleSaveCommentClick(i: CommentNode) { + let saved = (i.props.node.comment.saved == undefined) ? true : !i.props.node.comment.saved; + let form: SaveCommentForm = { + comment_id: i.props.node.comment.id, + save: saved + }; + + WebSocketService.Instance.saveComment(form); + } + handleReplyCancel() { this.state.showReply = false; this.state.showEdit = false; @@ -257,8 +310,15 @@ export class CommentNode extends Component { i.setState(i.state); } + handleModBanFromCommunityShow(i: CommentNode) { + i.state.showBanDialog = true; + i.state.banType = BanType.Community; + i.setState(i.state); + } + handleModBanShow(i: CommentNode) { i.state.showBanDialog = true; + i.state.banType = BanType.Site; i.setState(i.state); } @@ -272,16 +332,42 @@ export class CommentNode extends Component { i.setState(i.state); } + handleModBanFromCommunitySubmit(i: CommentNode) { + i.state.banType = BanType.Community; + i.setState(i.state); + i.handleModBanBothSubmit(i); + } + handleModBanSubmit(i: CommentNode) { + i.state.banType = BanType.Site; + i.setState(i.state); + i.handleModBanBothSubmit(i); + } + + handleModBanBothSubmit(i: CommentNode) { event.preventDefault(); - let form: BanFromCommunityForm = { - user_id: i.props.node.comment.creator_id, - community_id: i.props.node.comment.community_id, - ban: !i.props.node.comment.banned, - reason: i.state.banReason, - expires: getUnixTime(i.state.banExpires), - }; - WebSocketService.Instance.banFromCommunity(form); + + console.log(BanType[i.state.banType]); + console.log(i.props.node.comment.banned); + + if (i.state.banType == BanType.Community) { + let form: BanFromCommunityForm = { + user_id: i.props.node.comment.creator_id, + community_id: i.props.node.comment.community_id, + ban: !i.props.node.comment.banned_from_community, + reason: i.state.banReason, + expires: getUnixTime(i.state.banExpires), + }; + WebSocketService.Instance.banFromCommunity(form); + } else { + let form: BanUserForm = { + user_id: i.props.node.comment.creator_id, + ban: !i.props.node.comment.banned, + reason: i.state.banReason, + expires: getUnixTime(i.state.banExpires), + }; + WebSocketService.Instance.banUser(form); + } i.state.showBanDialog = false; i.setState(i.state); @@ -296,4 +382,13 @@ export class CommentNode extends Component { WebSocketService.Instance.addModToCommunity(form); i.setState(i.state); } + + addAdmin(i: CommentNode) { + let form: AddAdminForm = { + user_id: i.props.node.comment.creator_id, + added: !i.isAdmin, + }; + WebSocketService.Instance.addAdmin(form); + i.setState(i.state); + } } diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx index 498c69b8..abbb1719 100644 --- a/ui/src/components/comment-nodes.tsx +++ b/ui/src/components/comment-nodes.tsx @@ -1,5 +1,5 @@ import { Component } from 'inferno'; -import { CommentNode as CommentNodeI, CommunityUser } from '../interfaces'; +import { CommentNode as CommentNodeI, CommunityUser, UserView } from '../interfaces'; import { CommentNode } from './comment-node'; interface CommentNodesState { @@ -8,6 +8,7 @@ interface CommentNodesState { interface CommentNodesProps { nodes: Array; moderators?: Array; + admins?: Array; noIndent?: boolean; viewOnly?: boolean; locked?: boolean; @@ -27,7 +28,9 @@ export class CommentNodes extends Component + moderators={this.props.moderators} + admins={this.props.admins} + /> )}
    ) diff --git a/ui/src/components/communities.tsx b/ui/src/components/communities.tsx index 86800684..9145c1cd 100644 --- a/ui/src/components/communities.tsx +++ b/ui/src/components/communities.tsx @@ -53,9 +53,9 @@ export class Communities extends Component { return (
    {this.state.loading ? -

    : +
    :
    -

    Communities

    +
    Communities
    diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index cd95f991..6271bde5 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -60,14 +60,14 @@ export class Community extends Component { return (
    {this.state.loading ? -

    : +
    :
    -

    {this.state.community.title} +

    {this.state.community.title} {this.state.community.removed && removed } -
    +
    diff --git a/ui/src/components/create-community.tsx b/ui/src/components/create-community.tsx index 5f397411..0e806dbb 100644 --- a/ui/src/components/create-community.tsx +++ b/ui/src/components/create-community.tsx @@ -13,7 +13,7 @@ export class CreateCommunity extends Component {
    -

    Create Forum

    +
    Create Forum
    diff --git a/ui/src/components/create-post.tsx b/ui/src/components/create-post.tsx index 041ffd17..7d2f1dd4 100644 --- a/ui/src/components/create-post.tsx +++ b/ui/src/components/create-post.tsx @@ -13,7 +13,7 @@ export class CreatePost extends Component {
    -

    Create a Post

    +
    Create a Post
    diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx index 4d0b22d0..6d15a382 100644 --- a/ui/src/components/login.tsx +++ b/ui/src/components/login.tsx @@ -67,7 +67,7 @@ export class Login extends Component { return (
    -

    Login

    +
    Login
    @@ -94,7 +94,7 @@ export class Login extends Component { registerForm() { return ( -

    Sign Up

    +
    Sign Up
    diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 0b5923c0..01c70f94 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -78,12 +78,12 @@ export class Main extends Component {
    {this.state.loading ? -

    : +
    :
    {this.trendingCommunities()} {UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
    -

    Subscribed forums

    +
    Subscribed forums
      {this.state.subscribedCommunities.map(community =>
    • {community.community_name}
    • @@ -103,7 +103,7 @@ export class Main extends Component { trendingCommunities() { return (
      -

      Trending forums

      +
      Trending forums
        {this.state.trendingCommunities.map(community =>
      • {community.name}
      • @@ -116,7 +116,7 @@ export class Main extends Component { landing() { return (
        -

        {`${this.state.site.site.name}`}

        +
        {`${this.state.site.site.name}`}
        • {this.state.site.site.number_of_users} Users
        • {this.state.site.site.number_of_posts} Posts
        • @@ -136,10 +136,10 @@ export class Main extends Component {
        } -

        Welcome to +

        Welcome to LemmyBeta -
        +

        Lemmy is a link aggregator / reddit alternative, intended to work in the fediverse.

        Its self-hostable, has live-updating comment threads, and is tiny (~80kB). Federation into the ActivityPub network is on the roadmap.

        This is a very early beta version, and a lot of features are currently broken or missing.

        diff --git a/ui/src/components/modlog.tsx b/ui/src/components/modlog.tsx index 56b08a7e..7145b4f6 100644 --- a/ui/src/components/modlog.tsx +++ b/ui/src/components/modlog.tsx @@ -9,7 +9,7 @@ import { MomentTime } from './moment-time'; import * as moment from 'moment'; interface ModlogState { - combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}>, + combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity | ModAdd | ModBan}>, communityId?: number, communityName?: string, page: number; @@ -51,6 +51,8 @@ export class Modlog extends Component { let removed_communities = addTypeInfo(res.removed_communities, "removed_communities"); let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community"); let added_to_community = addTypeInfo(res.added_to_community, "added_to_community"); + let added = addTypeInfo(res.added, "added"); + let banned = addTypeInfo(res.banned, "banned"); this.state.combined = []; this.state.combined.push(...removed_posts); @@ -59,9 +61,11 @@ export class Modlog extends Component { this.state.combined.push(...removed_communities); this.state.combined.push(...banned_from_community); this.state.combined.push(...added_to_community); + this.state.combined.push(...added); + this.state.combined.push(...banned); if (this.state.communityId && this.state.combined.length > 0) { - this.state.communityName = this.state.combined[0].data.community_name; + this.state.communityName = (this.state.combined[0].data as ModRemovePost).community_name; } // Sort them by time @@ -95,13 +99,14 @@ export class Modlog extends Component { <> {(i.data as ModRemoveComment).removed? 'Removed' : 'Restored'} Comment {(i.data as ModRemoveComment).comment_content} + by {(i.data as ModRemoveComment).comment_user_name}
        {(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}
        } {i.type_ == 'removed_communities' && <> {(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'} - Community {i.data.community_name} + Community {(i.data as ModRemoveCommunity).community_name}
        {(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}
        {(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}
        @@ -110,6 +115,8 @@ export class Modlog extends Component { <> {(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} {(i.data as ModBanFromCommunity).other_user_name} + from the community + {(i.data as ModBanFromCommunity).community_name}
        {(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}
        {(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}
        @@ -119,12 +126,27 @@ export class Modlog extends Component { {(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} {(i.data as ModAddCommunity).other_user_name} as a mod to the community - {i.data.community_name} + {(i.data as ModAddCommunity).community_name} + + } + {i.type_ == 'banned' && + <> + {(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '} + {(i.data as ModBan).other_user_name} +
        {(i.data as ModBan).reason && ` reason: ${(i.data as ModBan).reason}`}
        +
        {(i.data as ModBan).expires && ` expires: ${moment.utc((i.data as ModBan).expires).fromNow()}`}
        + + } + {i.type_ == 'added' && + <> + {(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '} + {(i.data as ModAdd).other_user_name} + as an admin } - ) + ) } @@ -136,12 +158,12 @@ export class Modlog extends Component { return (
        {this.state.loading ? -

        : +
        :
        -

        +

        {this.state.communityName && /f/{this.state.communityName} } Modlog -
        +
    @@ -183,7 +205,7 @@ export class Modlog extends Component { i.setState(i.state); i.refetch(); } - + refetch(){ let modlogForm: GetModlogForm = { community_id: this.state.communityId, diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index ae693825..be98912e 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -64,16 +64,22 @@ export class Navbar extends Component { diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 1a52bf79..da375aee 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -1,7 +1,7 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { WebSocketService, UserService } from '../services'; -import { Post, CreatePostLikeForm, PostForm as PostFormI } from '../interfaces'; +import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm } from '../interfaces'; import { MomentTime } from './moment-time'; import { PostForm } from './post-form'; import { mdToHtml } from '../utils'; @@ -60,17 +60,17 @@ export class PostListing extends Component {
    {post.score}
    â–¼
    -
    +
    {post.url ?
    -

    {post.name} +

    {post.name} {post.removed && removed } {post.locked && locked } -
    + {(new URL(post.url)).hostname} { !this.state.iframeExpanded ? + @@ -83,14 +83,14 @@ export class PostListing extends Component { }
    - :

    {post.name} + :

    {post.name} {post.removed && removed } {post.locked && locked } -
    + }
    @@ -120,17 +120,20 @@ export class PostListing extends Component { {post.number_of_comments} Comments - {this.props.editable && + {UserService.Instance.user && this.props.editable &&
      +
    • + {this.props.post.saved ? 'unsave' : 'save'} +
    • {this.myPost && - + <>
    • edit
    • delete
    • -
      + } {this.props.post.am_mod && @@ -204,11 +207,23 @@ export class PostListing extends Component { url: '', edit_id: i.props.post.id, creator_id: i.props.post.creator_id, + removed: !i.props.post.removed, + locked: !i.props.post.locked, auth: null }; WebSocketService.Instance.editPost(deleteForm); } + handleSavePostClick(i: PostListing) { + let saved = (i.props.post.saved == undefined) ? true : !i.props.post.saved; + let form: SavePostForm = { + post_id: i.props.post.id, + save: saved + }; + + WebSocketService.Instance.savePost(form); + } + handleModRemoveShow(i: PostListing) { i.state.showRemoveDialog = true; i.setState(i.state); @@ -227,6 +242,7 @@ export class PostListing extends Component { edit_id: i.props.post.id, creator_id: i.props.post.creator_id, removed: !i.props.post.removed, + locked: !i.props.post.locked, reason: i.state.removeReason, auth: null, }; @@ -242,6 +258,7 @@ export class PostListing extends Component { community_id: i.props.post.community_id, edit_id: i.props.post.id, creator_id: i.props.post.creator_id, + removed: !i.props.post.removed, locked: !i.props.post.locked, auth: null, }; diff --git a/ui/src/components/post-listings.tsx b/ui/src/components/post-listings.tsx index 8fc19b30..b1e48a61 100644 --- a/ui/src/components/post-listings.tsx +++ b/ui/src/components/post-listings.tsx @@ -61,7 +61,7 @@ export class PostListings extends Component {this.state.loading ? -

      : +
      :
      {this.selects()} {this.state.posts.length > 0 diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index d79a6c97..64f56d88 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -1,7 +1,7 @@ import { Component, linkEvent } from 'inferno'; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; -import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, AddModToCommunityResponse } from '../interfaces'; +import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView } from '../interfaces'; import { WebSocketService } from '../services'; import { msgOp, hotRank } from '../utils'; import { PostListing } from './post-listing'; @@ -17,6 +17,7 @@ interface PostState { commentSort: CommentSortType; community: Community; moderators: Array; + admins: Array; scrolled?: boolean; scrolled_comment_id?: number; loading: boolean; @@ -31,6 +32,7 @@ export class Post extends Component { commentSort: CommentSortType.Hot, community: null, moderators: [], + admins: [], scrolled: false, loading: true } @@ -77,7 +79,7 @@ export class Post extends Component { return (
      {this.state.loading ? -

      : +
      :
      @@ -123,9 +125,15 @@ export class Post extends Component { newComments() { return (
      -

      New Comments

      +
      New Comments
      {this.state.comments.map(comment => - + )}
      ) @@ -187,8 +195,13 @@ export class Post extends Component { commentsTree() { let nodes = this.buildCommentsTree(); return ( -
      - +
      +
      ); } @@ -202,9 +215,11 @@ export class Post extends Component { } else if (op == UserOperation.GetPost) { let res: GetPostResponse = msg; this.state.post = res.post; + this.state.post = res.post; this.state.comments = res.comments; this.state.community = res.community; this.state.moderators = res.moderators; + this.state.admins = res.admins; this.state.loading = false; this.setState(this.state); } else if (op == UserOperation.CreateComment) { @@ -222,8 +237,12 @@ export class Post extends Component { found.score = res.comment.score; this.setState(this.state); - } - else if (op == UserOperation.CreateCommentLike) { + } else if (op == UserOperation.SaveComment) { + let res: CommentResponse = msg; + let found = this.state.comments.find(c => c.id == res.comment.id); + found.saved = res.comment.saved; + this.setState(this.state); + } else if (op == UserOperation.CreateCommentLike) { let res: CommentResponse = msg; let found: Comment = this.state.comments.find(c => c.id === res.comment.id); found.score = res.comment.score; @@ -243,6 +262,10 @@ export class Post extends Component { let res: PostResponse = msg; this.state.post = res.post; this.setState(this.state); + } else if (op == UserOperation.SavePost) { + let res: PostResponse = msg; + this.state.post = res.post; + this.setState(this.state); } else if (op == UserOperation.EditCommunity) { let res: CommunityResponse = msg; this.state.community = res.community; @@ -257,12 +280,21 @@ export class Post extends Component { } else if (op == UserOperation.BanFromCommunity) { let res: BanFromCommunityResponse = msg; this.state.comments.filter(c => c.creator_id == res.user.id) - .forEach(c => c.banned = res.banned); + .forEach(c => c.banned_from_community = res.banned); this.setState(this.state); } else if (op == UserOperation.AddModToCommunity) { let res: AddModToCommunityResponse = msg; this.state.moderators = res.moderators; this.setState(this.state); + } else if (op == UserOperation.BanUser) { + let res: BanUserResponse = msg; + this.state.comments.filter(c => c.creator_id == res.user.id) + .forEach(c => c.banned = res.banned); + this.setState(this.state); + } else if (op == UserOperation.AddAdmin) { + let res: AddAdminResponse = msg; + this.state.admins = res.admins; + this.setState(this.state); } } diff --git a/ui/src/components/setup.tsx b/ui/src/components/setup.tsx index 9560a60d..9a671359 100644 --- a/ui/src/components/setup.tsx +++ b/ui/src/components/setup.tsx @@ -61,7 +61,7 @@ export class Setup extends Component { registerUser() { return ( -

      Set up Site Administrator

      +
      Set up Site Administrator
      diff --git a/ui/src/components/sidebar.tsx b/ui/src/components/sidebar.tsx index b0c0b7b9..2f231f9a 100644 --- a/ui/src/components/sidebar.tsx +++ b/ui/src/components/sidebar.tsx @@ -48,11 +48,11 @@ export class Sidebar extends Component { let community = this.props.community; return (
      -

      {community.title} +

      {community.title} {community.removed && removed } -
      + /f/{community.name} {community.am_mod &&
        diff --git a/ui/src/components/site-form.tsx b/ui/src/components/site-form.tsx index 7ca45b86..55da1667 100644 --- a/ui/src/components/site-form.tsx +++ b/ui/src/components/site-form.tsx @@ -33,7 +33,7 @@ export class SiteForm extends Component { render() { return ( -

        {`${this.props.site ? 'Edit' : 'Name'} your Site`}

        +
        {`${this.props.site ? 'Edit' : 'Name'} your Site`}
        diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index fdcd378e..8ebde48a 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -77,7 +77,7 @@ export class User extends Component {
        -

        /u/{this.state.user.name}

        +
        /u/{this.state.user.name}
        {this.selects()} {this.state.view == View.Overview && this.overview() @@ -88,6 +88,9 @@ export class User extends Component { {this.state.view == View.Posts && this.posts() } + {this.state.view == View.Saved && + this.overview() + } {this.paginator()}
        @@ -108,7 +111,7 @@ export class User extends Component { - {/* */} +
    @@ -200,7 +203,7 @@ export class User extends Component {
    {this.state.moderates.length > 0 &&
    -

    Moderates

    +
    Moderates
      {this.state.moderates.map(community =>
    • {community.community_name}
    • @@ -218,7 +221,7 @@ export class User extends Component { {this.state.follows.length > 0 &&

      -

      Subscribed

      +
      Subscribed
        {this.state.follows.map(community =>
      • {community.community_name}
      • @@ -257,6 +260,7 @@ export class User extends Component { let form: GetUserDetailsForm = { user_id: this.state.user_id, sort: SortType[this.state.sort], + saved_only: this.state.view == View.Saved, page: this.state.page, limit: fetchLimit, }; diff --git a/ui/src/index.html b/ui/src/index.html index 5b1f84af..efa5b969 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -7,9 +7,7 @@ Lemmy - - diff --git a/ui/src/index.tsx b/ui/src/index.tsx index cefcac02..d830bd3a 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -15,7 +15,8 @@ import { Modlog } from './components/modlog'; import { Setup } from './components/setup'; import { Symbols } from './components/symbols'; -import './main.css'; +import './css/bootstrap.min.css'; +import './css/main.css'; import { WebSocketService, UserService } from './services'; diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 6affc0e1..4a4ee643 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, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser + Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser } export enum CommentSortType { @@ -41,65 +41,69 @@ export interface CommunityUser { } export interface Community { - user_id?: number; - subscribed?: boolean; - am_mod?: boolean; - removed?: boolean; id: number; name: string; title: string; description?: string; - creator_id: number; - creator_name: string; category_id: number; + creator_id: number; + removed: boolean; + published: string; + updated?: string; + creator_name: string; category_name: string; number_of_subscribers: number; number_of_posts: number; number_of_comments: number; - published: string; - updated?: string; + user_id?: number; + subscribed?: boolean; } export interface Post { - user_id?: number; - my_vote?: number; - am_mod?: boolean; - removed?: boolean; - locked?: boolean; id: number; name: string; url?: string; body?: string; creator_id: number; - creator_name: string; community_id: number; + removed: boolean; + locked: boolean; + published: string; + updated?: string; + creator_name: string; community_name: string; number_of_comments: number; score: number; upvotes: number; downvotes: number; hot_rank: number; - published: string; - updated?: string; + user_id?: number; + my_vote?: number; + subscribed?: boolean; + read?: boolean; + saved?: boolean; } export interface Comment { id: number; - content: string; creator_id: number; - creator_name: string; post_id: number, - community_id: number, parent_id?: number; + content: string; + removed: boolean; + read: boolean; published: string; updated?: string; + community_id: number, + banned: boolean; + banned_from_community: boolean; + creator_name: string; score: number; upvotes: number; downvotes: number; + user_id?: number; my_vote?: number; - am_mod?: boolean; - removed?: boolean; - banned?: boolean; + saved?: boolean; } export interface Category { @@ -137,7 +141,7 @@ export interface GetUserDetailsForm { page?: number; limit?: number; community_id?: number; - auth?: string; + saved_only: boolean; } export interface UserDetailsResponse { @@ -147,7 +151,6 @@ export interface UserDetailsResponse { moderates: Array; comments: Array; posts: Array; - saved?: Array; } export interface BanFromCommunityForm { @@ -324,7 +327,7 @@ export interface CommunityForm { description?: string, category_id: number, edit_id?: number; - removed?: boolean; + removed: boolean; reason?: string; expires?: number; auth?: string; @@ -367,9 +370,9 @@ export interface PostForm { updated?: number; edit_id?: number; creator_id: number; - removed?: boolean; + removed: boolean; + locked: boolean; reason?: string; - locked?: boolean; auth: string; } @@ -379,6 +382,13 @@ export interface GetPostResponse { comments: Array; community: Community; moderators: Array; + admins: Array; +} + +export interface SavePostForm { + post_id: number; + save: boolean; + auth?: string; } export interface PostResponse { @@ -392,11 +402,17 @@ export interface CommentForm { parent_id?: number; edit_id?: number; creator_id: number; - removed?: boolean; + removed: boolean; reason?: string; auth: string; } +export interface SaveCommentForm { + comment_id: number; + save: boolean; + auth?: string; +} + export interface CommentResponse { op: string; comment: Comment; diff --git a/ui/src/main.css b/ui/src/main.css deleted file mode 100644 index 3fbb6eff..00000000 --- a/ui/src/main.css +++ /dev/null @@ -1,87 +0,0 @@ -body { - font-family: 'Open Sans', sans-serif; -} - -.pointer { - cursor: pointer; -} - -.no-click { - pointer-events:none; - opacity: 0.65; -} - -.upvote:hover { - color: var(--info); -} - -.downvote:hover { - color: var(--danger); -} - -.form-control, .form-control:focus { - background-color: var(--secondary); - color: #fff; -} - -.form-control:disabled { - background-color: var(--secondary); - opacity: .5; -} - -.custom-select { - color: #fff; - background-color: var(--secondary); -} - -.mark { - background-color: #322a00; -} - -.md-div p { - margin-bottom: 0px; -} - -.md-div img { - max-width: 100%; - height: auto; -} - -.listing { - min-height: 61px; -} - -.icon { - display: inline-flex; - width: 1em; - height: 1em; - stroke-width: 0; - stroke: currentColor; - fill: currentColor; - vertical-align: middle; - align-self: center; -} - - -.spin { - animation: spins 2s linear infinite; -} - -@keyframes spins { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(359deg); } -} - -.dropdown-menu { - z-index: 2000; -} - -.navbar-bg { - background-color: #222; -} - -blockquote { - border-left: 3px solid #ccc; - margin: 0.5em 5px; - padding: 0.1em 5px; -} diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 80555fd9..b2c2a9e0 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, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, SiteForm, Site, UserView } from '../interfaces'; +import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, AddAdminForm, BanUserForm, SiteForm, Site, UserView } from '../interfaces'; import { webSocket } from 'rxjs/webSocket'; import { Subject } from 'rxjs'; import { retryWhen, delay, take } from 'rxjs/operators'; @@ -96,6 +96,11 @@ export class WebSocketService { this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form)); } + public saveComment(form: SaveCommentForm) { + this.setAuth(form); + this.subject.next(this.wsSendWrapper(UserOperation.SaveComment, form)); + } + public getPosts(form: GetPostsForm) { this.setAuth(form, false); this.subject.next(this.wsSendWrapper(UserOperation.GetPosts, form)); @@ -111,6 +116,11 @@ export class WebSocketService { this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm)); } + public savePost(form: SavePostForm) { + this.setAuth(form); + this.subject.next(this.wsSendWrapper(UserOperation.SavePost, form)); + } + public banFromCommunity(form: BanFromCommunityForm) { this.setAuth(form); this.subject.next(this.wsSendWrapper(UserOperation.BanFromCommunity, form)); @@ -121,8 +131,17 @@ export class WebSocketService { this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form)); } + public banUser(form: BanUserForm) { + this.setAuth(form); + this.subject.next(this.wsSendWrapper(UserOperation.BanUser, form)); + } + + public addAdmin(form: AddAdminForm) { + this.setAuth(form); + this.subject.next(this.wsSendWrapper(UserOperation.AddAdmin, form)); + } + public getUserDetails(form: GetUserDetailsForm) { - this.setAuth(form, false); this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form)); } diff --git a/ui/src/utils.ts b/ui/src/utils.ts index c7f3bad8..61744e90 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -1,4 +1,4 @@ -import { UserOperation, Comment } from './interfaces'; +import { UserOperation, Comment, User } from './interfaces'; import * as markdown_it from 'markdown-it'; export let repoUrl = 'https://github.com/dessalines/lemmy'; @@ -40,4 +40,23 @@ export function addTypeInfo(arr: Array, name: string): Array<{type_: strin return arr.map(e => {return {type_: name, data: e}}); } +export function canMod(user: User, modIds: Array, creator_id: number): boolean { + // You can do moderator actions only on the mods added after you. + if (user) { + let yourIndex = modIds.findIndex(id => id == user.id); + if (yourIndex == -1) { + return false; + } else { + modIds = modIds.slice(0, yourIndex+1); // +1 cause you cant mod yourself + return !modIds.includes(creator_id); + } + } else { + return false; + } +} + +export function isMod(modIds: Array, creator_id: number): boolean { + return modIds.includes(creator_id); +} + export let fetchLimit: number = 20; From e14e6e53cd969039087df17bc4407d1b7444e05d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 20 Apr 2019 00:24:59 -0700 Subject: [PATCH 2/3] More moderation fixed --- server/src/actions/comment.rs | 6 ++-- server/src/actions/comment_view.rs | 6 ++-- server/src/actions/community.rs | 4 +-- server/src/actions/moderator.rs | 6 ++-- server/src/actions/post.rs | 10 +++--- server/src/actions/post_view.rs | 6 ++-- server/src/websocket_server/server.rs | 44 ++++++++++++++++----------- ui/src/components/comment-node.tsx | 7 ++--- ui/src/components/post-listing.tsx | 41 ++++++++++++++++++++----- ui/src/components/post.tsx | 9 +++++- ui/src/interfaces.ts | 6 ++-- 11 files changed, 93 insertions(+), 52 deletions(-) diff --git a/server/src/actions/comment.rs b/server/src/actions/comment.rs index c3aa0107..36da90c6 100644 --- a/server/src/actions/comment.rs +++ b/server/src/actions/comment.rs @@ -184,7 +184,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: false, + removed: None, updated: None }; @@ -196,8 +196,8 @@ mod tests { url: None, body: None, community_id: inserted_community.id, - removed: false, - locked: false, + removed: None, + locked: None, updated: None }; diff --git a/server/src/actions/comment_view.rs b/server/src/actions/comment_view.rs index 36043716..4e3d99b7 100644 --- a/server/src/actions/comment_view.rs +++ b/server/src/actions/comment_view.rs @@ -168,7 +168,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: false, + removed: None, updated: None }; @@ -180,8 +180,8 @@ mod tests { url: None, body: None, community_id: inserted_community.id, - removed: false, - locked: false, + removed: None, + locked: None, updated: None }; diff --git a/server/src/actions/community.rs b/server/src/actions/community.rs index 594518ba..7a69c807 100644 --- a/server/src/actions/community.rs +++ b/server/src/actions/community.rs @@ -27,7 +27,7 @@ pub struct CommunityForm { pub description: Option, pub category_id: i32, pub creator_id: i32, - pub removed: bool, + pub removed: Option, pub updated: Option } @@ -236,7 +236,7 @@ mod tests { title: "nada".to_owned(), description: None, category_id: 1, - removed: false, + removed: None, updated: None, }; diff --git a/server/src/actions/moderator.rs b/server/src/actions/moderator.rs index e0d885ce..a97b2120 100644 --- a/server/src/actions/moderator.rs +++ b/server/src/actions/moderator.rs @@ -441,7 +441,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: false, + removed: None, updated: None }; @@ -453,8 +453,8 @@ mod tests { body: None, creator_id: inserted_user.id, community_id: inserted_community.id, - removed: false, - locked: false, + removed: None, + locked: None, updated: None }; diff --git a/server/src/actions/post.rs b/server/src/actions/post.rs index 0fd0e5c5..4dd4561d 100644 --- a/server/src/actions/post.rs +++ b/server/src/actions/post.rs @@ -28,8 +28,8 @@ pub struct PostForm { pub body: Option, pub creator_id: i32, pub community_id: i32, - pub removed: bool, - pub locked: bool, + pub removed: Option, + pub locked: Option, pub updated: Option } @@ -198,7 +198,7 @@ mod tests { description: None, category_id: 1, creator_id: inserted_user.id, - removed: false, + removed: None, updated: None }; @@ -210,8 +210,8 @@ mod tests { body: None, creator_id: inserted_user.id, community_id: inserted_community.id, - removed: false, - locked: false, + removed: None, + locked: None, updated: None }; diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs index 78fcef63..28e5fb98 100644 --- a/server/src/actions/post_view.rs +++ b/server/src/actions/post_view.rs @@ -200,7 +200,7 @@ mod tests { description: None, creator_id: inserted_user.id, category_id: 1, - removed: false, + removed: None, updated: None }; @@ -212,8 +212,8 @@ mod tests { body: None, creator_id: inserted_user.id, community_id: inserted_community.id, - removed: false, - locked: false, + removed: None, + locked: None, updated: None }; diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index d1f72109..63d767c2 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -261,8 +261,8 @@ pub struct EditPost { name: String, url: Option, body: Option, - removed: bool, - locked: bool, + removed: Option, + locked: Option, reason: Option, auth: String } @@ -281,7 +281,7 @@ pub struct EditCommunity { title: String, description: Option, category_id: i32, - removed: bool, + removed: Option, reason: Option, expires: Option, auth: String @@ -836,14 +836,13 @@ impl Perform for CreateCommunity { } // When you create a community, make sure the user becomes a moderator and a follower - let community_form = CommunityForm { name: self.name.to_owned(), title: self.title.to_owned(), description: self.description.to_owned(), category_id: self.category_id, creator_id: user_id, - removed: false, + removed: None, updated: None, }; @@ -988,8 +987,8 @@ impl Perform for CreatePost { body: self.body.to_owned(), community_id: self.community_id, creator_id: user_id, - removed: false, - locked: false, + removed: None, + locked: None, updated: None }; @@ -1612,15 +1611,24 @@ impl Perform for EditPost { let user_id = claims.id; - // Verify its the creator or a mod - let mut editors: Vec = CommunityModeratorView::for_community(&conn, self.community_id) + // Verify its the creator or a mod or admin + let mut editors: Vec = vec![self.creator_id]; + editors.append( + &mut CommunityModeratorView::for_community(&conn, self.community_id) .unwrap() .into_iter() .map(|m| m.user_id) - .collect(); - editors.push(self.creator_id); + .collect() + ); + editors.append( + &mut UserView::admins(&conn) + .unwrap() + .into_iter() + .map(|a| a.id) + .collect() + ); if !editors.contains(&user_id) { - return self.error("Not allowed to edit comment."); + return self.error("Not allowed to edit post."); } // Check for a community ban @@ -1652,21 +1660,21 @@ impl Perform for EditPost { }; // Mod tables - if self.removed { + if let Some(removed) = self.removed.to_owned() { let form = ModRemovePostForm { mod_user_id: user_id, post_id: self.edit_id, - removed: Some(self.removed), + removed: Some(removed), reason: self.reason.to_owned(), }; ModRemovePost::create(&conn, &form).unwrap(); } - if self.locked { + if let Some(locked) = self.locked.to_owned() { let form = ModLockPostForm { mod_user_id: user_id, post_id: self.edit_id, - locked: Some(self.locked), + locked: Some(locked), }; ModLockPost::create(&conn, &form).unwrap(); } @@ -1803,7 +1811,7 @@ impl Perform for EditCommunity { }; // Mod tables - if self.removed { + if let Some(removed) = self.removed.to_owned() { let expires = match self.expires { Some(time) => Some(naive_from_unix(time)), None => None @@ -1811,7 +1819,7 @@ impl Perform for EditCommunity { let form = ModRemoveCommunityForm { mod_user_id: user_id, community_id: self.edit_id, - removed: Some(self.removed), + removed: Some(removed), reason: self.reason.to_owned(), expires: expires }; diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index c1fc059b..90cf5a54 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -212,15 +212,15 @@ export class CommentNode extends Component { } get isMod(): boolean { - return isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id); + return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id); } get isAdmin(): boolean { - return isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id); + return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id); } get canAdmin(): boolean { - return canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id); + return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id); } handleReplyClick(i: CommentNode) { @@ -240,7 +240,6 @@ export class CommentNode extends Component { creator_id: i.props.node.comment.creator_id, post_id: i.props.node.comment.post_id, parent_id: i.props.node.comment.parent_id, - removed: i.props.node.comment.removed, auth: null }; WebSocketService.Instance.editComment(deleteForm); diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index da375aee..7103a8cf 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -1,10 +1,10 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { WebSocketService, UserService } from '../services'; -import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm } from '../interfaces'; +import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, CommunityUser, UserView } from '../interfaces'; import { MomentTime } from './moment-time'; import { PostForm } from './post-form'; -import { mdToHtml } from '../utils'; +import { mdToHtml, canMod, isMod } from '../utils'; interface PostListingState { showEdit: boolean; @@ -19,6 +19,8 @@ interface PostListingProps { showCommunity?: boolean; showBody?: boolean; viewOnly?: boolean; + moderators?: Array; + admins?: Array; } export class PostListing extends Component { @@ -98,6 +100,12 @@ export class PostListing extends Component {
      • by {post.creator_name} + {this.isMod && + mod + } + {this.isAdmin && + admin + } {this.props.showCommunity && to @@ -135,7 +143,7 @@ export class PostListing extends Component {
      • } - {this.props.post.am_mod && + {this.canMod &&
      • {!this.props.post.removed ? @@ -166,6 +174,29 @@ export class PostListing extends Component { return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id; } + get canMod(): boolean { + + if (this.props.editable) { + let adminsThenMods = this.props.admins.map(a => a.id) + .concat(this.props.moderators.map(m => m.user_id)); + + return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id); + + } else return false; + } + + get isMod(): boolean { + return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id); + } + + get isAdmin(): boolean { + return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id); + } + + get canAdmin(): boolean { + return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.post.creator_id); + } + handlePostLike(i: PostListing) { let form: CreatePostLikeForm = { @@ -207,8 +238,6 @@ export class PostListing extends Component { url: '', edit_id: i.props.post.id, creator_id: i.props.post.creator_id, - removed: !i.props.post.removed, - locked: !i.props.post.locked, auth: null }; WebSocketService.Instance.editPost(deleteForm); @@ -242,7 +271,6 @@ export class PostListing extends Component { edit_id: i.props.post.id, creator_id: i.props.post.creator_id, removed: !i.props.post.removed, - locked: !i.props.post.locked, reason: i.state.removeReason, auth: null, }; @@ -258,7 +286,6 @@ export class PostListing extends Component { community_id: i.props.post.community_id, edit_id: i.props.post.id, creator_id: i.props.post.creator_id, - removed: !i.props.post.removed, locked: !i.props.post.locked, auth: null, }; diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 64f56d88..56b73f6e 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -82,7 +82,14 @@ export class Post extends Component {
        :
        - +
        {this.sortRadios()} diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 4a4ee643..8927a171 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -370,8 +370,8 @@ export interface PostForm { updated?: number; edit_id?: number; creator_id: number; - removed: boolean; - locked: boolean; + removed?: boolean; + locked?: boolean; reason?: string; auth: string; } @@ -402,7 +402,7 @@ export interface CommentForm { parent_id?: number; edit_id?: number; creator_id: number; - removed: boolean; + removed?: boolean; reason?: string; auth: string; } From 682413613e75618489d262bd033c3101da96abd7 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 20 Apr 2019 11:17:00 -0700 Subject: [PATCH 3/3] Mostly working, before merge --- .../down.sql | 1 + .../up.sql | 25 +++ server/src/actions/comment.rs | 3 + server/src/actions/comment_view.rs | 101 ++++++++++ server/src/websocket_server/server.rs | 57 +++++- ui/src/components/comment-node.tsx | 24 ++- ui/src/components/comment-nodes.tsx | 2 + ui/src/components/inbox.tsx | 177 ++++++++++++++++++ ui/src/components/main.tsx | 18 +- ui/src/components/navbar.tsx | 16 +- ui/src/components/post.tsx | 1 - ui/src/css/main.css | 7 + ui/src/index.tsx | 2 + ui/src/interfaces.ts | 16 +- ui/src/services/UserService.ts | 7 +- ui/src/services/WebSocketService.ts | 7 +- 16 files changed, 450 insertions(+), 14 deletions(-) create mode 100644 ui/src/components/inbox.tsx diff --git a/server/migrations/2019-04-03-155309_create_comment_view/down.sql b/server/migrations/2019-04-03-155309_create_comment_view/down.sql index 2da934a4..c19d5ff7 100644 --- a/server/migrations/2019-04-03-155309_create_comment_view/down.sql +++ b/server/migrations/2019-04-03-155309_create_comment_view/down.sql @@ -1 +1,2 @@ +drop view reply_view; drop view comment_view; diff --git a/server/migrations/2019-04-03-155309_create_comment_view/up.sql b/server/migrations/2019-04-03-155309_create_comment_view/up.sql index a78e3ac3..24ce98fc 100644 --- a/server/migrations/2019-04-03-155309_create_comment_view/up.sql +++ b/server/migrations/2019-04-03-155309_create_comment_view/up.sql @@ -33,3 +33,28 @@ select null as saved from all_comment ac ; + +create view reply_view as +with closereply as ( + select + c2.id, + c2.creator_id as sender_id, + c.creator_id as recipient_id + from comment c + inner join comment c2 on c.id = c2.parent_id + where c2.creator_id != c.creator_id + -- Do union where post is null + union + select + c.id, + c.creator_id as sender_id, + p.creator_id as recipient_id + from comment c, post p + where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id +) +select cv.*, +closereply.recipient_id +from comment_view cv, closereply +where closereply.id = cv.id +; + diff --git a/server/src/actions/comment.rs b/server/src/actions/comment.rs index 36da90c6..9bb6d018 100644 --- a/server/src/actions/comment.rs +++ b/server/src/actions/comment.rs @@ -36,6 +36,7 @@ pub struct CommentForm { pub parent_id: Option, pub content: String, pub removed: Option, + pub read: Option, pub updated: Option } @@ -208,6 +209,7 @@ mod tests { creator_id: inserted_user.id, post_id: inserted_post.id, removed: None, + read: None, parent_id: None, updated: None }; @@ -232,6 +234,7 @@ mod tests { post_id: inserted_post.id, parent_id: Some(inserted_comment.id), removed: None, + read: None, updated: None }; diff --git a/server/src/actions/comment_view.rs b/server/src/actions/comment_view.rs index 4e3d99b7..e8b96e3a 100644 --- a/server/src/actions/comment_view.rs +++ b/server/src/actions/comment_view.rs @@ -136,6 +136,107 @@ impl CommentView { } +// The faked schema since diesel doesn't do views +table! { + reply_view (id) { + id -> Int4, + creator_id -> Int4, + post_id -> Int4, + parent_id -> Nullable, + content -> Text, + removed -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + community_id -> Int4, + banned -> Bool, + banned_from_community -> Bool, + creator_name -> Varchar, + score -> BigInt, + upvotes -> BigInt, + downvotes -> BigInt, + user_id -> Nullable, + my_vote -> Nullable, + saved -> Nullable, + recipient_id -> Int4, + } +} + +#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)] +#[table_name="reply_view"] +pub struct ReplyView { + pub id: i32, + pub creator_id: i32, + pub post_id: i32, + pub parent_id: Option, + pub content: String, + pub removed: bool, + pub read: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub community_id: i32, + pub banned: bool, + pub banned_from_community: bool, + pub creator_name: String, + pub score: i64, + pub upvotes: i64, + pub downvotes: i64, + pub user_id: Option, + pub my_vote: Option, + pub saved: Option, + pub recipient_id: i32, +} + +impl ReplyView { + + pub fn get_replies(conn: &PgConnection, + for_user_id: i32, + sort: &SortType, + unread_only: bool, + page: Option, + limit: Option, + ) -> Result, Error> { + use actions::comment_view::reply_view::dsl::*; + + let (limit, offset) = limit_and_offset(page, limit); + + let mut query = reply_view.into_boxed(); + + query = query + .filter(user_id.eq(for_user_id)) + .filter(recipient_id.eq(for_user_id)); + + if unread_only { + query = query.filter(read.eq(false)); + } + + query = match sort { + // SortType::Hot => query.order_by(hot_rank.desc()), + SortType::New => query.order_by(published.desc()), + SortType::TopAll => query.order_by(score.desc()), + SortType::TopYear => query + .filter(published.gt(now - 1.years())) + .order_by(score.desc()), + SortType::TopMonth => query + .filter(published.gt(now - 1.months())) + .order_by(score.desc()), + SortType::TopWeek => query + .filter(published.gt(now - 1.weeks())) + .order_by(score.desc()), + SortType::TopDay => query + .filter(published.gt(now - 1.days())) + .order_by(score.desc()), + _ => query.order_by(published.desc()) + }; + + query + .limit(limit) + .offset(offset) + .load::(conn) + } + +} + #[cfg(test)] mod tests { use establish_connection; diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index 63d767c2..dbcf5c5d 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -26,7 +26,7 @@ use actions::moderator::*; #[derive(EnumString,ToString,Debug)] pub enum UserOperation { - Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser + Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser } #[derive(Serialize, Deserialize)] @@ -215,6 +215,7 @@ pub struct EditComment { post_id: i32, removed: Option, reason: Option, + read: Option, auth: String } @@ -439,6 +440,21 @@ pub struct BanUserResponse { banned: bool, } +#[derive(Serialize, Deserialize)] +pub struct GetReplies { + sort: String, + page: Option, + limit: Option, + unread_only: bool, + auth: String +} + +#[derive(Serialize, Deserialize)] +pub struct GetRepliesResponse { + op: String, + replies: Vec, +} + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive pub struct ChatServer { @@ -671,6 +687,10 @@ impl Handler for ChatServer { let ban_user: BanUser = serde_json::from_str(data).unwrap(); ban_user.perform(self, msg.id) }, + UserOperation::GetReplies => { + let get_replies: GetReplies = serde_json::from_str(data).unwrap(); + get_replies.perform(self, msg.id) + }, }; MessageResult(res) @@ -1181,6 +1201,7 @@ impl Perform for CreateComment { post_id: self.post_id, creator_id: user_id, removed: None, + read: None, updated: None }; @@ -1292,6 +1313,7 @@ impl Perform for EditComment { post_id: self.post_id, creator_id: self.creator_id, removed: self.removed.to_owned(), + read: self.read.to_owned(), updated: Some(naive_now()) }; @@ -2027,6 +2049,39 @@ impl Perform for GetModlog { } } +impl Perform for GetReplies { + fn op_type(&self) -> UserOperation { + UserOperation::GetReplies + } + + 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 sort = SortType::from_str(&self.sort).expect("listing sort"); + + let replies = ReplyView::get_replies(&conn, user_id, &sort, self.unread_only, self.page, self.limit).unwrap(); + + // Return the jwt + serde_json::to_string( + &GetRepliesResponse { + op: self.op_type().to_string(), + replies: replies, + } + ) + .unwrap() + } +} + impl Perform for BanFromCommunity { fn op_type(&self) -> UserOperation { UserOperation::BanFromCommunity diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index 90cf5a54..cf7b1bce 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -25,6 +25,7 @@ interface CommentNodeProps { noIndent?: boolean; viewOnly?: boolean; locked?: boolean; + markable?: boolean; moderators: Array; admins: Array; } @@ -146,7 +147,7 @@ export class CommentNode extends Component { } {!this.props.node.comment.banned &&
      • - {`${this.isAdmin ? 'remove' : 'appoint'} as admin`} + {`${this.isAdmin ? 'remove' : 'appoint'} as admin`}
      • } @@ -156,6 +157,11 @@ export class CommentNode extends Component {
      • link
      • + {this.props.markable && +
      • + {`mark as ${node.comment.read ? 'unread' : 'read'}`} +
      • + }
      } @@ -309,6 +315,20 @@ export class CommentNode extends Component { i.setState(i.state); } + handleMarkRead(i: CommentNode) { + let form: CommentFormI = { + content: i.props.node.comment.content, + edit_id: i.props.node.comment.id, + creator_id: i.props.node.comment.creator_id, + post_id: i.props.node.comment.post_id, + parent_id: i.props.node.comment.parent_id, + read: !i.props.node.comment.read, + auth: null + }; + WebSocketService.Instance.editComment(form); + } + + handleModBanFromCommunityShow(i: CommentNode) { i.state.showBanDialog = true; i.state.banType = BanType.Community; @@ -382,7 +402,7 @@ export class CommentNode extends Component { i.setState(i.state); } - addAdmin(i: CommentNode) { + handleAddAdmin(i: CommentNode) { let form: AddAdminForm = { user_id: i.props.node.comment.creator_id, added: !i.isAdmin, diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx index abbb1719..da67bbc7 100644 --- a/ui/src/components/comment-nodes.tsx +++ b/ui/src/components/comment-nodes.tsx @@ -12,6 +12,7 @@ interface CommentNodesProps { noIndent?: boolean; viewOnly?: boolean; locked?: boolean; + markable?: boolean; } export class CommentNodes extends Component { @@ -30,6 +31,7 @@ export class CommentNodes extends Component )}
    diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx new file mode 100644 index 00000000..e6ce6d13 --- /dev/null +++ b/ui/src/components/inbox.tsx @@ -0,0 +1,177 @@ +import { Component, linkEvent } from 'inferno'; +import { Link } from 'inferno-router'; +import { Subscription } from "rxjs"; +import { retryWhen, delay, take } from 'rxjs/operators'; +import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, CommentResponse } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; +import { msgOp } from '../utils'; +import { CommentNodes } from './comment-nodes'; + +enum UnreadType { + Unread, All +} + +interface InboxState { + unreadType: UnreadType; + replies: Array; + sort: SortType; + page: number; +} + +export class Inbox extends Component { + + private subscription: Subscription; + private emptyState: InboxState = { + unreadType: UnreadType.Unread, + replies: [], + sort: SortType.New, + page: 1, + } + + constructor(props: any, context: any) { + super(props, context); + + this.state = this.emptyState; + + this.subscription = WebSocketService.Instance.subject + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + (msg) => this.parseMessage(msg), + (err) => console.error(err), + () => console.log('complete') + ); + + this.refetch(); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + let user = UserService.Instance.user; + return ( +
    +
    +
    +
    Inbox for {user.username}
    + {this.selects()} + {this.replies()} + {this.paginator()} +
    +
    +
    + ) + } + + selects() { + return ( +
    + + +
    + ) + + } + + replies() { + return ( +
    + {this.state.replies.map(reply => + + )} +
    + ); + } + + paginator() { + return ( +
    + {this.state.page > 1 && + + } + +
    + ); + } + + nextPage(i: Inbox) { + i.state.page++; + i.setState(i.state); + i.refetch(); + } + + prevPage(i: Inbox) { + i.state.page--; + i.setState(i.state); + i.refetch(); + } + + handleUnreadTypeChange(i: Inbox, event: any) { + i.state.unreadType = Number(event.target.value); + i.state.page = 1; + i.setState(i.state); + i.refetch(); + } + + refetch() { + let form: GetRepliesForm = { + sort: SortType[this.state.sort], + unread_only: (this.state.unreadType == UnreadType.Unread), + page: this.state.page, + limit: 9999, + }; + WebSocketService.Instance.getReplies(form); + } + + handleSortChange(i: Inbox, event: any) { + i.state.sort = Number(event.target.value); + i.state.page = 1; + i.setState(i.state); + i.refetch(); + } + + parseMessage(msg: any) { + console.log(msg); + let op: UserOperation = msgOp(msg); + if (msg.error) { + alert(msg.error); + return; + } else if (op == UserOperation.GetReplies) { + let res: GetRepliesResponse = msg; + this.state.replies = res.replies; + this.sendRepliesCount(); + this.setState(this.state); + } else if (op == UserOperation.EditComment) { + let res: CommentResponse = msg; + + // If youre in the unread view, just remove it from the list + if (this.state.unreadType == UnreadType.Unread && res.comment.read) { + this.state.replies = this.state.replies.filter(r => r.id !== res.comment.id); + } else { + let found = this.state.replies.find(c => c.id == res.comment.id); + found.read = res.comment.read; + } + + this.sendRepliesCount(); + this.setState(this.state); + } + } + + sendRepliesCount() { + UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: this.state.replies.filter(r => !r.read).length}); + } +} + diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 01c70f94..e3d6f844 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -2,7 +2,7 @@ import { Component } from 'inferno'; import { Link } from 'inferno-router'; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; -import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse } from '../interfaces'; +import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, GetRepliesResponse, GetRepliesForm } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { PostListings } from './post-listings'; import { msgOp, repoUrl, mdToHtml } from '../utils'; @@ -55,6 +55,15 @@ export class Main extends Component { if (UserService.Instance.user) { WebSocketService.Instance.getFollowedCommunities(); + + // Get replies for the count + let repliesForm: GetRepliesForm = { + sort: SortType[SortType.New], + unread_only: true, + page: 1, + limit: 9999, + }; + WebSocketService.Instance.getReplies(repliesForm); } let listCommunitiesForm: ListCommunitiesForm = { @@ -176,7 +185,14 @@ export class Main extends Component { this.state.site.site = res.site; this.state.site.banned = res.banned; this.setState(this.state); + } else if (op == UserOperation.GetReplies) { + let res: GetRepliesResponse = msg; + this.sendRepliesCount(res); } } + + sendRepliesCount(res: GetRepliesResponse) { + UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length}); + } } diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index be98912e..fed49e6f 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -7,12 +7,14 @@ interface NavbarState { isLoggedIn: boolean; expanded: boolean; expandUserDropdown: boolean; + unreadCount: number; } export class Navbar extends Component { emptyState: NavbarState = { - isLoggedIn: UserService.Instance.user !== undefined, + isLoggedIn: (UserService.Instance.user !== undefined), + unreadCount: 0, expanded: false, expandUserDropdown: false } @@ -24,8 +26,9 @@ export class Navbar extends Component { // Subscribe to user changes UserService.Instance.sub.subscribe(user => { - let loggedIn: boolean = user !== undefined; - this.setState({isLoggedIn: loggedIn}); + this.state.isLoggedIn = user.user !== undefined; + this.state.unreadCount = user.unreadCount; + this.setState(this.state); }); } @@ -65,9 +68,13 @@ export class Navbar extends Component {