]> Untitled Git - lemmy.git/commitdiff
Adding comment voting
authorDessalines <tyhou13@gmx.com>
Thu, 28 Mar 2019 19:32:08 +0000 (12:32 -0700)
committerDessalines <tyhou13@gmx.com>
Thu, 28 Mar 2019 19:32:08 +0000 (12:32 -0700)
- Extracting out some components.
- Fixing an issue with window refreshing websockets.

server/migrations/2019-02-27-170003_create_community/up.sql
server/migrations/2019-03-03-163336_create_post/up.sql
server/migrations/2019-03-05-233828_create_comment/up.sql
server/src/actions/comment.rs
server/src/actions/post.rs
server/src/lib.rs
server/src/schema.rs
server/src/websocket_server/server.rs
ui/src/components/post.tsx
ui/src/interfaces.ts
ui/src/services/WebSocketService.ts

index 1ee2e51df19c6a0cd401f18bf00913a93f3bd79c..651a94323583fa6e245c6dbe9c0402935c6742c4 100644 (file)
@@ -18,3 +18,5 @@ create table community_follower (
   fedi_user_id text not null,
   published timestamp not null default now()
 );
+
+insert into community (name) values ('main');
index f22192f3e5b7bfb99e9c3d3ce5b736d1733fdb15..14294c8f1d65c2d5fd0491e2897b67c95dcae812 100644 (file)
@@ -16,4 +16,3 @@ create table post_like (
   score smallint not null, -- -1, or 1 for dislike, like, no row for no opinion
   published timestamp not null default now()
 );
-
index 63fc758d2921ae63bb1cd2dc50fa1d6b39fac69d..c80f8d18a89b32dd3ebd902f98500bce83e6b3e8 100644 (file)
@@ -11,7 +11,9 @@ create table comment (
 create table comment_like (
   id serial primary key,
   comment_id int references comment on update cascade on delete cascade not null,
+  post_id int references post on update cascade on delete cascade not null,
   fedi_user_id text not null,
   score smallint not null, -- -1, or 1 for dislike, like, no row for no opinion
-  published timestamp not null default now()
+  published timestamp not null default now(),
+  unique(comment_id, fedi_user_id)
 );
index 93e808a4400d54f2c8221319c35777153016cf0b..7f2dace6c587cc35f3e598b9b3e7c3d2d7a2d915 100644 (file)
@@ -36,12 +36,13 @@ pub struct CommentForm {
   pub updated: Option<chrono::NaiveDateTime>
 }
 
-#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
+#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
 #[belongs_to(Comment)]
 #[table_name = "comment_like"]
 pub struct CommentLike {
   pub id: i32,
   pub comment_id: i32,
+  pub post_id: i32,
   pub fedi_user_id: String,
   pub score: i16,
   pub published: chrono::NaiveDateTime,
@@ -51,6 +52,7 @@ pub struct CommentLike {
 #[table_name="comment_like"]
 pub struct CommentLikeForm {
   pub comment_id: i32,
+  pub post_id: i32,
   pub fedi_user_id: String,
   pub score: i16
 }
@@ -70,9 +72,9 @@ impl Crud<CommentForm> for Comment {
 
   fn create(conn: &PgConnection, comment_form: &CommentForm) -> Result<Self, Error> {
     use schema::comment::dsl::*;
-      insert_into(comment)
-        .values(comment_form)
-        .get_result::<Self>(conn)
+    insert_into(comment)
+      .values(comment_form)
+      .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, comment_id: i32, comment_form: &CommentForm) -> Result<Self, Error> {
@@ -84,6 +86,13 @@ impl Crud<CommentForm> for Comment {
 }
 
 impl Likeable <CommentLikeForm> for CommentLike {
+  fn read(conn: &PgConnection, comment_id_from: i32) -> Result<Vec<Self>, Error> {
+    use schema::comment_like::dsl::*;
+    comment_like
+      .filter(comment_id.eq(comment_id_from))
+      .load::<Self>(conn) 
+  }
+
   fn like(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<Self, Error> {
     use schema::comment_like::dsl::*;
     insert_into(comment_like)
@@ -93,21 +102,116 @@ impl Likeable <CommentLikeForm> for CommentLike {
   fn remove(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<usize, Error> {
     use schema::comment_like::dsl::*;
     diesel::delete(comment_like
-      .filter(comment_id.eq(comment_like_form.comment_id))
-      .filter(fedi_user_id.eq(&comment_like_form.fedi_user_id)))
+                   .filter(comment_id.eq(comment_like_form.comment_id))
+                   .filter(fedi_user_id.eq(&comment_like_form.fedi_user_id)))
       .execute(conn)
   }
 }
 
+impl CommentLike {
+  pub fn from_post(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
+    use schema::comment_like::dsl::*;
+    comment_like
+      .filter(post_id.eq(post_id_from))
+      .load::<Self>(conn) 
+  }
+}
+
+
+
 impl Comment {
-  pub fn from_post(conn: &PgConnection, post: &Post) -> Result<Vec<Self>, Error> {
-    use schema::community::dsl::*;
-    Comment::belonging_to(post)
-      .order_by(comment::published.desc())
+  fn from_post(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
+    use schema::comment::dsl::*;
+    comment
+      .filter(post_id.eq(post_id_from))
+      .order_by(published.desc())
       .load::<Self>(conn) 
   }
 }
 
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct CommentView {
+  pub id: i32,
+  pub content: String,
+  pub attributed_to: String,
+  pub post_id: i32,
+  pub parent_id: Option<i32>,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+  pub score: i32,
+  pub upvotes: i32,
+  pub downvotes: i32,
+  pub my_vote: Option<i16>
+}
+
+impl CommentView {
+  fn from_comment(comment: &Comment, likes: &Vec<CommentLike>, fedi_user_id: &Option<String>) -> Self {
+    let mut upvotes: i32 = 0;
+    let mut downvotes: i32 = 0;
+    let mut my_vote: Option<i16> = Some(0);
+
+    for like in likes.iter() {
+      if like.score == 1 {
+        upvotes += 1
+      } else if like.score == -1 {
+        downvotes += 1;
+      }
+
+      if let Some(user) = fedi_user_id {
+        if like.fedi_user_id == *user {
+          my_vote = Some(like.score);
+        }
+      }
+
+    }
+
+    let score: i32 = upvotes - downvotes;
+
+    CommentView {
+      id: comment.id,
+      content: comment.content.to_owned(),
+      parent_id: comment.parent_id,
+      post_id: comment.post_id,
+      attributed_to: comment.attributed_to.to_owned(),
+      published: comment.published,
+      updated: None,
+      upvotes: upvotes,
+      score: score,
+      downvotes: downvotes,
+      my_vote: my_vote
+    }
+  }
+
+  pub fn from_new_comment(comment: &Comment) -> Self {
+    Self::from_comment(comment, &Vec::new(), &None)
+  }
+
+  pub fn read(conn: &PgConnection, comment_id: i32, fedi_user_id: &Option<String>) -> Self {
+    let comment = Comment::read(&conn, comment_id).unwrap();
+    let likes = CommentLike::read(&conn, comment_id).unwrap();
+    Self::from_comment(&comment, &likes, fedi_user_id)
+  }
+
+  pub fn from_post(conn: &PgConnection, post_id: i32, fedi_user_id: &Option<String>) -> Vec<Self> {
+    let comments = Comment::from_post(&conn, post_id).unwrap();
+    let post_comment_likes = CommentLike::from_post(&conn, post_id).unwrap();
+    
+    let mut views = Vec::new();
+    for comment in comments.iter() {
+      let comment_likes: Vec<CommentLike> = post_comment_likes
+        .iter()
+        .filter(|like| comment.id == like.comment_id)
+        .cloned()
+        .collect();
+      let comment_view = CommentView::from_comment(&comment, &comment_likes, fedi_user_id);
+      views.push(comment_view);
+    };
+
+    views
+  }
+}
+
+
 #[cfg(test)]
 mod tests {
   use establish_connection;
@@ -169,6 +273,7 @@ mod tests {
 
     let comment_like_form = CommentLikeForm {
       comment_id: inserted_comment.id,
+      post_id: inserted_post.id,
       fedi_user_id: "test".into(),
       score: 1
     };
@@ -178,6 +283,7 @@ mod tests {
     let expected_comment_like = CommentLike {
       id: inserted_comment_like.id,
       comment_id: inserted_comment.id,
+      post_id: inserted_post.id,
       fedi_user_id: "test".into(),
       published: inserted_comment_like.published,
       score: 1
index 71846dff7ef26c019bdbe465812b7d83ebd82748..fff87dfd6f332fd49ecaf4feff62f915f01aff39 100644 (file)
@@ -77,6 +77,12 @@ impl Crud<PostForm> for Post {
 }
 
 impl Likeable <PostLikeForm> for PostLike {
+  fn read(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
+    use schema::post_like::dsl::*;
+    post_like
+      .filter(post_id.eq(post_id_from))
+      .load::<Self>(conn) 
+  }
   fn like(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<Self, Error> {
     use schema::post_like::dsl::*;
     insert_into(post_like)
index fcc9c2c83ebd9f9ec739eb81ce028216d65f3ef2..0d81d507e5f6a6a03d653dd7a6d62963d7231790 100644 (file)
@@ -43,6 +43,7 @@ pub trait Joinable<T> {
 }
 
 pub trait Likeable<T> {
+  fn read(conn: &PgConnection, id: i32) -> Result<Vec<Self>, Error> where Self: Sized;
   fn like(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
   fn remove(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
 }
index 28c4e8cad98d7568b4f93d5112ff10a2ef444bee..93add9ba0057fca12526ee63b8383524098ef932 100644 (file)
@@ -14,6 +14,7 @@ table! {
     comment_like (id) {
         id -> Int4,
         comment_id -> Int4,
+        post_id -> Int4,
         fedi_user_id -> Text,
         score -> Int2,
         published -> Timestamp,
@@ -85,6 +86,7 @@ table! {
 
 joinable!(comment -> post (post_id));
 joinable!(comment_like -> comment (comment_id));
+joinable!(comment_like -> post (post_id));
 joinable!(community_follower -> community (community_id));
 joinable!(community_user -> community (community_id));
 joinable!(post -> community (community_id));
index d257f4c00f55a9d7d8e8e90071f913fc751d603e..78a71ec8e9242a91d3d8444583f09632d40abbee 100644 (file)
@@ -9,8 +9,9 @@ use serde::{Deserialize, Serialize};
 use serde_json::{Result, Value};
 use bcrypt::{verify};
 use std::str::FromStr;
+use std::{thread, time};
 
-use {Crud, Joinable, establish_connection};
+use {Crud, Joinable, Likeable, establish_connection};
 use actions::community::*;
 use actions::user::*;
 use actions::post::*;
@@ -19,7 +20,7 @@ use actions::comment::*;
 
 #[derive(EnumString,ToString,Debug)]
 pub enum UserOperation {
-  Login, Register, Logout, CreateCommunity, ListCommunities, CreatePost, GetPost, GetCommunity, CreateComment, Join, Edit, Reply, Vote, Delete, NextPage, Sticky
+  Login, Register, Logout, CreateCommunity, ListCommunities, CreatePost, GetPost, GetCommunity, CreateComment, CreateCommentLike, Join, Edit, Reply, Vote, Delete, NextPage, Sticky
 }
 
 
@@ -151,14 +152,15 @@ pub struct CreatePostResponse {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetPost {
-  id: i32
+  id: i32,
+  auth: Option<String>
 }
 
 #[derive(Serialize, Deserialize)]
 pub struct GetPostResponse {
   op: String,
   post: Post,
-  comments: Vec<Comment>
+  comments: Vec<CommentView>
 }
 
 #[derive(Serialize, Deserialize)]
@@ -183,7 +185,22 @@ pub struct CreateComment {
 #[derive(Serialize, Deserialize)]
 pub struct CreateCommentResponse {
   op: String,
-  comment: Comment
+  comment: CommentView
+}
+
+
+#[derive(Serialize, Deserialize)]
+pub struct CreateCommentLike {
+  comment_id: i32,
+  post_id: i32,
+  score: i16,
+  auth: String
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CreateCommentLikeResponse {
+  op: String,
+  comment: CommentView
 }
 
 /// `ChatServer` manages chat rooms and responsible for coordinating chat
@@ -343,6 +360,10 @@ impl Handler<StandardMessage> for ChatServer {
         let create_comment: CreateComment = serde_json::from_str(&data.to_string()).unwrap();
         create_comment.perform(self, msg.id)
       },
+      UserOperation::CreateCommentLike => {
+        let create_comment_like: CreateCommentLike = serde_json::from_str(&data.to_string()).unwrap();
+        create_comment_like.perform(self, msg.id)
+      },
       _ => {
         let e = ErrorMessage { 
           op: "Unknown".to_string(),
@@ -576,6 +597,22 @@ impl Perform for GetPost {
 
     let conn = establish_connection();
 
+    println!("{:?}", self.auth);
+
+    let fedi_user_id: Option<String> = match &self.auth {
+      Some(auth) => {
+        match Claims::decode(&auth) {
+          Ok(claims) => {
+            let user_id = claims.claims.id;
+            let iss = claims.claims.iss;
+            Some(format!("{}/{}", iss, user_id))
+          }
+          Err(e) => None
+        }
+      }
+      None => None
+    };
+
     let post = match Post::read(&conn, self.id) {
       Ok(post) => post,
       Err(e) => {
@@ -583,37 +620,21 @@ impl Perform for GetPost {
       }
     };
 
-
-    // let mut rooms = Vec::new();
-
     // remove session from all rooms
     for (n, sessions) in &mut chat.rooms {
-      // if sessions.remove(&addr) {
-      //   // rooms.push(*n);
-      // }
       sessions.remove(&addr);
     }
-    //     // send message to other users
-    //     for room in rooms {
-    //       self.send_room_message(&room, "Someone disconnected", 0);
-    //     }
 
     if chat.rooms.get_mut(&self.id).is_none() {
       chat.rooms.insert(self.id, HashSet::new());
     }
 
-    // TODO send a Joined response
-
-
-
-    // chat.send_room_message(addr,)
-    //     self.send_room_message(&name, "Someone connected", id);
     chat.rooms.get_mut(&self.id).unwrap().insert(addr);
 
-    let comments = Comment::from_post(&conn, &post).unwrap();
+    let comments = CommentView::from_post(&conn, post.id, &fedi_user_id);
 
-    println!("{:?}", chat.rooms.keys());
-    println!("{:?}", chat.rooms.get(&5i32).unwrap());
+    // println!("{:?}", chat.rooms.keys());
+    // println!("{:?}", chat.rooms.get(&5i32).unwrap());
 
     // Return the jwt
     serde_json::to_string(
@@ -688,24 +709,98 @@ impl Perform for CreateComment {
       }
     };
 
+    // TODO You like your own comment by default
+
+    // Simulate a comment view to get back blank score, no need to fetch anything
+    let comment_view = CommentView::from_new_comment(&inserted_comment);
+
     let comment_out = serde_json::to_string(
       &CreateCommentResponse {
         op: self.op_type().to_string(), 
-        comment: inserted_comment
+        comment: comment_view
       }
       )
       .unwrap();
     
     chat.send_room_message(self.post_id, &comment_out, addr);
 
-    println!("{:?}", chat.rooms.keys());
-    println!("{:?}", chat.rooms.get(&5i32).unwrap());
+    // println!("{:?}", chat.rooms.keys());
+    // println!("{:?}", chat.rooms.get(&5i32).unwrap());
 
     comment_out
   }
 }
 
 
+impl Perform for CreateCommentLike {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::CreateCommentLike
+  }
+
+  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 iss = claims.iss;
+    let fedi_user_id = format!("{}/{}", iss, user_id);
+
+    let like_form = CommentLikeForm {
+      comment_id: self.comment_id,
+      post_id: self.post_id,
+      fedi_user_id: fedi_user_id.to_owned(),
+      score: self.score
+    };
+
+    // Remove any likes first
+    CommentLike::remove(&conn, &like_form).unwrap();
+
+    // Only add the like if the score isnt 0
+    if &like_form.score != &0 {
+      let inserted_like = match CommentLike::like(&conn, &like_form) {
+        Ok(like) => like,
+        Err(e) => {
+          return self.error("Couldn't like comment.");
+        }
+      };
+    }
+
+    // Have to refetch the comment to get the current state
+    // thread::sleep(time::Duration::from_secs(1));
+    let liked_comment = CommentView::read(&conn, self.comment_id, &Some(fedi_user_id));
+
+    let mut liked_comment_sent = liked_comment.clone();
+    liked_comment_sent.my_vote = None;
+
+    let like_out = serde_json::to_string(
+      &CreateCommentLikeResponse {
+        op: self.op_type().to_string(), 
+        comment: liked_comment
+      }
+      )
+      .unwrap();
+
+    let like_sent_out = serde_json::to_string(
+      &CreateCommentLikeResponse {
+        op: self.op_type().to_string(), 
+        comment: liked_comment_sent
+      }
+      )
+      .unwrap();
+
+      chat.send_room_message(self.post_id, &like_sent_out, addr);
+
+      like_out
+  }
+}
+
 
 // impl Handler<Login> for ChatServer {
 
index 867e1a4a49cde36a342f3d10f7aa7c790d901714..2a780cf767c6a53cfd86f0d5f858acaba045a7f1 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, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse } from '../interfaces';
+import { UserOperation, Community, Post as PostI, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CreateCommentLikeResponse } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp } from '../utils';
 import { MomentTime } from './moment-time';
@@ -9,7 +9,6 @@ import { MomentTime } from './moment-time';
 interface CommentNodeI {
   comment: Comment;
   children?: Array<CommentNodeI>;
-  showReply?: boolean;
 };
 
 interface State {
@@ -78,7 +77,7 @@ export class Post extends Component<any, State> {
       ? <h5>
       <a href={this.state.post.url}>{this.state.post.name}</a>
       <small><a className="ml-2 text-muted font-italic" href={this.state.post.url}>{(new URL(this.state.post.url)).hostname}</a></small>
-      </h5> 
+    </h5> 
       : <h5>{this.state.post.name}</h5>;
     return (
       <div>
@@ -141,7 +140,6 @@ export class Post extends Component<any, State> {
     );
   }
 
-
   parseMessage(msg: any) {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
@@ -157,6 +155,16 @@ export class Post extends Component<any, State> {
       let res: CommentResponse = msg;
       this.state.comments.unshift(res.comment);
       this.setState(this.state);
+    } else if (op == UserOperation.CreateCommentLike) {
+      let res: CreateCommentLikeResponse = msg;
+      let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
+      found.score = res.comment.score;
+      found.upvotes = res.comment.upvotes;
+      found.downvotes = res.comment.downvotes;
+      if (res.comment.my_vote !== null) 
+        found.my_vote = res.comment.my_vote;
+      console.log(res.comment.my_vote);
+      this.setState(this.state);
     }
 
   }
@@ -174,75 +182,128 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
 
   constructor(props, context) {
     super(props, context);
-    this.handleReplyClick = this.handleReplyClick.bind(this);
-    this.handleReplyCancel = this.handleReplyCancel.bind(this);
   }
 
   render() {
     return (
       <div className="comments">
         {this.props.nodes.map(node =>
-          <div className={`comment ${node.comment.parent_id  && !this.props.noIndent ? 'ml-4' : ''}`}>
-            <div className="float-left small text-center">
-              <div className="pointer upvote">▲</div>
-              <div>20</div>
-              <div className="pointer downvote">▼</div>
-            </div>
-            <div className="details ml-4">
-            <ul class="list-inline mb-0 text-muted small">
-              <li className="list-inline-item">
-                <a href={node.comment.attributed_to}>{node.comment.attributed_to}</a>
-              </li>
-              <li className="list-inline-item">
-                <span>(
-                  <span className="text-info">+1300</span>
-                  <span> | </span>
-                  <span className="text-danger">-29</span>
-                  <span>) </span>
-                </span>
-              </li>
-              <li className="list-inline-item">
-                <span><MomentTime data={node.comment} /></span>
-              </li>
-            </ul>
-            <p className="mb-0">{node.comment.content}</p>
-            <ul class="list-inline mb-1 text-muted small font-weight-bold">
-              <li className="list-inline-item">
-                <span class="pointer" onClick={linkEvent(node, this.handleReplyClick)}>reply</span>
-              </li>
-              <li className="list-inline-item">
-                <a className="text-muted" href="test">link</a>
-              </li>
-            </ul>
-          </div>
-          {node.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
-          {node.children && <CommentNodes nodes={node.children}/>}
-          </div>
+          <CommentNode node={node} noIndent={this.props.noIndent} />
         )}
       </div>
     )
   }
+}
 
-  handleReplyClick(i: CommentNodeI, event) {
-    i.showReply = true;
-    this.setState(this.state);
+
+interface CommentNodeState {
+  showReply: boolean;
+}
+
+interface CommentNodeProps {
+  node: CommentNodeI;
+  noIndent?: boolean;
+}
+
+export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
+
+  private emptyState: CommentNodeState = {
+    showReply: false
+  }
+
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = this.emptyState;
+    this.handleReplyCancel = this.handleReplyCancel.bind(this);
+    this.handleCommentLike = this.handleCommentLike.bind(this);
+    this.handleCommentDisLike = this.handleCommentDisLike.bind(this);
+  }
+
+  render() {
+    let node = this.props.node;
+    return (
+      <div className={`comment ${node.comment.parent_id  && !this.props.noIndent ? 'ml-4' : ''}`}>
+        <div className="float-left small text-center">
+          <div className={`pointer upvote ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>▲</div>
+          <div>{node.comment.score}</div>
+          <div className={`pointer downvote ${node.comment.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>▼</div>
+        </div>
+        <div className="details ml-4">
+          <ul class="list-inline mb-0 text-muted small">
+            <li className="list-inline-item">
+              <a href={node.comment.attributed_to}>{node.comment.attributed_to}</a>
+            </li>
+            <li className="list-inline-item">
+              <span>(
+                <span className="text-info">+{node.comment.upvotes}</span>
+                <span> | </span>
+                <span className="text-danger">-{node.comment.downvotes}</span>
+                <span>) </span>
+              </span>
+            </li>
+            <li className="list-inline-item">
+              <span><MomentTime data={node.comment} /></span>
+            </li>
+          </ul>
+          <p className="mb-0">{node.comment.content}</p>
+          <ul class="list-inline mb-1 text-muted small font-weight-bold">
+            <li className="list-inline-item">
+              <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
+            </li>
+            <li className="list-inline-item">
+              <a className="text-muted" href="test">link</a>
+            </li>
+          </ul>
+        </div>
+        {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
+        {this.props.node.children && <CommentNodes nodes={this.props.node.children}/>}
+      </div>
+    )
+  }
+
+  private getScore(): number {
+    return (this.props.node.comment.upvotes - this.props.node.comment.downvotes) || 0;
+  }
+
+  handleReplyClick(i: CommentNode, event) {
+    i.state.showReply = true;
+    i.setState(i.state);
   }
 
-  handleReplyCancel(i: CommentNodeI): any {
-    i.showReply = false;
+  handleReplyCancel(): any {
+    this.state.showReply = false;
     this.setState(this.state);
   }
+
+  handleCommentLike(i: CommentNodeI, event) {
+
+    let form: CommentLikeForm = {
+      comment_id: i.comment.id,
+      post_id: i.comment.post_id,
+      score: (i.comment.my_vote == 1) ? 0 : 1
+    };
+    WebSocketService.Instance.likeComment(form);
+  }
+
+  handleCommentDisLike(i: CommentNodeI, event) {
+    let form: CommentLikeForm = {
+      comment_id: i.comment.id,
+      post_id: i.comment.post_id,
+      score: (i.comment.my_vote == -1) ? 0 : -1
+    };
+    WebSocketService.Instance.likeComment(form);
+  }
 }
 
 interface CommentFormProps {
   postId?: number;
   node?: CommentNodeI;
-  onReplyCancel?(node: CommentNodeI);
+  onReplyCancel?();
 }
 
 interface CommentFormState {
   commentForm: CommentFormI;
-  topReply: boolean;
 }
 
 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
@@ -253,8 +314,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       content: null,
       post_id: null,
       parent_id: null
-    },
-    topReply: true
+    }
   }
 
   constructor(props, context) {
@@ -262,16 +322,11 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
     this.state = this.emptyState;
     if (this.props.node) {
-      this.state.topReply = false;
       this.state.commentForm.post_id = this.props.node.comment.post_id;
       this.state.commentForm.parent_id = this.props.node.comment.id;
     } else {
       this.state.commentForm.post_id = this.props.postId;
     }
-
-    console.log(this.state);
-
-    this.handleReplyCancel = this.handleReplyCancel.bind(this);
   }
 
   render() {
@@ -286,7 +341,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
           <div class="row">
             <div class="col-sm-12">
               <button type="submit" class="btn btn-secondary mr-2">Post</button>
-              {!this.state.topReply && <button type="button" class="btn btn-secondary" onClick={this.handleReplyCancel}>Cancel</button>}
+              {this.props.node && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
             </div>
           </div>
         </form>
@@ -299,6 +354,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     i.state.commentForm.content = undefined;
     i.setState(i.state);
     event.target.reset();
+    if (i.props.node) {
+      i.props.onReplyCancel();
+    }
   }
 
   handleCommentContentChange(i: CommentForm, event) {
@@ -306,7 +364,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     i.state.commentForm.content = event.target.value;
   }
 
-  handleReplyCancel(event) {
-    this.props.onReplyCancel(this.props.node);
+  handleReplyCancel(i: CommentForm, event) {
+    i.props.onReplyCancel();
   }
 }
index 14c28438368f86a32312415a6558662a7f2e60a0..d499eb0a7554e7c20a0f23b5888f924c7510a764 100644 (file)
@@ -1,5 +1,5 @@
 export enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, GetPost, GetCommunity, CreateComment
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, GetPost, GetCommunity, CreateComment, CreateCommentLike
 }
 
 export interface User {
@@ -63,6 +63,10 @@ export interface Comment {
   parent_id?: number;
   published: string;
   updated?: string;
+  score: number;
+  upvotes: number;
+  downvotes: number;
+  my_vote?: number;
 }
 
 export interface CommentForm {
@@ -77,6 +81,18 @@ export interface CommentResponse {
   comment: Comment;
 }
 
+export interface CommentLikeForm {
+  comment_id: number;
+  post_id: number;
+  score: number;
+  auth?: string;
+}
+
+export interface CreateCommentLikeResponse {
+  op: string;
+  comment: Comment;
+}
+
 export interface LoginForm {
   username_or_email: string;
   password: string;
index beefac8570d12a070a980781884aa7b90d49e8ed..ed08fa1ec65284191ff48943798ddbe102cc5934 100644 (file)
@@ -1,5 +1,5 @@
 import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -47,7 +47,8 @@ export class WebSocketService {
   }
 
   public getPost(postId: number) {
-    this.subject.next(this.wsSendWrapper(UserOperation.GetPost, {id: postId}));
+    let data = {id: postId, auth: UserService.Instance.auth };
+    this.subject.next(this.wsSendWrapper(UserOperation.GetPost, data));
   }
 
   public getCommunity(communityId: number) {
@@ -59,6 +60,11 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
   }
 
+  public likeComment(form: CommentLikeForm) {
+    this.setAuth(form);
+    this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form));
+  }
+
   private wsSendWrapper(op: UserOperation, data: any) {
     let send = { op: UserOperation[op], data: data };
     console.log(send);
@@ -72,4 +78,10 @@ export class WebSocketService {
       throw "Not logged in";
     }
   }
+
 }
+
+window.onbeforeunload = (e => {
+  WebSocketService.Instance.subject.unsubscribe();
+});
+