Browse Source

Adding user details / overview page.

- Fixes #19
online_users
Dessalines 3 years ago
parent
commit
49bf16e7d4
  1. 11
      README.md
  2. 4
      server/migrations/2019-02-27-170003_create_community/up.sql
  3. 1
      server/migrations/2019-04-08-015947_create_user_view/down.sql
  4. 11
      server/migrations/2019-04-08-015947_create_user_view/up.sql
  5. 50
      server/src/actions/comment_view.rs
  6. 1
      server/src/actions/mod.rs
  7. 57
      server/src/actions/post_view.rs
  8. 40
      server/src/actions/user_view.rs
  9. 8
      server/src/lib.rs
  10. 100
      server/src/websocket_server/server.rs
  11. 3
      ui/package.json
  12. 93
      ui/src/components/comment-form.tsx
  13. 148
      ui/src/components/comment-node.tsx
  14. 30
      ui/src/components/comment-nodes.tsx
  15. 12
      ui/src/components/communities.tsx
  16. 22
      ui/src/components/community-form.tsx
  17. 12
      ui/src/components/community.tsx
  18. 4
      ui/src/components/create-community.tsx
  19. 4
      ui/src/components/create-post.tsx
  20. 1
      ui/src/components/home.tsx
  21. 20
      ui/src/components/login.tsx
  22. 10
      ui/src/components/main.tsx
  23. 4
      ui/src/components/moment-time.tsx
  24. 7
      ui/src/components/navbar.tsx
  25. 23
      ui/src/components/post-form.tsx
  26. 21
      ui/src/components/post-listing.tsx
  27. 36
      ui/src/components/post-listings.tsx
  28. 279
      ui/src/components/post.tsx
  29. 10
      ui/src/components/sidebar.tsx
  30. 264
      ui/src/components/user.tsx
  31. 6
      ui/src/index.tsx
  32. 41
      ui/src/interfaces.ts
  33. 9
      ui/src/main.css
  34. 9
      ui/src/services/WebSocketService.ts
  35. 1
      ui/src/utils.ts
  36. 36
      ui/yarn.lock

11
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
```

4
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);

1
server/migrations/2019-04-08-015947_create_user_view/down.sql

@ -0,0 +1 @@
drop view user_view;

11
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;

50
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());
}

1
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;

57
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();

40
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)
}
}

8
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)
}

100
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>;

3
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",

93
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();
}
}

148
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>
}
</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);
}
}

30
ui/src/components/comment-nodes.tsx

@ -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>
)
}
}

12
ui/src/components/communities.tsx

@ -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);

22
ui/src/components/community-form.tsx

@ -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();
}

12
ui/src/components/community.tsx

@ -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;

4
ui/src/components/create-community.tsx

@ -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);
}

4
ui/src/components/create-post.tsx

@ -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);
}

1
ui/src/components/home.tsx

@ -1,5 +1,4 @@
import { Component } from 'inferno';
import { repoUrl } from '../utils';
import { Main } from './main';
export class Home extends Component<any, any> {

20
ui/src/components/login.tsx

@ -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('/');
}
}

10
ui/src/components/main.tsx

@ -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;

4
ui/src/components/moment-time.tsx

@ -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);
}

7
ui/src/components/navbar.tsx

@ -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);
}

23
ui/src/components/post-form.tsx

@ -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();
}

21
ui/src/components/post-listing.tsx

@ -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);
}

36
ui/src/components/post-listings.tsx

@ -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">
<