-<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)
## Features
- TBD
+-
+the name
+
+Lead singer from motorhead.
+The old school video game.
+The furry rodents.
+
+Goals r/ censorship
+
## Install
### Docker
```
('Meta'),
('Other');
-
-
create table community (
id serial primary key,
name varchar(20) not null unique,
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);
--- /dev/null
+drop view user_view;
--- /dev/null
+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;
+
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! {
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());
}
pub mod comment_view;
pub mod category;
pub mod community_view;
+pub mod user_view;
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) {
}
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())
};
}
- 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::*;
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());
};
};
- 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();
--- /dev/null
+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)
+ }
+}
+
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;
}
}
-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)
}
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::*;
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)]
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
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)
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();
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);
}
}
+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>;
},
"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",
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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>
+ )
+ }
+}
+
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;
communities: []
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
}
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
componentDidMount() {
let table = document.querySelector('#community_table');
Sortable.initTable(table);
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 {
categories: []
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
);
}
- handleCreateCommunitySubmit(i: CommunityForm, event) {
+ handleCreateCommunitySubmit(i: CommunityForm, event: any) {
event.preventDefault();
if (i.props.community) {
WebSocketService.Instance.editCommunity(i.state.communityForm);
}
}
- 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();
}
-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;
communityId: Number(this.props.match.params.id)
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
-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);
}
-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);
}
import { Component } from 'inferno';
-import { repoUrl } from '../utils';
import { Main } from './main';
export class Home extends Component<any, any> {
export class Login extends Component<any, State> {
private subscription: Subscription;
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = emptyState;
);
}
- 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);
}
} else {
if (op == UserOperation.Register || op == UserOperation.Login) {
let res: LoginResponse = msg;
- UserService.Instance.login(msg);
+ UserService.Instance.login(res);
this.props.history.push('/');
}
}
-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>;
subscribedCommunities: []
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
-import { Component, linkEvent } from 'inferno';
+import { Component } from 'inferno';
import * as moment from 'moment';
interface MomentTimeProps {
export class MomentTime extends Component<MomentTimeProps, any> {
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
}
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};
);
}
- 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);
}
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 {
communities: []
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
);
}
- handlePostSubmit(i: PostForm, event) {
+ handlePostSubmit(i: PostForm, event: any) {
event.preventDefault();
if (i.props.post) {
WebSocketService.Instance.editPost(i.state.postForm);
}
}
- 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();
}
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';
editable?: boolean;
showCommunity?: boolean;
showBody?: boolean;
+ viewOnly?: boolean;
}
export class PostListing extends Component<PostListingProps, PostListingState> {
iframeExpanded: false
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
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>
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,
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
WebSocketService.Instance.likePost(form);
}
- handleEditClick(i: PostListing, event) {
+ handleEditClick(i: PostListing) {
i.state.showEdit = true;
i.setState(i.state);
}
}
// 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,
WebSocketService.Instance.editPost(deleteForm);
}
- handleIframeExpandClick(i: PostListing, event) {
+ handleIframeExpandClick(i: PostListing) {
i.state.iframeExpanded = !i.state.iframeExpanded;
i.setState(i.state);
}
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 {
community: CommunityI;
moderators: Array<CommunityUser>;
posts: Array<Post>;
- sortType: ListingSortType;
+ sortType: SortType;
type_: ListingType;
}
},
moderators: [],
posts: [],
- sortType: ListingSortType.Hot,
+ sortType: SortType.Hot,
type_: this.props.communityId
? ListingType.Community
: UserService.Instance.loggedIn
: ListingType.All
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
type_: ListingType[this.state.type_],
community_id: this.props.communityId,
limit: 10,
- sort: ListingSortType[ListingSortType.Hot],
+ sort: SortType[SortType.Hot],
}
WebSocketService.Instance.getPosts(getPostsForm);
}
<div className="mb-2">
<select value={this.state.sortType} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
<option disabled>Sort Type</option>
- <option value={ListingSortType.Hot}>Hot</option>
- <option value={ListingSortType.New}>New</option>
+ <option value={SortType.Hot}>Hot</option>
+ <option value={SortType.New}>New</option>
<option disabled>──────────</option>
- <option value={ListingSortType.TopDay}>Top Day</option>
- <option value={ListingSortType.TopWeek}>Week</option>
- <option value={ListingSortType.TopMonth}>Month</option>
- <option value={ListingSortType.TopYear}>Year</option>
- <option value={ListingSortType.TopAll}>All</option>
+ <option value={SortType.TopDay}>Top Day</option>
+ <option value={SortType.TopWeek}>Week</option>
+ <option value={SortType.TopMonth}>Month</option>
+ <option value={SortType.TopYear}>Year</option>
+ <option value={SortType.TopAll}>All</option>
</select>
{!this.props.communityId &&
UserService.Instance.loggedIn &&
}
- handleSortChange(i: PostListings, event) {
+ handleSortChange(i: PostListings, event: any) {
i.state.sortType = Number(event.target.value);
i.setState(i.state);
let getPostsForm: GetPostsForm = {
community_id: i.state.community.id,
limit: 10,
- sort: ListingSortType[i.state.sortType],
+ sort: SortType[i.state.sortType],
type_: ListingType[ListingType.Community]
}
WebSocketService.Instance.getPosts(getPostsForm);
}
- handleTypeChange(i: PostListings, event) {
+ handleTypeChange(i: PostListings, event: any) {
i.state.type_ = Number(event.target.value);
i.setState(i.state);
let getPostsForm: GetPostsForm = {
limit: 10,
- sort: ListingSortType[i.state.sortType],
+ sort: SortType[i.state.sortType],
type_: ListingType[i.state.type_]
}
WebSocketService.Instance.getPosts(getPostsForm);
import { Component, linkEvent } from 'inferno';
-import { Link } from 'inferno-router';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse } from '../interfaces';
-import { WebSocketService, UserService } from '../services';
-import { msgOp, hotRank,mdToHtml } from '../utils';
-import { MomentTime } from './moment-time';
+import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI } from '../interfaces';
+import { WebSocketService } from '../services';
+import { msgOp, hotRank } from '../utils';
import { PostListing } from './post-listing';
import { Sidebar } from './sidebar';
+import { CommentForm } from './comment-form';
+import { CommentNodes } from './comment-nodes';
import * as autosize from 'autosize';
-interface CommentNodeI {
- comment: Comment;
- children?: Array<CommentNodeI>;
-};
interface PostState {
post: PostI;
commentSort: CommentSortType;
community: Community;
moderators: Array<CommunityUser>;
- scrolled: boolean;
+ scrolled?: boolean;
+ scrolled_comment_id?: number;
}
export class Post extends Component<any, PostState> {
scrolled: false
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
let postId = Number(this.props.match.params.id);
+ if (this.props.match.params.comment_id) {
+ this.state.scrolled_comment_id = this.props.match.params.comment_id;
+ }
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
autosize(document.querySelectorAll('textarea'));
}
- componentDidUpdate(lastProps: any, lastState: PostState, snapshot: any) {
- if (!this.state.scrolled && lastState.comments.length > 0 && window.location.href.includes('#comment-')) {
- let id = window.location.hash.split("#")[2];
- var elmnt = document.getElementById(`${id}`);
+ componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
+ if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) {
+ var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`);
elmnt.scrollIntoView();
+ elmnt.classList.add("mark");
this.state.scrolled = true;
}
}
);
}
- handleCommentSortChange(i: Post, event) {
+ handleCommentSortChange(i: Post, event: any) {
i.state.commentSort = Number(event.target.value);
i.setState(i.state);
}
}
}
-interface CommentNodesState {
-}
-
-interface CommentNodesProps {
- nodes: Array<CommentNodeI>;
- noIndent?: boolean;
-}
-
-export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
-
- constructor(props, context) {
- super(props, context);
- }
-
- render() {
- return (
- <div className="comments">
- {this.props.nodes.map(node =>
- <CommentNode node={node} noIndent={this.props.noIndent} />
- )}
- </div>
- )
- }
-}
-
-
-interface CommentNodeState {
- showReply: boolean;
- showEdit: boolean;
-}
-
-interface CommentNodeProps {
- node: CommentNodeI;
- noIndent?: boolean;
-}
-
-export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
-
- private emptyState: CommentNodeState = {
- showReply: false,
- showEdit: false
- }
-
- constructor(props, context) {
- super(props, context);
-
- this.state = this.emptyState;
- this.handleReplyCancel = this.handleReplyCancel.bind(this);
- this.handleCommentLike = this.handleCommentLike.bind(this);
- this.handleCommentDisLike = this.handleCommentDisLike.bind(this);
- }
-
- render() {
- let node = this.props.node;
- return (
- <div id={`comment-${node.comment.id}`} className={`comment ${node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''}`}>
- <div className="float-left small text-center">
- <div className={`pointer upvote ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>â–²</div>
- <div>{node.comment.score}</div>
- <div className={`pointer downvote ${node.comment.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>â–¼</div>
- </div>
- <div className="details ml-4">
- <ul class="list-inline mb-0 text-muted small">
- <li className="list-inline-item">
- <Link to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link>
- </li>
- <li className="list-inline-item">
- <span>(
- <span className="text-info">+{node.comment.upvotes}</span>
- <span> | </span>
- <span className="text-danger">-{node.comment.downvotes}</span>
- <span>) </span>
- </span>
- </li>
- <li className="list-inline-item">
- <span><MomentTime data={node.comment} /></span>
- </li>
- </ul>
- {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />}
- {!this.state.showEdit &&
- <div>
- <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.content)} />
- <ul class="list-inline mb-1 text-muted small font-weight-bold">
- <li className="list-inline-item">
- <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
- </li>
- {this.myComment &&
- <li className="list-inline-item">
- <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
- </li>
- }
- {this.myComment &&
- <li className="list-inline-item">
- <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
- </li>
- }
- <li className="list-inline-item">
- <Link className="text-muted" to={`/post/${node.comment.post_id}#comment-${node.comment.id}`}>link</Link>
- </li>
- </ul>
- </div>
- }
- </div>
- {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
- {this.props.node.children && <CommentNodes nodes={this.props.node.children} />}
- </div>
- )
- }
-
- private get myComment(): boolean {
- return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id;
- }
-
- handleReplyClick(i: CommentNode, event) {
- i.state.showReply = true;
- i.setState(i.state);
- }
-
- handleEditClick(i: CommentNode, event) {
- i.state.showEdit = true;
- i.setState(i.state);
- }
-
- handleDeleteClick(i: CommentNode, event) {
- let deleteForm: CommentFormI = {
- content: "*deleted*",
- edit_id: i.props.node.comment.id,
- post_id: i.props.node.comment.post_id,
- parent_id: i.props.node.comment.parent_id,
- auth: null
- };
- WebSocketService.Instance.editComment(deleteForm);
- }
-
- handleReplyCancel(): any {
- this.state.showReply = false;
- this.state.showEdit = false;
- this.setState(this.state);
- }
-
-
- handleCommentLike(i: CommentNodeI, event) {
-
- let form: CommentLikeForm = {
- comment_id: i.comment.id,
- post_id: i.comment.post_id,
- score: (i.comment.my_vote == 1) ? 0 : 1
- };
- WebSocketService.Instance.likeComment(form);
- }
-
- handleCommentDisLike(i: CommentNodeI, event) {
- let form: CommentLikeForm = {
- comment_id: i.comment.id,
- post_id: i.comment.post_id,
- score: (i.comment.my_vote == -1) ? 0 : -1
- };
- WebSocketService.Instance.likeComment(form);
- }
-}
-
-interface CommentFormProps {
- postId?: number;
- node?: CommentNodeI;
- onReplyCancel?();
- edit?: boolean;
-}
-
-interface CommentFormState {
- commentForm: CommentFormI;
- buttonTitle: string;
-}
-
-export class CommentForm extends Component<CommentFormProps, CommentFormState> {
-
- private emptyState: CommentFormState = {
- commentForm: {
- auth: null,
- content: null,
- post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId
- },
- buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply"
- }
-
- constructor(props, context) {
- super(props, context);
-
- this.state = this.emptyState;
- if (this.props.node) {
- if (this.props.edit) {
- this.state.commentForm.edit_id = this.props.node.comment.id;
- this.state.commentForm.parent_id = this.props.node.comment.parent_id;
- this.state.commentForm.content = this.props.node.comment.content;
- } else {
- // A reply gets a new parent id
- this.state.commentForm.parent_id = this.props.node.comment.id;
- }
- }
- }
- componentDidMount() {
- autosize(document.querySelectorAll('textarea'));
- }
-
- render() {
- return (
- <div>
- <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
- <div class="form-group row">
- <div class="col-sm-12">
- <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required />
- </div>
- </div>
- <div class="row">
- <div class="col-sm-12">
- <button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button>
- {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
- </div>
- </div>
- </form>
- </div>
- );
- }
-
- handleCommentSubmit(i: CommentForm, event) {
- if (i.props.edit) {
- WebSocketService.Instance.editComment(i.state.commentForm);
- } else {
- WebSocketService.Instance.createComment(i.state.commentForm);
- }
-
- i.state.commentForm.content = undefined;
- i.setState(i.state);
- event.target.reset();
- if (i.props.node) {
- i.props.onReplyCancel();
- }
- }
-
- handleCommentContentChange(i: CommentForm, event) {
- i.state.commentForm.content = event.target.value;
- i.setState(i.state);
- }
-
- handleReplyCancel(i: CommentForm, event) {
- i.props.onReplyCancel();
- }
-}
showEdit: false
}
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.handleEditCommunity = this.handleEditCommunity.bind(this);
);
}
- handleEditClick(i: Sidebar, event) {
+ handleEditClick(i: Sidebar) {
i.state.showEdit = true;
i.setState(i.state);
}
- handleEditCommunity(community: Community) {
+ handleEditCommunity() {
this.state.showEdit = false;
this.setState(this.state);
}
}
// TODO no deleting communities yet
- handleDeleteClick(i: Sidebar, event) {
- }
+ // handleDeleteClick(i: Sidebar, event) {
+ // }
handleUnsubscribe(communityId: number) {
let form: FollowCommunityForm = {
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView } from '../interfaces';
+import { WebSocketService } from '../services';
+import { msgOp } from '../utils';
+import { PostListing } from './post-listing';
+import { CommentNodes } from './comment-nodes';
+import { MomentTime } from './moment-time';
+
+enum View {
+ Overview, Comments, Posts, Saved
+}
+
+interface UserState {
+ user: UserView;
+ follows: Array<CommunityUser>;
+ moderates: Array<CommunityUser>;
+ comments: Array<Comment>;
+ posts: Array<Post>;
+ saved?: Array<Post>;
+ view: View;
+ sort: SortType;
+}
+
+export class User extends Component<any, UserState> {
+
+ private subscription: Subscription;
+ private emptyState: UserState = {
+ user: {
+ id: null,
+ name: null,
+ fedi_name: null,
+ published: null,
+ number_of_posts: null,
+ post_score: null,
+ number_of_comments: null,
+ comment_score: null,
+ },
+ follows: [],
+ moderates: [],
+ comments: [],
+ posts: [],
+ view: View.Overview,
+ sort: SortType.New
+ }
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ let userId = Number(this.props.match.params.id);
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ (msg) => this.parseMessage(msg),
+ (err) => console.error(err),
+ () => console.log('complete')
+ );
+
+ let form: GetUserDetailsForm = {
+ user_id: userId,
+ sort: SortType[this.state.sort],
+ limit: 999
+ };
+ WebSocketService.Instance.getUserDetails(form);
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-lg-9">
+ <h4>/u/{this.state.user.name}</h4>
+ {this.selects()}
+ {this.state.view == View.Overview &&
+ this.overview()
+ }
+ {this.state.view == View.Comments &&
+ this.comments()
+ }
+ {this.state.view == View.Posts &&
+ this.posts()
+ }
+ </div>
+ <div class="col-12 col-lg-3">
+ {this.userInfo()}
+ {this.moderates()}
+ {this.follows()}
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ selects() {
+ return (
+ <div className="mb-2">
+ <select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select w-auto">
+ <option disabled>View</option>
+ <option value={View.Overview}>Overview</option>
+ <option value={View.Comments}>Comments</option>
+ <option value={View.Posts}>Posts</option>
+ {/* <option value={View.Saved}>Saved</option> */}
+ </select>
+ <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
+ <option disabled>Sort Type</option>
+ <option value={SortType.New}>New</option>
+ <option value={SortType.TopDay}>Top Day</option>
+ <option value={SortType.TopWeek}>Week</option>
+ <option value={SortType.TopMonth}>Month</option>
+ <option value={SortType.TopYear}>Year</option>
+ <option value={SortType.TopAll}>All</option>
+ </select>
+ </div>
+ )
+
+ }
+
+ overview() {
+ let combined: Array<any> = [];
+ combined.push(...this.state.comments);
+ combined.push(...this.state.posts);
+
+ // Sort it
+ if (this.state.sort == SortType.New) {
+ combined.sort((a, b) => b.published.localeCompare(a.published));
+ } else {
+ combined.sort((a, b) => b.score - a.score);
+ }
+
+ return (
+ <div>
+ {combined.map(i =>
+ <div>
+ {i.community_id
+ ? <PostListing post={i} showCommunity viewOnly />
+ : <CommentNodes nodes={[{comment: i}]} noIndent viewOnly />
+ }
+ </div>
+ )
+ }
+ </div>
+ )
+ }
+
+ comments() {
+ return (
+ <div>
+ {this.state.comments.map(comment =>
+ <CommentNodes nodes={[{comment: comment}]} noIndent viewOnly />
+ )}
+ </div>
+ );
+ }
+
+ posts() {
+ return (
+ <div>
+ {this.state.posts.map(post =>
+ <PostListing post={post} showCommunity viewOnly />
+ )}
+ </div>
+ );
+ }
+
+ userInfo() {
+ let user = this.state.user;
+ return (
+ <div>
+ <h4>{user.name}</h4>
+ <div>Joined <MomentTime data={user} /></div>
+ <table class="table table-bordered table-sm mt-2">
+ <tr>
+ <td>{user.post_score} points</td>
+ <td>{user.number_of_posts} posts</td>
+ </tr>
+ <tr>
+ <td>{user.comment_score} points</td>
+ <td>{user.number_of_comments} comments</td>
+ </tr>
+ </table>
+ <hr />
+ </div>
+ )
+ }
+
+ moderates() {
+ return (
+ <div>
+ {this.state.moderates.length > 0 &&
+ <div>
+ <h4>Moderates</h4>
+ <ul class="list-unstyled">
+ {this.state.moderates.map(community =>
+ <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
+ )}
+ </ul>
+ </div>
+ }
+ </div>
+ )
+ }
+
+ follows() {
+ return (
+ <div>
+ {this.state.follows.length > 0 &&
+ <div>
+ <hr />
+ <h4>Subscribed</h4>
+ <ul class="list-unstyled">
+ {this.state.follows.map(community =>
+ <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
+ )}
+ </ul>
+ </div>
+ }
+ </div>
+ )
+ }
+
+ handleSortChange(i: User, event: any) {
+ i.state.sort = Number(event.target.value);
+ i.setState(i.state);
+
+ let form: GetUserDetailsForm = {
+ user_id: i.state.user.id,
+ sort: SortType[i.state.sort],
+ limit: 999
+ };
+ WebSocketService.Instance.getUserDetails(form);
+ }
+
+ handleViewChange(i: User, event: any) {
+ i.state.view = Number(event.target.value);
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: any) {
+ console.log(msg);
+ let op: UserOperation = msgOp(msg);
+ if (msg.error) {
+ alert(msg.error);
+ return;
+ } else if (op == UserOperation.GetUserDetails) {
+ let res: UserDetailsResponse = msg;
+ this.state.user = res.user;
+ this.state.comments = res.comments;
+ this.state.follows = res.follows;
+ this.state.moderates = res.moderates;
+ this.state.posts = res.posts;
+ this.setState(this.state);
+ }
+ }
+}
+
import { Post } from './components/post';
import { Community } from './components/community';
import { Communities } from './components/communities';
+import { User } from './components/user';
import './main.css';
class Index extends Component<any, any> {
- constructor(props, context) {
+ constructor(props: any, context: any) {
super(props, context);
WebSocketService.Instance;
UserService.Instance;
<Route path={`/create_post`} component={CreatePost} />
<Route path={`/create_community`} component={CreateCommunity} />
<Route path={`/communities`} component={Communities} />
+ <Route path={`/post/:id/comment/:comment_id`} component={Post} />
<Route path={`/post/:id`} component={Post} />
<Route path={`/community/:id`} component={Community} />
+ <Route path={`/user/:id/:heading`} component={User} />
+ <Route path={`/user/:id`} component={User} />
</Switch>
{this.symbols()}
</div>
export enum UserOperation {
- Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities
+ Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails
}
export interface User {
username: string;
}
+export interface UserView {
+ id: number;
+ name: string;
+ fedi_name: string;
+ published: string;
+ number_of_posts: number;
+ post_score: number;
+ number_of_comments: number;
+ comment_score: number;
+}
+
export interface CommunityUser {
id: number;
user_id: number;
auth?: string;
}
+export interface CommentNode {
+ comment: Comment;
+ children?: Array<CommentNode>;
+}
+
export interface GetPostsForm {
type_: string;
sort: string;
communities: Array<CommunityUser>;
}
+export interface GetUserDetailsForm {
+ user_id: number;
+ sort: string; // TODO figure this one out
+ limit: number;
+ community_id?: number;
+ auth?: string;
+}
+
+
+
+export interface UserDetailsResponse {
+ op: string;
+ user: UserView;
+ follows: Array<CommunityUser>;
+ moderates: Array<CommunityUser>;
+ comments: Array<Comment>;
+ posts: Array<Post>;
+ saved?: Array<Post>;
+}
+
+
export interface LoginForm {
username_or_email: string;
password: string;
All, Subscribed, Community
}
-export enum ListingSortType {
+export enum SortType {
Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
}
cursor: pointer;
}
+.no-click {
+ pointer-events:none;
+ opacity: 0.65;
+}
+
.upvote:hover {
color: var(--info);
}
background-color: var(--secondary);
}
+.mark {
+ background-color: #322a00;
+}
+
.md-div p {
margin-bottom: 0px;
}
import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm } from '../interfaces';
import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm));
}
+ public getUserDetails(form: GetUserDetailsForm) {
+ this.setAuth(form, false);
+ this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
+ }
+
private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data };
console.log(send);
}
-window.onbeforeunload = (e => {
+window.onbeforeunload = (() => {
WebSocketService.Instance.subject.unsubscribe();
WebSocketService.Instance.subject = null;
});
import * as markdown_it from 'markdown-it';
export let repoUrl = 'https://github.com/dessalines/lemmy';
-export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/';
export function msgOp(msg: any): UserOperation {
let opStr: string = msg.op;
dependencies:
regenerator-runtime "^0.12.0"
+"@types/autosize@^3.0.6":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-3.0.6.tgz#9022e6a783ec5a4d5e570013701dbc0bfe7667fa"
+ integrity sha512-gpfmXswGISLSWNOOdF2PDK96SfkaZdNtNixWJbYH10xn3Hqdt4VyS1GmoutuwOshWyCLuJw2jGhF0zkK7PUhrg==
+ dependencies:
+ "@types/jquery" "*"
+
+"@types/jquery@*":
+ version "3.3.29"
+ resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd"
+ integrity sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==
+ dependencies:
+ "@types/sizzle" "*"
+
"@types/js-cookie@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.1.tgz#aa6f6d5e5aaf7d97959e9fa938ac2501cf1a76f4"
integrity sha512-VIVurImEhQ95jxtjs8baVU5qCzVfwYfuMrpXwdRykJ5MCI5iY7/jB4cDSgwBVeYqeXrhT7GfJUwoDOmN0OMVCA==
+"@types/jwt-decode@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@types/jwt-decode/-/jwt-decode-2.2.1.tgz#afdf5c527fcfccbd4009b5fd02d1e18241f2d2f2"
+ integrity sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==
+
+"@types/linkify-it@*":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
+ integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
+
+"@types/markdown-it@^0.0.7":
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
+ integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
+ dependencies:
+ "@types/linkify-it" "*"
+
+"@types/sizzle@*":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
+ integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
+
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"