summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDessalines <tyhou13@gmx.com>2019-04-07 22:19:02 -0700
committerDessalines <tyhou13@gmx.com>2019-04-07 22:19:02 -0700
commit49bf16e7d451388d894f93a994f3bf18571f9594 (patch)
tree1802bf775a0dd97503670c38ceb13d9aa78d95b6
parenta61516439406b7884e19d9ae8a1875c728bbe628 (diff)
Adding user details / overview page.
- Fixes #19
-rw-r--r--README.md11
-rw-r--r--server/migrations/2019-02-27-170003_create_community/up.sql4
-rw-r--r--server/migrations/2019-04-08-015947_create_user_view/down.sql1
-rw-r--r--server/migrations/2019-04-08-015947_create_user_view/up.sql11
-rw-r--r--server/src/actions/comment_view.rs50
-rw-r--r--server/src/actions/mod.rs1
-rw-r--r--server/src/actions/post_view.rs57
-rw-r--r--server/src/actions/user_view.rs40
-rw-r--r--server/src/lib.rs8
-rw-r--r--server/src/websocket_server/server.rs100
-rw-r--r--ui/package.json3
-rw-r--r--ui/src/components/comment-form.tsx93
-rw-r--r--ui/src/components/comment-node.tsx148
-rw-r--r--ui/src/components/comment-nodes.tsx30
-rw-r--r--ui/src/components/communities.tsx12
-rw-r--r--ui/src/components/community-form.tsx22
-rw-r--r--ui/src/components/community.tsx12
-rw-r--r--ui/src/components/create-community.tsx4
-rw-r--r--ui/src/components/create-post.tsx4
-rw-r--r--ui/src/components/home.tsx1
-rw-r--r--ui/src/components/login.tsx20
-rw-r--r--ui/src/components/main.tsx10
-rw-r--r--ui/src/components/moment-time.tsx4
-rw-r--r--ui/src/components/navbar.tsx7
-rw-r--r--ui/src/components/post-form.tsx23
-rw-r--r--ui/src/components/post-listing.tsx21
-rw-r--r--ui/src/components/post-listings.tsx36
-rw-r--r--ui/src/components/post.tsx279
-rw-r--r--ui/src/components/sidebar.tsx10
-rw-r--r--ui/src/components/user.tsx264
-rw-r--r--ui/src/index.tsx6
-rw-r--r--ui/src/interfaces.ts41
-rw-r--r--ui/src/main.css9
-rw-r--r--ui/src/services/WebSocketService.ts9
-rw-r--r--ui/src/utils.ts1
-rw-r--r--ui/yarn.lock36
36 files changed, 969 insertions, 419 deletions
diff --git a/README.md b/README.md
index 96d81ead..1c518a4e 100644
--- 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
```
diff --git a/server/migrations/2019-02-27-170003_create_community/up.sql b/server/migrations/2019-02-27-170003_create_community/up.sql
index f78486d5..46b4df52 100644
--- a/server/migrations/2019-02-27-170003_create_community/up.sql
+++ b/server/migrations/2019-02-27-170003_create_community/up.sql
@@ -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
index 00000000..c94d94c4
--- /dev/null
+++ b/server/migrations/2019-04-08-015947_create_user_view/down.sql
@@ -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
index 00000000..69d052de
--- /dev/null
+++ b/server/migrations/2019-04-08-015947_create_user_view/up.sql
@@ -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;
+
diff --git a/server/src/actions/comment_view.rs b/server/src/actions/comment_view.rs
index dcfcc250..417a6772 100644
--- a/server/src/actions/comment_view.rs
+++ b/server/src/actions/comment_view.rs
@@ -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());
}
diff --git a/server/src/actions/mod.rs b/server/src/actions/mod.rs
index c17fd81a..819d5cda 100644
--- a/server/src/actions/mod.rs
+++ b/server/src/actions/mod.rs
@@ -6,3 +6,4 @@ pub mod post_view;
pub mod comment_view;
pub mod category;
pub mod community_view;
+pub mod user_view;
diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs
index 1db15ea6..6afba18d 100644
--- a/server/src/actions/post_view.rs
+++ b/server/src/actions/post_view.rs
@@ -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
index 00000000..5873a5c8
--- /dev/null
+++ b/server/src/actions/user_view.rs
@@ -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)
+ }
+}
+
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 0d81d507..9cdbd33e 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -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)
}
diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs
index 6aae4f2f..e116fadc 100644
--- a/server/src/websocket_server/server.rs
+++ b/server/src/websocket_server/server.rs
@@ -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>;
diff --git a/ui/package.json b/ui/package.json
index 1b82db12..b5bb14ef 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -15,7 +15,10 @@
},
"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
index 00000000..a87dd356
--- /dev/null
+++ b/ui/src/components/comment-form.tsx
@@ -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
index 00000000..55be7621
--- /dev/null
+++ b/ui/src/components/comment-node.tsx
@@ -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>