mirror of
https://github.com/LemmyNet/lemmy
synced 2024-10-30 15:21:20 +00:00
Merge branch 'dev' into moderation
This commit is contained in:
commit
fd0cbfe4e2
11
README.md
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
|
||||
```
|
||||
|
@ -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);
|
||||
|
@ -0,0 +1 @@
|
||||
drop view user_view;
|
11
server/migrations/2019-04-08-015947_create_user_view/up.sql
Normal file
11
server/migrations/2019-04-08-015947_create_user_view/up.sql
Normal file
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
@ -178,8 +208,8 @@ mod tests {
|
||||
my_vote: Some(1),
|
||||
};
|
||||
|
||||
let read_comment_views_no_user = CommentView::list(&conn, inserted_post.id, None).unwrap();
|
||||
let read_comment_views_with_user = CommentView::list(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
|
||||
let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, 999).unwrap();
|
||||
let read_comment_views_with_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, Some(inserted_user.id), 999).unwrap();
|
||||
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
|
||||
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
||||
Post::delete(&conn, inserted_post.id).unwrap();
|
||||
|
@ -6,3 +6,4 @@ pub mod post_view;
|
||||
pub mod comment_view;
|
||||
pub mod category;
|
||||
pub mod community_view;
|
||||
pub mod user_view;
|
||||
|
@ -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), None, Some(inserted_user.id), 10).unwrap();
|
||||
let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, None, 10).unwrap();
|
||||
let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
|
||||
let read_post_listing_with_user = PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
|
||||
|
||||
|
40
server/src/actions/user_view.rs
Normal file
40
server/src/actions/user_view.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -82,11 +82,12 @@ impl Actor for WSSession {
|
||||
}
|
||||
|
||||
/// Handle messages from chat server, we simply send it to peer websocket
|
||||
/// These are room messages, IE sent to others in the room
|
||||
impl Handler<WSMessage> for WSSession {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
||||
println!("id: {} msg: {}", self.id, msg.0);
|
||||
// println!("id: {} msg: {}", self.id, msg.0);
|
||||
ctx.text(msg.0);
|
||||
}
|
||||
}
|
||||
@ -94,7 +95,7 @@ impl Handler<WSMessage> for WSSession {
|
||||
/// WebSocket message handler
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
||||
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
@ -307,16 +328,6 @@ impl ChatServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// /// Send message only to self
|
||||
// fn send(&self, message: &str, id: &usize) {
|
||||
// // println!("{:?}", self.sessions);
|
||||
// if let Some(addr) = self.sessions.get(id) {
|
||||
// println!("msg: {}", message);
|
||||
// // println!("{:?}", addr.connected());
|
||||
// let _ = addr.do_send(WSMessage(message.to_owned()));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/// Make actor from `ChatServer`
|
||||
@ -368,22 +379,9 @@ impl Handler<Disconnect> for ChatServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
// send message to other users
|
||||
// for room in rooms {
|
||||
// self.send_room_message(room, "Someone disconnected", 0);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for Message message.
|
||||
// impl Handler<ClientMessage> for ChatServer {
|
||||
// type Result = ();
|
||||
|
||||
// fn handle(&mut self, msg: ClientMessage, _: &mut Context<Self>) {
|
||||
// self.send_room_message(&msg.room, msg.msg.as_str(), msg.id);
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Handler for Message message.
|
||||
impl Handler<StandardMessage> for ChatServer {
|
||||
type Result = MessageResult<StandardMessage>;
|
||||
@ -466,13 +464,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)
|
||||
@ -775,8 +777,6 @@ impl Perform for GetPost {
|
||||
|
||||
let conn = establish_connection();
|
||||
|
||||
println!("{:?}", self.auth);
|
||||
|
||||
let user_id: Option<i32> = match &self.auth {
|
||||
Some(auth) => {
|
||||
match Claims::decode(&auth) {
|
||||
@ -808,7 +808,7 @@ impl Perform for GetPost {
|
||||
|
||||
chat.rooms.get_mut(&self.id).unwrap().insert(addr);
|
||||
|
||||
let comments = CommentView::list(&conn, self.id, user_id).unwrap();
|
||||
let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, user_id, 999).unwrap();
|
||||
|
||||
let community = CommunityView::read(&conn, post_view.community_id, user_id).unwrap();
|
||||
|
||||
@ -1110,13 +1110,12 @@ impl Perform for GetPosts {
|
||||
None => None
|
||||
};
|
||||
|
||||
let type_ = ListingType::from_str(&self.type_).expect("listing type");
|
||||
let sort = ListingSortType::from_str(&self.sort).expect("listing sort");
|
||||
let type_ = PostListingType::from_str(&self.type_).expect("listing type");
|
||||
let sort = SortType::from_str(&self.sort).expect("listing sort");
|
||||
|
||||
let posts = match PostView::list(&conn, type_, sort, self.community_id, user_id, self.limit) {
|
||||
let posts = match PostView::list(&conn, type_, &sort, self.community_id, None, user_id, self.limit) {
|
||||
Ok(posts) => posts,
|
||||
Err(_e) => {
|
||||
eprintln!("{}", _e);
|
||||
return self.error("Couldn't get posts");
|
||||
}
|
||||
};
|
||||
@ -1412,185 +1411,52 @@ impl Perform for GetFollowedCommunities {
|
||||
}
|
||||
}
|
||||
|
||||
// impl Handler<Login> for ChatServer {
|
||||
impl Perform for GetUserDetails {
|
||||
fn op_type(&self) -> UserOperation {
|
||||
UserOperation::GetUserDetails
|
||||
}
|
||||
|
||||
// type Result = MessageResult<Login>;
|
||||
// fn handle(&mut self, msg: Login, _: &mut Context<Self>) -> Self::Result {
|
||||
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
|
||||
|
||||
// let conn = establish_connection();
|
||||
let conn = establish_connection();
|
||||
|
||||
// // Fetch that username / email
|
||||
// let user: User_ = match User_::find_by_email_or_username(&conn, &msg.username_or_email) {
|
||||
// Ok(user) => user,
|
||||
// Err(_e) => return MessageResult(
|
||||
// Err(
|
||||
// ErrorMessage {
|
||||
// op: UserOperation::Login.to_string(),
|
||||
// error: "Couldn't find that username or email".to_string()
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// };
|
||||
|
||||
// // Verify the password
|
||||
// let valid: bool = verify(&msg.password, &user.password_encrypted).unwrap_or(false);
|
||||
// if !valid {
|
||||
// return MessageResult(
|
||||
// Err(
|
||||
// ErrorMessage {
|
||||
// op: UserOperation::Login.to_string(),
|
||||
// error: "Password incorrect".to_string()
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
|
||||
// // Return the jwt
|
||||
// MessageResult(
|
||||
// Ok(
|
||||
// LoginResponse {
|
||||
// op: UserOperation::Login.to_string(),
|
||||
// jwt: user.jwt()
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Handler<Register> for ChatServer {
|
||||
|
||||
// type Result = MessageResult<Register>;
|
||||
// fn handle(&mut self, msg: Register, _: &mut Context<Self>) -> Self::Result {
|
||||
|
||||
// let conn = establish_connection();
|
||||
|
||||
// // Make sure passwords match
|
||||
// if msg.password != msg.password_verify {
|
||||
// return MessageResult(
|
||||
// Err(
|
||||
// ErrorMessage {
|
||||
// op: UserOperation::Register.to_string(),
|
||||
// error: "Passwords do not match.".to_string()
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Register the new user
|
||||
// let user_form = UserForm {
|
||||
// name: msg.username,
|
||||
// email: msg.email,
|
||||
// password_encrypted: msg.password,
|
||||
// preferred_username: None,
|
||||
// updated: None
|
||||
// };
|
||||
|
||||
// // Create the user
|
||||
// let inserted_user = match User_::create(&conn, &user_form) {
|
||||
// Ok(user) => user,
|
||||
// Err(_e) => return MessageResult(
|
||||
// Err(
|
||||
// ErrorMessage {
|
||||
// op: UserOperation::Register.to_string(),
|
||||
// error: "User already exists.".to_string() // overwrite the diesel error
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// };
|
||||
|
||||
// // Return the jwt
|
||||
// MessageResult(
|
||||
// Ok(
|
||||
// LoginResponse {
|
||||
// op: UserOperation::Register.to_string(),
|
||||
// jwt: inserted_user.jwt()
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
|
||||
// }
|
||||
// }
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
// impl Handler<CreateCommunity> for ChatServer {
|
||||
//TODO add save
|
||||
let sort = SortType::from_str(&self.sort).expect("listing sort");
|
||||
|
||||
// type Result = MessageResult<CreateCommunity>;
|
||||
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();
|
||||
|
||||
// fn handle(&mut self, msg: CreateCommunity, _: &mut Context<Self>) -> Self::Result {
|
||||
// let conn = establish_connection();
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
// let user_id = Claims::decode(&msg.auth).id;
|
||||
|
||||
// let community_form = CommunityForm {
|
||||
// name: msg.name,
|
||||
// updated: None
|
||||
// };
|
||||
|
||||
// let community = match Community::create(&conn, &community_form) {
|
||||
// Ok(community) => community,
|
||||
// Err(_e) => return MessageResult(
|
||||
// Err(
|
||||
// ErrorMessage {
|
||||
// op: UserOperation::CreateCommunity.to_string(),
|
||||
// error: "Community already exists.".to_string() // overwrite the diesel error
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// };
|
||||
|
||||
// MessageResult(
|
||||
// Ok(
|
||||
// CommunityResponse {
|
||||
// op: UserOperation::CreateCommunity.to_string(),
|
||||
// community: community
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// /// Handler for `ListRooms` message.
|
||||
// impl Handler<ListRooms> for ChatServer {
|
||||
// type Result = MessageResult<ListRooms>;
|
||||
|
||||
// fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Self::Result {
|
||||
// let mut rooms = Vec::new();
|
||||
|
||||
// for key in self.rooms.keys() {
|
||||
// rooms.push(key.to_owned())
|
||||
// }
|
||||
|
||||
// MessageResult(rooms)
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// Join room, send disconnect message to old room
|
||||
// /// send join message to new room
|
||||
// impl Handler<Join> for ChatServer {
|
||||
// type Result = ();
|
||||
|
||||
// fn handle(&mut self, msg: Join, _: &mut Context<Self>) {
|
||||
// let Join { id, name } = msg;
|
||||
// let mut rooms = Vec::new();
|
||||
|
||||
// // remove session from all rooms
|
||||
// for (n, sessions) in &mut self.rooms {
|
||||
// if sessions.remove(&id) {
|
||||
// rooms.push(n.to_owned());
|
||||
// }
|
||||
// }
|
||||
// // send message to other users
|
||||
// for room in rooms {
|
||||
// self.send_room_message(&room, "Someone disconnected", 0);
|
||||
// }
|
||||
|
||||
// if self.rooms.get_mut(&name).is_none() {
|
||||
// self.rooms.insert(name.clone(), HashSet::new());
|
||||
// }
|
||||
// self.send_room_message(&name, "Someone connected", id);
|
||||
// self.rooms.get_mut(&name).unwrap().insert(id);
|
||||
// }
|
||||
|
||||
// }
|
||||
|
@ -11,6 +11,7 @@ const transformInferno = require('ts-transform-inferno').default;
|
||||
const transformClasscat = require('ts-transform-classcat').default;
|
||||
let fuse, app;
|
||||
let isProduction = false;
|
||||
var setVersion = require('./set_version.js').setVersion;
|
||||
|
||||
Sparky.task('config', _ => {
|
||||
fuse = new FuseBox({
|
||||
@ -41,6 +42,7 @@ Sparky.task('config', _ => {
|
||||
});
|
||||
app = fuse.bundle('app').instructions('>index.tsx');
|
||||
});
|
||||
Sparky.task('version', _ => setVersion());
|
||||
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
||||
Sparky.task('env', _ => (isProduction = true));
|
||||
Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/'));
|
||||
|
@ -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",
|
||||
|
9
ui/set_version.js
Normal file
9
ui/set_version.js
Normal file
@ -0,0 +1,9 @@
|
||||
const fs = require('fs');
|
||||
|
||||
exports.setVersion = function() {
|
||||
let revision = require('child_process')
|
||||
.execSync('git describe --tags --long')
|
||||
.toString().trim();
|
||||
let line = `export let version: string = "${revision}";`;
|
||||
fs.writeFileSync("./src/version.ts", line);
|
||||
}
|
93
ui/src/components/comment-form.tsx
Normal file
93
ui/src/components/comment-form.tsx
Normal file
@ -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
Normal file
148
ui/src/components/comment-node.tsx
Normal file
@ -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 className="text-info" 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
Normal file
30
ui/src/components/comment-nodes.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,36 +2,42 @@ 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;
|
||||
|
||||
interface CommunitiesState {
|
||||
communities: Array<Community>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class Communities extends Component<any, CommunitiesState> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: CommunitiesState = {
|
||||
communities: []
|
||||
communities: [],
|
||||
loading: true
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.state = this.emptyState;
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
);
|
||||
WebSocketService.Instance.listCommunities();
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let table = document.querySelector('#community_table');
|
||||
Sortable.initTable(table);
|
||||
@ -40,40 +46,45 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||
render() {
|
||||
return (
|
||||
<div class="container-fluid">
|
||||
<h4>Communities</h4>
|
||||
<div class="table-responsive">
|
||||
<table id="community_table" class="table table-sm table-hover">
|
||||
<thead class="pointer">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th class="text-right">Subscribers</th>
|
||||
<th class="text-right">Posts</th>
|
||||
<th class="text-right">Comments</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.communities.map(community =>
|
||||
{this.state.loading ?
|
||||
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div>
|
||||
<h4>Communities</h4>
|
||||
<div class="table-responsive">
|
||||
<table id="community_table" class="table table-sm table-hover">
|
||||
<thead class="pointer">
|
||||
<tr>
|
||||
<td><Link to={`/community/${community.id}`}>{community.name}</Link></td>
|
||||
<td>{community.title}</td>
|
||||
<td>{community.category_name}</td>
|
||||
<td class="text-right">{community.number_of_subscribers}</td>
|
||||
<td class="text-right">{community.number_of_posts}</td>
|
||||
<td class="text-right">{community.number_of_comments}</td>
|
||||
<td class="text-right">
|
||||
{community.subscribed
|
||||
? <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button>
|
||||
: <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
|
||||
}
|
||||
</td>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th class="text-right">Subscribers</th>
|
||||
<th class="text-right">Posts</th>
|
||||
<th class="text-right">Comments</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.communities.map(community =>
|
||||
<tr>
|
||||
<td><Link to={`/community/${community.id}`}>{community.name}</Link></td>
|
||||
<td>{community.title}</td>
|
||||
<td>{community.category_name}</td>
|
||||
<td class="text-right">{community.number_of_subscribers}</td>
|
||||
<td class="text-right">{community.number_of_posts}</td>
|
||||
<td class="text-right">{community.number_of_comments}</td>
|
||||
<td class="text-right">
|
||||
{community.subscribed ?
|
||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button> :
|
||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -105,6 +116,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||
let res: ListCommunitiesResponse = msg;
|
||||
this.state.communities = res.communities;
|
||||
this.state.communities.sort((a, b) => b.number_of_subscribers - a.number_of_subscribers);
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.FollowCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
|
@ -2,21 +2,23 @@ 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 * as autosize from 'autosize';
|
||||
|
||||
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 {
|
||||
communityForm: CommunityFormI;
|
||||
categories: Array<Category>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class CommunityForm extends Component<CommunityFormProps, CommunityFormState> {
|
||||
@ -28,10 +30,11 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||
title: null,
|
||||
category_id: null
|
||||
},
|
||||
categories: []
|
||||
categories: [],
|
||||
loading: false
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
@ -58,6 +61,10 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||
WebSocketService.Instance.listCategories();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
autosize(document.querySelectorAll('textarea'));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
@ -81,7 +88,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label">Sidebar</label>
|
||||
<div class="col-12">
|
||||
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={6} />
|
||||
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@ -96,44 +103,49 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-secondary mr-2">{this.props.community ? 'Save' : 'Create'}</button>
|
||||
{this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
|
||||
<button type="submit" class="btn btn-secondary mr-2">
|
||||
{this.state.loading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
||||
this.props.community ? 'Save' : 'Create'}</button>
|
||||
{this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
handleCreateCommunitySubmit(i: CommunityForm, event) {
|
||||
handleCreateCommunitySubmit(i: CommunityForm, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.loading = true;
|
||||
if (i.props.community) {
|
||||
WebSocketService.Instance.editCommunity(i.state.communityForm);
|
||||
} else {
|
||||
WebSocketService.Instance.createCommunity(i.state.communityForm);
|
||||
}
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -142,6 +154,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||
console.log(msg);
|
||||
if (msg.error) {
|
||||
alert(msg.error);
|
||||
this.state.loading = false;
|
||||
return;
|
||||
} else if (op == UserOperation.ListCategories){
|
||||
let res: ListCategoriesResponse = msg;
|
||||
@ -150,9 +163,11 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreateCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
this.state.loading = false;
|
||||
this.props.onCreate(res.community.id);
|
||||
} else if (op == UserOperation.EditCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
this.state.loading = false;
|
||||
this.props.onEdit(res.community);
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,17 @@
|
||||
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;
|
||||
moderators: Array<CommunityUser>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class Community extends Component<any, State> {
|
||||
@ -33,21 +32,22 @@ export class Community extends Component<any, State> {
|
||||
published: null
|
||||
},
|
||||
moderators: [],
|
||||
communityId: Number(this.props.match.params.id)
|
||||
communityId: Number(this.props.match.params.id),
|
||||
loading: true
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
);
|
||||
|
||||
WebSocketService.Instance.getCommunity(this.state.communityId);
|
||||
}
|
||||
@ -59,15 +59,18 @@ export class Community extends Component<any, State> {
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-9">
|
||||
<h4>/f/{this.state.community.name}</h4>
|
||||
<div class="col-12 col-md-9">
|
||||
<h4>{this.state.community.title}</h4>
|
||||
<PostListings communityId={this.state.communityId} />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="col-12 col-md-3">
|
||||
<Sidebar community={this.state.community} moderators={this.state.moderators} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -83,6 +86,7 @@ export class Community extends Component<any, State> {
|
||||
let res: GetCommunityResponse = msg;
|
||||
this.state.community = res.community;
|
||||
this.state.moderators = res.moderators;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.EditCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,5 +1,4 @@
|
||||
import { Component } from 'inferno';
|
||||
import { repoUrl } from '../utils';
|
||||
import { Main } from './main';
|
||||
|
||||
export class Home extends Component<any, any> {
|
||||
|
@ -8,6 +8,8 @@ import { msgOp } from '../utils';
|
||||
interface State {
|
||||
loginForm: LoginForm;
|
||||
registerForm: RegisterForm;
|
||||
loginLoading: boolean;
|
||||
registerLoading: boolean;
|
||||
}
|
||||
|
||||
let emptyState: State = {
|
||||
@ -19,24 +21,26 @@ let emptyState: State = {
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
password_verify: undefined
|
||||
}
|
||||
},
|
||||
loginLoading: false,
|
||||
registerLoading: false
|
||||
}
|
||||
|
||||
export class Login extends Component<any, State> {
|
||||
private subscription: Subscription;
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = emptyState;
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log("complete")
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -77,7 +81,8 @@ export class Login extends Component<any, State> {
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary">Login</button>
|
||||
<button type="submit" class="btn btn-secondary">{this.state.loginLoading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Login'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -115,49 +120,55 @@ export class Login extends Component<any, State> {
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary">Sign Up</button>
|
||||
<button type="submit" class="btn btn-secondary">{this.state.registerLoading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
handleLoginSubmit(i: Login, event) {
|
||||
handleLoginSubmit(i: Login, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.loginLoading = true;
|
||||
i.setState(i.state);
|
||||
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) {
|
||||
i.state.registerLoading = true;
|
||||
i.setState(i.state);
|
||||
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);
|
||||
}
|
||||
@ -166,11 +177,16 @@ export class Login extends Component<any, State> {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (msg.error) {
|
||||
alert(msg.error);
|
||||
this.state.loginLoading = false;
|
||||
this.state.registerLoading = false;
|
||||
this.setState(this.state);
|
||||
return;
|
||||
} else {
|
||||
if (op == UserOperation.Register || op == UserOperation.Login) {
|
||||
this.state.loginLoading = false;
|
||||
this.state.registerLoading = false;
|
||||
let res: LoginResponse = msg;
|
||||
UserService.Instance.login(msg);
|
||||
UserService.Instance.login(res);
|
||||
this.props.history.push('/');
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,37 @@
|
||||
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>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class Main extends Component<any, State> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private emptyState: State = {
|
||||
subscribedCommunities: []
|
||||
subscribedCommunities: [],
|
||||
loading: true
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
);
|
||||
|
||||
if (UserService.Instance.loggedIn) {
|
||||
WebSocketService.Instance.getFollowedCommunities();
|
||||
@ -46,20 +46,25 @@ export class Main extends Component<any, State> {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-9">
|
||||
<div class="col-12 col-md-9">
|
||||
<PostListings />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="col-12 col-md-3">
|
||||
<h4>A Landing message</h4>
|
||||
{UserService.Instance.loggedIn &&
|
||||
<div>
|
||||
<hr />
|
||||
<h4>Subscribed forums</h4>
|
||||
<ul class="list-unstyled">
|
||||
{this.state.subscribedCommunities.map(community =>
|
||||
<li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
{this.state.loading ?
|
||||
<h4 class="mt-3"><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div>
|
||||
<hr />
|
||||
<h4>Subscribed forums</h4>
|
||||
<ul class="list-unstyled">
|
||||
{this.state.subscribedCommunities.map(community =>
|
||||
<li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -78,6 +83,7 @@ export class Main extends Component<any, State> {
|
||||
} else if (op == UserOperation.GetFollowedCommunities) {
|
||||
let res: GetFollowedCommunitiesResponse = msg;
|
||||
this.state.subscribedCommunities = res.communities;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, linkEvent } from 'inferno';
|
||||
import { Component } from 'inferno';
|
||||
import * as moment from 'moment';
|
||||
|
||||
interface MomentTimeProps {
|
||||
@ -10,18 +10,18 @@ interface MomentTimeProps {
|
||||
|
||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.updated) {
|
||||
return (
|
||||
<span className="font-italics">modified {moment.utc(this.props.data.updated).fromNow()}</span>
|
||||
<span title={this.props.data.updated} className="font-italics">modified {moment.utc(this.props.data.updated).fromNow()}</span>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<span>{moment.utc(this.props.data.published).fromNow()}</span>
|
||||
<span title={this.props.data.published}>{moment.utc(this.props.data.published).fromNow()}</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,26 @@ import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { repoUrl } from '../utils';
|
||||
import { UserService } from '../services';
|
||||
import { version } from '../version';
|
||||
|
||||
export class Navbar extends Component<any, any> {
|
||||
interface NavbarState {
|
||||
isLoggedIn: boolean;
|
||||
expanded: boolean;
|
||||
expandUserDropdown: boolean;
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
export class Navbar extends Component<any, NavbarState> {
|
||||
|
||||
emptyState: NavbarState = {
|
||||
isLoggedIn: UserService.Instance.loggedIn,
|
||||
expanded: false,
|
||||
expandUserDropdown: false
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.state = {isLoggedIn: UserService.Instance.loggedIn, expanded: false};
|
||||
this.state = this.emptyState;
|
||||
this.handleOverviewClick = this.handleOverviewClick.bind(this);
|
||||
|
||||
// Subscribe to user changes
|
||||
UserService.Instance.sub.subscribe(user => {
|
||||
@ -27,7 +41,7 @@ export class Navbar extends Component<any, any> {
|
||||
navbar() {
|
||||
return (
|
||||
<nav class="navbar navbar-expand-sm navbar-light bg-light p-0 px-3 shadow">
|
||||
<a class="navbar-brand" href="#">
|
||||
<a title={version} class="navbar-brand" href="#">
|
||||
<svg class="icon mr-2"><use xlinkHref="#icon-mouse"></use></svg>
|
||||
Lemmy
|
||||
</a>
|
||||
@ -50,25 +64,44 @@ export class Navbar extends Component<any, any> {
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto mr-2">
|
||||
<li class="nav-item">
|
||||
{this.state.isLoggedIn ?
|
||||
<a role="button" class="nav-link pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a> :
|
||||
{this.state.isLoggedIn ?
|
||||
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
|
||||
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
|
||||
{UserService.Instance.user.username}
|
||||
</a>
|
||||
<div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
|
||||
<a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}>Overview</a>
|
||||
<a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a>
|
||||
</div>
|
||||
</li> :
|
||||
<Link class="nav-link" to="/login">Login</Link>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
handleLogoutClick(i: Navbar, event) {
|
||||
UserService.Instance.logout();
|
||||
// i.props.history.push('/');
|
||||
expandUserDropdown(i: Navbar) {
|
||||
i.state.expandUserDropdown = !i.state.expandUserDropdown;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
expandNavbar(i: Navbar, event) {
|
||||
handleLogoutClick(i: Navbar) {
|
||||
i.state.expandUserDropdown = false;
|
||||
UserService.Instance.logout();
|
||||
}
|
||||
|
||||
handleOverviewClick(i: Navbar) {
|
||||
i.state.expandUserDropdown = false;
|
||||
i.setState(i.state);
|
||||
let userPage = `/user/${UserService.Instance.user.id}`;
|
||||
i.context.router.history.push(userPage);
|
||||
}
|
||||
|
||||
expandNavbar(i: Navbar) {
|
||||
i.state.expanded = !i.state.expanded;
|
||||
i.setState(i.state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,20 +2,21 @@ 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';
|
||||
import * as autosize from 'autosize';
|
||||
|
||||
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 {
|
||||
postForm: PostFormI;
|
||||
communities: Array<Community>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
@ -27,10 +28,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
auth: null,
|
||||
community_id: null
|
||||
},
|
||||
communities: []
|
||||
communities: [],
|
||||
loading: false
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
@ -57,6 +59,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
WebSocketService.Instance.listCommunities();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
autosize(document.querySelectorAll('textarea'));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
@ -74,13 +80,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={3} />
|
||||
<textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={2} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Body</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={6} />
|
||||
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@ -95,7 +101,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary mr-2">{this.props.post ? 'Save' : 'Create'}</button>
|
||||
<button type="submit" class="btn btn-secondary mr-2">
|
||||
{this.state.loading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
||||
this.props.post ? 'Save' : 'Create'}</button>
|
||||
{this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
|
||||
</div>
|
||||
</div>
|
||||
@ -104,42 +113,45 @@ 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);
|
||||
} else {
|
||||
WebSocketService.Instance.createPost(i.state.postForm);
|
||||
}
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
parseMessage(msg: any) {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (msg.error) {
|
||||
this.state.loading = false;
|
||||
return;
|
||||
} else if (op == UserOperation.ListCommunities) {
|
||||
let res: ListCommunitiesResponse = msg;
|
||||
@ -151,9 +163,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
}
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreatePost) {
|
||||
this.state.loading = false;
|
||||
let res: PostResponse = msg;
|
||||
this.props.onCreate(res.post.id);
|
||||
} else if (op == UserOperation.EditPost) {
|
||||
this.state.loading = false;
|
||||
let res: PostResponse = msg;
|
||||
this.props.onEdit(res.post);
|
||||
}
|
||||
|
@ -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>
|
||||
@ -80,7 +79,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||
<ul class="list-inline mb-0 text-muted small">
|
||||
<li className="list-inline-item">
|
||||
<span>by </span>
|
||||
<Link to={`/user/${post.creator_id}`}>{post.creator_name}</Link>
|
||||
<Link className="text-info" to={`/user/${post.creator_id}`}>{post.creator_name}</Link>
|
||||
{this.props.showCommunity &&
|
||||
<span>
|
||||
<span> to </span>
|
||||
@ -100,7 +99,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link>
|
||||
<Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link>
|
||||
</li>
|
||||
</ul>
|
||||
{this.myPost &&
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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,8 +16,9 @@ interface PostListingsState {
|
||||
community: CommunityI;
|
||||
moderators: Array<CommunityUser>;
|
||||
posts: Array<Post>;
|
||||
sortType: ListingSortType;
|
||||
sortType: SortType;
|
||||
type_: ListingType;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class PostListings extends Component<PostListingsProps, PostListingsState> {
|
||||
@ -41,15 +40,16 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
|
||||
},
|
||||
moderators: [],
|
||||
posts: [],
|
||||
sortType: ListingSortType.Hot,
|
||||
sortType: SortType.Hot,
|
||||
type_: this.props.communityId
|
||||
? ListingType.Community
|
||||
: UserService.Instance.loggedIn
|
||||
? ListingType.Subscribed
|
||||
: ListingType.All
|
||||
: ListingType.All,
|
||||
loading: true
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
||||
@ -67,7 +67,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);
|
||||
}
|
||||
@ -79,11 +79,16 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.selects()}</div>
|
||||
{this.state.posts.length > 0
|
||||
? this.state.posts.map(post =>
|
||||
<PostListing post={post} showCommunity={!this.props.communityId}/>)
|
||||
: <div>No Listings. Subscribe to some <Link to="/communities">forums</Link>.</div>
|
||||
{this.state.loading ?
|
||||
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div>
|
||||
<div>{this.selects()}</div>
|
||||
{this.state.posts.length > 0
|
||||
? this.state.posts.map(post =>
|
||||
<PostListing post={post} showCommunity={!this.props.communityId}/>)
|
||||
: <div>No Listings. Subscribe to some <Link to="/communities">forums</Link>.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
@ -94,22 +99,22 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
|
||||
<div className="mb-2">
|
||||
<select value={this.state.sortType} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
|
||||
<option disabled>Sort Type</option>
|
||||
<option value={ListingSortType.Hot}>Hot</option>
|
||||
<option value={ListingSortType.New}>New</option>
|
||||
<option value={SortType.Hot}>Hot</option>
|
||||
<option value={SortType.New}>New</option>
|
||||
<option disabled>──────────</option>
|
||||
<option value={ListingSortType.TopDay}>Top Day</option>
|
||||
<option value={ListingSortType.TopWeek}>Week</option>
|
||||
<option value={ListingSortType.TopMonth}>Month</option>
|
||||
<option value={ListingSortType.TopYear}>Year</option>
|
||||
<option value={ListingSortType.TopAll}>All</option>
|
||||
<option value={SortType.TopDay}>Top Day</option>
|
||||
<option value={SortType.TopWeek}>Week</option>
|
||||
<option value={SortType.TopMonth}>Month</option>
|
||||
<option value={SortType.TopYear}>Year</option>
|
||||
<option value={SortType.TopAll}>All</option>
|
||||
</select>
|
||||
{!this.props.communityId &&
|
||||
UserService.Instance.loggedIn &&
|
||||
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
|
||||
<option disabled>Type</option>
|
||||
<option value={ListingType.All}>All</option>
|
||||
<option value={ListingType.Subscribed}>Subscribed</option>
|
||||
</select>
|
||||
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
|
||||
<option disabled>Type</option>
|
||||
<option value={ListingType.All}>All</option>
|
||||
<option value={ListingType.Subscribed}>Subscribed</option>
|
||||
</select>
|
||||
|
||||
}
|
||||
</div>
|
||||
@ -117,26 +122,26 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
|
||||
|
||||
}
|
||||
|
||||
handleSortChange(i: PostListings, event) {
|
||||
handleSortChange(i: PostListings, event: any) {
|
||||
i.state.sortType = Number(event.target.value);
|
||||
i.setState(i.state);
|
||||
|
||||
let getPostsForm: GetPostsForm = {
|
||||
community_id: i.state.community.id,
|
||||
limit: 10,
|
||||
sort: ListingSortType[i.state.sortType],
|
||||
sort: SortType[i.state.sortType],
|
||||
type_: ListingType[ListingType.Community]
|
||||
}
|
||||
WebSocketService.Instance.getPosts(getPostsForm);
|
||||
}
|
||||
|
||||
handleTypeChange(i: PostListings, event) {
|
||||
handleTypeChange(i: PostListings, event: any) {
|
||||
i.state.type_ = Number(event.target.value);
|
||||
i.setState(i.state);
|
||||
|
||||
let getPostsForm: GetPostsForm = {
|
||||
limit: 10,
|
||||
sort: ListingSortType[i.state.sortType],
|
||||
sort: SortType[i.state.sortType],
|
||||
type_: ListingType[i.state.type_]
|
||||
}
|
||||
WebSocketService.Instance.getPosts(getPostsForm);
|
||||
@ -151,6 +156,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
|
||||
} else if (op == UserOperation.GetPosts) {
|
||||
let res: GetPostsResponse = msg;
|
||||
this.state.posts = res.posts;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreatePostLike) {
|
||||
let res: CreatePostLikeResponse = msg;
|
||||
|
@ -1,19 +1,15 @@
|
||||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse } from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { msgOp, hotRank,mdToHtml } from '../utils';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { msgOp, hotRank } from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import * as autosize from 'autosize';
|
||||
|
||||
interface CommentNodeI {
|
||||
comment: Comment;
|
||||
children?: Array<CommentNodeI>;
|
||||
};
|
||||
|
||||
interface PostState {
|
||||
post: PostI;
|
||||
@ -21,7 +17,9 @@ interface PostState {
|
||||
commentSort: CommentSortType;
|
||||
community: Community;
|
||||
moderators: Array<CommunityUser>;
|
||||
scrolled: boolean;
|
||||
scrolled?: boolean;
|
||||
scrolled_comment_id?: number;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class Post extends Component<any, PostState> {
|
||||
@ -33,15 +31,19 @@ export class Post extends Component<any, PostState> {
|
||||
commentSort: CommentSortType.Hot,
|
||||
community: null,
|
||||
moderators: [],
|
||||
scrolled: false
|
||||
scrolled: false,
|
||||
loading: true
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
let postId = Number(this.props.match.params.id);
|
||||
if (this.props.match.params.comment_id) {
|
||||
this.state.scrolled_comment_id = this.props.match.params.comment_id;
|
||||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
@ -62,11 +64,11 @@ export class Post extends Component<any, PostState> {
|
||||
autosize(document.querySelectorAll('textarea'));
|
||||
}
|
||||
|
||||
componentDidUpdate(lastProps: any, lastState: PostState, snapshot: any) {
|
||||
if (!this.state.scrolled && lastState.comments.length > 0 && window.location.href.includes('#comment-')) {
|
||||
let id = window.location.hash.split("#")[2];
|
||||
var elmnt = document.getElementById(`${id}`);
|
||||
componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
|
||||
if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) {
|
||||
var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`);
|
||||
elmnt.scrollIntoView();
|
||||
elmnt.classList.add("mark");
|
||||
this.state.scrolled = true;
|
||||
}
|
||||
}
|
||||
@ -74,8 +76,9 @@ export class Post extends Component<any, PostState> {
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.post &&
|
||||
<div class="row">
|
||||
{this.state.loading ?
|
||||
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-8 col-lg-7 mb-3">
|
||||
<PostListing post={this.state.post} showBody showCommunity editable />
|
||||
<div className="mb-2" />
|
||||
@ -136,7 +139,7 @@ export class Post extends Component<any, PostState> {
|
||||
);
|
||||
}
|
||||
|
||||
handleCommentSortChange(i: Post, event) {
|
||||
handleCommentSortChange(i: Post, event: any) {
|
||||
i.state.commentSort = Number(event.target.value);
|
||||
i.setState(i.state);
|
||||
}
|
||||
@ -202,6 +205,7 @@ export class Post extends Component<any, PostState> {
|
||||
this.state.comments = res.comments;
|
||||
this.state.community = res.community;
|
||||
this.state.moderators = res.moderators;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreateComment) {
|
||||
let res: CommentResponse = msg;
|
||||
@ -250,252 +254,5 @@ export class Post extends Component<any, PostState> {
|
||||
}
|
||||
}
|
||||
|
||||
interface CommentNodesState {
|
||||
}
|
||||
|
||||
interface CommentNodesProps {
|
||||
nodes: Array<CommentNodeI>;
|
||||
noIndent?: boolean;
|
||||
}
|
||||
|
||||
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="comments">
|
||||
{this.props.nodes.map(node =>
|
||||
<CommentNode node={node} noIndent={this.props.noIndent} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface CommentNodeState {
|
||||
showReply: boolean;
|
||||
showEdit: boolean;
|
||||
}
|
||||
|
||||
interface CommentNodeProps {
|
||||
node: CommentNodeI;
|
||||
noIndent?: boolean;
|
||||
}
|
||||
|
||||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
|
||||
private emptyState: CommentNodeState = {
|
||||
showReply: false,
|
||||
showEdit: false
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
||||
this.handleCommentLike = this.handleCommentLike.bind(this);
|
||||
this.handleCommentDisLike = this.handleCommentDisLike.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
let node = this.props.node;
|
||||
return (
|
||||
<div id={`comment-${node.comment.id}`} className={`comment ${node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''}`}>
|
||||
<div className="float-left small text-center">
|
||||
<div className={`pointer upvote ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>▲</div>
|
||||
<div>{node.comment.score}</div>
|
||||
<div className={`pointer downvote ${node.comment.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>▼</div>
|
||||
</div>
|
||||
<div className="details ml-4">
|
||||
<ul class="list-inline mb-0 text-muted small">
|
||||
<li className="list-inline-item">
|
||||
<Link to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span>(
|
||||
<span className="text-info">+{node.comment.upvotes}</span>
|
||||
<span> | </span>
|
||||
<span className="text-danger">-{node.comment.downvotes}</span>
|
||||
<span>) </span>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span><MomentTime data={node.comment} /></span>
|
||||
</li>
|
||||
</ul>
|
||||
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />}
|
||||
{!this.state.showEdit &&
|
||||
<div>
|
||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.content)} />
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li className="list-inline-item">
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
|
||||
</li>
|
||||
{this.myComment &&
|
||||
<li className="list-inline-item">
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
|
||||
</li>
|
||||
}
|
||||
{this.myComment &&
|
||||
<li className="list-inline-item">
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
|
||||
</li>
|
||||
}
|
||||
<li className="list-inline-item">
|
||||
<Link className="text-muted" to={`/post/${node.comment.post_id}#comment-${node.comment.id}`}>link</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
|
||||
{this.props.node.children && <CommentNodes nodes={this.props.node.children} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get myComment(): boolean {
|
||||
return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id;
|
||||
}
|
||||
|
||||
handleReplyClick(i: CommentNode, event) {
|
||||
i.state.showReply = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleEditClick(i: CommentNode, event) {
|
||||
i.state.showEdit = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleDeleteClick(i: CommentNode, event) {
|
||||
let deleteForm: CommentFormI = {
|
||||
content: "*deleted*",
|
||||
edit_id: i.props.node.comment.id,
|
||||
post_id: i.props.node.comment.post_id,
|
||||
parent_id: i.props.node.comment.parent_id,
|
||||
auth: null
|
||||
};
|
||||
WebSocketService.Instance.editComment(deleteForm);
|
||||
}
|
||||
|
||||
handleReplyCancel(): any {
|
||||
this.state.showReply = false;
|
||||
this.state.showEdit = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
|
||||
handleCommentLike(i: CommentNodeI, event) {
|
||||
|
||||
let form: CommentLikeForm = {
|
||||
comment_id: i.comment.id,
|
||||
post_id: i.comment.post_id,
|
||||
score: (i.comment.my_vote == 1) ? 0 : 1
|
||||
};
|
||||
WebSocketService.Instance.likeComment(form);
|
||||
}
|
||||
|
||||
handleCommentDisLike(i: CommentNodeI, event) {
|
||||
let form: CommentLikeForm = {
|
||||
comment_id: i.comment.id,
|
||||
post_id: i.comment.post_id,
|
||||
score: (i.comment.my_vote == -1) ? 0 : -1
|
||||
};
|
||||
WebSocketService.Instance.likeComment(form);
|
||||
}
|
||||
}
|
||||
|
||||
interface CommentFormProps {
|
||||
postId?: number;
|
||||
node?: CommentNodeI;
|
||||
onReplyCancel?();
|
||||
edit?: boolean;
|
||||
}
|
||||
|
||||
interface CommentFormState {
|
||||
commentForm: CommentFormI;
|
||||
buttonTitle: string;
|
||||
}
|
||||
|
||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
|
||||
private emptyState: CommentFormState = {
|
||||
commentForm: {
|
||||
auth: null,
|
||||
content: null,
|
||||
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId
|
||||
},
|
||||
buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply"
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
if (this.props.node) {
|
||||
if (this.props.edit) {
|
||||
this.state.commentForm.edit_id = this.props.node.comment.id;
|
||||
this.state.commentForm.parent_id = this.props.node.comment.parent_id;
|
||||
this.state.commentForm.content = this.props.node.comment.content;
|
||||
} else {
|
||||
// A reply gets a new parent id
|
||||
this.state.commentForm.parent_id = this.props.node.comment.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
autosize(document.querySelectorAll('textarea'));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button>
|
||||
{this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleCommentSubmit(i: CommentForm, event) {
|
||||
if (i.props.edit) {
|
||||
WebSocketService.Instance.editComment(i.state.commentForm);
|
||||
} else {
|
||||
WebSocketService.Instance.createComment(i.state.commentForm);
|
||||
}
|
||||
|
||||
i.state.commentForm.content = undefined;
|
||||
i.setState(i.state);
|
||||
event.target.reset();
|
||||
if (i.props.node) {
|
||||
i.props.onReplyCancel();
|
||||
}
|
||||
}
|
||||
|
||||
handleCommentContentChange(i: CommentForm, event) {
|
||||
i.state.commentForm.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleReplyCancel(i: CommentForm, event) {
|
||||
i.props.onReplyCancel();
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||
showEdit: false
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.state = this.emptyState;
|
||||
this.handleEditCommunity = this.handleEditCommunity.bind(this);
|
||||
@ -42,7 +42,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||
let community = this.props.community;
|
||||
return (
|
||||
<div>
|
||||
<h4>{community.title}</h4>
|
||||
<h4 className="mb-0">{community.title}</h4>
|
||||
<Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
|
||||
{this.amMod &&
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li className="list-inline-item">
|
||||
@ -82,12 +83,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||
);
|
||||
}
|
||||
|
||||
handleEditClick(i: Sidebar, event) {
|
||||
handleEditClick(i: Sidebar) {
|
||||
i.state.showEdit = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleEditCommunity(community: Community) {
|
||||
handleEditCommunity() {
|
||||
this.state.showEdit = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
@ -98,8 +99,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||
}
|
||||
|
||||
// TODO no deleting communities yet
|
||||
handleDeleteClick(i: Sidebar, event) {
|
||||
}
|
||||
// handleDeleteClick(i: Sidebar, event) {
|
||||
// }
|
||||
|
||||
handleUnsubscribe(communityId: number) {
|
||||
let form: FollowCommunityForm = {
|
||||
|
80
ui/src/components/symbols.tsx
Normal file
80
ui/src/components/symbols.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { Component } from 'inferno';
|
||||
|
||||
export class Symbols extends Component<any, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<symbol id="icon-mouse" version="1.1" x="0px" y="0px"
|
||||
viewBox="0 0 512 512">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M499.059,323.505l-7.52-32.532l-70.047,16.19c1.513-11.983,2.297-24.042,2.297-36.037c0-18.334-1.801-35.785-5.316-52.19
|
||||
c29.365-12.101,55.143-28.885,69.372-45.529c17.524-20.498,25.985-46.568,23.822-73.406
|
||||
c-2.163-26.862-14.706-51.268-35.316-68.724C433.879-4.694,369.917,0.439,333.774,42.718
|
||||
c-9.546,11.168-18.318,27.381-25.379,46.649c-16.512-5.419-34.132-8.243-52.395-8.243s-35.885,2.824-52.395,8.243
|
||||
c-7.06-19.267-15.832-35.481-25.379-46.649C142.082,0.44,78.123-4.695,35.648,31.277C15.038,48.733,2.494,73.141,0.332,100.001
|
||||
c-2.161,26.838,6.297,52.907,23.822,73.406c14.229,16.644,40.006,33.427,69.372,45.529c-3.515,16.405-5.316,33.856-5.316,52.189
|
||||
c0,11.995,0.785,24.053,2.297,36.037l-70.047-16.19l-7.52,32.532l84.337,19.492c4.349,17.217,10.201,33.953,17.421,49.752
|
||||
L12.941,416.27l7.52,32.532l110.634-25.57c1.38,2.197,2.779,4.373,4.218,6.509c32.548,48.323,75.409,74.934,120.687,74.934
|
||||
c45.278,0,88.138-26.612,120.687-74.934c1.439-2.136,2.839-4.313,4.218-6.509l110.634,25.57l7.52-32.532l-101.758-23.519
|
||||
c7.221-15.799,13.072-32.535,17.421-49.752L499.059,323.505z M183.578,220.372c0-11.41,9.189-20.65,20.482-20.65
|
||||
c11.306,0,20.494,9.24,20.494,20.65c0,11.408-9.188,20.656-20.494,20.656C192.768,241.028,183.578,231.78,183.578,220.372z
|
||||
M256,413.29c-29.895,0-54.216-19.471-54.216-43.403c0-23.932,24.322-43.403,54.216-43.403s54.216,19.471,54.216,43.403
|
||||
C310.216,393.819,285.895,413.29,256,413.29z M307.785,241.183c-11.402,0-20.65-9.317-20.65-20.81
|
||||
c0-11.494,9.248-20.81,20.65-20.81c11.387,0,20.635,9.317,20.635,20.81C328.422,231.866,319.173,241.183,307.785,241.183z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 32 32">
|
||||
<title>search</title>
|
||||
<path d="M31.008 27.231l-7.58-6.447c-0.784-0.705-1.622-1.029-2.299-0.998 1.789-2.096 2.87-4.815 2.87-7.787 0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12c2.972 0 5.691-1.081 7.787-2.87-0.031 0.677 0.293 1.515 0.998 2.299l6.447 7.58c1.104 1.226 2.907 1.33 4.007 0.23s0.997-2.903-0.23-4.007zM12 20c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-github" viewBox="0 0 32 32">
|
||||
<title>github</title>
|
||||
<path d="M16 0.395c-8.836 0-16 7.163-16 16 0 7.069 4.585 13.067 10.942 15.182 0.8 0.148 1.094-0.347 1.094-0.77 0-0.381-0.015-1.642-0.022-2.979-4.452 0.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993 0.11-0.973 0.11-0.973 1.606 0.113 2.452 1.649 2.452 1.649 1.427 2.446 3.743 1.739 4.656 1.33 0.143-1.034 0.558-1.74 1.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907 0-1.747 0.625-3.174 1.649-4.295-0.166-0.403-0.714-2.030 0.155-4.234 0 0 1.344-0.43 4.401 1.64 1.276-0.355 2.645-0.532 4.005-0.539 1.359 0.006 2.729 0.184 4.008 0.539 3.054-2.070 4.395-1.64 4.395-1.64 0.871 2.204 0.323 3.831 0.157 4.234 1.026 1.12 1.647 2.548 1.647 4.295 0 6.145-3.743 7.498-7.306 7.895 0.574 0.497 1.085 1.47 1.085 2.963 0 2.141-0.019 3.864-0.019 4.391 0 0.426 0.288 0.925 1.099 0.768 6.354-2.118 10.933-8.113 10.933-15.18 0-8.837-7.164-16-16-16z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-spinner" viewBox="0 0 32 32">
|
||||
<title>spinner</title>
|
||||
<path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
264
ui/src/components/user.tsx
Normal file
264
ui/src/components/user.tsx
Normal file
@ -0,0 +1,264 @@
|
||||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { msgOp } from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { MomentTime } from './moment-time';
|
||||
|
||||
enum View {
|
||||
Overview, Comments, Posts, Saved
|
||||
}
|
||||
|
||||
interface UserState {
|
||||
user: UserView;
|
||||
follows: Array<CommunityUser>;
|
||||
moderates: Array<CommunityUser>;
|
||||
comments: Array<Comment>;
|
||||
posts: Array<Post>;
|
||||
saved?: Array<Post>;
|
||||
view: View;
|
||||
sort: SortType;
|
||||
}
|
||||
|
||||
export class User extends Component<any, UserState> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private emptyState: UserState = {
|
||||
user: {
|
||||
id: null,
|
||||
name: null,
|
||||
fedi_name: null,
|
||||
published: null,
|
||||
number_of_posts: null,
|
||||
post_score: null,
|
||||
number_of_comments: null,
|
||||
comment_score: null,
|
||||
},
|
||||
follows: [],
|
||||
moderates: [],
|
||||
comments: [],
|
||||
posts: [],
|
||||
view: View.Overview,
|
||||
sort: SortType.New
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
let userId = Number(this.props.match.params.id);
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
let form: GetUserDetailsForm = {
|
||||
user_id: userId,
|
||||
sort: SortType[this.state.sort],
|
||||
limit: 999
|
||||
};
|
||||
WebSocketService.Instance.getUserDetails(form);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-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-md-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,4 @@
|
||||
export const endpoint = `${window.location.hostname}:8536`;
|
||||
export let wsUri = (window.location.protocol=='https:') ? 'wss://' : 'ws://' + endpoint + '/service/ws';
|
||||
let host = `${window.location.hostname}`;
|
||||
let port = `${window.location.port == "4444" ? '8536' : window.location.port}`;
|
||||
let endpoint = `${host}:${port}`;
|
||||
export let wsUri = `${(window.location.protocol=='https:') ? 'wss://' : 'ws://'}${endpoint}/service/ws`;
|
||||
|
@ -9,6 +9,8 @@ import { CreateCommunity } from './components/create-community';
|
||||
import { Post } from './components/post';
|
||||
import { Community } from './components/community';
|
||||
import { Communities } from './components/communities';
|
||||
import { User } from './components/user';
|
||||
import { Symbols } from './components/symbols';
|
||||
|
||||
import './main.css';
|
||||
|
||||
@ -18,7 +20,7 @@ const container = document.getElementById('app');
|
||||
|
||||
class Index extends Component<any, any> {
|
||||
|
||||
constructor(props, context) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
WebSocketService.Instance;
|
||||
UserService.Instance;
|
||||
@ -35,71 +37,18 @@ class Index extends Component<any, any> {
|
||||
<Route path={`/create_post`} component={CreatePost} />
|
||||
<Route path={`/create_community`} component={CreateCommunity} />
|
||||
<Route path={`/communities`} component={Communities} />
|
||||
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
||||
<Route path={`/post/:id`} component={Post} />
|
||||
<Route path={`/community/:id`} component={Community} />
|
||||
<Route path={`/user/:id/:heading`} component={User} />
|
||||
<Route path={`/user/:id`} component={User} />
|
||||
</Switch>
|
||||
{this.symbols()}
|
||||
<Symbols />
|
||||
</div>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
|
||||
symbols() {
|
||||
return(
|
||||
<symbol id="icon-mouse" version="1.1" x="0px" y="0px"
|
||||
viewBox="0 0 512 512">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M499.059,323.505l-7.52-32.532l-70.047,16.19c1.513-11.983,2.297-24.042,2.297-36.037c0-18.334-1.801-35.785-5.316-52.19
|
||||
c29.365-12.101,55.143-28.885,69.372-45.529c17.524-20.498,25.985-46.568,23.822-73.406
|
||||
c-2.163-26.862-14.706-51.268-35.316-68.724C433.879-4.694,369.917,0.439,333.774,42.718
|
||||
c-9.546,11.168-18.318,27.381-25.379,46.649c-16.512-5.419-34.132-8.243-52.395-8.243s-35.885,2.824-52.395,8.243
|
||||
c-7.06-19.267-15.832-35.481-25.379-46.649C142.082,0.44,78.123-4.695,35.648,31.277C15.038,48.733,2.494,73.141,0.332,100.001
|
||||
c-2.161,26.838,6.297,52.907,23.822,73.406c14.229,16.644,40.006,33.427,69.372,45.529c-3.515,16.405-5.316,33.856-5.316,52.189
|
||||
c0,11.995,0.785,24.053,2.297,36.037l-70.047-16.19l-7.52,32.532l84.337,19.492c4.349,17.217,10.201,33.953,17.421,49.752
|
||||
L12.941,416.27l7.52,32.532l110.634-25.57c1.38,2.197,2.779,4.373,4.218,6.509c32.548,48.323,75.409,74.934,120.687,74.934
|
||||
c45.278,0,88.138-26.612,120.687-74.934c1.439-2.136,2.839-4.313,4.218-6.509l110.634,25.57l7.52-32.532l-101.758-23.519
|
||||
c7.221-15.799,13.072-32.535,17.421-49.752L499.059,323.505z M183.578,220.372c0-11.41,9.189-20.65,20.482-20.65
|
||||
c11.306,0,20.494,9.24,20.494,20.65c0,11.408-9.188,20.656-20.494,20.656C192.768,241.028,183.578,231.78,183.578,220.372z
|
||||
M256,413.29c-29.895,0-54.216-19.471-54.216-43.403c0-23.932,24.322-43.403,54.216-43.403s54.216,19.471,54.216,43.403
|
||||
C310.216,393.819,285.895,413.29,256,413.29z M307.785,241.183c-11.402,0-20.65-9.317-20.65-20.81
|
||||
c0-11.494,9.248-20.81,20.65-20.81c11.387,0,20.635,9.317,20.635,20.81C328.422,231.866,319.173,241.183,307.785,241.183z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</symbol>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render(<Index />, container);
|
||||
|
@ -1,5 +1,5 @@
|
||||
export enum UserOperation {
|
||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities
|
||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails
|
||||
}
|
||||
|
||||
export interface User {
|
||||
@ -8,6 +8,17 @@ export interface User {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface UserView {
|
||||
id: number;
|
||||
name: string;
|
||||
fedi_name: string;
|
||||
published: string;
|
||||
number_of_posts: number;
|
||||
post_score: number;
|
||||
number_of_comments: number;
|
||||
comment_score: number;
|
||||
}
|
||||
|
||||
export interface CommunityUser {
|
||||
id: number;
|
||||
user_id: number;
|
||||
@ -144,6 +155,11 @@ export interface CommentLikeForm {
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface CommentNode {
|
||||
comment: Comment;
|
||||
children?: Array<CommentNode>;
|
||||
}
|
||||
|
||||
export interface GetPostsForm {
|
||||
type_: string;
|
||||
sort: string;
|
||||
@ -184,6 +200,27 @@ export interface GetFollowedCommunitiesResponse {
|
||||
communities: Array<CommunityUser>;
|
||||
}
|
||||
|
||||
export interface GetUserDetailsForm {
|
||||
user_id: number;
|
||||
sort: string; // TODO figure this one out
|
||||
limit: number;
|
||||
community_id?: number;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface UserDetailsResponse {
|
||||
op: string;
|
||||
user: UserView;
|
||||
follows: Array<CommunityUser>;
|
||||
moderates: Array<CommunityUser>;
|
||||
comments: Array<Comment>;
|
||||
posts: Array<Post>;
|
||||
saved?: Array<Post>;
|
||||
}
|
||||
|
||||
|
||||
export interface LoginForm {
|
||||
username_or_email: string;
|
||||
password: string;
|
||||
@ -210,7 +247,7 @@ export enum ListingType {
|
||||
All, Subscribed, Community
|
||||
}
|
||||
|
||||
export enum ListingSortType {
|
||||
export enum SortType {
|
||||
Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,11 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.no-click {
|
||||
pointer-events:none;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.upvote:hover {
|
||||
color: var(--info);
|
||||
}
|
||||
@ -24,6 +29,10 @@ body {
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
|
||||
.mark {
|
||||
background-color: #322a00;
|
||||
}
|
||||
|
||||
.md-div p {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
@ -45,5 +54,15 @@ body {
|
||||
stroke: currentColor;
|
||||
fill: currentColor;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 6px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
||||
.spin {
|
||||
animation: spins 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spins {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(359deg); }
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { wsUri } from '../env';
|
||||
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm } from '../interfaces';
|
||||
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm } from '../interfaces';
|
||||
import { webSocket } from 'rxjs/webSocket';
|
||||
import { Subject } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
@ -106,6 +106,11 @@ export class WebSocketService {
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm));
|
||||
}
|
||||
|
||||
public getUserDetails(form: GetUserDetailsForm) {
|
||||
this.setAuth(form, false);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
|
||||
}
|
||||
|
||||
private wsSendWrapper(op: UserOperation, data: any) {
|
||||
let send = { op: UserOperation[op], data: data };
|
||||
console.log(send);
|
||||
@ -122,7 +127,7 @@ export class WebSocketService {
|
||||
|
||||
}
|
||||
|
||||
window.onbeforeunload = (e => {
|
||||
window.onbeforeunload = (() => {
|
||||
WebSocketService.Instance.subject.unsubscribe();
|
||||
WebSocketService.Instance.subject = null;
|
||||
});
|
||||
|
@ -2,7 +2,6 @@ import { UserOperation, Comment } from './interfaces';
|
||||
import * as markdown_it from 'markdown-it';
|
||||
|
||||
export let repoUrl = 'https://github.com/dessalines/lemmy';
|
||||
export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/';
|
||||
|
||||
export function msgOp(msg: any): UserOperation {
|
||||
let opStr: string = msg.op;
|
||||
|
1
ui/src/version.ts
Normal file
1
ui/src/version.ts
Normal file
@ -0,0 +1 @@
|
||||
export let version: string = "v0.0.2-0-gdae6651";
|
36
ui/yarn.lock
36
ui/yarn.lock
@ -9,11 +9,47 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@types/autosize@^3.0.6":
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-3.0.6.tgz#9022e6a783ec5a4d5e570013701dbc0bfe7667fa"
|
||||
integrity sha512-gpfmXswGISLSWNOOdF2PDK96SfkaZdNtNixWJbYH10xn3Hqdt4VyS1GmoutuwOshWyCLuJw2jGhF0zkK7PUhrg==
|
||||
dependencies:
|
||||
"@types/jquery" "*"
|
||||
|
||||
"@types/jquery@*":
|
||||
version "3.3.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd"
|
||||
integrity sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==
|
||||
dependencies:
|
||||
"@types/sizzle" "*"
|
||||
|
||||
"@types/js-cookie@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.1.tgz#aa6f6d5e5aaf7d97959e9fa938ac2501cf1a76f4"
|
||||
integrity sha512-VIVurImEhQ95jxtjs8baVU5qCzVfwYfuMrpXwdRykJ5MCI5iY7/jB4cDSgwBVeYqeXrhT7GfJUwoDOmN0OMVCA==
|
||||
|
||||
"@types/jwt-decode@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jwt-decode/-/jwt-decode-2.2.1.tgz#afdf5c527fcfccbd4009b5fd02d1e18241f2d2f2"
|
||||
integrity sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==
|
||||
|
||||
"@types/linkify-it@*":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
|
||||
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
|
||||
|
||||
"@types/markdown-it@^0.0.7":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
|
||||
integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
|
||||
dependencies:
|
||||
"@types/linkify-it" "*"
|
||||
|
||||
"@types/sizzle@*":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
|
||||
integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
Loading…
Reference in New Issue
Block a user