]> Untitled Git - lemmy.git/commitdiff
Adding user details / overview page.
authorDessalines <tyhou13@gmx.com>
Mon, 8 Apr 2019 05:19:02 +0000 (22:19 -0700)
committerDessalines <tyhou13@gmx.com>
Mon, 8 Apr 2019 05:19:02 +0000 (22:19 -0700)
- Fixes #19

36 files changed:
README.md
server/migrations/2019-02-27-170003_create_community/up.sql
server/migrations/2019-04-08-015947_create_user_view/down.sql [new file with mode: 0644]
server/migrations/2019-04-08-015947_create_user_view/up.sql [new file with mode: 0644]
server/src/actions/comment_view.rs
server/src/actions/mod.rs
server/src/actions/post_view.rs
server/src/actions/user_view.rs [new file with mode: 0644]
server/src/lib.rs
server/src/websocket_server/server.rs
ui/package.json
ui/src/components/comment-form.tsx [new file with mode: 0644]
ui/src/components/comment-node.tsx [new file with mode: 0644]
ui/src/components/comment-nodes.tsx [new file with mode: 0644]
ui/src/components/communities.tsx
ui/src/components/community-form.tsx
ui/src/components/community.tsx
ui/src/components/create-community.tsx
ui/src/components/create-post.tsx
ui/src/components/home.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/moment-time.tsx
ui/src/components/navbar.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listing.tsx
ui/src/components/post-listings.tsx
ui/src/components/post.tsx
ui/src/components/sidebar.tsx
ui/src/components/user.tsx [new file with mode: 0644]
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/main.css
ui/src/services/WebSocketService.ts
ui/src/utils.ts
ui/yarn.lock

index 96d81eade0915b199c723149e395a8c739926115..1c518a4e78ff4eaa15e974d93fb8ded295da3890 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-<h1><img src="https://image.flaticon.com/icons/svg/194/194242.svg" width="30px"/> Lemmy</h1>
+<h1><img src="https://image.flaticon.com/icons/svg/194/194242.svg" width="50px" height="50px" /> Lemmy</h1>
 
 [![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
 [![star this repo](http://githubbadges.com/star.svg?user=dessalines&repo=lemmy&style=flat)](https://github.com/dessalines/lemmy)
@@ -19,6 +19,15 @@ Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Infern
 
 ## Features
 - TBD
+-
+the name
+
+Lead singer from motorhead.
+The old school video game.
+The furry rodents.
+
+Goals r/ censorship
+
 ## Install
 ### Docker
 ```
index f78486d583ead9464ad3f3808bcb3b96f98fe7aa..46b4df52d38a75c5bcc8687bb3a99db2242b6912 100644 (file)
@@ -31,8 +31,6 @@ insert into category (name) values
 ('Meta'),
 ('Other');
 
-
-
 create table community (
   id serial primary key,
   name varchar(20) not null unique,
@@ -58,4 +56,4 @@ create table community_follower (
   published timestamp not null default now()
 );
 
-insert into community (name, title, category_id, creator_id) values ('main', 'The default Community', 1, 1);
+insert into community (name, title, category_id, creator_id) values ('main', 'The Default Community', 1, 1);
diff --git a/server/migrations/2019-04-08-015947_create_user_view/down.sql b/server/migrations/2019-04-08-015947_create_user_view/down.sql
new file mode 100644 (file)
index 0000000..c94d94c
--- /dev/null
@@ -0,0 +1 @@
+drop view user_view;
diff --git a/server/migrations/2019-04-08-015947_create_user_view/up.sql b/server/migrations/2019-04-08-015947_create_user_view/up.sql
new file mode 100644 (file)
index 0000000..69d052d
--- /dev/null
@@ -0,0 +1,11 @@
+create view user_view as 
+select id,
+name,
+fedi_name,
+published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
+
index dcfcc250468cbb29175d7f0ec0d584a06a890c37..417a677250683ea9518482fdc06702f2ffa07d2b 100644 (file)
@@ -1,7 +1,9 @@
 extern crate diesel;
 use diesel::*;
 use diesel::result::Error;
+use diesel::dsl::*;
 use serde::{Deserialize, Serialize};
+use { SortType };
 
 // The faked schema since diesel doesn't do views
 table! {
@@ -42,33 +44,61 @@ pub struct CommentView {
 
 impl CommentView {
 
-  pub fn list(conn: &PgConnection, from_post_id: i32, from_user_id: Option<i32>) -> Result<Vec<Self>, Error> {
+  pub fn list(conn: &PgConnection, 
+              sort: &SortType, 
+              for_post_id: Option<i32>, 
+              for_creator_id: Option<i32>, 
+              my_user_id: Option<i32>, 
+              limit: i64) -> Result<Vec<Self>, Error> {
     use actions::comment_view::comment_view::dsl::*;
-    use diesel::prelude::*;
 
-    let mut query = comment_view.into_boxed();
+    let mut query = comment_view.limit(limit).into_boxed();
 
     // The view lets you pass a null user_id, if you're not logged in
-    if let Some(from_user_id) = from_user_id {
-      query = query.filter(user_id.eq(from_user_id));
+    if let Some(my_user_id) = my_user_id {
+      query = query.filter(user_id.eq(my_user_id));
     } else {
       query = query.filter(user_id.is_null());
     }
 
-    query = query.filter(post_id.eq(from_post_id)).order_by(published.desc());
+    if let Some(for_creator_id) = for_creator_id {
+      query = query.filter(creator_id.eq(for_creator_id));
+    };
+
+    if let Some(for_post_id) = for_post_id {
+      query = query.filter(post_id.eq(for_post_id));
+    };
+
+    query = match sort {
+      // SortType::Hot => query.order_by(hot_rank.desc()),
+      SortType::New => query.order_by(published.desc()),
+      SortType::TopAll => query.order_by(score.desc()),
+      SortType::TopYear => query
+        .filter(published.gt(now - 1.years()))
+        .order_by(score.desc()),
+        SortType::TopMonth => query
+          .filter(published.gt(now - 1.months()))
+          .order_by(score.desc()),
+          SortType::TopWeek => query
+            .filter(published.gt(now - 1.weeks()))
+            .order_by(score.desc()),
+            SortType::TopDay => query
+              .filter(published.gt(now - 1.days()))
+              .order_by(score.desc()),
+              _ => query.order_by(published.desc())
+    };
 
     query.load::<Self>(conn) 
   }
 
-  pub fn read(conn: &PgConnection, from_comment_id: i32, from_user_id: Option<i32>) -> Result<Self, Error> {
+  pub fn read(conn: &PgConnection, from_comment_id: i32, my_user_id: Option<i32>) -> Result<Self, Error> {
     use actions::comment_view::comment_view::dsl::*;
-    use diesel::prelude::*;
 
     let mut query = comment_view.into_boxed();
 
     // The view lets you pass a null user_id, if you're not logged in
-    if let Some(from_user_id) = from_user_id {
-      query = query.filter(user_id.eq(from_user_id));
+    if let Some(my_user_id) = my_user_id {
+      query = query.filter(user_id.eq(my_user_id));
     } else {
       query = query.filter(user_id.is_null());
     }
index c17fd81ad5ad1425a06f8e2661f6a07d3105e54a..819d5cdaf13db76ae123357731dc5080274d2fe6 100644 (file)
@@ -6,3 +6,4 @@ pub mod post_view;
 pub mod comment_view;
 pub mod category;
 pub mod community_view;
+pub mod user_view;
index 1db15ea68c085b3d0c5064264578e3d8e6efcd73..6afba18d58d42a357f6013d3c9d194d7058acde1 100644 (file)
@@ -1,18 +1,15 @@
 extern crate diesel;
 use diesel::*;
 use diesel::result::Error;
+use diesel::dsl::*;
 use serde::{Deserialize, Serialize};
+use { SortType };
 
 #[derive(EnumString,ToString,Debug, Serialize, Deserialize)]
-pub enum ListingType {
+pub enum PostListingType {
   All, Subscribed, Community
 }
 
-#[derive(EnumString,ToString,Debug, Serialize, Deserialize)]
-pub enum ListingSortType {
-  Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
-}
-
 // The faked schema since diesel doesn't do views
 table! {
   post_view (id) {
@@ -62,45 +59,53 @@ pub struct PostView {
 }
 
 impl PostView {
-  pub fn list(conn: &PgConnection, type_: ListingType, sort: ListingSortType, from_community_id: Option<i32>, from_user_id: Option<i32>, limit: i64) -> Result<Vec<Self>, Error> {
+  pub fn list(conn: &PgConnection, 
+              type_: PostListingType, 
+              sort: &SortType, 
+              for_community_id: Option<i32>, 
+              for_creator_id: Option<i32>, 
+              my_user_id: Option<i32>, 
+              limit: i64) -> Result<Vec<Self>, Error> {
     use actions::post_view::post_view::dsl::*;
-    use diesel::dsl::*;
-    use diesel::prelude::*;
 
     let mut query = post_view.limit(limit).into_boxed();
 
-    if let Some(from_community_id) = from_community_id {
-      query = query.filter(community_id.eq(from_community_id));
+    if let Some(for_community_id) = for_community_id {
+      query = query.filter(community_id.eq(for_community_id));
+    };
+
+    if let Some(for_creator_id) = for_creator_id {
+      query = query.filter(creator_id.eq(for_creator_id));
     };
 
     match type_ {
-      ListingType::Subscribed  => {
+      PostListingType::Subscribed  => {
         query = query.filter(subscribed.eq(true));
       },
       _ => {}
     };
 
     // The view lets you pass a null user_id, if you're not logged in
-    if let Some(from_user_id) = from_user_id {
-      query = query.filter(user_id.eq(from_user_id));
+    if let Some(my_user_id) = my_user_id {
+      query = query.filter(user_id.eq(my_user_id));
     } else {
       query = query.filter(user_id.is_null());
     }
 
     query = match sort {
-      ListingSortType::Hot => query.order_by(hot_rank.desc()),
-      ListingSortType::New => query.order_by(published.desc()),
-      ListingSortType::TopAll => query.order_by(score.desc()),
-      ListingSortType::TopYear => query
+      SortType::Hot => query.order_by(hot_rank.desc()),
+      SortType::New => query.order_by(published.desc()),
+      SortType::TopAll => query.order_by(score.desc()),
+      SortType::TopYear => query
         .filter(published.gt(now - 1.years()))
         .order_by(score.desc()),
-        ListingSortType::TopMonth => query
+        SortType::TopMonth => query
           .filter(published.gt(now - 1.months()))
           .order_by(score.desc()),
-          ListingSortType::TopWeek => query
+          SortType::TopWeek => query
             .filter(published.gt(now - 1.weeks()))
             .order_by(score.desc()),
-            ListingSortType::TopDay => query
+            SortType::TopDay => query
               .filter(published.gt(now - 1.days()))
               .order_by(score.desc())
     };
@@ -109,7 +114,7 @@ impl PostView {
   }
 
 
-  pub fn read(conn: &PgConnection, from_post_id: i32, from_user_id: Option<i32>) -> Result<Self, Error> {
+  pub fn read(conn: &PgConnection, from_post_id: i32, my_user_id: Option<i32>) -> Result<Self, Error> {
 
     use actions::post_view::post_view::dsl::*;
     use diesel::prelude::*;
@@ -118,8 +123,8 @@ impl PostView {
 
     query = query.filter(id.eq(from_post_id));
 
-    if let Some(from_user_id) = from_user_id {
-      query = query.filter(user_id.eq(from_user_id));
+    if let Some(my_user_id) = my_user_id {
+      query = query.filter(user_id.eq(my_user_id));
     } else {
       query = query.filter(user_id.is_null());
     };
@@ -244,8 +249,8 @@ mod tests {
     };
 
 
-    let read_post_listings_with_user = PostView::list(&conn, ListingType::Community, ListingSortType::New, Some(inserted_community.id), Some(inserted_user.id), 10).unwrap();
-    let read_post_listings_no_user = PostView::list(&conn, ListingType::Community, ListingSortType::New, Some(inserted_community.id), None, 10).unwrap();
+    let read_post_listings_with_user = PostView::list(&conn, PostListingType::Community, SortType::New, Some(inserted_community.id), Some(inserted_user.id), 10).unwrap();
+    let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, SortType::New, Some(inserted_community.id), None, 10).unwrap();
     let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
     let read_post_listing_with_user = PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
 
diff --git a/server/src/actions/user_view.rs b/server/src/actions/user_view.rs
new file mode 100644 (file)
index 0000000..5873a5c
--- /dev/null
@@ -0,0 +1,40 @@
+extern crate diesel;
+use diesel::*;
+use diesel::result::Error;
+use serde::{Deserialize, Serialize};
+
+table! {
+  user_view (id) {
+    id -> Int4,
+    name -> Varchar,
+    fedi_name -> Varchar,
+    published -> Timestamp,
+    number_of_posts -> BigInt,
+    post_score -> BigInt,
+    number_of_comments -> BigInt,
+    comment_score -> BigInt,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="user_view"]
+pub struct UserView {
+  pub id: i32,
+  pub name: String,
+  pub fedi_name: String,
+  pub published: chrono::NaiveDateTime,
+  pub number_of_posts: i64,
+  pub post_score: i64,
+  pub number_of_comments: i64,
+  pub comment_score: i64,
+}
+
+impl UserView {
+  pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
+    use actions::user_view::user_view::dsl::*;
+
+    user_view.find(from_user_id)
+    .first::<Self>(conn)
+  }
+}
+
index 0d81d507e5f6a6a03d653dd7a6d62963d7231790..9cdbd33ec89cd54b26f5e813c031a831b92fcac5 100644 (file)
@@ -24,6 +24,8 @@ use diesel::result::Error;
 use dotenv::dotenv;
 use std::env;
 use regex::Regex;
+use serde::{Deserialize, Serialize};
+use chrono::{DateTime, NaiveDateTime, Utc};
 
 pub trait Crud<T> {
   fn create(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
@@ -73,7 +75,11 @@ impl Settings {
   }
 }
 
-use chrono::{DateTime, NaiveDateTime, Utc};
+#[derive(EnumString,ToString,Debug, Serialize, Deserialize)]
+pub enum SortType {
+  Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
+}
+
 pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
   DateTime::<Utc>::from_utc(ndt, Utc)
 }
index 6aae4f2fdbebc477946ffc3a0deca060df44cf4d..e116fadc6301faf08432f4d0bcc148599e86b89e 100644 (file)
@@ -10,7 +10,7 @@ use serde_json::{Value};
 use bcrypt::{verify};
 use std::str::FromStr;
 
-use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now};
+use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now, SortType};
 use actions::community::*;
 use actions::user::*;
 use actions::post::*;
@@ -19,10 +19,11 @@ use actions::post_view::*;
 use actions::comment_view::*;
 use actions::category::*;
 use actions::community_view::*;
+use actions::user_view::*;
 
 #[derive(EnumString,ToString,Debug)]
 pub enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails
 }
 
 #[derive(Serialize, Deserialize)]
@@ -272,6 +273,26 @@ pub struct GetFollowedCommunitiesResponse {
   communities: Vec<CommunityFollowerView>
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct GetUserDetails {
+  user_id: i32,
+  sort: String,
+  limit: i64,
+  community_id: Option<i32>,
+  auth: Option<String>
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetUserDetailsResponse {
+  op: String,
+  user: UserView,
+  follows: Vec<CommunityFollowerView>,
+  moderates: Vec<CommunityModeratorView>,
+  comments: Vec<CommentView>,
+  posts: Vec<PostView>,
+  saved_posts: Vec<PostView>,
+  saved_comments: Vec<CommentView>,
+}
 
 /// `ChatServer` manages chat rooms and responsible for coordinating chat
 /// session. implementation is super primitive
@@ -466,13 +487,17 @@ impl Handler<StandardMessage> for ChatServer {
         let followed_communities: GetFollowedCommunities = serde_json::from_str(&data.to_string()).unwrap();
         followed_communities.perform(self, msg.id)
       },
-      _ => {
-        let e = ErrorMessage { 
-          op: "Unknown".to_string(),
-          error: "Unknown User Operation".to_string()
-        };
-        serde_json::to_string(&e).unwrap()
-      }
+      UserOperation::GetUserDetails => {
+        let get_user_details: GetUserDetails = serde_json::from_str(&data.to_string()).unwrap();
+        get_user_details.perform(self, msg.id)
+      },
+      // _ => {
+      //   let e = ErrorMessage { 
+      //     op: "Unknown".to_string(),
+      //     error: "Unknown User Operation".to_string()
+      //   };
+      //   serde_json::to_string(&e).unwrap()
+      // }
     };
 
     MessageResult(res)
@@ -808,7 +833,7 @@ impl Perform for GetPost {
 
     chat.rooms.get_mut(&self.id).unwrap().insert(addr);
 
-    let comments = CommentView::list(&conn, self.id, user_id).unwrap();
+    let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, user_id, 999).unwrap();
 
     let community = CommunityView::read(&conn, post_view.community_id, user_id).unwrap();
 
@@ -1110,10 +1135,10 @@ impl Perform for GetPosts {
       None => None
     };
 
-    let type_ = ListingType::from_str(&self.type_).expect("listing type");
-    let sort = ListingSortType::from_str(&self.sort).expect("listing sort");
+    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, user_id, self.limit) {
+    let posts = match PostView::list(&conn, type_, &sort, self.community_id, None, user_id, self.limit) {
       Ok(posts) => posts,
       Err(_e) => {
         eprintln!("{}", _e);
@@ -1412,6 +1437,55 @@ impl Perform for GetFollowedCommunities {
   }
 }
 
+impl Perform for GetUserDetails {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::GetUserDetails
+  }
+
+  fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
+
+    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.limit).unwrap();
+    let comments = CommentView::list(&conn, &sort, None, Some(self.user_id), user_id, self.limit).unwrap();
+    let follows = CommunityFollowerView::for_user(&conn, self.user_id).unwrap();
+    let moderates = CommunityModeratorView::for_user(&conn, self.user_id).unwrap();
+
+    // Return the jwt
+    serde_json::to_string(
+      &GetUserDetailsResponse {
+        op: self.op_type().to_string(),
+        user: user_view,
+        follows: follows,
+        moderates: moderates, 
+        comments: comments,
+        posts: posts,
+        saved_posts: Vec::new(),
+        saved_comments: Vec::new(),
+      }
+      )
+      .unwrap()
+  }
+}
+
 // impl Handler<Login> for ChatServer {
 
 //   type Result = MessageResult<Login>;
index 1b82db12a3723802e06a50b67dcc9e6ad8c24e8a..b5bb14ef95caf2070f53f9d220870600966f92ad 100644 (file)
   },
   "engineStrict": true,
   "dependencies": {
+    "@types/autosize": "^3.0.6",
     "@types/js-cookie": "^2.2.1",
+    "@types/jwt-decode": "^2.2.1",
+    "@types/markdown-it": "^0.0.7",
     "autosize": "^4.0.2",
     "classcat": "^1.1.3",
     "dotenv": "^6.1.0",
diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx
new file mode 100644 (file)
index 0000000..a87dd35
--- /dev/null
@@ -0,0 +1,93 @@
+import { Component, linkEvent } from 'inferno';
+import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces';
+import { WebSocketService } from '../services';
+import * as autosize from 'autosize';
+
+interface CommentFormProps {
+  postId?: number;
+  node?: CommentNodeI;
+  onReplyCancel?(): any;
+  edit?: boolean;
+}
+
+interface CommentFormState {
+  commentForm: CommentFormI;
+  buttonTitle: string;
+}
+
+export class CommentForm extends Component<CommentFormProps, CommentFormState> {
+
+  private emptyState: CommentFormState = {
+    commentForm: {
+      auth: null,
+      content: null,
+      post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId
+    },
+    buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply"
+  }
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+
+    if (this.props.node) {
+      if (this.props.edit) {
+        this.state.commentForm.edit_id = this.props.node.comment.id;
+        this.state.commentForm.parent_id = this.props.node.comment.parent_id;
+        this.state.commentForm.content = this.props.node.comment.content;
+      } else {
+        // A reply gets a new parent id
+        this.state.commentForm.parent_id = this.props.node.comment.id;
+      }
+    }  
+  }
+
+  componentDidMount() {
+    autosize(document.querySelectorAll('textarea'));
+  }
+
+  render() {
+    return (
+      <div>
+        <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
+          <div class="form-group row">
+            <div class="col-sm-12">
+              <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required />
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-sm-12">
+              <button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button>
+              {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+
+  handleCommentSubmit(i: CommentForm, event: any) {
+    if (i.props.edit) {
+      WebSocketService.Instance.editComment(i.state.commentForm);
+    } else {
+      WebSocketService.Instance.createComment(i.state.commentForm);
+    }
+
+    i.state.commentForm.content = undefined;
+    i.setState(i.state);
+    event.target.reset();
+    if (i.props.node) {
+      i.props.onReplyCancel();
+    }
+  }
+
+  handleCommentContentChange(i: CommentForm, event: any) {
+    i.state.commentForm.content = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleReplyCancel(i: CommentForm) {
+    i.props.onReplyCancel();
+  }
+}
diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx
new file mode 100644 (file)
index 0000000..55be762
--- /dev/null
@@ -0,0 +1,148 @@
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { mdToHtml } from '../utils';
+import { MomentTime } from './moment-time';
+import { CommentForm } from './comment-form';
+import { CommentNodes } from './comment-nodes';
+
+interface CommentNodeState {
+  showReply: boolean;
+  showEdit: boolean;
+}
+
+interface CommentNodeProps {
+  node: CommentNodeI;
+  noIndent?: boolean;
+  viewOnly?: boolean;
+}
+
+export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
+
+  private emptyState: CommentNodeState = {
+    showReply: false,
+    showEdit: false
+  }
+
+  constructor(props: any, context: any) {
+    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 id={`comment-${node.comment.id}`} className={`comment ${node.comment.parent_id  && !this.props.noIndent ? 'ml-4' : ''}`}>
+        <div className={`float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
+          <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">
+              <Link to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link>
+            </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>
+          {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />}
+          {!this.state.showEdit &&
+            <div>
+              <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.content)} />
+              <ul class="list-inline mb-1 text-muted small font-weight-bold">
+                {!this.props.viewOnly && 
+                  <span class="mr-2">
+                    <li className="list-inline-item">
+                      <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
+                    </li>
+                    {this.myComment && 
+                      <li className="list-inline-item">
+                        <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
+                      </li>
+                    }
+                    {this.myComment &&
+                      <li className="list-inline-item">
+                        <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</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>
+                </li>
+              </ul>
+            </div>
+          }
+        </div>
+        {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
+        {this.props.node.children && <CommentNodes nodes={this.props.node.children} />}
+      </div>
+    )
+  }
+
+  private get myComment(): boolean {
+    return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id;
+  }
+
+  handleReplyClick(i: CommentNode) {
+    i.state.showReply = true;
+    i.setState(i.state);
+  }
+
+  handleEditClick(i: CommentNode) {
+    i.state.showEdit = true;
+    i.setState(i.state);
+  }
+
+  handleDeleteClick(i: CommentNode) {
+    let deleteForm: CommentFormI = {
+      content: "*deleted*",
+      edit_id: i.props.node.comment.id,
+      post_id: i.props.node.comment.post_id,
+      parent_id: i.props.node.comment.parent_id,
+      auth: null
+    };
+    WebSocketService.Instance.editComment(deleteForm);
+  }
+
+  handleReplyCancel() {
+    this.state.showReply = false;
+    this.state.showEdit = false;
+    this.setState(this.state);
+  }
+
+
+  handleCommentLike(i: CommentNodeI) {
+
+    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) {
+    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);
+  }
+}
diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx
new file mode 100644 (file)
index 0000000..76d5c57
--- /dev/null
@@ -0,0 +1,30 @@
+import { Component } from 'inferno';
+import { CommentNode as CommentNodeI } from '../interfaces';
+import { CommentNode } from './comment-node';
+
+interface CommentNodesState {
+}
+
+interface CommentNodesProps {
+  nodes: Array<CommentNodeI>;
+  noIndent?: boolean;
+  viewOnly?: boolean;
+}
+
+export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
+
+  constructor(props: any, context: any) {
+    super(props, context);
+  }
+
+  render() {
+    return (
+      <div className="comments">
+        {this.props.nodes.map(node =>
+          <CommentNode node={node} noIndent={this.props.noIndent} viewOnly={this.props.viewOnly}/>
+        )}
+      </div>
+    )
+  }
+}
+
index c3cde177419bbf1e937ebac0b9f6ec8f3b240896..b81b11cd0b1ec3b9547326644873386d5a739ae4 100644 (file)
@@ -2,9 +2,9 @@ import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm } from '../interfaces';
-import { WebSocketService, UserService } from '../services';
-import { msgOp, hotRank,mdToHtml } from '../utils';
+import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm } from '../interfaces';
+import { WebSocketService } from '../services';
+import { msgOp } from '../utils';
 
 declare const Sortable: any;
 
@@ -18,7 +18,7 @@ export class Communities extends Component<any, CommunitiesState> {
     communities: []
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
     this.state = this.emptyState;
     this.subscription = WebSocketService.Instance.subject
@@ -32,6 +32,10 @@ export class Communities extends Component<any, CommunitiesState> {
 
   }
 
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
   componentDidMount() {
     let table = document.querySelector('#community_table');
     Sortable.initTable(table);
index 0cf67983ed11efecf4150bd935288d5d8626c0e2..6250ef9ca4509920b458f837c74a267274a420eb 100644 (file)
@@ -2,16 +2,16 @@ import { Component, linkEvent } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces';
-import { WebSocketService, UserService } from '../services';
+import { WebSocketService } from '../services';
 import { msgOp } from '../utils';
 
 import { Community } from '../interfaces';
 
 interface CommunityFormProps {
   community?: Community; // If a community is given, that means this is an edit
-  onCancel?();
-  onCreate?(id: number);
-  onEdit?(community: Community);
+  onCancel?(): any;
+  onCreate?(id: number): any;
+  onEdit?(community: Community): any;
 }
 
 interface CommunityFormState {
@@ -31,7 +31,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     categories: []
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
@@ -104,7 +104,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     );
   }
 
-  handleCreateCommunitySubmit(i: CommunityForm, event) {
+  handleCreateCommunitySubmit(i: CommunityForm, event: any) {
     event.preventDefault();
     if (i.props.community) {
       WebSocketService.Instance.editCommunity(i.state.communityForm);
@@ -113,27 +113,27 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     }
   }
 
-  handleCommunityNameChange(i: CommunityForm, event) {
+  handleCommunityNameChange(i: CommunityForm, event: any) {
     i.state.communityForm.name = event.target.value;
     i.setState(i.state);
   }
 
-  handleCommunityTitleChange(i: CommunityForm, event) {
+  handleCommunityTitleChange(i: CommunityForm, event: any) {
     i.state.communityForm.title = event.target.value;
     i.setState(i.state);
   }
 
-  handleCommunityDescriptionChange(i: CommunityForm, event) {
+  handleCommunityDescriptionChange(i: CommunityForm, event: any) {
     i.state.communityForm.description = event.target.value;
     i.setState(i.state);
   }
 
-  handleCommunityCategoryChange(i: CommunityForm, event) {
+  handleCommunityCategoryChange(i: CommunityForm, event: any) {
     i.state.communityForm.category_id = Number(event.target.value);
     i.setState(i.state);
   }
 
-  handleCancel(i: CommunityForm, event) {
+  handleCancel(i: CommunityForm) {
     i.props.onCancel();
   }
 
index 0d6d353df622e14e26d3f41fe831de1c9aa174d0..ed693c55c639cbac42cc912e0cb5d4e9e7fa5cf5 100644 (file)
@@ -1,13 +1,11 @@
-import { Component, linkEvent } from 'inferno';
-import { Link } from 'inferno-router';
+import { Component } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
-import { WebSocketService, UserService } from '../services';
-import { MomentTime } from './moment-time';
+import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse,  CommunityUser} from '../interfaces';
+import { WebSocketService } from '../services';
 import { PostListings } from './post-listings';
 import { Sidebar } from './sidebar';
-import { msgOp, mdToHtml } from '../utils';
+import { msgOp } from '../utils';
 
 interface State {
   community: CommunityI;
@@ -36,7 +34,7 @@ export class Community extends Component<any, State> {
     communityId: Number(this.props.match.params.id)
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
index e98352a2285ef4baec636ddb3db4b1afd28fcfaf..5f39741191fa46dd41e379085d436ae1e5ee164a 100644 (file)
@@ -1,9 +1,9 @@
-import { Component, linkEvent } from 'inferno';
+import { Component } from 'inferno';
 import { CommunityForm } from './community-form';
 
 export class CreateCommunity extends Component<any, any> {
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
     this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
   }
index 784465a0d1c4ad68271e1a417cfc08b34b2e7751..041ffd1732f7662e36f895309db58297526f6c0c 100644 (file)
@@ -1,9 +1,9 @@
-import { Component, linkEvent } from 'inferno';
+import { Component } from 'inferno';
 import { PostForm } from './post-form';
 
 export class CreatePost extends Component<any, any> {
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
     this.handlePostCreate = this.handlePostCreate.bind(this);
   }
index 356534f75036b778de763cfda442b4e5d2f8b4e2..8fced5be6fccfbfb170fcf20d358ec36e6f5f365 100644 (file)
@@ -1,5 +1,4 @@
 import { Component } from 'inferno';
-import { repoUrl } from '../utils';
 import { Main } from './main';
 
 export class Home extends Component<any, any> {
index 30b0aabcd6e522a6fb3afd709b9c3c84e30039b9..0cb026167efbb6a3563592eeba488e21f834de94 100644 (file)
@@ -25,7 +25,7 @@ let emptyState: State = {
 export class Login extends Component<any, State> {
   private subscription: Subscription;
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
     this.state = emptyState;
@@ -122,42 +122,42 @@ export class Login extends Component<any, State> {
     );
   }
 
-  handleLoginSubmit(i: Login, event) {
+  handleLoginSubmit(i: Login, event: any) {
     event.preventDefault();
     WebSocketService.Instance.login(i.state.loginForm);
   }
 
-  handleLoginUsernameChange(i: Login, event) {
+  handleLoginUsernameChange(i: Login, event: any) {
     i.state.loginForm.username_or_email = event.target.value;
     i.setState(i.state);
   }
 
-  handleLoginPasswordChange(i: Login, event) {
+  handleLoginPasswordChange(i: Login, event: any) {
     i.state.loginForm.password = event.target.value;
     i.setState(i.state);
   }
 
-  handleRegisterSubmit(i: Login, event) {
+  handleRegisterSubmit(i: Login, event: any) {
     event.preventDefault();
     WebSocketService.Instance.register(i.state.registerForm);
   }
 
-  handleRegisterUsernameChange(i: Login, event) {
+  handleRegisterUsernameChange(i: Login, event: any) {
     i.state.registerForm.username = event.target.value;
     i.setState(i.state);
   }
 
-  handleRegisterEmailChange(i: Login, event) {
+  handleRegisterEmailChange(i: Login, event: any) {
     i.state.registerForm.email = event.target.value;
     i.setState(i.state);
   }
 
-  handleRegisterPasswordChange(i: Login, event) {
+  handleRegisterPasswordChange(i: Login, event: any) {
     i.state.registerForm.password = event.target.value;
     i.setState(i.state);
   }
 
-  handleRegisterPasswordVerifyChange(i: Login, event) {
+  handleRegisterPasswordVerifyChange(i: Login, event: any) {
     i.state.registerForm.password_verify = event.target.value;
     i.setState(i.state);
   }
@@ -170,7 +170,7 @@ export class Login extends Component<any, State> {
     } else {
       if (op == UserOperation.Register || op == UserOperation.Login) {
         let res: LoginResponse = msg;
-        UserService.Instance.login(msg);
+        UserService.Instance.login(res);
         this.props.history.push('/');
       }
     }
index b7b0a56228f4bf87bd8d877b4d0745262df44632..aa3c3a8cd925b7e38f51f18c134c1061b0818d28 100644 (file)
@@ -1,13 +1,11 @@
-import { Component, linkEvent } from 'inferno';
+import { Component } from 'inferno';
 import { Link } from 'inferno-router';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser, GetFollowedCommunitiesResponse } from '../interfaces';
+import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { MomentTime } from './moment-time';
 import { PostListings } from './post-listings';
-import { Sidebar } from './sidebar';
-import { msgOp, mdToHtml } from '../utils';
+import { msgOp } from '../utils';
 
 interface State {
   subscribedCommunities: Array<CommunityUser>;
@@ -20,7 +18,7 @@ export class Main extends Component<any, State> {
     subscribedCommunities: []
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
index e0886cbc218ac2d619475fb351442971c6dedce1..f07f04a3f02534f7d69cdcb506132d9761fcef74 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, linkEvent } from 'inferno';
+import { Component } from 'inferno';
 import * as moment from 'moment';
 
 interface MomentTimeProps {
@@ -10,7 +10,7 @@ interface MomentTimeProps {
 
 export class MomentTime extends Component<MomentTimeProps, any> {
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
   }
 
index 2032d49bbc1699f2f645fa6cf8ca91d953b02a4f..9754c935e20454ac54653dd3c5cab0f732bc352e 100644 (file)
@@ -5,7 +5,7 @@ import { UserService } from '../services';
 
 export class Navbar extends Component<any, any> {
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
     this.state = {isLoggedIn: UserService.Instance.loggedIn, expanded: false};
 
@@ -62,12 +62,11 @@ export class Navbar extends Component<any, any> {
     );
   }
 
-  handleLogoutClick(i: Navbar, event) {
+  handleLogoutClick() {
     UserService.Instance.logout();
-    // i.props.history.push('/');
   }
 
-  expandNavbar(i: Navbar, event) {
+  expandNavbar(i: Navbar) {
     i.state.expanded = !i.state.expanded;
     i.setState(i.state);
   }
index a8621ceda748d6f9f6b5a479267fc59a87491ed7..1f23cae0536b4fb81563141fc614f2764813925a 100644 (file)
@@ -2,15 +2,14 @@ import { Component, linkEvent } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse } from '../interfaces';
-import { WebSocketService, UserService } from '../services';
+import { WebSocketService } from '../services';
 import { msgOp } from '../utils';
-import { MomentTime } from './moment-time';
 
 interface PostFormProps {
   post?: Post; // If a post is given, that means this is an edit
-  onCancel?();
-  onCreate?(id: number);
-  onEdit?(post: Post);
+  onCancel?(): any;
+  onCreate?(id: number): any;
+  onEdit?(post: Post): any;
 }
 
 interface PostFormState {
@@ -30,7 +29,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     communities: []
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
@@ -104,7 +103,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     );
   }
 
-  handlePostSubmit(i: PostForm, event) {
+  handlePostSubmit(i: PostForm, event: any) {
     event.preventDefault();
     if (i.props.post) {
       WebSocketService.Instance.editPost(i.state.postForm);
@@ -113,27 +112,27 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     }
   }
 
-  handlePostUrlChange(i: PostForm, event) {
+  handlePostUrlChange(i: PostForm, event: any) {
     i.state.postForm.url = event.target.value;
     i.setState(i.state);
   }
 
-  handlePostNameChange(i: PostForm, event) {
+  handlePostNameChange(i: PostForm, event: any) {
     i.state.postForm.name = event.target.value;
     i.setState(i.state);
   }
 
-  handlePostBodyChange(i: PostForm, event) {
+  handlePostBodyChange(i: PostForm, event: any) {
     i.state.postForm.body = event.target.value;
     i.setState(i.state);
   }
 
-  handlePostCommunityChange(i: PostForm, event) {
+  handlePostCommunityChange(i: PostForm, event: any) {
     i.state.postForm.community_id = Number(event.target.value);
     i.setState(i.state);
   }
 
-  handleCancel(i: PostForm, event) {
+  handleCancel(i: PostForm) {
     i.props.onCancel();
   }
 
index c5052efb40cceae58653451e5b95b79bc8be4446..f3145eff5cff380640b49d1107c7d5b8b7b8b683 100644 (file)
@@ -1,9 +1,7 @@
 import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
-import { Subscription } from "rxjs";
-import { retryWhen, delay, take } from 'rxjs/operators';
 import { WebSocketService, UserService } from '../services';
-import { Post, CreatePostLikeResponse, CreatePostLikeForm, PostForm as PostFormI } from '../interfaces';
+import { Post, CreatePostLikeForm, PostForm as PostFormI } from '../interfaces';
 import { MomentTime } from './moment-time';
 import { PostForm } from './post-form';
 import { mdToHtml } from '../utils';
@@ -18,6 +16,7 @@ interface PostListingProps {
   editable?: boolean;
   showCommunity?: boolean;
   showBody?: boolean;
+  viewOnly?: boolean;
 }
 
 export class PostListing extends Component<PostListingProps, PostListingState> {
@@ -27,7 +26,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     iframeExpanded: false
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
@@ -52,7 +51,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     let post = this.props.post;
     return (
       <div class="listing">
-        <div className="float-left small text-center">
+        <div className={`float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
           <div className={`pointer upvote ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}>â–²</div>
           <div>{post.score}</div>
           <div className={`pointer downvote ${post.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(this, this.handlePostDisLike)}>â–¼</div>
@@ -123,7 +122,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return this.props.editable && UserService.Instance.loggedIn && this.props.post.creator_id == UserService.Instance.user.id;
   }
 
-  handlePostLike(i: PostListing, event) {
+  handlePostLike(i: PostListing) {
 
     let form: CreatePostLikeForm = {
       post_id: i.props.post.id,
@@ -132,7 +131,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     WebSocketService.Instance.likePost(form);
   }
 
-  handlePostDisLike(i: PostListing, event) {
+  handlePostDisLike(i: PostListing) {
     let form: CreatePostLikeForm = {
       post_id: i.props.post.id,
       score: (i.props.post.my_vote == -1) ? 0 : -1
@@ -140,7 +139,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     WebSocketService.Instance.likePost(form);
   }
 
-  handleEditClick(i: PostListing, event) {
+  handleEditClick(i: PostListing) {
     i.state.showEdit = true;
     i.setState(i.state);
   }
@@ -151,12 +150,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   // The actual editing is done in the recieve for post
-  handleEditPost(post: Post) {
+  handleEditPost() {
     this.state.showEdit = false;
     this.setState(this.state);
   }
 
-  handleDeleteClick(i: PostListing, event) {
+  handleDeleteClick(i: PostListing) {
     let deleteForm: PostFormI = {
       body: '',
       community_id: i.props.post.community_id,
@@ -168,7 +167,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     WebSocketService.Instance.editPost(deleteForm);
   }
 
-  handleIframeExpandClick(i: PostListing, event) {
+  handleIframeExpandClick(i: PostListing) {
     i.state.iframeExpanded = !i.state.iframeExpanded;
     i.setState(i.state);
   }
index 98fe8f21171695728667734fcab63e5473848496..bb36cabccfb0ec0b93e469e7a426670070c9c4a4 100644 (file)
@@ -2,12 +2,10 @@ import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
+import { UserOperation, Community as CommunityI, Post, GetPostsForm, SortType, ListingType, GetPostsResponse, CreatePostLikeResponse, CommunityUser} from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { MomentTime } from './moment-time';
 import { PostListing } from './post-listing';
-import { Sidebar } from './sidebar';
-import { msgOp, mdToHtml } from '../utils';
+import { msgOp } from '../utils';
 
 
 interface PostListingsProps {
@@ -18,7 +16,7 @@ interface PostListingsState {
   community: CommunityI;
   moderators: Array<CommunityUser>;
   posts: Array<Post>;
-  sortType: ListingSortType;
+  sortType: SortType;
   type_: ListingType;
 }
 
@@ -41,7 +39,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
     },
     moderators: [],
     posts: [],
-    sortType: ListingSortType.Hot,
+    sortType: SortType.Hot,
     type_: this.props.communityId 
     ? ListingType.Community 
     : UserService.Instance.loggedIn
@@ -49,7 +47,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
     : ListingType.All
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
 
@@ -67,7 +65,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
       type_: ListingType[this.state.type_],
       community_id: this.props.communityId,
       limit: 10,
-      sort: ListingSortType[ListingSortType.Hot],
+      sort: SortType[SortType.Hot],
     }
     WebSocketService.Instance.getPosts(getPostsForm);
   }
@@ -94,14 +92,14 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
       <div className="mb-2">
         <select value={this.state.sortType} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
           <option disabled>Sort Type</option>
-          <option value={ListingSortType.Hot}>Hot</option>
-          <option value={ListingSortType.New}>New</option>
+          <option value={SortType.Hot}>Hot</option>
+          <option value={SortType.New}>New</option>
           <option disabled>──────────</option>
-          <option value={ListingSortType.TopDay}>Top Day</option>
-          <option value={ListingSortType.TopWeek}>Week</option>
-          <option value={ListingSortType.TopMonth}>Month</option>
-          <option value={ListingSortType.TopYear}>Year</option>
-          <option value={ListingSortType.TopAll}>All</option>
+          <option value={SortType.TopDay}>Top Day</option>
+          <option value={SortType.TopWeek}>Week</option>
+          <option value={SortType.TopMonth}>Month</option>
+          <option value={SortType.TopYear}>Year</option>
+          <option value={SortType.TopAll}>All</option>
         </select>
         {!this.props.communityId && 
           UserService.Instance.loggedIn &&
@@ -117,26 +115,26 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
 
   }
 
-  handleSortChange(i: PostListings, event) {
+  handleSortChange(i: PostListings, event: any) {
     i.state.sortType = Number(event.target.value);
     i.setState(i.state);
 
     let getPostsForm: GetPostsForm = {
       community_id: i.state.community.id,
       limit: 10,
-      sort: ListingSortType[i.state.sortType],
+      sort: SortType[i.state.sortType],
       type_: ListingType[ListingType.Community]
     }
     WebSocketService.Instance.getPosts(getPostsForm);
   }
 
-  handleTypeChange(i: PostListings, event) {
+  handleTypeChange(i: PostListings, event: any) {
     i.state.type_ = Number(event.target.value);
     i.setState(i.state);
 
     let getPostsForm: GetPostsForm = {
       limit: 10,
-      sort: ListingSortType[i.state.sortType],
+      sort: SortType[i.state.sortType],
       type_: ListingType[i.state.type_]
     }
     WebSocketService.Instance.getPosts(getPostsForm);
index e7c66e94b194b2fdeedd09c311deb11b66bb219b..e830856125d95e25d43a5f8203754169a9ba7bff 100644 (file)
@@ -1,19 +1,15 @@
 import { Component, linkEvent } from 'inferno';
-import { Link } from 'inferno-router';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse } from '../interfaces';
-import { WebSocketService, UserService } from '../services';
-import { msgOp, hotRank,mdToHtml } from '../utils';
-import { MomentTime } from './moment-time';
+import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment,  CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI } from '../interfaces';
+import { WebSocketService } from '../services';
+import { msgOp, hotRank } from '../utils';
 import { PostListing } from './post-listing';
 import { Sidebar } from './sidebar';
+import { CommentForm } from './comment-form';
+import { CommentNodes } from './comment-nodes';
 import * as autosize from 'autosize';
 
-interface CommentNodeI {
-  comment: Comment;
-  children?: Array<CommentNodeI>;
-};
 
 interface PostState {
   post: PostI;
@@ -21,7 +17,8 @@ interface PostState {
   commentSort: CommentSortType;
   community: Community;
   moderators: Array<CommunityUser>;
-  scrolled: boolean;
+  scrolled?: boolean;
+  scrolled_comment_id?: number;
 }
 
 export class Post extends Component<any, PostState> {
@@ -36,12 +33,15 @@ export class Post extends Component<any, PostState> {
     scrolled: false
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
 
     let postId = Number(this.props.match.params.id);
+    if (this.props.match.params.comment_id) {
+      this.state.scrolled_comment_id = this.props.match.params.comment_id;
+    }
 
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
@@ -62,11 +62,11 @@ export class Post extends Component<any, PostState> {
     autosize(document.querySelectorAll('textarea'));
   }
 
-  componentDidUpdate(lastProps: any, lastState: PostState, snapshot: any) {
-    if (!this.state.scrolled && lastState.comments.length > 0 && window.location.href.includes('#comment-')) {
-      let id = window.location.hash.split("#")[2];
-      var elmnt = document.getElementById(`${id}`);
+  componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
+    if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) {
+      var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`);
       elmnt.scrollIntoView(); 
+      elmnt.classList.add("mark");
       this.state.scrolled = true;
     }
   }
@@ -136,7 +136,7 @@ export class Post extends Component<any, PostState> {
     );
   }
   
-  handleCommentSortChange(i: Post, event) {
+  handleCommentSortChange(i: Post, event: any) {
     i.state.commentSort = Number(event.target.value);
     i.setState(i.state);
   }
@@ -250,252 +250,5 @@ export class Post extends Component<any, PostState> {
   }
 }
 
-interface CommentNodesState {
-}
-
-interface CommentNodesProps {
-  nodes: Array<CommentNodeI>;
-  noIndent?: boolean;
-}
-
-export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
-
-  constructor(props, context) {
-    super(props, context);
-  }
-
-  render() {
-    return (
-      <div className="comments">
-        {this.props.nodes.map(node =>
-          <CommentNode node={node} noIndent={this.props.noIndent} />
-        )}
-      </div>
-    )
-  }
-}
-
-
-interface CommentNodeState {
-  showReply: boolean;
-  showEdit: boolean;
-}
-
-interface CommentNodeProps {
-  node: CommentNodeI;
-  noIndent?: boolean;
-}
-
-export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
-
-  private emptyState: CommentNodeState = {
-    showReply: false,
-    showEdit: 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 id={`comment-${node.comment.id}`} 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">
-              <Link to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link>
-            </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>
-          {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />}
-          {!this.state.showEdit &&
-            <div>
-              <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.content)} />
-              <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>
-                {this.myComment && 
-                  <li className="list-inline-item">
-                    <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
-                  </li>
-                }
-                {this.myComment &&
-                  <li className="list-inline-item">
-                    <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
-                  </li>
-                }
-                <li className="list-inline-item">
-                  <Link className="text-muted" to={`/post/${node.comment.post_id}#comment-${node.comment.id}`}>link</Link>
-                </li>
-              </ul>
-            </div>
-          }
-        </div>
-        {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
-        {this.props.node.children && <CommentNodes nodes={this.props.node.children} />}
-      </div>
-    )
-  }
-
-  private get myComment(): boolean {
-    return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id;
-  }
-
-  handleReplyClick(i: CommentNode, event) {
-    i.state.showReply = true;
-    i.setState(i.state);
-  }
-
-  handleEditClick(i: CommentNode, event) {
-    i.state.showEdit = true;
-    i.setState(i.state);
-  }
-
-  handleDeleteClick(i: CommentNode, event) {
-    let deleteForm: CommentFormI = {
-      content: "*deleted*",
-      edit_id: i.props.node.comment.id,
-      post_id: i.props.node.comment.post_id,
-      parent_id: i.props.node.comment.parent_id,
-      auth: null
-    };
-    WebSocketService.Instance.editComment(deleteForm);
-  }
-
-  handleReplyCancel(): any {
-    this.state.showReply = false;
-    this.state.showEdit = 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?();
-  edit?: boolean;
-}
-
-interface CommentFormState {
-  commentForm: CommentFormI;
-  buttonTitle: string;
-}
-
-export class CommentForm extends Component<CommentFormProps, CommentFormState> {
-
-  private emptyState: CommentFormState = {
-    commentForm: {
-      auth: null,
-      content: null,
-      post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId
-    },
-    buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply"
-  }
-
-  constructor(props, context) {
-    super(props, context);
-
-    this.state = this.emptyState;
 
-    if (this.props.node) {
-      if (this.props.edit) {
-        this.state.commentForm.edit_id = this.props.node.comment.id;
-        this.state.commentForm.parent_id = this.props.node.comment.parent_id;
-        this.state.commentForm.content = this.props.node.comment.content;
-      } else {
-        // A reply gets a new parent id
-        this.state.commentForm.parent_id = this.props.node.comment.id;
-      }
-    }  
-  }
 
-  componentDidMount() {
-    autosize(document.querySelectorAll('textarea'));
-  }
-
-  render() {
-    return (
-      <div>
-        <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
-          <div class="form-group row">
-            <div class="col-sm-12">
-              <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required />
-            </div>
-          </div>
-          <div class="row">
-            <div class="col-sm-12">
-              <button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button>
-              {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
-            </div>
-          </div>
-        </form>
-      </div>
-    );
-  }
-
-  handleCommentSubmit(i: CommentForm, event) {
-    if (i.props.edit) {
-      WebSocketService.Instance.editComment(i.state.commentForm);
-    } else {
-      WebSocketService.Instance.createComment(i.state.commentForm);
-    }
-
-    i.state.commentForm.content = undefined;
-    i.setState(i.state);
-    event.target.reset();
-    if (i.props.node) {
-      i.props.onReplyCancel();
-    }
-  }
-
-  handleCommentContentChange(i: CommentForm, event) {
-    i.state.commentForm.content = event.target.value;
-    i.setState(i.state);
-  }
-
-  handleReplyCancel(i: CommentForm, event) {
-    i.props.onReplyCancel();
-  }
-}
index ad3eeccc36edf010bcbfbe9de7e052d46891cd92..ffc44562acd863cb7531dd0dfdd386cc2e879f46 100644 (file)
@@ -20,7 +20,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     showEdit: false
   }
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
     this.state = this.emptyState;
     this.handleEditCommunity = this.handleEditCommunity.bind(this);
@@ -82,12 +82,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     );
   }
 
-  handleEditClick(i: Sidebar, event) {
+  handleEditClick(i: Sidebar) {
     i.state.showEdit = true;
     i.setState(i.state);
   }
 
-  handleEditCommunity(community: Community) {
+  handleEditCommunity() {
     this.state.showEdit = false;
     this.setState(this.state);
   }
@@ -98,8 +98,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
   }
 
   // TODO no deleting communities yet
-  handleDeleteClick(i: Sidebar, event) {
-  }
+  // handleDeleteClick(i: Sidebar, event) {
+  // }
 
   handleUnsubscribe(communityId: number) {
     let form: FollowCommunityForm = {
diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx
new file mode 100644 (file)
index 0000000..4754111
--- /dev/null
@@ -0,0 +1,264 @@
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView } from '../interfaces';
+import { WebSocketService } from '../services';
+import { msgOp } from '../utils';
+import { PostListing } from './post-listing';
+import { CommentNodes } from './comment-nodes';
+import { MomentTime } from './moment-time';
+
+enum View {
+  Overview, Comments, Posts, Saved
+}
+
+interface UserState {
+  user: UserView;
+  follows: Array<CommunityUser>;
+  moderates: Array<CommunityUser>;
+  comments: Array<Comment>;
+  posts: Array<Post>;
+  saved?: Array<Post>;
+  view: View;
+  sort: SortType;
+}
+
+export class User extends Component<any, UserState> {
+
+  private subscription: Subscription;
+  private emptyState: UserState = {
+    user: {
+      id: null,
+      name: null,
+      fedi_name: null,
+      published: null,
+      number_of_posts: null,
+      post_score: null,
+      number_of_comments: null,
+      comment_score: null,
+    },
+    follows: [],
+    moderates: [],
+    comments: [],
+    posts: [],
+    view: View.Overview,
+    sort: SortType.New
+  }
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+
+    let userId = Number(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 form: GetUserDetailsForm = {
+      user_id: userId,
+      sort: SortType[this.state.sort],
+      limit: 999
+    };
+    WebSocketService.Instance.getUserDetails(form);
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  render() {
+    return (
+      <div class="container">
+        <div class="row">
+          <div class="col-12 col-lg-9">
+            <h4>/u/{this.state.user.name}</h4>
+            {this.selects()}
+            {this.state.view == View.Overview &&
+              this.overview()
+            }
+            {this.state.view == View.Comments &&
+              this.comments()
+            }
+            {this.state.view == View.Posts &&
+              this.posts()
+            }
+          </div>
+          <div class="col-12 col-lg-3">
+            {this.userInfo()}
+            {this.moderates()}
+            {this.follows()}
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  selects() {
+    return (
+      <div className="mb-2">
+        <select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select w-auto">
+          <option disabled>View</option>
+          <option value={View.Overview}>Overview</option>
+          <option value={View.Comments}>Comments</option>
+          <option value={View.Posts}>Posts</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>
+          <option value={SortType.New}>New</option>
+          <option value={SortType.TopDay}>Top Day</option>
+          <option value={SortType.TopWeek}>Week</option>
+          <option value={SortType.TopMonth}>Month</option>
+          <option value={SortType.TopYear}>Year</option>
+          <option value={SortType.TopAll}>All</option>
+        </select>
+      </div>
+    )
+
+  }
+
+  overview() {
+    let combined: Array<any> = [];
+    combined.push(...this.state.comments);
+    combined.push(...this.state.posts);
+
+    // Sort it
+    if (this.state.sort == SortType.New) {
+      combined.sort((a, b) => b.published.localeCompare(a.published));
+    } else {
+      combined.sort((a, b) => b.score - a.score);
+    }
+
+    return (
+      <div>
+        {combined.map(i =>
+          <div>
+            {i.community_id 
+              ? <PostListing post={i} showCommunity viewOnly />
+              : <CommentNodes nodes={[{comment: i}]} noIndent viewOnly />
+            }
+          </div>
+                     )
+        }
+      </div>
+    )
+  }
+
+  comments() {
+    return (
+      <div>
+        {this.state.comments.map(comment => 
+          <CommentNodes nodes={[{comment: comment}]} noIndent viewOnly />
+        )}
+      </div>
+    );
+  }
+
+  posts() {
+    return (
+      <div>
+        {this.state.posts.map(post => 
+          <PostListing post={post} showCommunity viewOnly />
+        )}
+      </div>
+    );
+  }
+
+  userInfo() {
+    let user = this.state.user;
+    return (
+      <div>
+        <h4>{user.name}</h4>
+        <div>Joined <MomentTime data={user} /></div>
+        <table class="table table-bordered table-sm mt-2">
+          <tr>
+            <td>{user.post_score} points</td>
+            <td>{user.number_of_posts} posts</td>
+          </tr>
+          <tr>
+            <td>{user.comment_score} points</td>
+            <td>{user.number_of_comments} comments</td>
+          </tr>
+        </table>
+        <hr />
+      </div>
+    )
+  }
+
+  moderates() {
+    return (
+      <div>
+        {this.state.moderates.length > 0 &&
+          <div>
+            <h4>Moderates</h4>
+            <ul class="list-unstyled"> 
+              {this.state.moderates.map(community =>
+                <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
+              )}
+            </ul>
+          </div>
+        }
+      </div>
+    )
+  }
+
+  follows() {
+    return (
+      <div>
+        {this.state.follows.length > 0 &&
+          <div>
+            <hr />
+            <h4>Subscribed</h4>
+            <ul class="list-unstyled"> 
+              {this.state.follows.map(community =>
+                <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
+              )}
+            </ul>
+          </div>
+        }
+      </div>
+    )
+  }
+
+  handleSortChange(i: User, event: any) {
+    i.state.sort = Number(event.target.value);
+    i.setState(i.state);
+
+    let form: GetUserDetailsForm = {
+      user_id: i.state.user.id,
+      sort: SortType[i.state.sort],
+      limit: 999
+    };
+    WebSocketService.Instance.getUserDetails(form);
+  }
+
+  handleViewChange(i: User, event: any) {
+    i.state.view = 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 if (op == UserOperation.GetUserDetails) {
+      let res: UserDetailsResponse = msg;
+      this.state.user = res.user;
+      this.state.comments = res.comments;
+      this.state.follows = res.follows;
+      this.state.moderates = res.moderates;
+      this.state.posts = res.posts;
+      this.setState(this.state);
+    } 
+  }
+}
+
index d374ec1eb28631cb5ec823acc0d84e263dead29c..45bae0f7ddbfc754fe1cb516e24fcb900cb187b5 100644 (file)
@@ -9,6 +9,7 @@ import { CreateCommunity } from './components/create-community';
 import { Post } from './components/post';
 import { Community } from './components/community';
 import { Communities } from './components/communities';
+import { User } from './components/user';
 
 import './main.css';
 
@@ -18,7 +19,7 @@ const container = document.getElementById('app');
 
 class Index extends Component<any, any> {
 
-  constructor(props, context) {
+  constructor(props: any, context: any) {
     super(props, context);
     WebSocketService.Instance;
     UserService.Instance;
@@ -35,8 +36,11 @@ class Index extends Component<any, any> {
             <Route path={`/create_post`} component={CreatePost} />
             <Route path={`/create_community`} component={CreateCommunity} />
             <Route path={`/communities`} component={Communities} />
+            <Route path={`/post/:id/comment/:comment_id`} component={Post} />
             <Route path={`/post/:id`} component={Post} />
             <Route path={`/community/:id`} component={Community} />
+            <Route path={`/user/:id/:heading`} component={User} />
+            <Route path={`/user/:id`} component={User} />
           </Switch>
           {this.symbols()}
         </div>
index 6d314c6217d61e98ed5c16b9d056411480eb4920..6608864798981712bb34cb018f066aa1e3b5eed0 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
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails
 }
 
 export interface User {
@@ -8,6 +8,17 @@ export interface User {
   username: string;
 }
 
+export interface UserView {
+  id: number;
+  name: string;
+  fedi_name: string;
+  published: string;
+  number_of_posts: number;
+  post_score: number;
+  number_of_comments: number;
+  comment_score: number;
+}
+
 export interface CommunityUser {
   id: number;
   user_id: number;
@@ -144,6 +155,11 @@ export interface CommentLikeForm {
   auth?: string;
 }
 
+export interface CommentNode {
+  comment: Comment;
+  children?: Array<CommentNode>;
+}
+
 export interface GetPostsForm {
   type_: string;
   sort: string;
@@ -184,6 +200,27 @@ export interface GetFollowedCommunitiesResponse {
   communities: Array<CommunityUser>;
 }
 
+export interface GetUserDetailsForm {
+  user_id: number;
+  sort: string; // TODO figure this one out
+  limit: number;
+  community_id?: number;
+  auth?: string;
+}
+
+
+
+export interface UserDetailsResponse {
+  op: string;
+  user: UserView;
+  follows: Array<CommunityUser>;
+  moderates: Array<CommunityUser>;
+  comments: Array<Comment>;
+  posts: Array<Post>;
+  saved?: Array<Post>;
+}
+
+
 export interface LoginForm {
   username_or_email: string;
   password: string;
@@ -210,7 +247,7 @@ export enum ListingType {
   All, Subscribed, Community
 }
 
-export enum ListingSortType {
+export enum SortType {
   Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
 }
 
index ac58c993fe77d1b718eac7a271bea4835a227de1..1802eaa3d52cff79df16ef67a8f2772f1225f6f7 100644 (file)
@@ -6,6 +6,11 @@ body {
   cursor: pointer;
 }
 
+.no-click {
+  pointer-events:none;
+  opacity: 0.65;
+}
+
 .upvote:hover {
  color: var(--info);
 }
@@ -24,6 +29,10 @@ body {
   background-color: var(--secondary);
 }
 
+.mark {
+  background-color: #322a00;
+}
+
 .md-div p {
   margin-bottom: 0px;
 }
index 79f6750abc1b075229be573abd9201f37f9acf53..b5efd6a7f4bca0c3dad67d3a23744b5e66050fc1 100644 (file)
@@ -1,5 +1,5 @@
 import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -106,6 +106,11 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm));
   }
 
+  public getUserDetails(form: GetUserDetailsForm) {
+    this.setAuth(form, false);
+    this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
+  }
+
   private wsSendWrapper(op: UserOperation, data: any) {
     let send = { op: UserOperation[op], data: data };
     console.log(send);
@@ -122,7 +127,7 @@ export class WebSocketService {
 
 }
 
-window.onbeforeunload = (e => {
+window.onbeforeunload = (() => {
   WebSocketService.Instance.subject.unsubscribe();
   WebSocketService.Instance.subject = null;
 });
index 21122d0acdd0b7d42c3d2a291a4dc4d59c9810b0..e01e4ce11ae182f8282ab2bf68f3b69c657fe93a 100644 (file)
@@ -2,7 +2,6 @@ import { UserOperation, Comment } from './interfaces';
 import * as markdown_it from 'markdown-it';
 
 export let repoUrl = 'https://github.com/dessalines/lemmy';
-export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/';
 
 export function msgOp(msg: any): UserOperation {
   let opStr: string = msg.op;
index 49469788649a5cf7ded26aa3e7f7947af18a2e5a..5756a444b0ab14ae8a714c334de413cb2a625311 100644 (file)
@@ -9,11 +9,47 @@
   dependencies:
     regenerator-runtime "^0.12.0"
 
+"@types/autosize@^3.0.6":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-3.0.6.tgz#9022e6a783ec5a4d5e570013701dbc0bfe7667fa"
+  integrity sha512-gpfmXswGISLSWNOOdF2PDK96SfkaZdNtNixWJbYH10xn3Hqdt4VyS1GmoutuwOshWyCLuJw2jGhF0zkK7PUhrg==
+  dependencies:
+    "@types/jquery" "*"
+
+"@types/jquery@*":
+  version "3.3.29"
+  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd"
+  integrity sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==
+  dependencies:
+    "@types/sizzle" "*"
+
 "@types/js-cookie@^2.2.1":
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.1.tgz#aa6f6d5e5aaf7d97959e9fa938ac2501cf1a76f4"
   integrity sha512-VIVurImEhQ95jxtjs8baVU5qCzVfwYfuMrpXwdRykJ5MCI5iY7/jB4cDSgwBVeYqeXrhT7GfJUwoDOmN0OMVCA==
 
+"@types/jwt-decode@^2.2.1":
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/@types/jwt-decode/-/jwt-decode-2.2.1.tgz#afdf5c527fcfccbd4009b5fd02d1e18241f2d2f2"
+  integrity sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==
+
+"@types/linkify-it@*":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
+  integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
+
+"@types/markdown-it@^0.0.7":
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
+  integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
+  dependencies:
+    "@types/linkify-it" "*"
+
+"@types/sizzle@*":
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
+  integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
+
 abbrev@1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"