]> Untitled Git - lemmy.git/commitdiff
Adding a few endpoints.
authorDessalines <tyhou13@gmx.com>
Tue, 26 Mar 2019 18:00:18 +0000 (11:00 -0700)
committerDessalines <tyhou13@gmx.com>
Tue, 26 Mar 2019 18:00:18 +0000 (11:00 -0700)
- Adding CreatePost, CreateComment, CreateCommunity

16 files changed:
server/migrations/2019-03-03-163336_create_post/up.sql
server/src/actions/comment.rs
server/src/actions/community.rs
server/src/actions/post.rs
server/src/schema.rs
server/src/websocket_server/server.rs
ui/src/components/community.tsx [new file with mode: 0644]
ui/src/components/create-community.tsx
ui/src/components/create-post.tsx
ui/src/components/login.tsx
ui/src/components/navbar.tsx
ui/src/components/post.tsx [new file with mode: 0644]
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/services/UserService.ts
ui/src/services/WebSocketService.ts

index a617ea33794c22d754e497d119f35919cd2e6e36..f22192f3e5b7bfb99e9c3d3ce5b736d1733fdb15 100644 (file)
@@ -1,8 +1,10 @@
 create table post (
   id serial primary key,
   name varchar(100) not null,
-  url text not null,
+  url text, -- These are both optional, a post can just have a title
+  body text,
   attributed_to text not null,
+  community_id int references community on update cascade on delete cascade not null,
   published timestamp not null default now(),
   updated timestamp
 );
index 98d5322c58b0a2032c0411dc8c3570f8d5fd3101..ceedf2945eb27a99a985303acc4ca812cefbb230 100644 (file)
@@ -2,7 +2,9 @@ extern crate diesel;
 use schema::{comment, comment_like};
 use diesel::*;
 use diesel::result::Error;
+use serde::{Deserialize, Serialize};
 use {Crud, Likeable};
+use actions::post::Post;
 
 // WITH RECURSIVE MyTree AS (
 //     SELECT * FROM comment WHERE parent_id IS NULL
@@ -11,7 +13,8 @@ use {Crud, Likeable};
 // )
 // SELECT * FROM MyTree;
 
-#[derive(Queryable, Identifiable, PartialEq, Debug)]
+#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[belongs_to(Post)]
 #[table_name="comment"]
 pub struct Comment {
   pub id: i32,
@@ -96,20 +99,38 @@ impl Likeable <CommentLikeForm> for CommentLike {
   }
 }
 
+impl Comment {
+  pub fn from_post(conn: &PgConnection, post: &Post) -> Result<Vec<Self>, Error> {
+    use schema::community::dsl::*;
+    Comment::belonging_to(post)
+      .load::<Self>(conn) 
+  }
+}
+
 #[cfg(test)]
 mod tests {
   use establish_connection;
   use super::*;
   use actions::post::*;
+  use actions::community::*;
   use Crud;
  #[test]
   fn test_crud() {
     let conn = establish_connection();
+
+    let new_community = CommunityForm {
+      name: "test community".to_string(),
+      updated: None
+    };
+
+    let inserted_community = Community::create(&conn, &new_community).unwrap();
     
     let new_post = PostForm {
       name: "A test post".into(),
-      url: "https://test.com".into(),
+      url: None,
+      body: None,
       attributed_to: "test_user.com".into(),
+      community_id: inserted_community.id,
       updated: None
     };
 
@@ -167,6 +188,7 @@ mod tests {
     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();
+    Community::delete(&conn, inserted_community.id).unwrap();
 
     assert_eq!(expected_comment, read_comment);
     assert_eq!(expected_comment, inserted_comment);
index 44d7b749c78abbefebfbb3695ea13dca610db68c..97600620d14a0be7c5901d9658645ac0f51f1470 100644 (file)
@@ -117,6 +117,13 @@ impl Joinable<CommunityUserForm> for CommunityUser {
   }
 }
 
+impl Community {
+  pub fn list_all(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+    use schema::community::dsl::*;
+    community.load::<Self>(conn)
+  }
+}
+
 #[cfg(test)]
 mod tests {
   use establish_connection;
@@ -183,6 +190,7 @@ mod tests {
     let updated_community = Community::update(&conn, inserted_community.id, &new_community).unwrap();
     let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap();
     let left_community = CommunityUser::leave(&conn, &community_user_form).unwrap();
+    let loaded_count = Community::list_all(&conn).unwrap().len();
     let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
     User_::delete(&conn, inserted_user.id).unwrap();
 
@@ -193,6 +201,7 @@ mod tests {
     assert_eq!(expected_community_user, inserted_community_user);
     assert_eq!(1, ignored_community);
     assert_eq!(1, left_community);
+    assert_eq!(1, loaded_count);
     assert_eq!(1, num_deleted);
 
   }
index 889fcf037e85378a4e9f980c47a0679813cd1bf7..71846dff7ef26c019bdbe465812b7d83ebd82748 100644 (file)
@@ -2,15 +2,18 @@ extern crate diesel;
 use schema::{post, post_like};
 use diesel::*;
 use diesel::result::Error;
+use serde::{Deserialize, Serialize};
 use {Crud, Likeable};
 
-#[derive(Queryable, Identifiable, PartialEq, Debug)]
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name="post"]
 pub struct Post {
   pub id: i32,
   pub name: String,
-  pub url: String,
+  pub url: Option<String>,
+  pub body: Option<String>,
   pub attributed_to: String,
+  pub community_id: i32,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -19,8 +22,10 @@ pub struct Post {
 #[table_name="post"]
 pub struct PostForm {
   pub name: String,
-  pub url: String,
+  pub url: Option<String>,
+  pub body: Option<String>,
   pub attributed_to: String,
+  pub community_id: i32,
   pub updated: Option<chrono::NaiveDateTime>
 }
 
@@ -92,14 +97,24 @@ mod tests {
   use establish_connection;
   use super::*;
   use Crud;
+  use actions::community::*;
  #[test]
   fn test_crud() {
     let conn = establish_connection();
+
+    let new_community = CommunityForm {
+      name: "test community_2".to_string(),
+      updated: None
+    };
+
+    let inserted_community = Community::create(&conn, &new_community).unwrap();
     
     let new_post = PostForm {
       name: "A test post".into(),
-      url: "https://test.com".into(),
+      url: None,
+      body: None,
       attributed_to: "test_user.com".into(),
+      community_id: inserted_community.id,
       updated: None
     };
 
@@ -108,8 +123,10 @@ mod tests {
     let expected_post = Post {
       id: inserted_post.id,
       name: "A test post".into(),
-      url: "https://test.com".into(),
+      url: None,
+      body: None,
       attributed_to: "test_user.com".into(),
+      community_id: inserted_community.id,
       published: inserted_post.published,
       updated: None
     };
@@ -134,6 +151,7 @@ mod tests {
     let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap();
     let like_removed = PostLike::remove(&conn, &post_like_form).unwrap();
     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
+    Community::delete(&conn, inserted_community.id).unwrap();
 
     assert_eq!(expected_post, read_post);
     assert_eq!(expected_post, inserted_post);
index 4ab54bc45af5ca99037a3452e58fa68864f1e6b4..28c4e8cad98d7568b4f93d5112ff10a2ef444bee 100644 (file)
@@ -51,8 +51,10 @@ table! {
     post (id) {
         id -> Int4,
         name -> Varchar,
-        url -> Text,
+        url -> Nullable<Text>,
+        body -> Nullable<Text>,
         attributed_to -> Text,
+        community_id -> Int4,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
@@ -85,6 +87,7 @@ joinable!(comment -> post (post_id));
 joinable!(comment_like -> comment (comment_id));
 joinable!(community_follower -> community (community_id));
 joinable!(community_user -> community (community_id));
+joinable!(post -> community (community_id));
 joinable!(post_like -> post (post_id));
 
 allow_tables_to_appear_in_same_query!(
index 760bd78c80c5ec5f13df0231c5bf023273150043..224843978987ce48f2ba1accbaacae364a046090 100644 (file)
@@ -13,10 +13,13 @@ use std::str::FromStr;
 use {Crud, Joinable, establish_connection};
 use actions::community::*;
 use actions::user::*;
+use actions::post::*;
+use actions::comment::*;
+
 
 #[derive(EnumString,ToString,Debug)]
 pub enum UserOperation {
-  Login, Register, Logout, CreateCommunity, Join, Edit, Reply, Vote, Delete, NextPage, Sticky
+  Login, Register, Logout, CreateCommunity, ListCommunities, CreatePost, GetPost, GetCommunity, CreateComment, Join, Edit, Reply, Vote, Delete, NextPage, Sticky
 }
 
 
@@ -73,12 +76,6 @@ impl actix::Message for StandardMessage {
   type Result = String;
 }
 
-#[derive(Serialize, Deserialize)]
-pub struct StandardResponse<T> {
-  op: String,
-  response: T
-}
-
 /// List of available rooms
 pub struct ListRooms;
 
@@ -118,12 +115,75 @@ pub struct LoginResponse {
 #[derive(Serialize, Deserialize)]
 pub struct CreateCommunity {
   name: String,
+  auth: String
 }
 
 #[derive(Serialize, Deserialize)]
 pub struct CreateCommunityResponse {
   op: String,
-  data: Community
+  community: Community
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct ListCommunities;
+
+#[derive(Serialize, Deserialize)]
+pub struct ListCommunitiesResponse {
+  op: String,
+  communities: Vec<Community>
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CreatePost {
+  name: String,
+  url: Option<String>,
+  body: Option<String>,
+  community_id: i32,
+  auth: String
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CreatePostResponse {
+  op: String,
+  post: Post
+}
+
+
+#[derive(Serialize, Deserialize)]
+pub struct GetPost {
+  id: i32
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetPostResponse {
+  op: String,
+  post: Post,
+  comments: Vec<Comment>
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetCommunity {
+  id: i32
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetCommunityResponse {
+  op: String,
+  community: Community
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CreateComment {
+  content: String,
+  parent_id: Option<i32>,
+  post_id: i32,
+  auth: String
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CreateCommentResponse {
+  op: String,
+  comment: Comment
 }
 
 /// `ChatServer` manages chat rooms and responsible for coordinating chat
@@ -249,7 +309,6 @@ impl Handler<StandardMessage> for ChatServer {
 
     let data: &Value = &json["data"];
     let op = &json["op"].as_str().unwrap();
-    let auth = &json["auth"].as_str();
     let user_operation: UserOperation = UserOperation::from_str(&op).unwrap();
 
     let res: String = match user_operation {
@@ -263,18 +322,27 @@ impl Handler<StandardMessage> for ChatServer {
       },
       UserOperation::CreateCommunity => {
         let create_community: CreateCommunity = serde_json::from_str(&data.to_string()).unwrap();
-        match auth {
-          Some(auth) => {
-            create_community.perform(auth)
-          },
-          None => serde_json::to_string(
-            &ErrorMessage {
-              op: UserOperation::CreateCommunity.to_string(),
-              error: "Not logged in.".to_string()
-            }
-            )
-            .unwrap()
-        }
+        create_community.perform()
+      },
+      UserOperation::ListCommunities => {
+        let list_communities: ListCommunities = ListCommunities;
+        list_communities.perform()
+      },
+      UserOperation::CreatePost => {
+        let create_post: CreatePost = serde_json::from_str(&data.to_string()).unwrap();
+        create_post.perform()
+      },
+      UserOperation::GetPost => {
+        let get_post: GetPost = serde_json::from_str(&data.to_string()).unwrap();
+        get_post.perform()
+      },
+      UserOperation::GetCommunity => {
+        let get_community: GetCommunity = serde_json::from_str(&data.to_string()).unwrap();
+        get_community.perform()
+      },
+      UserOperation::CreateComment => {
+        let create_comment: CreateComment = serde_json::from_str(&data.to_string()).unwrap();
+        create_comment.perform()
       },
       _ => {
         let e = ErrorMessage { 
@@ -286,68 +354,29 @@ impl Handler<StandardMessage> for ChatServer {
       // _ => "no".to_string()
     };
 
-
-
-    // let data: &Value = &json["data"];
-    // let res = StandardResponse {op: "nope".to_string(), response: "hi".to_string()};
-    // let out = serde_json::to_string(&res).unwrap();
     MessageResult(res)
   }
 }
 
-// /// Handler for `ListRooms` message.
-// impl Handler<ListRooms> for ChatServer {
-//   type Result = MessageResult<ListRooms>;
-
-//   fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Self::Result {
-//     let mut rooms = Vec::new();
-
-//     for key in self.rooms.keys() {
-//       rooms.push(key.to_owned())
-//     }
-
-//     MessageResult(rooms)
-//   }
-// }
-
-// /// Join room, send disconnect message to old room
-// /// send join message to new room
-// impl Handler<Join> for ChatServer {
-//   type Result = ();
-
-//   fn handle(&mut self, msg: Join, _: &mut Context<Self>) {
-//     let Join { id, name } = msg;
-//     let mut rooms = Vec::new();
-
-//     // remove session from all rooms
-//     for (n, sessions) in &mut self.rooms {
-//       if sessions.remove(&id) {
-//         rooms.push(n.to_owned());
-//       }
-//     }
-//     // send message to other users
-//     for room in rooms {
-//       self.send_room_message(&room, "Someone disconnected", 0);
-//     }
-
-//     if self.rooms.get_mut(&name).is_none() {
-//       self.rooms.insert(name.clone(), HashSet::new());
-//     }
-//     self.send_room_message(&name, "Someone connected", id);
-//     self.rooms.get_mut(&name).unwrap().insert(id);
-//   }
-
-// }
 
 pub trait Perform {
   fn perform(&self) -> String;
-}
-
-pub trait PerformAuth {
-  fn perform(&self, auth: &str) -> String;
+  fn op_type(&self) -> UserOperation;
+  fn error(&self, error_msg: &str) -> String {
+    serde_json::to_string(
+      &ErrorMessage {
+        op: self.op_type().to_string(), 
+        error: error_msg.to_string()
+      }
+      )
+      .unwrap()
+  }
 }
 
 impl Perform for Login {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::Login
+  }
   fn perform(&self) -> String {
 
     let conn = establish_connection();
@@ -355,52 +384,38 @@ impl Perform for Login {
     // Fetch that username / email
     let user: User_ = match User_::find_by_email_or_username(&conn, &self.username_or_email) {
       Ok(user) => user,
-      Err(e) => return serde_json::to_string(
-        &ErrorMessage {
-          op: UserOperation::Login.to_string(), 
-          error: "Couldn't find that username or email".to_string()
-        }
-        )
-        .unwrap()
+      Err(e) => return self.error("Couldn't find that username or email")
     };
 
     // Verify the password
     let valid: bool = verify(&self.password, &user.password_encrypted).unwrap_or(false);
     if !valid {
-      return serde_json::to_string(
-        &ErrorMessage {
-          op: UserOperation::Login.to_string(), 
-          error: "Password incorrect".to_string()
-        }
-        )
-        .unwrap()
+      return self.error("Password incorrect")
     }
 
     // Return the jwt
     serde_json::to_string(
       &LoginResponse {
-        op: UserOperation::Login.to_string(),
+        op: self.op_type().to_string(),
         jwt: user.jwt()
       }
       )
       .unwrap()
   }
+
 }
 
 impl Perform for Register {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::Register
+  }
   fn perform(&self) -> String {
 
     let conn = establish_connection();
 
     // Make sure passwords match
     if &self.password != &self.password_verify {
-      return serde_json::to_string(
-        &ErrorMessage {
-          op: UserOperation::Register.to_string(), 
-          error: "Passwords do not match.".to_string()
-        }
-        )
-        .unwrap();
+      return self.error("Passwords do not match.");
     }
 
     // Register the new user
@@ -416,20 +431,14 @@ impl Perform for Register {
     let inserted_user = match User_::create(&conn, &user_form) {
       Ok(user) => user,
       Err(e) => {
-        return serde_json::to_string(
-          &ErrorMessage {
-            op: UserOperation::Register.to_string(), 
-            error: "User already exists.".to_string() // overwrite the diesel error
-          }
-          )
-          .unwrap()
+        return self.error("User already exists.");
       }
     };
 
     // Return the jwt
     serde_json::to_string(
       &LoginResponse {
-        op: UserOperation::Register.to_string(), 
+        op: self.op_type().to_string(), 
         jwt: inserted_user.jwt()
       }
       )
@@ -438,28 +447,25 @@ impl Perform for Register {
   }
 }
 
-impl PerformAuth for CreateCommunity {
-  fn perform(&self, auth: &str) -> String {
+impl Perform for CreateCommunity {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::CreateCommunity
+  }
+
+  fn perform(&self) -> String {
 
     let conn = establish_connection();
 
-    let claims = match Claims::decode(&auth) {
+    let claims = match Claims::decode(&self.auth) {
       Ok(claims) => claims.claims,
       Err(e) => {
-        return serde_json::to_string(
-          &ErrorMessage {
-            op: UserOperation::CreateCommunity.to_string(), 
-            error: "Community user already exists.".to_string() // overwrite the diesel error
-          }
-          )
-          .unwrap();
+        return self.error("Not logged in.");
       }
     };
 
     let user_id = claims.id;
     let iss = claims.iss;
 
-    // Register the new user
     let community_form = CommunityForm {
       name: self.name.to_owned(),
       updated: None
@@ -468,13 +474,7 @@ impl PerformAuth for CreateCommunity {
     let inserted_community = match Community::create(&conn, &community_form) {
       Ok(community) => community,
       Err(e) => {
-        return serde_json::to_string(
-          &ErrorMessage {
-            op: UserOperation::CreateCommunity.to_string(), 
-            error: "Community already exists.".to_string() // overwrite the diesel error
-          }
-          )
-          .unwrap()
+        return self.error("Community already exists.");
       }
     };
 
@@ -486,28 +486,192 @@ impl PerformAuth for CreateCommunity {
     let inserted_community_user = match CommunityUser::join(&conn, &community_user_form) {
       Ok(user) => user,
       Err(e) => {
-        return serde_json::to_string(
-          &ErrorMessage {
-            op: UserOperation::CreateCommunity.to_string(), 
-            error: "Community user already exists.".to_string() // overwrite the diesel error
-          }
-          )
-          .unwrap()
+        return self.error("Community user already exists.");
       }
     };
 
+    serde_json::to_string(
+      &CreateCommunityResponse {
+        op: self.op_type().to_string(), 
+        community: inserted_community
+      }
+      )
+      .unwrap()
+  }
+}
+
+impl Perform for ListCommunities {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::ListCommunities
+  }
+
+  fn perform(&self) -> String {
+
+    let conn = establish_connection();
+
+    let communities: Vec<Community> = Community::list_all(&conn).unwrap();
 
     // Return the jwt
     serde_json::to_string(
-      &CreateCommunityResponse {
-        op: UserOperation::CreateCommunity.to_string(), 
-        data: inserted_community
+      &ListCommunitiesResponse {
+        op: self.op_type().to_string(),
+        communities: communities
+      }
+      )
+      .unwrap()
+  }
+}
+
+impl Perform for CreatePost {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::CreatePost
+  }
+
+  fn perform(&self) -> 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 iss = claims.iss;
+
+
+    let post_form = PostForm {
+      name: self.name.to_owned(),
+      url: self.url.to_owned(),
+      body: self.body.to_owned(),
+      community_id: self.community_id,
+      attributed_to: format!("{}/{}", iss, user_id),
+      updated: None
+    };
+
+    let inserted_post = match Post::create(&conn, &post_form) {
+      Ok(post) => post,
+      Err(e) => {
+        return self.error("Couldn't create Post");
+      }
+    };
+
+    serde_json::to_string(
+      &CreatePostResponse {
+        op: self.op_type().to_string(), 
+        post: inserted_post
+      }
+      )
+      .unwrap()
+  }
+}
+
+
+impl Perform for GetPost {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::GetPost
+  }
+
+  fn perform(&self) -> String {
+
+    let conn = establish_connection();
+
+    let post = match Post::read(&conn, self.id) {
+      Ok(post) => post,
+      Err(e) => {
+        return self.error("Couldn't find Post");
+      }
+    };
+
+    let comments = Comment::from_post(&conn, &post).unwrap();
+
+    // Return the jwt
+    serde_json::to_string(
+      &GetPostResponse {
+        op: self.op_type().to_string(),
+        post: post,
+        comments: comments
       }
       )
       .unwrap()
+  }
+}
+
+impl Perform for GetCommunity {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::GetCommunity
+  }
+
+  fn perform(&self) -> String {
+
+    let conn = establish_connection();
 
+    let community = match Community::read(&conn, self.id) {
+      Ok(community) => community,
+      Err(e) => {
+        return self.error("Couldn't find Community");
+      }
+    };
+
+    // Return the jwt
+    serde_json::to_string(
+      &GetCommunityResponse {
+        op: self.op_type().to_string(),
+        community: community
+      }
+      )
+      .unwrap()
   }
 }
+
+impl Perform for CreateComment {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::CreateComment
+  }
+
+  fn perform(&self) -> 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 iss = claims.iss;
+
+    let comment_form = CommentForm {
+      content: self.content.to_owned(),
+      parent_id: self.parent_id.to_owned(),
+      post_id: self.post_id,
+      attributed_to: format!("{}/{}", iss, user_id),
+      updated: None
+    };
+
+    let inserted_comment = match Comment::create(&conn, &comment_form) {
+      Ok(comment) => comment,
+      Err(e) => {
+        return self.error("Couldn't create Post");
+      }
+    };
+
+    serde_json::to_string(
+      &CreateCommentResponse {
+        op: self.op_type().to_string(), 
+        comment: inserted_comment
+      }
+      )
+      .unwrap()
+  }
+}
+
+
+
 // impl Handler<Login> for ChatServer {
 
 //   type Result = MessageResult<Login>;
@@ -644,3 +808,49 @@ impl PerformAuth for CreateCommunity {
 //       )
 //   }
 // }
+//
+//
+//
+// /// Handler for `ListRooms` message.
+// impl Handler<ListRooms> for ChatServer {
+//   type Result = MessageResult<ListRooms>;
+
+//   fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Self::Result {
+//     let mut rooms = Vec::new();
+
+//     for key in self.rooms.keys() {
+//       rooms.push(key.to_owned())
+//     }
+
+//     MessageResult(rooms)
+//   }
+// }
+
+// /// Join room, send disconnect message to old room
+// /// send join message to new room
+// impl Handler<Join> for ChatServer {
+//   type Result = ();
+
+//   fn handle(&mut self, msg: Join, _: &mut Context<Self>) {
+//     let Join { id, name } = msg;
+//     let mut rooms = Vec::new();
+
+//     // remove session from all rooms
+//     for (n, sessions) in &mut self.rooms {
+//       if sessions.remove(&id) {
+//         rooms.push(n.to_owned());
+//       }
+//     }
+//     // send message to other users
+//     for room in rooms {
+//       self.send_room_message(&room, "Someone disconnected", 0);
+//     }
+
+//     if self.rooms.get_mut(&name).is_none() {
+//       self.rooms.insert(name.clone(), HashSet::new());
+//     }
+//     self.send_room_message(&name, "Someone connected", id);
+//     self.rooms.get_mut(&name).unwrap().insert(id);
+//   }
+
+// }
diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx
new file mode 100644 (file)
index 0000000..b032263
--- /dev/null
@@ -0,0 +1,72 @@
+import { Component, linkEvent } from 'inferno';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { UserOperation, Community as CommunityI, CommunityResponse, Post } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { msgOp } from '../utils';
+
+interface State {
+  community: CommunityI;
+  posts: Array<Post>;
+}
+
+export class Community extends Component<any, State> {
+
+  private subscription: Subscription;
+  private emptyState: State = {
+    community: {
+      id: null,
+      name: null,
+      published: null
+    },
+    posts: []
+  }
+
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = this.emptyState;
+
+    console.log(this.props.match.params.id);
+
+    this.subscription = WebSocketService.Instance.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        (msg) => this.parseMessage(msg),
+        (err) => console.error(err),
+        () => console.log('complete')
+      );
+
+    let communityId = Number(this.props.match.params.id);
+    WebSocketService.Instance.getCommunity(communityId);
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  render() {
+    return (
+      <div class="container">
+        <div class="row">
+          <div class="col-12 col-lg-6 mb-4">
+            {this.state.community.name}
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  parseMessage(msg: any) {
+    console.log(msg);
+    let op: UserOperation = msgOp(msg);
+    if (msg.error) {
+      alert(msg.error);
+      return;
+    } else if (op == UserOperation.GetCommunity) {
+      let res: CommunityResponse = msg;
+      this.state.community = res.community;
+      this.setState(this.state);
+    }  
+  }
+}
index 159147b6cd9671ceafd326fba4ed43ec5f89fe19..0a0edae6afb667065d5e7a6c2c09b014ce7730a4 100644 (file)
@@ -11,20 +11,20 @@ interface State {
   communityForm: CommunityForm;
 }
 
-let emptyState: State = {
-  communityForm: {
-    name: null,
-  }
-}
-
 export class CreateCommunity extends Component<any, State> {
   private subscription: Subscription;
 
+  private emptyState: State = {
+    communityForm: {
+      name: null,
+    }
+  }
+
   constructor(props, context) {
     super(props, context);
 
-    this.state = emptyState;
-
+    this.state = this.emptyState;
+    
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
@@ -89,7 +89,8 @@ export class CreateCommunity extends Component<any, State> {
       return;
     } else {
       if (op == UserOperation.CreateCommunity) {
-        let community: Community = msg.data;
+        let community: Community = msg.community;
+        this.props.history.push(`/community/${community.id}`);
       }
     }
   }
index bb6e60e2702514e1e62fa7d06fd3e262780fd686..9ddf8c97f6510ab576d8735574ee702b3c856f58 100644 (file)
 import { Component, linkEvent } from 'inferno';
-
-import { LoginForm, PostForm, UserOperation } from '../interfaces';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { PostForm, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp } from '../utils';
 
 interface State {
   postForm: PostForm;
+  communities: Array<Community>;
 }
 
-let emptyState: State = {
-  postForm: {
-    name: null,
-    url: null,
-    attributed_to: null
-  }
-}
 
 export class CreatePost extends Component<any, State> {
 
+  private subscription: Subscription;
+  private emptyState: State = {
+    postForm: {
+      name: null,
+      auth: null,
+      community_id: null
+    },
+    communities: []
+  }
+
   constructor(props, context) {
     super(props, context);
 
-    this.state = emptyState;
+    this.state = this.emptyState;
 
-    WebSocketService.Instance.subject.subscribe(
-      (msg) => this.parseMessage(msg),
-      (err) => console.error(err),
-      () => console.log('complete')
-    );
+    this.subscription = WebSocketService.Instance.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        (msg) => this.parseMessage(msg),
+        (err) => console.error(err),
+        () => console.log('complete')
+      );
+
+    WebSocketService.Instance.listCommunities();
   }
 
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
 
   render() {
     return (
       <div class="container">
         <div class="row">
           <div class="col-12 col-lg-6 mb-4">
-            create post
-            {/* {this.postForm()} */}
+            {this.postForm()}
           </div>
         </div>
       </div>
     )
   }
 
+  postForm() {
+    return (
+      <div>
+        <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
+          <h3>Create a Post</h3>
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">URL</label>
+            <div class="col-sm-10">
+              <input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, this.handlePostUrlChange)} />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">Title</label>
+            <div class="col-sm-10">
+              <textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows="3" />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">Body</label>
+            <div class="col-sm-10">
+              <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows="6" />
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">Forum</label>
+            <div class="col-sm-10">
+              <select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
+                {this.state.communities.map(community =>
+                  <option value={community.id}>{community.name}</option>
+                )}
+              </select>
+            </div>
+          </div>
+          <div class="form-group row">
+            <div class="col-sm-10">
+              <button type="submit" class="btn btn-secondary">Create Post</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+
+  handlePostSubmit(i: CreatePost, event) {
+    event.preventDefault();
+    WebSocketService.Instance.createPost(i.state.postForm);
+  }
+
+  handlePostUrlChange(i: CreatePost, event) {
+    i.state.postForm.url = event.target.value;
+    i.setState(i.state);
+  }
+
+  handlePostNameChange(i: CreatePost, event) {
+    i.state.postForm.name = event.target.value;
+    i.setState(i.state);
+  }
+
+  handlePostBodyChange(i: CreatePost, event) {
+    i.state.postForm.body = event.target.value;
+    i.setState(i.state);
+  }
+
+  handlePostCommunityChange(i: CreatePost, event) {
+    i.state.postForm.community_id = Number(event.target.value);
+    i.setState(i.state);
+  }
+
   parseMessage(msg: any) {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
       alert(msg.error);
       return;
-    } else {
+    } else if (op == UserOperation.ListCommunities) {
+      let res: ListCommunitiesResponse = msg;
+      this.state.communities = res.communities;
+      this.setState(this.state);
+    } else if (op == UserOperation.CreatePost) {
+      let res: PostResponse = msg;
+      this.props.history.push(`/post/${res.post.id}`);
     }
   }
 
index 60ee9e0152dd75e8949f21bdd75ce661ecdc673a..cad4593ea1155c07c1ccc959476ca871f390b263 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, linkEvent } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { LoginForm, RegisterForm, UserOperation } from '../interfaces';
+import { LoginForm, RegisterForm, LoginResponse, UserOperation } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp } from '../utils';
 
@@ -169,7 +169,8 @@ export class Login extends Component<any, State> {
       return;
     } else {
       if (op == UserOperation.Register || op == UserOperation.Login) {
-        UserService.Instance.login(msg.jwt);
+        let res: LoginResponse = msg;
+        UserService.Instance.login(msg);
         this.props.history.push('/');
       }
     }
index 4cf6d6d27cecbcada3c4ff1773f4216457e0873b..ae2d90b3fbe1c596eb9ae0449eeed2b11575531e 100644 (file)
@@ -58,5 +58,6 @@ export class Navbar extends Component<any, any> {
 
   handleLogoutClick(i: Navbar, event) {
     UserService.Instance.logout();
+    // i.props.history.push('/');
   }
 }
diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx
new file mode 100644 (file)
index 0000000..8d84f27
--- /dev/null
@@ -0,0 +1,126 @@
+import { Component, linkEvent } from 'inferno';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { UserOperation, Community, Post as PostI, PostResponse, Comment, CommentForm } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { msgOp } from '../utils';
+
+interface State {
+  post: PostI;
+  commentForm: CommentForm;
+  comments: Array<Comment>;
+}
+
+export class Post extends Component<any, State> {
+
+  private subscription: Subscription;
+  private emptyState: State = {
+    post: {
+      name: null,
+      attributed_to: null,
+      community_id: null,
+      id: null,
+      published: null,
+    },
+    commentForm: {
+      auth: null,
+      content: null,
+      post_id: null
+    },
+    comments: []
+  }
+
+  constructor(props, context) {
+    super(props, context);
+
+    let postId = Number(this.props.match.params.id);
+
+    this.state = this.emptyState;
+    this.state.commentForm.post_id = postId;
+
+    this.subscription = WebSocketService.Instance.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        (msg) => this.parseMessage(msg),
+        (err) => console.error(err),
+        () => console.log('complete')
+      );
+
+    WebSocketService.Instance.getPost(postId);
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  render() {
+    return (
+      <div class="container">
+        <div class="row">
+          <div class="col-12 col-lg-6 mb-4">
+            {this.state.post.name}
+            {this.commentForm()}
+            {this.comments()}
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  comments() {
+    return (
+      <div>
+        <h3>Comments</h3>
+        {this.state.comments.map(comment => 
+          <div>{comment.content}</div>
+        )}
+      </div>
+    )
+  }
+  
+  
+  commentForm() {
+    return (
+      <div>
+        <form onSubmit={linkEvent(this, this.handleCreateCommentSubmit)}>
+          <h3>Create Comment</h3>
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">Name</label>
+            <div class="col-sm-10">
+              <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required minLength={3} />
+            </div>
+          </div>
+          <div class="form-group row">
+            <div class="col-sm-10">
+              <button type="submit" class="btn btn-secondary">Create</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+  
+  handleCreateCommentSubmit(i: Post, event) {
+    event.preventDefault();
+    WebSocketService.Instance.createComment(i.state.commentForm);
+  }
+
+  handleCommentContentChange(i: Post, event) {
+    i.state.commentForm.content = event.target.value;
+    i.setState(i.state);
+  }
+
+  parseMessage(msg: any) {
+    console.log(msg);
+    let op: UserOperation = msgOp(msg);
+    if (msg.error) {
+      alert(msg.error);
+      return;
+    } else if (op == UserOperation.GetPost) {
+      let res: PostResponse = msg;
+      this.state.post = res.post;
+      this.state.comments = res.comments;
+      this.setState(this.state);
+    }
+  }
+}
index 1eb3a7d58f223f8b8d5d276b984f30891fa778bf..e68fc92a8850381c08a84471473558586813813e 100644 (file)
@@ -6,6 +6,8 @@ import { Home } from './components/home';
 import { Login } from './components/login';
 import { CreatePost } from './components/create-post';
 import { CreateCommunity } from './components/create-community';
+import { Post } from './components/post';
+import { Community } from './components/community';
 
 import './main.css';
 
@@ -31,12 +33,8 @@ class Index extends Component<any, any> {
             <Route path={`/login`} component={Login} />
             <Route path={`/create_post`} component={CreatePost} />
             <Route path={`/create_community`} component={CreateCommunity} />
-            {/*
-            <Route path={`/search/:type_/:q/:page`} component={Search} />
-            <Route path={`/submit`} component={Submit} />
-            <Route path={`/user/:id`} component={Login} />
-            <Route path={`/community/:id`} component={Login} /> 
-            */}
+            <Route path={`/post/:id`} component={Post} />
+            <Route path={`/community/:id`} component={Community} />
           </Switch>
         </div>
       </HashRouter>
index e620aa4ed4e9cbf7ad6e609b96a5eafc67705021..14c28438368f86a32312415a6558662a7f2e60a0 100644 (file)
@@ -1,5 +1,5 @@
 export enum UserOperation {
-  Login, Register, CreateCommunity
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, GetPost, GetCommunity, CreateComment
 }
 
 export interface User {
@@ -10,8 +10,71 @@ export interface User {
 export interface Community {
   id: number;
   name: string;
-  published: Date;
-  updated?: Date;
+  published: string;
+  updated?: string;
+}
+
+export interface CommunityForm {
+  name: string;
+  auth?: string;
+}
+
+export interface CommunityResponse {
+  op: string;
+  community: Community;
+}
+
+export interface ListCommunitiesResponse {
+  op: string;
+  communities: Array<Community>;
+}
+
+export interface Post {
+  id: number;
+  name: string;
+  url?: string;
+  body?: string;
+  attributed_to: string;
+  community_id: number;
+  published: string;
+  updated?: string;
+}
+
+export interface PostForm {
+  name: string;
+  url?: string;
+  body?: string;
+  community_id: number;
+  updated?: number;
+  auth: string;
+}
+
+export interface PostResponse {
+  op: string;
+  post: Post;
+  comments: Array<Comment>;
+}
+
+export interface Comment {
+  id: number;
+  content: string;
+  attributed_to: string;
+  post_id: number,
+  parent_id?: number;
+  published: string;
+  updated?: string;
+}
+
+export interface CommentForm {
+  content: string;
+  post_id: number;
+  parent_id?: number;
+  auth: string;
+}
+
+export interface CommentResponse {
+  op: string;
+  comment: Comment;
 }
 
 export interface LoginForm {
@@ -26,13 +89,9 @@ export interface RegisterForm {
   password_verify: string;
 }
 
-export interface CommunityForm {
-  name: string;
+export interface LoginResponse {
+  op: string;
+  jwt: string;
 }
 
-export interface PostForm {
-  name: string;
-  url: string;
-  attributed_to: string;
-  updated?: number
-}
+
index d90fbde5af0720967b3b8d6bf2b59699922c48f9..42411f8832be419ee4ff6a8cae41ab1fb1a51c3e 100644 (file)
@@ -1,5 +1,5 @@
 import * as Cookies from 'js-cookie';
-import { User } from '../interfaces';
+import { User, LoginResponse } from '../interfaces';
 import * as jwt_decode from 'jwt-decode';
 import { Subject } from 'rxjs';
 
@@ -18,9 +18,9 @@ export class UserService {
 
   }
 
-  public login(jwt: string) {
-    this.setUser(jwt);
-    Cookies.set("jwt", jwt);
+  public login(res: LoginResponse) {
+    this.setUser(res.jwt);
+    Cookies.set("jwt", res.jwt);
     console.log("jwt cookie set");
   }
 
index 1882b125e47372da1c0e18c6856004a17d9efd2b..cd67e2f9f1b3e70ad8e7c280d40d63e217afbacc 100644 (file)
@@ -1,15 +1,22 @@
 import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
 import { UserService } from './';
 
 export class WebSocketService {
   private static _instance: WebSocketService;
-  public subject: Subject<{}>;
+  public subject: Subject<any>;
 
   private constructor() {
     this.subject = webSocket(wsUri);
+
+    // Even tho this isn't used, its necessary to not keep reconnecting
+    this.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe();
+
     console.log(`Connected to ${wsUri}`);
   }
 
@@ -26,12 +33,47 @@ export class WebSocketService {
   }
 
   public createCommunity(communityForm: CommunityForm) {
-    this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm, UserService.Instance.auth));
+    this.setAuth(communityForm);
+    this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm));
+  }
+
+  public listCommunities() {
+    this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, undefined));
+  }
+
+  public createPost(postForm: PostForm) {
+    this.setAuth(postForm);
+    this.subject.next(this.wsSendWrapper(UserOperation.CreatePost, postForm));
+  }
+
+  public getPost(postId: number) {
+    this.subject.next(this.wsSendWrapper(UserOperation.GetPost, {id: postId}));
   }
 
-  private wsSendWrapper(op: UserOperation, data: any, auth?: string) {
-    let send = { op: UserOperation[op], data: data, auth: auth };
+  public getCommunity(communityId: number) {
+    this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, {id: communityId}));
+  }
+
+  public createComment(commentForm: CommentForm) {
+    this.setAuth(commentForm);
+    this.subject.next(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
+  }
+
+  public getComments(postId: number) {
+    this.subject.next(this.wsSendWrapper(UserOperation.GetComments, {post_id: postId}));
+  }
+
+  private wsSendWrapper(op: UserOperation, data: any) {
+    let send = { op: UserOperation[op], data: data };
     console.log(send);
     return send;
   }
+
+  private setAuth(obj: any) {
+    obj.auth = UserService.Instance.auth;
+    if (obj.auth == null) {
+      alert("Not logged in.");
+      throw "Not logged in";
+    }
+  }
 }