]> Untitled Git - lemmy.git/commitdiff
Saving replies, the actual fixes will be in the merge to dev.
authorDessalines <tyhou13@gmx.com>
Sat, 20 Apr 2019 04:06:25 +0000 (21:06 -0700)
committerDessalines <tyhou13@gmx.com>
Sat, 20 Apr 2019 04:06:25 +0000 (21:06 -0700)
42 files changed:
server/migrations/2019-02-27-170003_create_community/up.sql
server/migrations/2019-03-03-163336_create_post/down.sql
server/migrations/2019-03-03-163336_create_post/up.sql
server/migrations/2019-03-05-233828_create_comment/down.sql
server/migrations/2019-03-05-233828_create_comment/up.sql
server/migrations/2019-03-30-212058_create_post_view/up.sql
server/migrations/2019-04-03-155205_create_community_view/up.sql
server/migrations/2019-04-03-155309_create_comment_view/up.sql
server/migrations/2019-04-11-144915_create_mod_views/up.sql
server/src/actions/comment.rs
server/src/actions/comment_view.rs
server/src/actions/community.rs
server/src/actions/community_view.rs
server/src/actions/moderator.rs
server/src/actions/post.rs
server/src/actions/post_view.rs
server/src/lib.rs
server/src/schema.rs
server/src/websocket_server/server.rs
ui/src/components/comment-node.tsx
ui/src/components/comment-nodes.tsx
ui/src/components/communities.tsx
ui/src/components/community.tsx
ui/src/components/create-community.tsx
ui/src/components/create-post.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/modlog.tsx
ui/src/components/navbar.tsx
ui/src/components/post-listing.tsx
ui/src/components/post-listings.tsx
ui/src/components/post.tsx
ui/src/components/setup.tsx
ui/src/components/sidebar.tsx
ui/src/components/site-form.tsx
ui/src/components/user.tsx
ui/src/index.html
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/main.css [deleted file]
ui/src/services/WebSocketService.ts
ui/src/utils.ts

index 2d6856b3e51363f59845b36f62e7fe1d07ab9094..363f99f27ca2ecff5515472b9f7d23cdc7985a62 100644 (file)
@@ -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
 );
index acc0b5d174c417a578dab3741a8436d7f9afc598..a671c2e76c79c212be8263346935c8ad8723e631 100644 (file)
@@ -1,2 +1,4 @@
+drop table post_read;
+drop table post_saved;
 drop table post_like;
 drop table post;
index c3b7c0b8bc8876b368ba86549d73d9de391bd86b..9073781298ac4788cc44cd87c22ea6b7d2cf22ae 100644 (file)
@@ -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)
+);
index 5b92a44c1db03df6b833312a336c707f8bebc7d5..80fe0b1f5234f49f1a6604f5550a7238b12d6316 100644 (file)
@@ -1,2 +1,3 @@
+drop table comment_saved;
 drop table comment_like;
 drop table comment;
index 214d50a6a43d55e40a6ec8b38fdcc82a56ac0f54..4b754ece1a13ec66a97acd1ef342f0ab54377ecb 100644 (file)
@@ -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)
+);
index ecf3280a4d7299a3919f01296b8df1f3c5142fd5..2f71b6fb984ad5885dbfcc0f3fb17d4fb8fd9e2f 100644 (file)
@@ -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
 ;
index 1b73af5123deb7072113f100468c3f26d0bd8afe..7d38dbfa3e2f8e47f26e2a356de8a0a9354e04b8 100644 (file)
@@ -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
 ;
 
index a73b61825fae4cdc64875e3247ab9bc73c162d3d..a78e3ac340fdf6ba2c51adedb3b4879512fb2d85 100644 (file)
@@ -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
 ;
index 908028d034a637f70f4d41e7cace634b9560fbbd..70a33e469ca4bcb59103a8fcb5cc3acabe8667c6 100644 (file)
@@ -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,
index f6eee5f128395bce578e43554114a34ea084c862..c3aa01070d592646d1295f28ea8aa29ffe36d42f 100644 (file)
@@ -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<i32>,
   pub content: String,
-  pub removed: Option<bool>,
+  pub removed: bool,
+  pub read: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -38,27 +39,6 @@ pub struct CommentForm {
   pub updated: Option<chrono::NaiveDateTime>
 }
 
-#[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<CommentForm> for Comment {
   fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
     use schema::comment::dsl::*;
@@ -87,6 +67,27 @@ impl Crud<CommentForm> 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 <CommentLikeForm> for CommentLike {
   fn read(conn: &PgConnection, comment_id_from: i32) -> Result<Vec<Self>, 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 <CommentSavedForm> for CommentSaved {
+  fn save(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result<Self, Error> {
+    use schema::comment_saved::dsl::*;
+    insert_into(comment_saved)
+      .values(comment_saved_form)
+      .get_result::<Self>(conn)
+  }
+  fn unsave(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result<usize, Error> {
+    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);
 
   }
index 0848ee1c15370bccd9084fc5f2b7b47057c1dd65..3604371665c6cef08d5ef09443bdb82b1fdbd0bc 100644 (file)
@@ -13,18 +13,20 @@ table! {
     post_id -> Int4,
     parent_id -> Nullable<Int4>,
     content -> Text,
-    removed -> Nullable<Bool>,
+    removed -> Bool,
+    read -> Bool,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     community_id -> Int4,
-    banned -> Nullable<Bool>,
+    banned -> Bool,
+    banned_from_community -> Bool,
     creator_name -> Varchar,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
-    am_mod -> Nullable<Bool>,
+    saved -> Nullable<Bool>,
   }
 }
 
@@ -36,18 +38,20 @@ pub struct CommentView {
   pub post_id: i32,
   pub parent_id: Option<i32>,
   pub content: String,
-  pub removed: Option<bool>,
+  pub removed: bool,
+  pub read: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub community_id: i32,
-  pub banned: Option<bool>,
+  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<i32>,
   pub my_vote: Option<i32>,
-  pub am_mod: Option<bool>,
+  pub saved: Option<bool>,
 }
 
 impl CommentView {
@@ -57,6 +61,7 @@ impl CommentView {
               for_post_id: Option<i32>, 
               for_creator_id: Option<i32>, 
               my_user_id: Option<i32>, 
+              saved_only: bool,
               page: Option<i64>,
               limit: Option<i64>,
               ) -> Result<Vec<Self>, 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();
index ac33193403fcd27ac4d9321201fda81fb0d74d6b..594518bada07de4a01173c3c393b7e111ea9c35b 100644 (file)
@@ -14,7 +14,7 @@ pub struct Community {
   pub description: Option<String>,
   pub category_id: i32,
   pub creator_id: i32,
-  pub removed: Option<bool>,
+  pub removed: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -27,7 +27,7 @@ pub struct CommunityForm {
   pub description: Option<String>,
   pub category_id: i32,
   pub creator_id: i32,
-  pub removed: Option<bool>,
+  pub removed: bool,
   pub updated: Option<chrono::NaiveDateTime>
 }
 
@@ -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
     };
index 4db97491a4bf38f21e3be84473ba4fb0956fb6d2..8966ee15a302e9beebc2b6c1e11eee88afd439b0 100644 (file)
@@ -12,7 +12,7 @@ table! {
     description -> Nullable<Text>,
     category_id -> Int4,
     creator_id -> Int4,
-    removed -> Nullable<Bool>,
+    removed -> Bool,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     creator_name -> Varchar,
@@ -22,7 +22,6 @@ table! {
     number_of_comments -> BigInt,
     user_id -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
-    am_mod -> Nullable<Bool>,
   }
 }
 
@@ -83,7 +82,7 @@ pub struct CommunityView {
   pub description: Option<String>,
   pub category_id: i32,
   pub creator_id: i32,
-  pub removed: Option<bool>,
+  pub removed: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub creator_name: String,
@@ -93,7 +92,6 @@ pub struct CommunityView {
   pub number_of_comments: i64,
   pub user_id: Option<i32>,
   pub subscribed: Option<bool>,
-  pub am_mod: Option<bool>,
 }
 
 impl CommunityView {
index a97b21202d3d3ece1e7b91f7e79aacbb821ee8af..e0d885ce86978c0a075072e938faeaaa3e91e143 100644 (file)
@@ -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
     };
 
index 468b3a9bd8cb68ec4f0cf5df569fac76e20b3657..0fd0e5c53bd73c06b19f86827b13d242a5397a77 100644 (file)
@@ -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<String>,
   pub creator_id: i32,
   pub community_id: i32,
-  pub removed: Option<bool>,
-  pub locked: Option<bool>,
+  pub removed: bool,
+  pub locked: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -28,30 +28,11 @@ pub struct PostForm {
   pub body: Option<String>,
   pub creator_id: i32,
   pub community_id: i32,
-  pub removed: Option<bool>,
-  pub locked: Option<bool>,
+  pub removed: bool,
+  pub locked: bool,
   pub updated: Option<chrono::NaiveDateTime>
 }
 
-#[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<PostForm> for Post {
   fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
     use schema::post::dsl::*;
@@ -80,6 +61,25 @@ impl Crud<PostForm> 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 <PostLikeForm> for PostLike {
   fn read(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
     use schema::post_like::dsl::*;
@@ -102,6 +102,72 @@ impl Likeable <PostLikeForm> 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 <PostSavedForm> for PostSaved {
+  fn save(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result<Self, Error> {
+    use schema::post_saved::dsl::*;
+    insert_into(post_saved)
+      .values(post_saved_form)
+      .get_result::<Self>(conn)
+  }
+  fn unsave(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result<usize, Error> {
+    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 <PostReadForm> for PostRead {
+  fn mark_as_read(conn: &PgConnection, post_read_form: &PostReadForm) -> Result<Self, Error> {
+    use schema::post_read::dsl::*;
+    insert_into(post_read)
+      .values(post_read_form)
+      .get_result::<Self>(conn)
+  }
+  fn mark_as_unread(conn: &PgConnection, post_read_form: &PostReadForm) -> Result<usize, Error> {
+    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);
 
   }
index 7ab490aacb45e292f795c06b204e24bec9621415..78fcef637055c1cc2d06cc5b19d1a4fa902ceb5a 100644 (file)
@@ -19,8 +19,8 @@ table! {
     body -> Nullable<Text>,
     creator_id -> Int4,
     community_id -> Int4,
-    removed -> Nullable<Bool>,
-    locked -> Nullable<Bool>,
+    removed -> Bool,
+    locked -> Bool,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     creator_name -> Varchar,
@@ -33,7 +33,8 @@ table! {
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
-    am_mod -> Nullable<Bool>,
+    read -> Nullable<Bool>,
+    saved -> Nullable<Bool>,
   }
 }
 
@@ -47,8 +48,8 @@ pub struct PostView {
   pub body: Option<String>,
   pub creator_id: i32,
   pub community_id: i32,
-  pub removed: Option<bool>,
-  pub locked: Option<bool>,
+  pub removed: bool,
+  pub locked: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub creator_name: String,
@@ -61,7 +62,8 @@ pub struct PostView {
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
   pub subscribed: Option<bool>,
-  pub am_mod: Option<bool>,
+  pub read: Option<bool>,
+  pub saved: Option<bool>,
 }
 
 impl PostView {
@@ -71,6 +73,8 @@ impl PostView {
               for_community_id: Option<i32>, 
               for_creator_id: Option<i32>, 
               my_user_id: Option<i32>, 
+              saved_only: bool,
+              unread_only: bool,
               page: Option<i64>,
               limit: Option<i64>,
               ) -> Result<Vec<Self>, 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();
 
index 3390dbdc5e8d721db21160eed7fe7d948e6dcfcc..31c1af7c3309e657b23b5783460dd691c589c214 100644 (file)
@@ -55,6 +55,16 @@ pub trait Bannable<T> {
   fn unban(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
 }
 
+pub trait Saveable<T> {
+  fn save(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
+  fn unsave(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
+}
+
+pub trait Readable<T> {
+  fn mark_as_read(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
+  fn mark_as_unread(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
+}
+
 pub fn establish_connection() -> PgConnection {
   let db_url = Settings::get().db_url;
   PgConnection::establish(&db_url)
index f431610a589553991e28820e0ebbfcbfcc03e6fd..65c2ae552bdfcc03fe1f55fd6b3a0d2477418087 100644 (file)
@@ -12,7 +12,8 @@ table! {
         post_id -> Int4,
         parent_id -> Nullable<Int4>,
         content -> Text,
-        removed -> Nullable<Bool>,
+        removed -> Bool,
+        read -> Bool,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
@@ -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<Text>,
         category_id -> Int4,
         creator_id -> Int4,
-        removed -> Nullable<Bool>,
+        removed -> Bool,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
@@ -168,8 +178,8 @@ table! {
         body -> Nullable<Text>,
         creator_id -> Int4,
         community_id -> Int4,
-        removed -> Nullable<Bool>,
-        locked -> Nullable<Bool>,
+        removed -> Bool,
+        locked -> Bool,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
@@ -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,
index a4c5b62037e15cebfc8ce19705576200df8c2f19..d1f7210951d1241cc20a3ad4966cb211280445d5 100644 (file)
@@ -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<CommentView>,
   community: CommunityView,
-  moderators: Vec<CommunityModeratorView>
+  moderators: Vec<CommunityModeratorView>,
+  admins: Vec<UserView>,
 }
 
 #[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<String>,
   body: Option<String>,
-  removed: Option<bool>,
+  removed: bool,
+  locked: bool,
   reason: Option<String>,
-  locked: Option<bool>,
+  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<String>,
   category_id: i32,
-  removed: Option<bool>,
+  removed: bool,
   reason: Option<String>,
   expires: Option<i64>,
   auth: String
@@ -297,7 +312,7 @@ pub struct GetUserDetails {
   page: Option<i64>,
   limit: Option<i64>,
   community_id: Option<i32>,
-  auth: Option<String>
+  saved_only: bool,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -308,8 +323,6 @@ pub struct GetUserDetailsResponse {
   moderates: Vec<CommunityModeratorView>,
   comments: Vec<CommentView>,
   posts: Vec<PostView>,
-  saved_posts: Vec<PostView>,
-  saved_comments: Vec<CommentView>,
 }
 
 #[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<Connect> for ChatServer {
   type Result = usize;
 
   fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> 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<Disconnect> for ChatServer {
   type Result = ();
 
   fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
-    println!("Someone disconnected");
 
     // let mut rooms: Vec<i32> = Vec::new();
 
@@ -586,6 +599,10 @@ impl Handler<StandardMessage> 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<StandardMessage> 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<i32> = CommunityModeratorView::for_community(&conn, orig_comment.community_id)
+    let mut editors: Vec<i32> = 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<i32> = 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<i32> = 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()
index dcfb18a9ce50383276f1347b2bd1bfe4a886b77c..c1fc059b462dd8ae3de7fec5c13e79c9f4df84a0 100644 (file)
@@ -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<CommunityUser>;
+  admins: Array<UserView>;
 }
 
 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@@ -35,6 +39,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     showBanDialog: false,
     banReason: null,
     banExpires: null,
+    banType: BanType.Community
   }
 
   constructor(props: any, context: any) {
@@ -60,6 +65,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             <li className="list-inline-item">
               <Link className="text-info" to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link>
             </li>
+            {this.isMod && 
+              <li className="list-inline-item badge badge-secondary">mod</li>
+            }
+            {this.isAdmin && 
+              <li className="list-inline-item badge badge-secondary">admin</li>
+            }
             <li className="list-inline-item">
               <span>(
                 <span className="text-info">+{node.comment.upvotes}</span>
@@ -77,47 +88,70 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             <div>
               <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.content)} />
               <ul class="list-inline mb-1 text-muted small font-weight-bold">
-                {!this.props.viewOnly && 
-                  <span class="mr-2">
+                {UserService.Instance.user && !this.props.viewOnly && 
+                  <>
                     <li className="list-inline-item">
                       <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
                     </li>
+                    <li className="list-inline-item mr-2">
+                      <span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? 'unsave' : 'save'}</span>
+                    </li>
                     {this.myComment && 
                       <>
-                      <li className="list-inline-item">
-                        <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
-                      </li>
-                      <li className="list-inline-item">
-                        <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
-                      </li>
-                    </>
+                        <li className="list-inline-item">
+                          <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
+                        </li>
+                        <li className="list-inline-item">
+                          <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
+                        </li>
+                      </>
                     }
-                    {this.canMod &&
-                      <>
+                    {/* Admins and mods can remove comments */}
+                    {this.canMod && 
                       <li className="list-inline-item">
                         {!this.props.node.comment.removed ? 
                         <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
                         <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
                         }
                       </li>
-                      {!this.isMod &&
-                        <>
+                    }
+                    {/* Mods can ban from community, and appoint as mods to community */}
+                    {this.canMod &&
+                      <>
+                        {!this.isMod && 
+                          <li className="list-inline-item">
+                            {!this.props.node.comment.banned_from_community ? 
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}>ban</span> :
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}>unban</span>
+                            }
+                          </li>
+                        }
+                        {!this.props.node.comment.banned_from_community &&
+                          <li className="list-inline-item">
+                            <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
+                          </li>
+                        }
+                      </>
+                    }
+                    {/* Admins can ban from all, and appoint other admins */}
+                    {this.canAdmin &&
+                      <>
+                        {!this.isAdmin && 
                           <li className="list-inline-item">
                             {!this.props.node.comment.banned ? 
-                            <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban</span> :
-                            <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban</span>
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban from site</span> :
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban from site</span>
                             }
                           </li>
-                        </>
-                      }
-                      {!this.props.node.comment.banned &&
-                        <li className="list-inline-item">
-                          <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
-                        </li>
-                      }
-                    </>
+                        }
+                        {!this.props.node.comment.banned &&
+                          <li className="list-inline-item">
+                            <span class="pointer" onClick={linkEvent(this, this.addAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span>
+                          </li>
+                        }
+                      </>
                     }
-                  </span>
+                  </>
                 }
                 <li className="list-inline-item">
                   <Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link>
@@ -133,22 +167,35 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
           </form>
         }
         {this.state.showBanDialog && 
-        <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
-          <div class="form-group row">
-            <label class="col-form-label">Reason</label>
-            <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
-          </div>
-          <div class="form-group row">
-            <label class="col-form-label">Expires</label>
-            <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} />
-          </div>
-          <div class="form-group row">
-            <button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
-          </div>
-        </form>
+          <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
+            <div class="form-group row">
+              <label class="col-form-label">Reason</label>
+              <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
+            </div>
+            <div class="form-group row">
+              <label class="col-form-label">Expires</label>
+              <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} />
+            </div>
+            <div class="form-group row">
+              <button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
+            </div>
+          </form>
+        }
+        {this.state.showReply && 
+          <CommentForm 
+            node={node} 
+            onReplyCancel={this.handleReplyCancel} 
+            disabled={this.props.locked} 
+          />
+        }
+        {this.props.node.children && 
+          <CommentNodes 
+            nodes={this.props.node.children} 
+            locked={this.props.locked} 
+            moderators={this.props.moderators}
+            admins={this.props.admins}
+          />
         }
-        {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
-        {this.props.node.children && <CommentNodes nodes={this.props.node.children} locked={this.props.locked} moderators={this.props.moderators}/>}
       </div>
     )
   }
@@ -158,27 +205,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   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<CommentNodeProps, CommentNodeState> {
 
   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<CommentNodeProps, CommentNodeState> {
     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<CommentNodeProps, CommentNodeState> {
     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<CommentNodeProps, CommentNodeState> {
     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);
+  }
 }
index 498c69b8fe75d304a25ce70f12006a35b10ab943..abbb1719077109129af3574e0fa3b5b1318de767 100644 (file)
@@ -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<CommentNodeI>;
   moderators?: Array<CommunityUser>;
+  admins?: Array<UserView>;
   noIndent?: boolean;
   viewOnly?: boolean;
   locked?: boolean;
@@ -27,7 +28,9 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
             noIndent={this.props.noIndent} 
             viewOnly={this.props.viewOnly} 
             locked={this.props.locked} 
-            moderators={this.props.moderators}/>
+            moderators={this.props.moderators}
+            admins={this.props.admins}
+            />
         )}
       </div>
     )
index 868006844960f51b99585f8cbf3078d2dd0b4155..9145c1cd5445b62219fd41d47bde82460ee683b2 100644 (file)
@@ -53,9 +53,9 @@ export class Communities extends Component<any, CommunitiesState> {
     return (
       <div class="container">
         {this.state.loading ? 
-        <h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
         <div>
-          <h4>Communities</h4>
+          <h5>Communities</h5>
           <div class="table-responsive">
             <table id="community_table" class="table table-sm table-hover">
               <thead class="pointer">
index cd95f991325043327d621c5836fb336853a7734f..6271bde5a695095071f3b88a9019fec7f7d254b3 100644 (file)
@@ -60,14 +60,14 @@ export class Community extends Component<any, State> {
     return (
       <div class="container">
         {this.state.loading ? 
-        <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
         <div class="row">
           <div class="col-12 col-md-9">
-            <h4>{this.state.community.title}
+            <h5>{this.state.community.title}
             {this.state.community.removed &&
               <small className="ml-2 text-muted font-italic">removed</small>
             }
-          </h4>
+          </h5>
             <PostListings communityId={this.state.communityId} />
           </div>
           <div class="col-12 col-md-3">
index 5f39741191fa46dd41e379085d436ae1e5ee164a..0e806dbb10b1d58b43778875d1bc1f9127cb8c71 100644 (file)
@@ -13,7 +13,7 @@ export class CreateCommunity extends Component<any, any> {
       <div class="container">
         <div class="row">
           <div class="col-12 col-lg-6 mb-4">
-            <h4>Create Forum</h4>
+            <h5>Create Forum</h5>
             <CommunityForm onCreate={this.handleCommunityCreate}/>
           </div>
         </div>
index 041ffd1732f7662e36f895309db58297526f6c0c..7d2f1dd4ebbd895855b8f7a520c7f9cbb8cda6a0 100644 (file)
@@ -13,7 +13,7 @@ export class CreatePost extends Component<any, any> {
       <div class="container">
         <div class="row">
           <div class="col-12 col-lg-6 mb-4">
-            <h4>Create a Post</h4>
+            <h5>Create a Post</h5>
             <PostForm onCreate={this.handlePostCreate}/>
           </div>
         </div>
index 4d0b22d02e8fc8714d1f178fcb6bb310800ade7c..6d15a382de2c07f153bafbe7fe620081587fed8f 100644 (file)
@@ -67,7 +67,7 @@ export class Login extends Component<any, State> {
     return (
       <div>
         <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
-          <h4>Login</h4>
+          <h5>Login</h5>
           <div class="form-group row">
             <label class="col-sm-2 col-form-label">Email or Username</label>
             <div class="col-sm-10">
@@ -94,7 +94,7 @@ export class Login extends Component<any, State> {
   registerForm() {
     return (
       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
-        <h4>Sign Up</h4>
+        <h5>Sign Up</h5>
         <div class="form-group row">
           <label class="col-sm-2 col-form-label">Username</label>
           <div class="col-sm-10">
index 0b5923c080b75ea7232400029136ea258e61ffc0..01c70f946c185dbde31c02dc175b1c2e11cfde5d 100644 (file)
@@ -78,12 +78,12 @@ export class Main extends Component<any, State> {
           </div>
           <div class="col-12 col-md-4">
             {this.state.loading ? 
-            <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+            <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
             <div>
               {this.trendingCommunities()}
               {UserService.Instance.user && this.state.subscribedCommunities.length > 0 && 
               <div>
-                <h4>Subscribed forums</h4>
+                <h5>Subscribed forums</h5>
                 <ul class="list-inline"> 
                   {this.state.subscribedCommunities.map(community =>
                     <li class="list-inline-item"><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
@@ -103,7 +103,7 @@ export class Main extends Component<any, State> {
   trendingCommunities() {
     return (
       <div>
-        <h4>Trending <Link class="text-white" to="/communities">forums</Link></h4
+        <h5>Trending <Link class="text-white" to="/communities">forums</Link></h5
         <ul class="list-inline"> 
           {this.state.trendingCommunities.map(community =>
             <li class="list-inline-item"><Link to={`/community/${community.id}`}>{community.name}</Link></li>
@@ -116,7 +116,7 @@ export class Main extends Component<any, State> {
   landing() {
     return (
       <div>
-        <h4>{`${this.state.site.site.name}`}</h4>
+        <h5>{`${this.state.site.site.name}`}</h5>
         <ul class="my-1 list-inline">
           <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_users} Users</li>
           <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li>
@@ -136,10 +136,10 @@ export class Main extends Component<any, State> {
             <hr />
           </div>
         }
-        <h4>Welcome to 
+        <h5>Welcome to 
           <svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
           <a href={repoUrl}>Lemmy<sup>Beta</sup></a>
-        </h4>
+        </h5>
         <p>Lemmy is a <a href="https://en.wikipedia.org/wiki/Link_aggregation">link aggregator</a> / reddit alternative, intended to work in the <a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a>.</p>
         <p>Its self-hostable, has live-updating comment threads, and is tiny (<code>~80kB</code>). Federation into the ActivityPub network is on the roadmap.</p>
         <p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p>
index 56b08a7e255afe4db94cfa22e11aac3324ed89db..7145b4f6ce0d0cc79a3624de035829e19e39fa23 100644 (file)
@@ -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<any, ModlogState> {
     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<any, ModlogState> {
     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<any, ModlogState> {
                 <>
                   {(i.data as ModRemoveComment).removed? 'Removed' : 'Restored'} 
                   <span> Comment <Link to={`/post/${(i.data as ModRemoveComment).post_id}/comment/${(i.data as ModRemoveComment).comment_id}`}>{(i.data as ModRemoveComment).comment_content}</Link></span>
+                  <span> by <Link to={`/user/${(i.data as ModRemoveComment).comment_user_id}`}>{(i.data as ModRemoveComment).comment_user_name}</Link></span>
                   <div>{(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}</div>
                 </>
               }
               {i.type_ == 'removed_communities' && 
                 <>
                   {(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'} 
-                  <span> Community <Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
+                  <span> Community <Link to={`/community/${(i.data as ModRemoveCommunity).community_id}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span>
                   <div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
                   <div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div>
                 </>
@@ -110,6 +115,8 @@ export class Modlog extends Component<any, ModlogState> {
                 <>
                   <span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span>
                   <span><Link to={`/user/${(i.data as ModBanFromCommunity).other_user_id}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
+                  <span> from the community </span>
+                  <span><Link to={`/community/${(i.data as ModBanFromCommunity).community_id}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span>
                   <div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
                   <div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div>
                 </>
@@ -119,12 +126,27 @@ export class Modlog extends Component<any, ModlogState> {
                   <span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
                   <span><Link to={`/user/${(i.data as ModAddCommunity).other_user_id}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
                   <span> as a mod to the community </span>
-                  <span><Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
+                  <span><Link to={`/community/${(i.data as ModAddCommunity).community_id}`}>{(i.data as ModAddCommunity).community_name}</Link></span>
+                </>
+              }
+              {i.type_ == 'banned' && 
+                <>
+                  <span>{(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '} </span>
+                  <span><Link to={`/user/${(i.data as ModBan).other_user_id}`}>{(i.data as ModBan).other_user_name}</Link></span>
+                  <div>{(i.data as ModBan).reason && ` reason: ${(i.data as ModBan).reason}`}</div>
+                  <div>{(i.data as ModBan).expires && ` expires: ${moment.utc((i.data as ModBan).expires).fromNow()}`}</div>
+                </>
+              }
+              {i.type_ == 'added' && 
+                <>
+                  <span>{(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '} </span>
+                  <span><Link to={`/user/${(i.data as ModAdd).other_user_id}`}>{(i.data as ModAdd).other_user_name}</Link></span>
+                  <span> as an admin </span>
                 </>
               }
             </td>
           </tr>
-                     )
+                                )
         }
 
       </tbody>
@@ -136,12 +158,12 @@ export class Modlog extends Component<any, ModlogState> {
     return (
       <div class="container">
         {this.state.loading ? 
-        <h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
         <div>
-          <h4>
+          <h5>
             {this.state.communityName && <Link className="text-white" to={`/community/${this.state.communityId}`}>/f/{this.state.communityName} </Link>}
             <span>Modlog</span>
-          </h4>
+          </h5>
           <div class="table-responsive">
             <table id="modlog_table" class="table table-sm table-hover">
               <thead class="pointer">
@@ -183,7 +205,7 @@ export class Modlog extends Component<any, ModlogState> {
     i.setState(i.state);
     i.refetch();
   }
-  
+
   refetch(){
     let modlogForm: GetModlogForm = {
       community_id: this.state.communityId,
index ae69382591b672f1cb9977e9bb4d15aa39ca8cdc..be98912eea17aebf639647afdd4aa8666b32d392 100644 (file)
@@ -64,16 +64,22 @@ export class Navbar extends Component<any, NavbarState> {
           </ul>
           <ul class="navbar-nav ml-auto mr-2">
             {this.state.isLoggedIn ? 
-            <li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
-              <a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
-                {UserService.Instance.user.username}
-              </a>
-              <div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
-                <a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}>Overview</a>
-                <a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a>
-              </div>
-            </li> : 
-            <Link class="nav-link" to="/login">Login / Sign up</Link>
+            <>
+              <li className="nav-item">
+                <Link class="nav-link" to="/communities">🖂</Link>
+              </li>
+              <li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
+                <a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
+                  {UserService.Instance.user.username}
+                </a>
+                <div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
+                  <a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}>Overview</a>
+                  <a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a>
+                </div>
+              </li> 
+            </>
+              : 
+              <Link class="nav-link" to="/login">Login / Sign up</Link>
             }
           </ul>
         </div>
index 1a52bf79c01ed7573f9b9df9909c36331c8febc0..da375aee43d315790e311110f28f89b88d0f82fc 100644 (file)
@@ -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<PostListingProps, PostListingState> {
           <div>{post.score}</div>
           <div className={`pointer downvote ${post.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(this, this.handlePostDisLike)}>â–¼</div>
         </div>
-        <div className="ml-4">
+        <div className="pt-1 ml-4">
           {post.url 
             ? <div className="mb-0">
-            <h4 className="d-inline"><a className="text-white" href={post.url} title={post.url}>{post.name}</a>
+            <h5 className="d-inline"><a className="text-white" href={post.url} title={post.url}>{post.name}</a>
             {post.removed &&
               <small className="ml-2 text-muted font-italic">removed</small>
             }
             {post.locked &&
               <small className="ml-2 text-muted font-italic">locked</small>
             }
-          </h4>
+          </h5>
           <small><a className="ml-2 text-muted font-italic" href={post.url} title={post.url}>{(new URL(post.url)).hostname}</a></small>
           { !this.state.iframeExpanded
             ? <span class="badge badge-light pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
@@ -83,14 +83,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             </span>
           }
         </div> 
-          : <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link>
+          : <h5 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link>
           {post.removed &&
             <small className="ml-2 text-muted font-italic">removed</small>
           }
           {post.locked &&
             <small className="ml-2 text-muted font-italic">locked</small>
           }
-        </h4>
+        </h5>
           }
         </div>
         <div className="details ml-4 mb-1">
@@ -120,17 +120,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               <Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link>
             </li>
           </ul>
-          {this.props.editable &&
+          {UserService.Instance.user && this.props.editable &&
             <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
+              <li className="list-inline-item mr-2">
+                <span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{this.props.post.saved ? 'unsave' : 'save'}</span>
+              </li>
               {this.myPost && 
-                <span>
+                <>
                   <li className="list-inline-item">
                     <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
                   </li>
                   <li className="list-inline-item mr-2">
                     <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
                   </li>
-                </span>
+                </>
               }
               {this.props.post.am_mod &&
                 <span>
@@ -204,11 +207,23 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       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<PostListingProps, PostListingState> {
       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<PostListingProps, PostListingState> {
       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,
     };
index 8fc19b3002183b7de8aa313ce3067300493c2c14..b1e48a61ca51f5025340743e73ecf96d0600e8cd 100644 (file)
@@ -61,7 +61,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
     return (
       <div>
         {this.state.loading ? 
-        <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
         <div>
           {this.selects()}
           {this.state.posts.length > 0 
index d79a6c97cd8bcd74bc9d06bd6a0493c993bb6028..64f56d88973b553aae32591b8f6bfd006769dfb4 100644 (file)
@@ -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<CommunityUser>;
+  admins: Array<UserView>;
   scrolled?: boolean;
   scrolled_comment_id?: number;
   loading: boolean;
@@ -31,6 +32,7 @@ export class Post extends Component<any, PostState> {
     commentSort: CommentSortType.Hot,
     community: null,
     moderators: [],
+    admins: [],
     scrolled: false, 
     loading: true
   }
@@ -77,7 +79,7 @@ export class Post extends Component<any, PostState> {
     return (
       <div class="container">
         {this.state.loading ? 
-        <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
         <div class="row">
             <div class="col-12 col-md-8 col-lg-7 mb-3">
               <PostListing post={this.state.post} showBody showCommunity editable />
@@ -123,9 +125,15 @@ export class Post extends Component<any, PostState> {
   newComments() {
     return (
       <div class="sticky-top">
-        <h4>New Comments</h4>
+        <h5>New Comments</h5>
         {this.state.comments.map(comment => 
-          <CommentNodes nodes={[{comment: comment}]} noIndent locked={this.state.post.locked} moderators={this.state.moderators} />
+          <CommentNodes 
+            nodes={[{comment: comment}]} 
+            noIndent 
+            locked={this.state.post.locked} 
+            moderators={this.state.moderators} 
+            admins={this.state.admins}
+          />
         )}
       </div>
     )
@@ -187,8 +195,13 @@ export class Post extends Component<any, PostState> {
   commentsTree() {
     let nodes = this.buildCommentsTree();
     return (
-      <div className="">
-        <CommentNodes nodes={nodes} locked={this.state.post.locked} moderators={this.state.moderators} />
+      <div>
+        <CommentNodes 
+          nodes={nodes} 
+          locked={this.state.post.locked} 
+          moderators={this.state.moderators} 
+          admins={this.state.admins}
+        />
       </div>
     );
   }
@@ -202,9 +215,11 @@ export class Post extends Component<any, PostState> {
     } 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<any, PostState> {
       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<any, PostState> {
       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<any, PostState> {
     } 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);
     }
 
   }
index 9560a60dce42313a61c57397d739b70a8fbb64de..9a671359a7d3caa1403d24959c6f3d69e7221f7d 100644 (file)
@@ -61,7 +61,7 @@ export class Setup extends Component<any, State> {
   registerUser() {
     return (
       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
-        <h4>Set up Site Administrator</h4>
+        <h5>Set up Site Administrator</h5>
         <div class="form-group row">
           <label class="col-sm-2 col-form-label">Username</label>
           <div class="col-sm-10">
index b0c0b7b9c79e5a774fb5705eaec7a5615babb02b..2f231f9a09dcfcf44c11e11b11991f8435f9cd9b 100644 (file)
@@ -48,11 +48,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     let community = this.props.community;
     return (
       <div>
-        <h4 className="mb-0">{community.title}
+        <h5 className="mb-0">{community.title}
         {community.removed &&
           <small className="ml-2 text-muted font-italic">removed</small>
         }
-      </h4>
+      </h5>
       <Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
       {community.am_mod && 
         <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
index 7ca45b86e3aab24f632a64bbca1d567b1c67a872..55da1667e1241d97a5d62d3ef1ecb2b15a457865 100644 (file)
@@ -33,7 +33,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   render() {
     return (
       <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
-        <h4>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h4>
+        <h5>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h5>
         <div class="form-group row">
           <label class="col-12 col-form-label">Name</label>
           <div class="col-12">
index fdcd378e0b7cd1580fd9cd81013c65fade50242a..8ebde48a9f641e2f2ebc0ec2880ee2176587b8ed 100644 (file)
@@ -77,7 +77,7 @@ export class User extends Component<any, UserState> {
       <div class="container">
         <div class="row">
           <div class="col-12 col-md-9">
-            <h4>/u/{this.state.user.name}</h4>
+            <h5>/u/{this.state.user.name}</h5>
             {this.selects()}
             {this.state.view == View.Overview &&
               this.overview()
@@ -88,6 +88,9 @@ export class User extends Component<any, UserState> {
             {this.state.view == View.Posts &&
               this.posts()
             }
+            {this.state.view == View.Saved &&
+              this.overview()
+            }
             {this.paginator()}
           </div>
           <div class="col-12 col-md-3">
@@ -108,7 +111,7 @@ export class User extends Component<any, UserState> {
           <option value={View.Overview}>Overview</option>
           <option value={View.Comments}>Comments</option>
           <option value={View.Posts}>Posts</option>
-          {/* <option value={View.Saved}>Saved</option> */}
+          <option value={View.Saved}>Saved</option>
         </select>
         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
           <option disabled>Sort Type</option>
@@ -178,7 +181,7 @@ export class User extends Component<any, UserState> {
     let user = this.state.user;
     return (
       <div>
-        <h4>{user.name}</h4>
+        <h5>{user.name}</h5>
         <div>Joined <MomentTime data={user} /></div>
         <table class="table table-bordered table-sm mt-2">
           <tr>
@@ -200,7 +203,7 @@ export class User extends Component<any, UserState> {
       <div>
         {this.state.moderates.length > 0 &&
           <div>
-            <h4>Moderates</h4>
+            <h5>Moderates</h5>
             <ul class="list-unstyled"> 
               {this.state.moderates.map(community =>
                 <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
@@ -218,7 +221,7 @@ export class User extends Component<any, UserState> {
         {this.state.follows.length > 0 &&
           <div>
             <hr />
-            <h4>Subscribed</h4>
+            <h5>Subscribed</h5>
             <ul class="list-unstyled"> 
               {this.state.follows.map(community =>
                 <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
@@ -257,6 +260,7 @@ export class User extends Component<any, UserState> {
     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,
     };
index 5b1f84afe538944d91dcedf73e7ee698824bd089..efa5b9695165ec5ba542385778920822f171f708 100644 (file)
@@ -7,9 +7,7 @@
        <link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
 
        <title>Lemmy</title>
-  <link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
-  <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,700,800" rel="stylesheet"> 
   <script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
 </head>
 
index cefcac0200faade23ced2d1afed25f2f8c99576c..d830bd3ae050f0e1e354e58c03c5ed91d651efe2 100644 (file)
@@ -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';
 
index 6affc0e126150bd8cee33ae14ad3929583437570..4a4ee643caee55c67c88af0a992aff04da78e4ee 100644 (file)
@@ -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;
+  category_id: number;
   creator_id: number;
+  removed: boolean;
+  published: string;
+  updated?: string;
   creator_name: string;
-  category_id: number;
   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<CommunityUser>;
   comments: Array<Comment>;
   posts: Array<Post>;
-  saved?: Array<Post>;
 }
 
 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<Comment>;
   community: Community;
   moderators: Array<CommunityUser>;
+  admins: Array<UserView>;
+}
+
+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 (file)
index 3fbb6ef..0000000
+++ /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;
-}
index 80555fd98053aab46e7685660c4b6bbfeab402a2..b2c2a9e001bc74fca0b409ac7ede1e3f7386d423 100644 (file)
@@ -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));
   }
 
index c7f3bad8060432bc310745d76b1948c3b112599f..61744e90aaeae11e8924b3bb3b658a97ce176362 100644 (file)
@@ -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<T>(arr: Array<T>, name: string): Array<{type_: strin
   return arr.map(e => {return {type_: name, data: e}});
 }
 
+export function canMod(user: User, modIds: Array<number>, 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<number>, creator_id: number): boolean {
+  return modIds.includes(creator_id);
+}
+
 export let fetchLimit: number = 20;