diff --git a/README.md b/README.md index 96d81eade..1c518a4e7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Lemmy

+

Lemmy

[![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 f78486d58..46b4df52d 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 000000000..c94d94c47 --- /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 000000000..69d052de1 --- /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 dcfcc2504..3b4e00bb8 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) -> Result, Error> { + pub fn list(conn: &PgConnection, + sort: &SortType, + for_post_id: Option, + for_creator_id: Option, + my_user_id: Option, + limit: i64) -> Result, 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::(conn) } - pub fn read(conn: &PgConnection, from_comment_id: i32, from_user_id: Option) -> Result { + pub fn read(conn: &PgConnection, from_comment_id: i32, my_user_id: Option) -> Result { 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()); } @@ -178,8 +208,8 @@ mod tests { my_vote: Some(1), }; - let read_comment_views_no_user = CommentView::list(&conn, inserted_post.id, None).unwrap(); - let read_comment_views_with_user = CommentView::list(&conn, inserted_post.id, Some(inserted_user.id)).unwrap(); + let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, 999).unwrap(); + let read_comment_views_with_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, Some(inserted_user.id), 999).unwrap(); let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap(); diff --git a/server/src/actions/mod.rs b/server/src/actions/mod.rs index c17fd81ad..819d5cdaf 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 1db15ea68..6ca85c341 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, from_user_id: Option, limit: i64) -> Result, Error> { + pub fn list(conn: &PgConnection, + type_: PostListingType, + sort: &SortType, + for_community_id: Option, + for_creator_id: Option, + my_user_id: Option, + limit: i64) -> Result, 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) -> Result { + pub fn read(conn: &PgConnection, from_post_id: i32, my_user_id: Option) -> Result { 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), None, Some(inserted_user.id), 10).unwrap(); + let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, 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 000000000..5873a5c86 --- /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 { + use actions::user_view::user_view::dsl::*; + + user_view.find(from_user_id) + .first::(conn) + } +} + diff --git a/server/src/bin/main.rs b/server/src/bin/main.rs index ed1c86fe6..96f8181d0 100644 --- a/server/src/bin/main.rs +++ b/server/src/bin/main.rs @@ -82,11 +82,12 @@ impl Actor for WSSession { } /// Handle messages from chat server, we simply send it to peer websocket +/// These are room messages, IE sent to others in the room impl Handler for WSSession { type Result = (); fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) { - println!("id: {} msg: {}", self.id, msg.0); + // println!("id: {} msg: {}", self.id, msg.0); ctx.text(msg.0); } } @@ -94,7 +95,7 @@ impl Handler for WSSession { /// WebSocket message handler impl StreamHandler for WSSession { fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id); + // println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id); match msg { ws::Message::Ping(msg) => { self.hb = Instant::now(); diff --git a/server/src/lib.rs b/server/src/lib.rs index 0d81d507e..9cdbd33ec 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 { fn create(conn: &PgConnection, form: &T) -> Result 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 { DateTime::::from_utc(ndt, Utc) } diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index 6aae4f2fd..92542d0a7 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 } +#[derive(Serialize, Deserialize)] +pub struct GetUserDetails { + user_id: i32, + sort: String, + limit: i64, + community_id: Option, + auth: Option +} + +#[derive(Serialize, Deserialize)] +pub struct GetUserDetailsResponse { + op: String, + user: UserView, + follows: Vec, + moderates: Vec, + comments: Vec, + posts: Vec, + saved_posts: Vec, + saved_comments: Vec, +} /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive @@ -307,16 +328,6 @@ impl ChatServer { } } } - - // /// Send message only to self - // fn send(&self, message: &str, id: &usize) { - // // println!("{:?}", self.sessions); - // if let Some(addr) = self.sessions.get(id) { - // println!("msg: {}", message); - // // println!("{:?}", addr.connected()); - // let _ = addr.do_send(WSMessage(message.to_owned())); - // } - // } } /// Make actor from `ChatServer` @@ -368,22 +379,9 @@ impl Handler for ChatServer { } } } - // send message to other users - // for room in rooms { - // self.send_room_message(room, "Someone disconnected", 0); - // } } } -/// Handler for Message message. -// impl Handler for ChatServer { -// type Result = (); - -// fn handle(&mut self, msg: ClientMessage, _: &mut Context) { -// self.send_room_message(&msg.room, msg.msg.as_str(), msg.id); -// } -// } - /// Handler for Message message. impl Handler for ChatServer { type Result = MessageResult; @@ -466,13 +464,17 @@ impl Handler 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) @@ -775,8 +777,6 @@ impl Perform for GetPost { let conn = establish_connection(); - println!("{:?}", self.auth); - let user_id: Option = match &self.auth { Some(auth) => { match Claims::decode(&auth) { @@ -808,7 +808,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,13 +1110,12 @@ 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); return self.error("Couldn't get posts"); } }; @@ -1412,185 +1411,52 @@ impl Perform for GetFollowedCommunities { } } -// impl Handler for ChatServer { - -// type Result = MessageResult; -// fn handle(&mut self, msg: Login, _: &mut Context) -> Self::Result { - -// let conn = establish_connection(); - -// // Fetch that username / email -// let user: User_ = match User_::find_by_email_or_username(&conn, &msg.username_or_email) { -// Ok(user) => user, -// Err(_e) => return MessageResult( -// Err( -// ErrorMessage { -// op: UserOperation::Login.to_string(), -// error: "Couldn't find that username or email".to_string() -// } -// ) -// ) -// }; - -// // Verify the password -// let valid: bool = verify(&msg.password, &user.password_encrypted).unwrap_or(false); -// if !valid { -// return MessageResult( -// Err( -// ErrorMessage { -// op: UserOperation::Login.to_string(), -// error: "Password incorrect".to_string() -// } -// ) -// ) -// } - -// // Return the jwt -// MessageResult( -// Ok( -// LoginResponse { -// op: UserOperation::Login.to_string(), -// jwt: user.jwt() -// } -// ) -// ) -// } -// } - -// impl Handler for ChatServer { - -// type Result = MessageResult; -// fn handle(&mut self, msg: Register, _: &mut Context) -> Self::Result { - -// let conn = establish_connection(); - -// // Make sure passwords match -// if msg.password != msg.password_verify { -// return MessageResult( -// Err( -// ErrorMessage { -// op: UserOperation::Register.to_string(), -// error: "Passwords do not match.".to_string() -// } -// ) -// ); -// } - -// // Register the new user -// let user_form = UserForm { -// name: msg.username, -// email: msg.email, -// password_encrypted: msg.password, -// preferred_username: None, -// updated: None -// }; - -// // Create the user -// let inserted_user = match User_::create(&conn, &user_form) { -// Ok(user) => user, -// Err(_e) => return MessageResult( -// Err( -// ErrorMessage { -// op: UserOperation::Register.to_string(), -// error: "User already exists.".to_string() // overwrite the diesel error -// } -// ) -// ) -// }; - -// // Return the jwt -// MessageResult( -// Ok( -// LoginResponse { -// op: UserOperation::Register.to_string(), -// jwt: inserted_user.jwt() -// } -// ) -// ) - -// } -// } - - -// impl Handler for ChatServer { - -// type Result = MessageResult; - -// fn handle(&mut self, msg: CreateCommunity, _: &mut Context) -> Self::Result { -// let conn = establish_connection(); - -// let user_id = Claims::decode(&msg.auth).id; - -// let community_form = CommunityForm { -// name: msg.name, -// updated: None -// }; - -// let community = match Community::create(&conn, &community_form) { -// Ok(community) => community, -// Err(_e) => return MessageResult( -// Err( -// ErrorMessage { -// op: UserOperation::CreateCommunity.to_string(), -// error: "Community already exists.".to_string() // overwrite the diesel error -// } -// ) -// ) -// }; - -// MessageResult( -// Ok( -// CommunityResponse { -// op: UserOperation::CreateCommunity.to_string(), -// community: community -// } -// ) -// ) -// } -// } -// -// -// -// /// Handler for `ListRooms` message. -// impl Handler for ChatServer { -// type Result = MessageResult; - -// fn handle(&mut self, _: ListRooms, _: &mut Context) -> Self::Result { -// let mut rooms = Vec::new(); - -// for key in self.rooms.keys() { -// rooms.push(key.to_owned()) -// } - -// MessageResult(rooms) -// } -// } - -// /// Join room, send disconnect message to old room -// /// send join message to new room -// impl Handler for ChatServer { -// type Result = (); - -// fn handle(&mut self, msg: Join, _: &mut Context) { -// let Join { id, name } = msg; -// let mut rooms = Vec::new(); - -// // remove session from all rooms -// for (n, sessions) in &mut self.rooms { -// if sessions.remove(&id) { -// rooms.push(n.to_owned()); -// } -// } -// // send message to other users -// for room in rooms { -// self.send_room_message(&room, "Someone disconnected", 0); -// } - -// if self.rooms.get_mut(&name).is_none() { -// self.rooms.insert(name.clone(), HashSet::new()); -// } -// self.send_room_message(&name, "Someone connected", id); -// self.rooms.get_mut(&name).unwrap().insert(id); -// } - -// } +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 = 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() + } +} + diff --git a/ui/fuse.js b/ui/fuse.js index a9283fad2..fe2c7664c 100644 --- a/ui/fuse.js +++ b/ui/fuse.js @@ -11,6 +11,7 @@ const transformInferno = require('ts-transform-inferno').default; const transformClasscat = require('ts-transform-classcat').default; let fuse, app; let isProduction = false; +var setVersion = require('./set_version.js').setVersion; Sparky.task('config', _ => { fuse = new FuseBox({ @@ -41,6 +42,7 @@ Sparky.task('config', _ => { }); app = fuse.bundle('app').instructions('>index.tsx'); }); +Sparky.task('version', _ => setVersion()); Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); Sparky.task('env', _ => (isProduction = true)); Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/')); diff --git a/ui/package.json b/ui/package.json index 1b82db12a..b5bb14ef9 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/set_version.js b/ui/set_version.js new file mode 100644 index 000000000..bfd640c25 --- /dev/null +++ b/ui/set_version.js @@ -0,0 +1,9 @@ +const fs = require('fs'); + +exports.setVersion = function() { + let revision = require('child_process') + .execSync('git describe --tags --long') + .toString().trim(); + let line = `export let version: string = "${revision}";`; + fs.writeFileSync("./src/version.ts", line); +} diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx new file mode 100644 index 000000000..a87dd3567 --- /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 { + + 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 ( +
+
+
+
+