forked from nutomic/lemmy
Merge branch 'master' into master
This commit is contained in:
commit
124316dd4b
48 changed files with 1259 additions and 349 deletions
|
@ -51,6 +51,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
|
|
||||||
- Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
- Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
|
||||||
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
||||||
|
|
137
server/migrations/2019-04-29-175834_add_delete_columns/down.sql
Normal file
137
server/migrations/2019-04-29-175834_add_delete_columns/down.sql
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
drop view reply_view;
|
||||||
|
drop view comment_view;
|
||||||
|
drop view community_view;
|
||||||
|
drop view post_view;
|
||||||
|
alter table community drop column deleted;
|
||||||
|
alter table post drop column deleted;
|
||||||
|
alter table comment drop column deleted;
|
||||||
|
|
||||||
|
create view community_view as
|
||||||
|
with all_community as
|
||||||
|
(
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||||
|
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||||
|
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||||
|
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||||
|
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments
|
||||||
|
from community c
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||||
|
from user_ u
|
||||||
|
cross join all_community ac
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as subscribed
|
||||||
|
from all_community ac
|
||||||
|
;
|
||||||
|
|
||||||
|
create or replace view post_view as
|
||||||
|
with all_post as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
p.*,
|
||||||
|
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||||
|
(select name from community where p.community_id = community.id) as community_name,
|
||||||
|
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||||
|
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||||
|
coalesce(sum(pl.score), 0) as score,
|
||||||
|
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||||
|
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
|
||||||
|
from post p
|
||||||
|
left join post_like pl on p.id = pl.post_id
|
||||||
|
group by p.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(pl.score, 0) as my_vote,
|
||||||
|
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||||
|
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||||
|
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_post ap
|
||||||
|
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as subscribed,
|
||||||
|
null as read,
|
||||||
|
null as saved
|
||||||
|
from all_post ap
|
||||||
|
;
|
||||||
|
|
||||||
|
create view comment_view as
|
||||||
|
with all_comment as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
c.*,
|
||||||
|
(select community_id from post p where p.id = c.post_id),
|
||||||
|
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
||||||
|
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
||||||
|
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
||||||
|
coalesce(sum(cl.score), 0) as score,
|
||||||
|
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when cl.score = -1 then 1 else null end) as downvotes
|
||||||
|
from comment c
|
||||||
|
left join comment_like cl on c.id = cl.comment_id
|
||||||
|
group by c.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(cl.score, 0) as my_vote,
|
||||||
|
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_comment ac
|
||||||
|
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as saved
|
||||||
|
from all_comment ac
|
||||||
|
;
|
||||||
|
|
||||||
|
create view reply_view as
|
||||||
|
with closereply as (
|
||||||
|
select
|
||||||
|
c2.id,
|
||||||
|
c2.creator_id as sender_id,
|
||||||
|
c.creator_id as recipient_id
|
||||||
|
from comment c
|
||||||
|
inner join comment c2 on c.id = c2.parent_id
|
||||||
|
where c2.creator_id != c.creator_id
|
||||||
|
-- Do union where post is null
|
||||||
|
union
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
c.creator_id as sender_id,
|
||||||
|
p.creator_id as recipient_id
|
||||||
|
from comment c, post p
|
||||||
|
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
||||||
|
)
|
||||||
|
select cv.*,
|
||||||
|
closereply.recipient_id
|
||||||
|
from comment_view cv, closereply
|
||||||
|
where closereply.id = cv.id
|
||||||
|
;
|
||||||
|
|
141
server/migrations/2019-04-29-175834_add_delete_columns/up.sql
Normal file
141
server/migrations/2019-04-29-175834_add_delete_columns/up.sql
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
alter table community add column deleted boolean default false not null;
|
||||||
|
alter table post add column deleted boolean default false not null;
|
||||||
|
alter table comment add column deleted boolean default false not null;
|
||||||
|
|
||||||
|
-- The views
|
||||||
|
drop view community_view;
|
||||||
|
|
||||||
|
create view community_view as
|
||||||
|
with all_community as
|
||||||
|
(
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||||
|
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||||
|
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||||
|
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||||
|
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments
|
||||||
|
from community c
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||||
|
from user_ u
|
||||||
|
cross join all_community ac
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as subscribed
|
||||||
|
from all_community ac
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
drop view post_view;
|
||||||
|
create view post_view as
|
||||||
|
with all_post as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
p.*,
|
||||||
|
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||||
|
(select name from community where p.community_id = community.id) as community_name,
|
||||||
|
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||||
|
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
||||||
|
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||||
|
coalesce(sum(pl.score), 0) as score,
|
||||||
|
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||||
|
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
|
||||||
|
from post p
|
||||||
|
left join post_like pl on p.id = pl.post_id
|
||||||
|
group by p.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(pl.score, 0) as my_vote,
|
||||||
|
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||||
|
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||||
|
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_post ap
|
||||||
|
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as subscribed,
|
||||||
|
null as read,
|
||||||
|
null as saved
|
||||||
|
from all_post ap
|
||||||
|
;
|
||||||
|
|
||||||
|
drop view reply_view;
|
||||||
|
drop view comment_view;
|
||||||
|
create view comment_view as
|
||||||
|
with all_comment as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
c.*,
|
||||||
|
(select community_id from post p where p.id = c.post_id),
|
||||||
|
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
||||||
|
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
||||||
|
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
||||||
|
coalesce(sum(cl.score), 0) as score,
|
||||||
|
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when cl.score = -1 then 1 else null end) as downvotes
|
||||||
|
from comment c
|
||||||
|
left join comment_like cl on c.id = cl.comment_id
|
||||||
|
group by c.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(cl.score, 0) as my_vote,
|
||||||
|
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_comment ac
|
||||||
|
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as saved
|
||||||
|
from all_comment ac
|
||||||
|
;
|
||||||
|
|
||||||
|
create view reply_view as
|
||||||
|
with closereply as (
|
||||||
|
select
|
||||||
|
c2.id,
|
||||||
|
c2.creator_id as sender_id,
|
||||||
|
c.creator_id as recipient_id
|
||||||
|
from comment c
|
||||||
|
inner join comment c2 on c.id = c2.parent_id
|
||||||
|
where c2.creator_id != c.creator_id
|
||||||
|
-- Do union where post is null
|
||||||
|
union
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
c.creator_id as sender_id,
|
||||||
|
p.creator_id as recipient_id
|
||||||
|
from comment c, post p
|
||||||
|
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
||||||
|
)
|
||||||
|
select cv.*,
|
||||||
|
closereply.recipient_id
|
||||||
|
from comment_view cv, closereply
|
||||||
|
where closereply.id = cv.id
|
||||||
|
;
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
drop view community_view;
|
||||||
|
create view community_view as
|
||||||
|
with all_community as
|
||||||
|
(
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||||
|
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||||
|
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||||
|
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||||
|
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments
|
||||||
|
from community c
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||||
|
from user_ u
|
||||||
|
cross join all_community ac
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as subscribed
|
||||||
|
from all_community ac
|
||||||
|
;
|
|
@ -0,0 +1,29 @@
|
||||||
|
drop view community_view;
|
||||||
|
create view community_view as
|
||||||
|
with all_community as
|
||||||
|
(
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||||
|
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||||
|
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||||
|
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||||
|
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
|
||||||
|
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
|
||||||
|
from community c
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||||
|
from user_ u
|
||||||
|
cross join all_community ac
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as subscribed
|
||||||
|
from all_community ac
|
||||||
|
;
|
|
@ -25,7 +25,8 @@ pub struct Comment {
|
||||||
pub removed: bool,
|
pub removed: bool,
|
||||||
pub read: bool,
|
pub read: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone)]
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
@ -37,7 +38,8 @@ pub struct CommentForm {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub read: Option<bool>,
|
pub read: Option<bool>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<CommentForm> for Comment {
|
impl Crud<CommentForm> for Comment {
|
||||||
|
@ -186,6 +188,7 @@ mod tests {
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -198,6 +201,7 @@ mod tests {
|
||||||
body: None,
|
body: None,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
locked: None,
|
locked: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -209,6 +213,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
updated: None
|
updated: None
|
||||||
|
@ -222,6 +227,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
deleted: false,
|
||||||
read: false,
|
read: false,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
|
@ -234,6 +240,7 @@ mod tests {
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
parent_id: Some(inserted_comment.id),
|
parent_id: Some(inserted_comment.id),
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ table! {
|
||||||
read -> Bool,
|
read -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
|
@ -42,6 +43,7 @@ pub struct CommentView {
|
||||||
pub read: bool,
|
pub read: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
|
@ -115,6 +117,7 @@ impl CommentView {
|
||||||
_ => query.order_by(published.desc())
|
_ => query.order_by(published.desc())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Note: deleted and removed comments are done on the front side
|
||||||
query
|
query
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
|
@ -153,6 +156,7 @@ table! {
|
||||||
read -> Bool,
|
read -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
|
@ -179,6 +183,7 @@ pub struct ReplyView {
|
||||||
pub read: bool,
|
pub read: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
|
@ -275,6 +280,7 @@ mod tests {
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -287,6 +293,7 @@ mod tests {
|
||||||
body: None,
|
body: None,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
locked: None,
|
locked: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -299,6 +306,7 @@ mod tests {
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -322,6 +330,7 @@ mod tests {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
deleted: false,
|
||||||
read: false,
|
read: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
|
@ -344,6 +353,7 @@ mod tests {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
deleted: false,
|
||||||
read: false,
|
read: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
|
|
|
@ -16,7 +16,8 @@ pub struct Community {
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
pub removed: bool,
|
pub removed: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
||||||
|
@ -28,7 +29,8 @@ pub struct CommunityForm {
|
||||||
pub category_id: i32,
|
pub category_id: i32,
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<CommunityForm> for Community {
|
impl Crud<CommunityForm> for Community {
|
||||||
|
@ -245,6 +247,7 @@ mod tests {
|
||||||
description: None,
|
description: None,
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -258,6 +261,7 @@ mod tests {
|
||||||
description: None,
|
description: None,
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
deleted: false,
|
||||||
published: inserted_community.published,
|
published: inserted_community.published,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,11 +15,13 @@ table! {
|
||||||
removed -> Bool,
|
removed -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
category_name -> Varchar,
|
category_name -> Varchar,
|
||||||
number_of_subscribers -> BigInt,
|
number_of_subscribers -> BigInt,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
number_of_comments -> BigInt,
|
number_of_comments -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
subscribed -> Nullable<Bool>,
|
subscribed -> Nullable<Bool>,
|
||||||
}
|
}
|
||||||
|
@ -85,11 +87,13 @@ pub struct CommunityView {
|
||||||
pub removed: bool,
|
pub removed: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
pub category_name: String,
|
pub category_name: String,
|
||||||
pub number_of_subscribers: i64,
|
pub number_of_subscribers: i64,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
pub number_of_comments: i64,
|
pub number_of_comments: i64,
|
||||||
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub subscribed: Option<bool>,
|
pub subscribed: Option<bool>,
|
||||||
}
|
}
|
||||||
|
@ -125,6 +129,7 @@ impl CommunityView {
|
||||||
|
|
||||||
// The view lets you pass a null user_id, if you're not logged in
|
// The view lets you pass a null user_id, if you're not logged in
|
||||||
match sort {
|
match sort {
|
||||||
|
SortType::Hot => query = query.order_by(hot_rank.desc()).filter(user_id.is_null()),
|
||||||
SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
|
SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
|
||||||
SortType::TopAll => {
|
SortType::TopAll => {
|
||||||
match from_user_id {
|
match from_user_id {
|
||||||
|
@ -139,6 +144,7 @@ impl CommunityView {
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.filter(removed.eq(false))
|
.filter(removed.eq(false))
|
||||||
|
.filter(deleted.eq(false))
|
||||||
.load::<Self>(conn)
|
.load::<Self>(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -442,6 +442,7 @@ mod tests {
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -454,6 +455,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
locked: None,
|
locked: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -465,6 +467,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
updated: None
|
updated: None
|
||||||
|
|
|
@ -17,7 +17,8 @@ pub struct Post {
|
||||||
pub removed: bool,
|
pub removed: bool,
|
||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone)]
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
@ -30,7 +31,8 @@ pub struct PostForm {
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub locked: Option<bool>,
|
pub locked: Option<bool>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<PostForm> for Post {
|
impl Crud<PostForm> for Post {
|
||||||
|
@ -199,6 +201,7 @@ mod tests {
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -211,6 +214,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
locked: None,
|
locked: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -227,6 +231,7 @@ mod tests {
|
||||||
published: inserted_post.published,
|
published: inserted_post.published,
|
||||||
removed: false,
|
removed: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
|
deleted: false,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,11 @@ table! {
|
||||||
locked -> Bool,
|
locked -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
community_removed -> Bool,
|
community_removed -> Bool,
|
||||||
|
community_deleted -> Bool,
|
||||||
number_of_comments -> BigInt,
|
number_of_comments -> BigInt,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
|
@ -53,9 +55,11 @@ pub struct PostView {
|
||||||
pub locked: bool,
|
pub locked: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
pub community_removed: bool,
|
pub community_removed: bool,
|
||||||
|
pub community_deleted: bool,
|
||||||
pub number_of_comments: i64,
|
pub number_of_comments: i64,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
|
@ -144,7 +148,9 @@ impl PostView {
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.filter(removed.eq(false))
|
.filter(removed.eq(false))
|
||||||
.filter(community_removed.eq(false));
|
.filter(deleted.eq(false))
|
||||||
|
.filter(community_removed.eq(false))
|
||||||
|
.filter(community_deleted.eq(false));
|
||||||
|
|
||||||
query.load::<Self>(conn)
|
query.load::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
@ -206,6 +212,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
category_id: 1,
|
category_id: 1,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -218,6 +225,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
locked: None,
|
locked: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -258,9 +266,11 @@ mod tests {
|
||||||
creator_name: user_name.to_owned(),
|
creator_name: user_name.to_owned(),
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
deleted: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
community_name: community_name.to_owned(),
|
community_name: community_name.to_owned(),
|
||||||
community_removed: false,
|
community_removed: false,
|
||||||
|
community_deleted: false,
|
||||||
number_of_comments: 0,
|
number_of_comments: 0,
|
||||||
score: 1,
|
score: 1,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
|
@ -281,12 +291,14 @@ mod tests {
|
||||||
url: None,
|
url: None,
|
||||||
body: None,
|
body: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
deleted: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
creator_name: user_name.to_owned(),
|
creator_name: user_name.to_owned(),
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
community_name: community_name.to_owned(),
|
community_name: community_name.to_owned(),
|
||||||
community_removed: false,
|
community_removed: false,
|
||||||
|
community_deleted: false,
|
||||||
number_of_comments: 0,
|
number_of_comments: 0,
|
||||||
score: 1,
|
score: 1,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
|
|
|
@ -29,7 +29,8 @@ fn chat_route(req: &HttpRequest<WsChatSessionState>) -> Result<HttpResponse, Err
|
||||||
req,
|
req,
|
||||||
WSSession {
|
WSSession {
|
||||||
id: 0,
|
id: 0,
|
||||||
hb: Instant::now()
|
hb: Instant::now(),
|
||||||
|
ip: req.connection_info().remote().unwrap_or("127.0.0.1:12345").split(":").next().unwrap_or("127.0.0.1").to_string()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -37,6 +38,7 @@ fn chat_route(req: &HttpRequest<WsChatSessionState>) -> Result<HttpResponse, Err
|
||||||
struct WSSession {
|
struct WSSession {
|
||||||
/// unique session id
|
/// unique session id
|
||||||
id: usize,
|
id: usize,
|
||||||
|
ip: String,
|
||||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||||
/// otherwise we drop connection.
|
/// otherwise we drop connection.
|
||||||
hb: Instant
|
hb: Instant
|
||||||
|
@ -61,6 +63,7 @@ impl Actor for WSSession {
|
||||||
.addr
|
.addr
|
||||||
.send(Connect {
|
.send(Connect {
|
||||||
addr: addr.recipient(),
|
addr: addr.recipient(),
|
||||||
|
ip: self.ip.to_owned(),
|
||||||
})
|
})
|
||||||
.into_actor(self)
|
.into_actor(self)
|
||||||
.then(|res, act, ctx| {
|
.then(|res, act, ctx| {
|
||||||
|
@ -76,7 +79,10 @@ impl Actor for WSSession {
|
||||||
|
|
||||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
||||||
// notify chat server
|
// notify chat server
|
||||||
ctx.state().addr.do_send(Disconnect { id: self.id });
|
ctx.state().addr.do_send(Disconnect {
|
||||||
|
id: self.id,
|
||||||
|
ip: self.ip.to_owned(),
|
||||||
|
});
|
||||||
Running::Stop
|
Running::Stop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +117,7 @@ impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
||||||
.addr
|
.addr
|
||||||
.send(StandardMessage {
|
.send(StandardMessage {
|
||||||
id: self.id,
|
id: self.id,
|
||||||
msg: m
|
msg: m,
|
||||||
})
|
})
|
||||||
.into_actor(self)
|
.into_actor(self)
|
||||||
.then(|res, _, ctx| {
|
.then(|res, _, ctx| {
|
||||||
|
@ -215,7 +221,7 @@ impl WSSession {
|
||||||
// notify chat server
|
// notify chat server
|
||||||
ctx.state()
|
ctx.state()
|
||||||
.addr
|
.addr
|
||||||
.do_send(Disconnect { id: act.id });
|
.do_send(Disconnect { id: act.id, ip: act.ip.to_owned() });
|
||||||
|
|
||||||
// stop actor
|
// stop actor
|
||||||
ctx.stop();
|
ctx.stop();
|
||||||
|
|
|
@ -16,6 +16,7 @@ table! {
|
||||||
read -> Bool,
|
read -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ table! {
|
||||||
removed -> Bool,
|
removed -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,6 +184,7 @@ table! {
|
||||||
locked -> Bool,
|
locked -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ use bcrypt::{verify};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use diesel::PgConnection;
|
use diesel::PgConnection;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
|
use std::time::{SystemTime};
|
||||||
|
|
||||||
use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, SearchType, has_slurs, remove_slurs};
|
use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, SearchType, has_slurs, remove_slurs};
|
||||||
use actions::community::*;
|
use actions::community::*;
|
||||||
|
@ -25,9 +26,14 @@ use actions::user_view::*;
|
||||||
use actions::moderator_views::*;
|
use actions::moderator_views::*;
|
||||||
use actions::moderator::*;
|
use actions::moderator::*;
|
||||||
|
|
||||||
|
const RATE_LIMIT_MESSAGES: i32 = 30;
|
||||||
|
const RATE_LIMIT_PER_SECOND: i32 = 60;
|
||||||
|
const RATE_LIMIT_REGISTER_MESSAGES: i32 = 1;
|
||||||
|
const RATE_LIMIT_REGISTER_PER_SECOND: i32 = 60;
|
||||||
|
|
||||||
#[derive(EnumString,ToString,Debug)]
|
#[derive(EnumString,ToString,Debug)]
|
||||||
pub enum UserOperation {
|
pub enum UserOperation {
|
||||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search
|
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Fail, Debug)]
|
||||||
|
@ -48,12 +54,14 @@ pub struct WSMessage(pub String);
|
||||||
#[rtype(usize)]
|
#[rtype(usize)]
|
||||||
pub struct Connect {
|
pub struct Connect {
|
||||||
pub addr: Recipient<WSMessage>,
|
pub addr: Recipient<WSMessage>,
|
||||||
|
pub ip: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Session is disconnected
|
/// Session is disconnected
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct Disconnect {
|
pub struct Disconnect {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
|
pub ip: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send message to specific room
|
/// Send message to specific room
|
||||||
|
@ -219,6 +227,7 @@ pub struct EditComment {
|
||||||
creator_id: i32,
|
creator_id: i32,
|
||||||
post_id: i32,
|
post_id: i32,
|
||||||
removed: Option<bool>,
|
removed: Option<bool>,
|
||||||
|
deleted: Option<bool>,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
read: Option<bool>,
|
read: Option<bool>,
|
||||||
auth: String
|
auth: String
|
||||||
|
@ -268,6 +277,7 @@ pub struct EditPost {
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
removed: Option<bool>,
|
removed: Option<bool>,
|
||||||
|
deleted: Option<bool>,
|
||||||
locked: Option<bool>,
|
locked: Option<bool>,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
auth: String
|
auth: String
|
||||||
|
@ -288,6 +298,7 @@ pub struct EditCommunity {
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
category_id: i32,
|
category_id: i32,
|
||||||
removed: Option<bool>,
|
removed: Option<bool>,
|
||||||
|
deleted: Option<bool>,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
expires: Option<i64>,
|
expires: Option<i64>,
|
||||||
auth: String
|
auth: String
|
||||||
|
@ -320,6 +331,7 @@ pub struct GetUserDetails {
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
community_id: Option<i32>,
|
community_id: Option<i32>,
|
||||||
saved_only: bool,
|
saved_only: bool,
|
||||||
|
auth: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -478,10 +490,27 @@ pub struct SearchResponse {
|
||||||
posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct MarkAllAsRead {
|
||||||
|
auth: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RateLimitBucket {
|
||||||
|
last_checked: SystemTime,
|
||||||
|
allowance: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SessionInfo {
|
||||||
|
pub addr: Recipient<WSMessage>,
|
||||||
|
pub ip: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
||||||
/// session. implementation is super primitive
|
/// session. implementation is super primitive
|
||||||
pub struct ChatServer {
|
pub struct ChatServer {
|
||||||
sessions: HashMap<usize, Recipient<WSMessage>>, // A map from generated random ID to session addr
|
sessions: HashMap<usize, SessionInfo>, // A map from generated random ID to session addr
|
||||||
|
rate_limits: HashMap<String, RateLimitBucket>,
|
||||||
rooms: HashMap<i32, HashSet<usize>>, // A map from room / post name to set of connectionIDs
|
rooms: HashMap<i32, HashSet<usize>>, // A map from room / post name to set of connectionIDs
|
||||||
rng: ThreadRng,
|
rng: ThreadRng,
|
||||||
}
|
}
|
||||||
|
@ -493,6 +522,7 @@ impl Default for ChatServer {
|
||||||
|
|
||||||
ChatServer {
|
ChatServer {
|
||||||
sessions: HashMap::new(),
|
sessions: HashMap::new(),
|
||||||
|
rate_limits: HashMap::new(),
|
||||||
rooms: rooms,
|
rooms: rooms,
|
||||||
rng: rand::thread_rng(),
|
rng: rand::thread_rng(),
|
||||||
}
|
}
|
||||||
|
@ -505,8 +535,8 @@ impl ChatServer {
|
||||||
if let Some(sessions) = self.rooms.get(&room) {
|
if let Some(sessions) = self.rooms.get(&room) {
|
||||||
for id in sessions {
|
for id in sessions {
|
||||||
if *id != skip_id {
|
if *id != skip_id {
|
||||||
if let Some(addr) = self.sessions.get(id) {
|
if let Some(info) = self.sessions.get(id) {
|
||||||
let _ = addr.do_send(WSMessage(message.to_owned()));
|
let _ = info.addr.do_send(WSMessage(message.to_owned()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -531,8 +561,51 @@ impl ChatServer {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_rate_limit_register(&mut self, addr: usize) -> Result<(), Error> {
|
||||||
|
self.check_rate_limit_full(addr, RATE_LIMIT_REGISTER_MESSAGES, RATE_LIMIT_REGISTER_PER_SECOND)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_rate_limit(&mut self, addr: usize) -> Result<(), Error> {
|
||||||
|
self.check_rate_limit_full(addr, RATE_LIMIT_MESSAGES, RATE_LIMIT_PER_SECOND)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_rate_limit_full(&mut self, addr: usize, rate: i32, per: i32) -> Result<(), Error> {
|
||||||
|
if let Some(info) = self.sessions.get(&addr) {
|
||||||
|
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
|
||||||
|
// The initial value
|
||||||
|
if rate_limit.allowance == -2f64 {
|
||||||
|
rate_limit.allowance = rate as f64;
|
||||||
|
};
|
||||||
|
|
||||||
|
let current = SystemTime::now();
|
||||||
|
let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
|
||||||
|
rate_limit.last_checked = current;
|
||||||
|
rate_limit.allowance += time_passed * (rate as f64 / per as f64);
|
||||||
|
if rate_limit.allowance > rate as f64 {
|
||||||
|
rate_limit.allowance = rate as f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rate_limit.allowance < 1.0 {
|
||||||
|
println!("Rate limited IP: {}, time_passed: {}, allowance: {}", &info.ip, time_passed, rate_limit.allowance);
|
||||||
|
Err(ErrorMessage {
|
||||||
|
op: "Rate Limit".to_string(),
|
||||||
|
message: format!("Too many requests. {} per {} seconds", rate, per),
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
rate_limit.allowance -= 1.0;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Make actor from `ChatServer`
|
/// Make actor from `ChatServer`
|
||||||
impl Actor for ChatServer {
|
impl Actor for ChatServer {
|
||||||
/// We are going to use simple Context, we just need ability to communicate
|
/// We are going to use simple Context, we just need ability to communicate
|
||||||
|
@ -546,14 +619,30 @@ impl Actor for ChatServer {
|
||||||
impl Handler<Connect> for ChatServer {
|
impl Handler<Connect> for ChatServer {
|
||||||
type Result = usize;
|
type Result = usize;
|
||||||
|
|
||||||
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
|
fn handle(&mut self, msg: Connect, _ctx: &mut Context<Self>) -> Self::Result {
|
||||||
|
|
||||||
// notify all users in same room
|
// notify all users in same room
|
||||||
// self.send_room_message(&"Main".to_owned(), "Someone joined", 0);
|
// self.send_room_message(&"Main".to_owned(), "Someone joined", 0);
|
||||||
|
|
||||||
// register session with random id
|
// register session with random id
|
||||||
let id = self.rng.gen::<usize>();
|
let id = self.rng.gen::<usize>();
|
||||||
self.sessions.insert(id, msg.addr);
|
println!("{} joined", &msg.ip);
|
||||||
|
|
||||||
|
self.sessions.insert(id, SessionInfo {
|
||||||
|
addr: msg.addr,
|
||||||
|
ip: msg.ip.to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.rate_limits.get(&msg.ip).is_none() {
|
||||||
|
self.rate_limits.insert(msg.ip, RateLimitBucket {
|
||||||
|
last_checked: SystemTime::now(),
|
||||||
|
allowance: -2f64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// for (k,v) in &self.rate_limits {
|
||||||
|
// println!("{}: {:?}", k,v);
|
||||||
|
// }
|
||||||
|
|
||||||
// auto join session to Main room
|
// auto join session to Main room
|
||||||
// self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id);
|
// self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id);
|
||||||
|
@ -563,6 +652,7 @@ impl Handler<Connect> for ChatServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handler for Disconnect message.
|
/// Handler for Disconnect message.
|
||||||
impl Handler<Disconnect> for ChatServer {
|
impl Handler<Disconnect> for ChatServer {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
|
@ -728,6 +818,10 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let search: Search = serde_json::from_str(data)?;
|
let search: Search = serde_json::from_str(data)?;
|
||||||
search.perform(chat, msg.id)
|
search.perform(chat, msg.id)
|
||||||
},
|
},
|
||||||
|
UserOperation::MarkAllAsRead => {
|
||||||
|
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
|
||||||
|
mark_all_as_read.perform(chat, msg.id)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -781,10 +875,12 @@ impl Perform for Register {
|
||||||
fn op_type(&self) -> UserOperation {
|
fn op_type(&self) -> UserOperation {
|
||||||
UserOperation::Register
|
UserOperation::Register
|
||||||
}
|
}
|
||||||
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> {
|
fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
chat.check_rate_limit_register(addr)?;
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if &self.password != &self.password_verify {
|
if &self.password != &self.password_verify {
|
||||||
return Err(self.error("Passwords do not match."))?
|
return Err(self.error("Passwords do not match."))?
|
||||||
|
@ -871,10 +967,12 @@ impl Perform for CreateCommunity {
|
||||||
UserOperation::CreateCommunity
|
UserOperation::CreateCommunity
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> {
|
fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
chat.check_rate_limit_register(addr)?;
|
||||||
|
|
||||||
let claims = match Claims::decode(&self.auth) {
|
let claims = match Claims::decode(&self.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
|
@ -903,6 +1001,7 @@ impl Perform for CreateCommunity {
|
||||||
category_id: self.category_id,
|
category_id: self.category_id,
|
||||||
creator_id: user_id,
|
creator_id: user_id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1016,10 +1115,12 @@ impl Perform for CreatePost {
|
||||||
UserOperation::CreatePost
|
UserOperation::CreatePost
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> {
|
fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
chat.check_rate_limit_register(addr)?;
|
||||||
|
|
||||||
let claims = match Claims::decode(&self.auth) {
|
let claims = match Claims::decode(&self.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
|
@ -1051,6 +1152,7 @@ impl Perform for CreatePost {
|
||||||
community_id: self.community_id,
|
community_id: self.community_id,
|
||||||
creator_id: user_id,
|
creator_id: user_id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
locked: None,
|
locked: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -1227,6 +1329,8 @@ impl Perform for CreateComment {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
chat.check_rate_limit(addr)?;
|
||||||
|
|
||||||
let claims = match Claims::decode(&self.auth) {
|
let claims = match Claims::decode(&self.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
|
@ -1255,6 +1359,7 @@ impl Perform for CreateComment {
|
||||||
post_id: self.post_id,
|
post_id: self.post_id,
|
||||||
creator_id: user_id,
|
creator_id: user_id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -1371,9 +1476,10 @@ impl Perform for EditComment {
|
||||||
post_id: self.post_id,
|
post_id: self.post_id,
|
||||||
creator_id: self.creator_id,
|
creator_id: self.creator_id,
|
||||||
removed: self.removed.to_owned(),
|
removed: self.removed.to_owned(),
|
||||||
|
deleted: self.deleted.to_owned(),
|
||||||
read: self.read.to_owned(),
|
read: self.read.to_owned(),
|
||||||
updated: if self.read.is_some() { orig_comment.updated } else {Some(naive_now())}
|
updated: if self.read.is_some() { orig_comment.updated } else {Some(naive_now())}
|
||||||
};
|
};
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, self.edit_id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, self.edit_id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
|
@ -1483,6 +1589,8 @@ impl Perform for CreateCommentLike {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
chat.check_rate_limit(addr)?;
|
||||||
|
|
||||||
let claims = match Claims::decode(&self.auth) {
|
let claims = match Claims::decode(&self.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
|
@ -1611,10 +1719,12 @@ impl Perform for CreatePostLike {
|
||||||
UserOperation::CreatePostLike
|
UserOperation::CreatePostLike
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> {
|
fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result<String, Error> {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
chat.check_rate_limit(addr)?;
|
||||||
|
|
||||||
let claims = match Claims::decode(&self.auth) {
|
let claims = match Claims::decode(&self.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
|
@ -1734,6 +1844,7 @@ impl Perform for EditPost {
|
||||||
creator_id: self.creator_id.to_owned(),
|
creator_id: self.creator_id.to_owned(),
|
||||||
community_id: self.community_id,
|
community_id: self.community_id,
|
||||||
removed: self.removed.to_owned(),
|
removed: self.removed.to_owned(),
|
||||||
|
deleted: self.deleted.to_owned(),
|
||||||
locked: self.locked.to_owned(),
|
locked: self.locked.to_owned(),
|
||||||
updated: Some(naive_now())
|
updated: Some(naive_now())
|
||||||
};
|
};
|
||||||
|
@ -1899,6 +2010,7 @@ impl Perform for EditCommunity {
|
||||||
category_id: self.category_id.to_owned(),
|
category_id: self.category_id.to_owned(),
|
||||||
creator_id: user_id,
|
creator_id: user_id,
|
||||||
removed: self.removed.to_owned(),
|
removed: self.removed.to_owned(),
|
||||||
|
deleted: self.deleted.to_owned(),
|
||||||
updated: Some(naive_now())
|
updated: Some(naive_now())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2051,6 +2163,19 @@ impl Perform for GetUserDetails {
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let user_id: Option<i32> = match &self.auth {
|
||||||
|
Some(auth) => {
|
||||||
|
match Claims::decode(&auth) {
|
||||||
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
Some(user_id)
|
||||||
|
}
|
||||||
|
Err(_e) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
//TODO add save
|
//TODO add save
|
||||||
let sort = SortType::from_str(&self.sort)?;
|
let sort = SortType::from_str(&self.sort)?;
|
||||||
|
|
||||||
|
@ -2081,7 +2206,7 @@ impl Perform for GetUserDetails {
|
||||||
self.community_id,
|
self.community_id,
|
||||||
Some(user_details_id),
|
Some(user_details_id),
|
||||||
None,
|
None,
|
||||||
None,
|
user_id,
|
||||||
self.saved_only,
|
self.saved_only,
|
||||||
false,
|
false,
|
||||||
self.page,
|
self.page,
|
||||||
|
@ -2103,7 +2228,7 @@ impl Perform for GetUserDetails {
|
||||||
None,
|
None,
|
||||||
Some(user_details_id),
|
Some(user_details_id),
|
||||||
None,
|
None,
|
||||||
None,
|
user_id,
|
||||||
self.saved_only,
|
self.saved_only,
|
||||||
self.page,
|
self.page,
|
||||||
self.limit)?
|
self.limit)?
|
||||||
|
@ -2663,7 +2788,7 @@ impl Perform for Search {
|
||||||
},
|
},
|
||||||
SearchType::Comments => {
|
SearchType::Comments => {
|
||||||
comments = CommentView::list(&conn,
|
comments = CommentView::list(&conn,
|
||||||
&sort,
|
&sort,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(self.q.to_owned()),
|
Some(self.q.to_owned()),
|
||||||
|
@ -2685,7 +2810,7 @@ impl Perform for Search {
|
||||||
self.page,
|
self.page,
|
||||||
self.limit)?;
|
self.limit)?;
|
||||||
comments = CommentView::list(&conn,
|
comments = CommentView::list(&conn,
|
||||||
&sort,
|
&sort,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(self.q.to_owned()),
|
Some(self.q.to_owned()),
|
||||||
|
@ -2709,3 +2834,57 @@ impl Perform for Search {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Perform for MarkAllAsRead {
|
||||||
|
fn op_type(&self) -> UserOperation {
|
||||||
|
UserOperation::MarkAllAsRead
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result<String, Error> {
|
||||||
|
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&self.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => {
|
||||||
|
return Err(self.error("Not logged in."))?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let replies = ReplyView::get_replies(&conn, user_id, &SortType::New, true, Some(1), Some(999))?;
|
||||||
|
|
||||||
|
for reply in &replies {
|
||||||
|
let comment_form = CommentForm {
|
||||||
|
content: reply.to_owned().content,
|
||||||
|
parent_id: reply.to_owned().parent_id,
|
||||||
|
post_id: reply.to_owned().post_id,
|
||||||
|
creator_id: reply.to_owned().creator_id,
|
||||||
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
|
read: Some(true),
|
||||||
|
updated: reply.to_owned().updated
|
||||||
|
};
|
||||||
|
|
||||||
|
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
||||||
|
Ok(comment) => comment,
|
||||||
|
Err(_e) => {
|
||||||
|
return Err(self.error("Couldn't update Comment"))?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let replies = ReplyView::get_replies(&conn, user_id, &SortType::New, true, Some(1), Some(999))?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
serde_json::to_string(
|
||||||
|
&GetRepliesResponse {
|
||||||
|
op: self.op_type().to_string(),
|
||||||
|
replies: replies,
|
||||||
|
}
|
||||||
|
)?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ Sparky.task('config', _ => {
|
||||||
// Sparky.task('version', _ => setVersion());
|
// Sparky.task('version', _ => setVersion());
|
||||||
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
||||||
Sparky.task('env', _ => (isProduction = true));
|
Sparky.task('env', _ => (isProduction = true));
|
||||||
Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/'));
|
Sparky.task('copy-assets', () => Sparky.src('assets/*.*').dest('dist/'));
|
||||||
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
|
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
|
||||||
fuse.dev();
|
fuse.dev();
|
||||||
app.hmr().watch();
|
app.hmr().watch();
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"@types/js-cookie": "^2.2.1",
|
"@types/js-cookie": "^2.2.1",
|
||||||
"@types/jwt-decode": "^2.2.1",
|
"@types/jwt-decode": "^2.2.1",
|
||||||
"@types/markdown-it": "^0.0.7",
|
"@types/markdown-it": "^0.0.7",
|
||||||
|
"@types/markdown-it-container": "^2.0.2",
|
||||||
"autosize": "^4.0.2",
|
"autosize": "^4.0.2",
|
||||||
"classcat": "^1.1.3",
|
"classcat": "^1.1.3",
|
||||||
"dotenv": "^6.1.0",
|
"dotenv": "^6.1.0",
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"markdown-it": "^8.4.2",
|
"markdown-it": "^8.4.2",
|
||||||
|
"markdown-it-container": "^2.0.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"rxjs": "^6.4.0"
|
"rxjs": "^6.4.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,7 +56,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} />
|
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -92,7 +92,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
|
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
|
||||||
{!this.state.showEdit &&
|
{!this.state.showEdit &&
|
||||||
<div>
|
<div>
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.content)} />
|
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.deleted ? '*deleted*' : node.comment.content)} />
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
{UserService.Instance.user && !this.props.viewOnly &&
|
{UserService.Instance.user && !this.props.viewOnly &&
|
||||||
<>
|
<>
|
||||||
|
@ -108,7 +108,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
|
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
|
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
||||||
|
{!this.props.node.comment.deleted ? 'delete' : 'restore'}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -252,11 +254,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
|
||||||
handleDeleteClick(i: CommentNode) {
|
handleDeleteClick(i: CommentNode) {
|
||||||
let deleteForm: CommentFormI = {
|
let deleteForm: CommentFormI = {
|
||||||
content: '*deleted*',
|
content: i.props.node.comment.content,
|
||||||
edit_id: i.props.node.comment.id,
|
edit_id: i.props.node.comment.id,
|
||||||
creator_id: i.props.node.comment.creator_id,
|
creator_id: i.props.node.comment.creator_id,
|
||||||
post_id: i.props.node.comment.post_id,
|
post_id: i.props.node.comment.post_id,
|
||||||
parent_id: i.props.node.comment.parent_id,
|
parent_id: i.props.node.comment.parent_id,
|
||||||
|
deleted: !i.props.node.comment.deleted,
|
||||||
auth: null
|
auth: null
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(deleteForm);
|
WebSocketService.Instance.editComment(deleteForm);
|
||||||
|
|
|
@ -10,6 +10,7 @@ declare const Sortable: any;
|
||||||
|
|
||||||
interface CommunitiesState {
|
interface CommunitiesState {
|
||||||
communities: Array<Community>;
|
communities: Array<Community>;
|
||||||
|
page: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +18,8 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: CommunitiesState = {
|
private emptyState: CommunitiesState = {
|
||||||
communities: [],
|
communities: [],
|
||||||
loading: true
|
loading: true,
|
||||||
|
page: this.getPageFromProps(this.props),
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -31,13 +33,12 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
let listCommunitiesForm: ListCommunitiesForm = {
|
this.refetch();
|
||||||
sort: SortType[SortType.TopAll],
|
|
||||||
limit: 100,
|
|
||||||
}
|
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
}
|
||||||
|
|
||||||
|
getPageFromProps(props: any): number {
|
||||||
|
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -45,40 +46,49 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = "Forums - Lemmy";
|
document.title = "Communities - Lemmy";
|
||||||
let table = document.querySelector('#community_table');
|
let table = document.querySelector('#community_table');
|
||||||
Sortable.initTable(table);
|
Sortable.initTable(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Necessary for back button for some reason
|
||||||
|
componentWillReceiveProps(nextProps: any) {
|
||||||
|
if (nextProps.history.action == 'POP') {
|
||||||
|
this.state = this.emptyState;
|
||||||
|
this.state.page = this.getPageFromProps(nextProps);
|
||||||
|
this.refetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{this.state.loading ?
|
{this.state.loading ?
|
||||||
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||||
<div>
|
<div>
|
||||||
<h5>Forums</h5>
|
<h5>List of communities</h5>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="community_table" class="table table-sm table-hover">
|
<table id="community_table" class="table table-sm table-hover">
|
||||||
<thead class="pointer">
|
<thead class="pointer">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Title</th>
|
<th class="d-none d-lg-table-cell">Title</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th class="text-right d-none d-md-table-cell">Subscribers</th>
|
<th class="text-right">Subscribers</th>
|
||||||
<th class="text-right d-none d-md-table-cell">Posts</th>
|
<th class="text-right d-none d-lg-table-cell">Posts</th>
|
||||||
<th class="text-right d-none d-md-table-cell">Comments</th>
|
<th class="text-right d-none d-lg-table-cell">Comments</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.state.communities.map(community =>
|
{this.state.communities.map(community =>
|
||||||
<tr>
|
<tr>
|
||||||
<td><Link to={`/f/${community.name}`}>{community.name}</Link></td>
|
<td><Link to={`/c/${community.name}`}>{community.name}</Link></td>
|
||||||
<td>{community.title}</td>
|
<td class="d-none d-lg-table-cell">{community.title}</td>
|
||||||
<td>{community.category_name}</td>
|
<td>{community.category_name}</td>
|
||||||
<td class="text-right d-none d-md-table-cell">{community.number_of_subscribers}</td>
|
<td class="text-right">{community.number_of_subscribers}</td>
|
||||||
<td class="text-right d-none d-md-table-cell">{community.number_of_posts}</td>
|
<td class="text-right d-none d-lg-table-cell">{community.number_of_posts}</td>
|
||||||
<td class="text-right d-none d-md-table-cell">{community.number_of_comments}</td>
|
<td class="text-right d-none d-lg-table-cell">{community.number_of_comments}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{community.subscribed ?
|
{community.subscribed ?
|
||||||
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</span> :
|
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</span> :
|
||||||
|
@ -90,12 +100,42 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paginator() {
|
||||||
|
return (
|
||||||
|
<div class="mt-2">
|
||||||
|
{this.state.page > 1 &&
|
||||||
|
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUrl() {
|
||||||
|
this.props.history.push(`/communities/page/${this.state.page}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage(i: Communities) {
|
||||||
|
i.state.page++;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPage(i: Communities) {
|
||||||
|
i.state.page--;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
handleUnsubscribe(communityId: number) {
|
handleUnsubscribe(communityId: number) {
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
community_id: communityId,
|
community_id: communityId,
|
||||||
|
@ -112,6 +152,17 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
WebSocketService.Instance.followCommunity(form);
|
WebSocketService.Instance.followCommunity(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refetch() {
|
||||||
|
let listCommunitiesForm: ListCommunitiesForm = {
|
||||||
|
sort: SortType[SortType.TopAll],
|
||||||
|
limit: 100,
|
||||||
|
page: this.state.page,
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
let op: UserOperation = msgOp(msg);
|
let op: UserOperation = msgOp(msg);
|
||||||
|
|
|
@ -88,7 +88,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label">Sidebar</label>
|
<label class="col-12 col-form-label">Sidebar</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} />
|
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
|
@ -120,10 +120,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
if (i.props.community) {
|
if (i.props.community) {
|
||||||
WebSocketService.Instance.editCommunity(i.state.communityForm);
|
WebSocketService.Instance.editCommunity(i.state.communityForm);
|
||||||
} else {
|
} else {
|
||||||
|
WebSocketService.Instance.createCommunity(i.state.communityForm);
|
||||||
setTimeout(function(){
|
|
||||||
WebSocketService.Instance.createCommunity(i.state.communityForm);
|
|
||||||
}, 10000);
|
|
||||||
}
|
}
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Component } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser, UserView } from '../interfaces';
|
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser, UserView, SortType, Post, GetPostsForm, ListingType, GetPostsResponse, CreatePostLikeResponse } from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
community: CommunityI;
|
community: CommunityI;
|
||||||
|
@ -14,6 +14,9 @@ interface State {
|
||||||
moderators: Array<CommunityUser>;
|
moderators: Array<CommunityUser>;
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
posts: Array<Post>;
|
||||||
|
sort: SortType;
|
||||||
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Community extends Component<any, State> {
|
export class Community extends Component<any, State> {
|
||||||
|
@ -38,7 +41,20 @@ export class Community extends Component<any, State> {
|
||||||
admins: [],
|
admins: [],
|
||||||
communityId: Number(this.props.match.params.id),
|
communityId: Number(this.props.match.params.id),
|
||||||
communityName: this.props.match.params.name,
|
communityName: this.props.match.params.name,
|
||||||
loading: true
|
loading: true,
|
||||||
|
posts: [],
|
||||||
|
sort: this.getSortTypeFromProps(this.props),
|
||||||
|
page: this.getPageFromProps(this.props),
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortTypeFromProps(props: any): SortType {
|
||||||
|
return (props.match.params.sort) ?
|
||||||
|
routeSortTypeToEnum(props.match.params.sort) :
|
||||||
|
SortType.Hot;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageFromProps(props: any): number {
|
||||||
|
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -66,6 +82,16 @@ export class Community extends Component<any, State> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Necessary for back button for some reason
|
||||||
|
componentWillReceiveProps(nextProps: any) {
|
||||||
|
if (nextProps.history.action == 'POP') {
|
||||||
|
this.state = this.emptyState;
|
||||||
|
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||||
|
this.state.page = this.getPageFromProps(nextProps);
|
||||||
|
this.fetchPosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -78,7 +104,9 @@ export class Community extends Component<any, State> {
|
||||||
<small className="ml-2 text-muted font-italic">removed</small>
|
<small className="ml-2 text-muted font-italic">removed</small>
|
||||||
}
|
}
|
||||||
</h5>
|
</h5>
|
||||||
{this.state.community && <PostListings communityId={this.state.community.id} />}
|
{this.selects()}
|
||||||
|
<PostListings posts={this.state.posts} />
|
||||||
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
|
@ -93,6 +121,72 @@ export class Community extends Component<any, State> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selects() {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto">
|
||||||
|
<option disabled>Sort Type</option>
|
||||||
|
<option value={SortType.Hot}>Hot</option>
|
||||||
|
<option value={SortType.New}>New</option>
|
||||||
|
<option disabled>──────────</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
paginator() {
|
||||||
|
return (
|
||||||
|
<div class="mt-2">
|
||||||
|
{this.state.page > 1 &&
|
||||||
|
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage(i: Community) {
|
||||||
|
i.state.page++;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPage(i: Community) {
|
||||||
|
i.state.page--;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSortChange(i: Community, event: any) {
|
||||||
|
i.state.sort = Number(event.target.value);
|
||||||
|
i.state.page = 1;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUrl() {
|
||||||
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
|
this.props.history.push(`/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPosts() {
|
||||||
|
let getPostsForm: GetPostsForm = {
|
||||||
|
page: this.state.page,
|
||||||
|
limit: fetchLimit,
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
type_: ListingType[ListingType.Community],
|
||||||
|
community_id: this.state.community.id,
|
||||||
|
}
|
||||||
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
|
@ -105,9 +199,9 @@ export class Community extends Component<any, State> {
|
||||||
this.state.community = res.community;
|
this.state.community = res.community;
|
||||||
this.state.moderators = res.moderators;
|
this.state.moderators = res.moderators;
|
||||||
this.state.admins = res.admins;
|
this.state.admins = res.admins;
|
||||||
this.state.loading = false;
|
document.title = `/c/${this.state.community.name} - Lemmy`;
|
||||||
document.title = `/f/${this.state.community.name} - Lemmy`;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
this.fetchPosts();
|
||||||
} else if (op == UserOperation.EditCommunity) {
|
} else if (op == UserOperation.EditCommunity) {
|
||||||
let res: CommunityResponse = msg;
|
let res: CommunityResponse = msg;
|
||||||
this.state.community = res.community;
|
this.state.community = res.community;
|
||||||
|
@ -117,6 +211,19 @@ export class Community extends Component<any, State> {
|
||||||
this.state.community.subscribed = res.community.subscribed;
|
this.state.community.subscribed = res.community.subscribed;
|
||||||
this.state.community.number_of_subscribers = res.community.number_of_subscribers;
|
this.state.community.number_of_subscribers = res.community.number_of_subscribers;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
} 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;
|
||||||
|
let found = this.state.posts.find(c => c.id == res.post.id);
|
||||||
|
found.my_vote = res.post.my_vote;
|
||||||
|
found.score = res.post.score;
|
||||||
|
found.upvotes = res.post.upvotes;
|
||||||
|
found.downvotes = res.post.downvotes;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class CreateCommunity extends Component<any, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = "Create Forum - Lemmy";
|
document.title = "Create Community - Lemmy";
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -18,7 +18,7 @@ export class CreateCommunity extends Component<any, any> {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 mb-4">
|
<div class="col-12 col-lg-6 mb-4">
|
||||||
<h5>Create Forum</h5>
|
<h5>Create Community</h5>
|
||||||
<CommunityForm onCreate={this.handleCommunityCreate}/>
|
<CommunityForm onCreate={this.handleCommunityCreate}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,7 +27,7 @@ export class CreateCommunity extends Component<any, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityCreate(community: Community) {
|
handleCommunityCreate(community: Community) {
|
||||||
this.props.history.push(`/f/${community.name}`);
|
this.props.history.push(`/c/${community.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,23 @@ export class CreatePost extends Component<any, any> {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 mb-4">
|
<div class="col-12 col-lg-6 mb-4">
|
||||||
<h5>Create a Post</h5>
|
<h5>Create a Post</h5>
|
||||||
<PostForm onCreate={this.handlePostCreate}/>
|
<PostForm onCreate={this.handlePostCreate} prevCommunityName={this.prevCommunityName} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get prevCommunityName(): string {
|
||||||
|
if (this.props.location.state) {
|
||||||
|
let lastLocation = this.props.location.state.prevPath;
|
||||||
|
if (lastLocation.includes("/c/")) {
|
||||||
|
return lastLocation.split("/c/")[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
handlePostCreate(id: number) {
|
handlePostCreate(id: number) {
|
||||||
this.props.history.push(`/post/${id}`);
|
this.props.history.push(`/post/${id}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { Component } from 'inferno';
|
|
||||||
import { Main } from './main';
|
|
||||||
import { ListingType } from '../interfaces';
|
|
||||||
|
|
||||||
export class Home extends Component<any, any> {
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
|
||||||
super(props, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Main type={this.listType()}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.title = "Lemmy";
|
|
||||||
}
|
|
||||||
|
|
||||||
listType(): ListingType {
|
|
||||||
return (this.props.match.path == '/all') ? ListingType.All : ListingType.Subscribed;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -58,7 +58,16 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h5>Inbox for <Link to={`/u/${user.username}`}>{user.username}</Link></h5>
|
<h5 class="mb-0">
|
||||||
|
<span>Inbox for <Link to={`/u/${user.username}`}>{user.username}</Link></span>
|
||||||
|
</h5>
|
||||||
|
{this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread &&
|
||||||
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span class="pointer" onClick={this.markAllAsRead}>mark all as read</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.replies()}
|
{this.replies()}
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
|
@ -71,12 +80,12 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select w-auto">
|
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto">
|
||||||
<option disabled>Type</option>
|
<option disabled>Type</option>
|
||||||
<option value={UnreadType.Unread}>Unread</option>
|
<option value={UnreadType.Unread}>Unread</option>
|
||||||
<option value={UnreadType.All}>All</option>
|
<option value={UnreadType.All}>All</option>
|
||||||
</select>
|
</select>
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
|
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
||||||
<option disabled>Sort Type</option>
|
<option disabled>Sort Type</option>
|
||||||
<option value={SortType.New}>New</option>
|
<option value={SortType.New}>New</option>
|
||||||
<option value={SortType.TopDay}>Top Day</option>
|
<option value={SortType.TopDay}>Top Day</option>
|
||||||
|
@ -147,13 +156,17 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markAllAsRead() {
|
||||||
|
WebSocketService.Instance.markAllAsRead();
|
||||||
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
let op: UserOperation = msgOp(msg);
|
let op: UserOperation = msgOp(msg);
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
alert(msg.error);
|
alert(msg.error);
|
||||||
return;
|
return;
|
||||||
} else if (op == UserOperation.GetReplies) {
|
} else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) {
|
||||||
let res: GetRepliesResponse = msg;
|
let res: GetRepliesResponse = msg;
|
||||||
this.state.replies = res.replies;
|
this.state.replies = res.replies;
|
||||||
this.sendRepliesCount();
|
this.sendRepliesCount();
|
||||||
|
@ -165,6 +178,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
found.content = res.comment.content;
|
found.content = res.comment.content;
|
||||||
found.updated = res.comment.updated;
|
found.updated = res.comment.updated;
|
||||||
found.removed = res.comment.removed;
|
found.removed = res.comment.removed;
|
||||||
|
found.deleted = res.comment.deleted;
|
||||||
found.upvotes = res.comment.upvotes;
|
found.upvotes = res.comment.upvotes;
|
||||||
found.downvotes = res.comment.downvotes;
|
found.downvotes = res.comment.downvotes;
|
||||||
found.score = res.comment.score;
|
found.score = res.comment.score;
|
||||||
|
|
|
@ -95,7 +95,7 @@ export class Login extends Component<any, State> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
Forgot your password or deleted your account? Reset your password. TODO
|
{/* Forgot your password or deleted your account? Reset your password. TODO */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,6 @@ export class Login extends Component<any, State> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.registerLoading = true;
|
i.state.registerLoading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let endTimer = new Date().getTime();
|
let endTimer = new Date().getTime();
|
||||||
let elapsed = endTimer - i.state.registerForm.spam_timeri;
|
let elapsed = endTimer - i.state.registerForm.spam_timeri;
|
||||||
|
@ -209,14 +208,14 @@ export class Login extends Component<any, State> {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (op == UserOperation.Login) {
|
if (op == UserOperation.Login) {
|
||||||
this.state.loginLoading = false;
|
this.state = this.emptyState;
|
||||||
this.state.registerLoading = false;
|
this.setState(this.state);
|
||||||
let res: LoginResponse = msg;
|
let res: LoginResponse = msg;
|
||||||
UserService.Instance.login(res);
|
UserService.Instance.login(res);
|
||||||
this.props.history.push('/');
|
this.props.history.push('/');
|
||||||
} else if (op == UserOperation.Register) {
|
} else if (op == UserOperation.Register) {
|
||||||
this.state.loginLoading = false;
|
this.state = this.emptyState;
|
||||||
this.state.registerLoading = false;
|
this.setState(this.state);
|
||||||
let res: LoginResponse = msg;
|
let res: LoginResponse = msg;
|
||||||
UserService.Instance.login(res);
|
UserService.Instance.login(res);
|
||||||
this.props.history.push('/communities');
|
this.props.history.push('/communities');
|
||||||
|
|
|
@ -2,16 +2,11 @@ import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, ListingType, SiteResponse } from '../interfaces';
|
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, ListingType, SiteResponse, GetPostsResponse, CreatePostLikeResponse, Post, GetPostsForm } from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
import { SiteForm } from './site-form';
|
import { SiteForm } from './site-form';
|
||||||
import { msgOp, repoUrl, mdToHtml } from '../utils';
|
import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils';
|
||||||
|
|
||||||
|
|
||||||
interface MainProps {
|
|
||||||
type: ListingType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MainState {
|
interface MainState {
|
||||||
subscribedCommunities: Array<CommunityUser>;
|
subscribedCommunities: Array<CommunityUser>;
|
||||||
|
@ -19,9 +14,13 @@ interface MainState {
|
||||||
site: GetSiteResponse;
|
site: GetSiteResponse;
|
||||||
showEditSite: boolean;
|
showEditSite: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
posts: Array<Post>;
|
||||||
|
type_: ListingType;
|
||||||
|
sort: SortType;
|
||||||
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Main extends Component<MainProps, MainState> {
|
export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: MainState = {
|
private emptyState: MainState = {
|
||||||
|
@ -43,7 +42,29 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
banned: [],
|
banned: [],
|
||||||
},
|
},
|
||||||
showEditSite: false,
|
showEditSite: false,
|
||||||
loading: true
|
loading: true,
|
||||||
|
posts: [],
|
||||||
|
type_: this.getListingTypeFromProps(this.props),
|
||||||
|
sort: this.getSortTypeFromProps(this.props),
|
||||||
|
page: this.getPageFromProps(this.props),
|
||||||
|
}
|
||||||
|
|
||||||
|
getListingTypeFromProps(props: any): ListingType {
|
||||||
|
return (props.match.params.type) ?
|
||||||
|
routeListingTypeToEnum(props.match.params.type) :
|
||||||
|
UserService.Instance.user ?
|
||||||
|
ListingType.Subscribed :
|
||||||
|
ListingType.All;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortTypeFromProps(props: any): SortType {
|
||||||
|
return (props.match.params.sort) ?
|
||||||
|
routeSortTypeToEnum(props.match.params.sort) :
|
||||||
|
SortType.Hot;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageFromProps(props: any): number {
|
||||||
|
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -66,43 +87,57 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let listCommunitiesForm: ListCommunitiesForm = {
|
let listCommunitiesForm: ListCommunitiesForm = {
|
||||||
sort: SortType[SortType.New],
|
sort: SortType[SortType.Hot],
|
||||||
limit: 6
|
limit: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
|
|
||||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
this.fetchPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.title = "Lemmy";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Necessary for back button for some reason
|
||||||
|
componentWillReceiveProps(nextProps: any) {
|
||||||
|
if (nextProps.history.action == 'POP') {
|
||||||
|
this.state = this.emptyState;
|
||||||
|
this.state.type_ = this.getListingTypeFromProps(nextProps);
|
||||||
|
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||||
|
this.state.page = this.getPageFromProps(nextProps);
|
||||||
|
this.fetchPosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
<PostListings type={this.props.type} />
|
{this.posts()}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
{this.state.loading ?
|
{!this.state.loading &&
|
||||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<div>
|
||||||
<div>
|
{this.trendingCommunities()}
|
||||||
{this.trendingCommunities()}
|
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
|
||||||
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
|
<div>
|
||||||
<div>
|
<h5>Subscribed <Link class="text-white" to="/communities">communities</Link></h5>
|
||||||
<h5>Subscribed forums</h5>
|
<ul class="list-inline">
|
||||||
<ul class="list-inline">
|
{this.state.subscribedCommunities.map(community =>
|
||||||
{this.state.subscribedCommunities.map(community =>
|
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
||||||
<li class="list-inline-item"><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li>
|
)}
|
||||||
)}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
{this.sidebar()}
|
||||||
{this.sidebar()}
|
</div>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,10 +148,10 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
trendingCommunities() {
|
trendingCommunities() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h5>Trending <Link class="text-white" to="/communities">forums</Link></h5>
|
<h5>Trending <Link class="text-white" to="/communities">communities</Link></h5>
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
{this.state.trendingCommunities.map(community =>
|
{this.state.trendingCommunities.map(community =>
|
||||||
<li class="list-inline-item"><Link to={`/f/${community.name}`}>{community.name}</Link></li>
|
<li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -138,6 +173,12 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUrl() {
|
||||||
|
let typeStr = ListingType[this.state.type_].toLowerCase();
|
||||||
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
|
this.props.history.push(`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`);
|
||||||
|
}
|
||||||
|
|
||||||
siteInfo() {
|
siteInfo() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -175,7 +216,7 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
landing() {
|
landing() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h5>Welcome to
|
<h5>Powered by
|
||||||
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
|
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
|
||||||
<a href={repoUrl}>Lemmy<sup>Beta</sup></a>
|
<a href={repoUrl}>Lemmy<sup>Beta</sup></a>
|
||||||
</h5>
|
</h5>
|
||||||
|
@ -188,6 +229,72 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posts() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.state.loading ?
|
||||||
|
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||||
|
<div>
|
||||||
|
{this.selects()}
|
||||||
|
<PostListings posts={this.state.posts} showCommunity />
|
||||||
|
{this.paginator()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
selects() {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<div class="btn-group btn-group-toggle">
|
||||||
|
<label className={`btn btn-sm btn-secondary
|
||||||
|
${this.state.type_ == ListingType.Subscribed && 'active'}
|
||||||
|
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
|
||||||
|
`}>
|
||||||
|
<input type="radio"
|
||||||
|
value={ListingType.Subscribed}
|
||||||
|
checked={this.state.type_ == ListingType.Subscribed}
|
||||||
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
|
disabled={UserService.Instance.user == undefined}
|
||||||
|
/>
|
||||||
|
Subscribed
|
||||||
|
</label>
|
||||||
|
<label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
|
||||||
|
<input type="radio"
|
||||||
|
value={ListingType.All}
|
||||||
|
checked={this.state.type_ == ListingType.All}
|
||||||
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
|
/>
|
||||||
|
All
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto">
|
||||||
|
<option disabled>Sort Type</option>
|
||||||
|
<option value={SortType.Hot}>Hot</option>
|
||||||
|
<option value={SortType.New}>New</option>
|
||||||
|
<option disabled>──────────</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
paginator() {
|
||||||
|
return (
|
||||||
|
<div class="mt-2">
|
||||||
|
{this.state.page > 1 &&
|
||||||
|
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get canAdmin(): boolean {
|
get canAdmin(): boolean {
|
||||||
return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
|
return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
|
||||||
}
|
}
|
||||||
|
@ -202,6 +309,46 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextPage(i: Main) {
|
||||||
|
i.state.page++;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPage(i: Main) {
|
||||||
|
i.state.page--;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSortChange(i: Main, event: any) {
|
||||||
|
i.state.sort = Number(event.target.value);
|
||||||
|
i.state.page = 1;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTypeChange(i: Main, event: any) {
|
||||||
|
i.state.type_ = Number(event.target.value);
|
||||||
|
i.state.page = 1;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
|
i.fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPosts() {
|
||||||
|
let getPostsForm: GetPostsForm = {
|
||||||
|
page: this.state.page,
|
||||||
|
limit: fetchLimit,
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
type_: ListingType[this.state.type_]
|
||||||
|
}
|
||||||
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
let op: UserOperation = msgOp(msg);
|
let op: UserOperation = msgOp(msg);
|
||||||
|
@ -211,12 +358,10 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
} else if (op == UserOperation.GetFollowedCommunities) {
|
} else if (op == UserOperation.GetFollowedCommunities) {
|
||||||
let res: GetFollowedCommunitiesResponse = msg;
|
let res: GetFollowedCommunitiesResponse = msg;
|
||||||
this.state.subscribedCommunities = res.communities;
|
this.state.subscribedCommunities = res.communities;
|
||||||
this.state.loading = false;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.ListCommunities) {
|
} else if (op == UserOperation.ListCommunities) {
|
||||||
let res: ListCommunitiesResponse = msg;
|
let res: ListCommunitiesResponse = msg;
|
||||||
this.state.trendingCommunities = res.communities;
|
this.state.trendingCommunities = res.communities;
|
||||||
this.state.loading = false;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.GetSite) {
|
} else if (op == UserOperation.GetSite) {
|
||||||
let res: GetSiteResponse = msg;
|
let res: GetSiteResponse = msg;
|
||||||
|
@ -234,6 +379,19 @@ export class Main extends Component<MainProps, MainState> {
|
||||||
this.state.site.site = res.site;
|
this.state.site.site = res.site;
|
||||||
this.state.showEditSite = false;
|
this.state.showEditSite = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
} 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;
|
||||||
|
let found = this.state.posts.find(c => c.id == res.post.id);
|
||||||
|
found.my_vote = res.post.my_vote;
|
||||||
|
found.score = res.post.score;
|
||||||
|
found.upvotes = res.post.upvotes;
|
||||||
|
found.downvotes = res.post.downvotes;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
{i.type_ == 'removed_communities' &&
|
{i.type_ == 'removed_communities' &&
|
||||||
<>
|
<>
|
||||||
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'}
|
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'}
|
||||||
<span> Community <Link to={`/f/${(i.data as ModRemoveCommunity).community_name}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span>
|
<span> Community <Link to={`/c/${(i.data as ModRemoveCommunity).community_name}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span>
|
||||||
<div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
|
<div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
|
||||||
<div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div>
|
<div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div>
|
||||||
</>
|
</>
|
||||||
|
@ -120,7 +120,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
<span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span>
|
<span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span>
|
||||||
<span><Link to={`/u/${(i.data as ModBanFromCommunity).other_user_name}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
|
<span><Link to={`/u/${(i.data as ModBanFromCommunity).other_user_name}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
|
||||||
<span> from the community </span>
|
<span> from the community </span>
|
||||||
<span><Link to={`/f/${(i.data as ModBanFromCommunity).community_name}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span>
|
<span><Link to={`/c/${(i.data as ModBanFromCommunity).community_name}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span>
|
||||||
<div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
|
<div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
|
||||||
<div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div>
|
<div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div>
|
||||||
</>
|
</>
|
||||||
|
@ -130,7 +130,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
<span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
|
<span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
|
||||||
<span><Link to={`/u/${(i.data as ModAddCommunity).other_user_name}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
|
<span><Link to={`/u/${(i.data as ModAddCommunity).other_user_name}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
|
||||||
<span> as a mod to the community </span>
|
<span> as a mod to the community </span>
|
||||||
<span><Link to={`/f/${(i.data as ModAddCommunity).community_name}`}>{(i.data as ModAddCommunity).community_name}</Link></span>
|
<span><Link to={`/c/${(i.data as ModAddCommunity).community_name}`}>{(i.data as ModAddCommunity).community_name}</Link></span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{i.type_ == 'banned' &&
|
{i.type_ == 'banned' &&
|
||||||
|
@ -165,7 +165,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||||
<div>
|
<div>
|
||||||
<h5>
|
<h5>
|
||||||
{this.state.communityName && <Link className="text-white" to={`/f/${this.state.communityName}`}>/f/{this.state.communityName} </Link>}
|
{this.state.communityName && <Link className="text-white" to={`/c/${this.state.communityName}`}>/c/{this.state.communityName} </Link>}
|
||||||
<span>Modlog</span>
|
<span>Modlog</span>
|
||||||
</h5>
|
</h5>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType } from '../interfaces';
|
import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse } from '../interfaces';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp } from '../utils';
|
||||||
import { version } from '../version';
|
import { version } from '../version';
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ interface NavbarState {
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
expandUserDropdown: boolean;
|
expandUserDropdown: boolean;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
|
siteName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navbar extends Component<any, NavbarState> {
|
export class Navbar extends Component<any, NavbarState> {
|
||||||
|
@ -21,7 +22,8 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
isLoggedIn: (UserService.Instance.user !== undefined),
|
isLoggedIn: (UserService.Instance.user !== undefined),
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
expandUserDropdown: false
|
expandUserDropdown: false,
|
||||||
|
siteName: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -45,6 +47,8 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
(err) => console.error(err),
|
(err) => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
WebSocketService.Instance.getSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -59,30 +63,29 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO class active corresponding to current page
|
// TODO class active corresponding to current page
|
||||||
// TODO toggle css collapse
|
|
||||||
navbar() {
|
navbar() {
|
||||||
return (
|
return (
|
||||||
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3">
|
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3">
|
||||||
<a title={version} class="navbar-brand" href="#">
|
<Link title={version} class="navbar-brand" to="/">
|
||||||
<svg class="icon mr-2"><use xlinkHref="#icon-mouse"></use></svg>
|
<svg class="icon mr-2 mouse-icon"><use xlinkHref="#icon-mouse"></use></svg>
|
||||||
Lemmy
|
{this.state.siteName}
|
||||||
</a>
|
</Link>
|
||||||
<button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}>
|
<button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
|
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/communities">Forums</Link>
|
<Link class="nav-link" to="/communities">Communities</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/search">Search</Link>
|
<Link class="nav-link" to="/search">Search</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/create_post">Create Post</Link>
|
<Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}>Create Post</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/create_community">Create Forum</Link>
|
<Link class="nav-link" to="/create_community">Create Community</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto mr-2">
|
<ul class="navbar-nav ml-auto mr-2">
|
||||||
|
@ -145,6 +148,14 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
} else if (op == UserOperation.GetReplies) {
|
} else if (op == UserOperation.GetReplies) {
|
||||||
let res: GetRepliesResponse = msg;
|
let res: GetRepliesResponse = msg;
|
||||||
this.sendRepliesCount(res);
|
this.sendRepliesCount(res);
|
||||||
|
} else if (op == UserOperation.GetSite) {
|
||||||
|
let res: GetSiteResponse = msg;
|
||||||
|
|
||||||
|
if (res.site) {
|
||||||
|
this.state.siteName = res.site.name;
|
||||||
|
WebSocketService.Instance.site = res.site;
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,6 +176,10 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentLocation() {
|
||||||
|
return this.context.router.history.location.pathname;
|
||||||
|
}
|
||||||
|
|
||||||
sendRepliesCount(res: GetRepliesResponse) {
|
sendRepliesCount(res: GetRepliesResponse) {
|
||||||
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length});
|
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { Subscription } from "rxjs";
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType } from '../interfaces';
|
import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType } from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp, getPageTitle } from '../utils';
|
||||||
import * as autosize from 'autosize';
|
import * as autosize from 'autosize';
|
||||||
|
|
||||||
interface PostFormProps {
|
interface PostFormProps {
|
||||||
post?: Post; // If a post is given, that means this is an edit
|
post?: Post; // If a post is given, that means this is an edit
|
||||||
|
prevCommunityName?: string;
|
||||||
onCancel?(): any;
|
onCancel?(): any;
|
||||||
onCreate?(id: number): any;
|
onCreate?(id: number): any;
|
||||||
onEdit?(post: Post): any;
|
onEdit?(post: Post): any;
|
||||||
|
@ -17,6 +18,7 @@ interface PostFormState {
|
||||||
postForm: PostFormI;
|
postForm: PostFormI;
|
||||||
communities: Array<Community>;
|
communities: Array<Community>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
suggestedTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
|
@ -30,7 +32,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
|
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
|
||||||
},
|
},
|
||||||
communities: [],
|
communities: [],
|
||||||
loading: false
|
loading: false,
|
||||||
|
suggestedTitle: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -82,6 +85,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
<label class="col-sm-2 col-form-label">URL</label>
|
<label class="col-sm-2 col-form-label">URL</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, this.handlePostUrlChange)} />
|
<input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, this.handlePostUrlChange)} />
|
||||||
|
{this.state.suggestedTitle &&
|
||||||
|
<span class="text-muted small font-weight-bold pointer" onClick={linkEvent(this, this.copySuggestedTitle)}>copy suggested title: {this.state.suggestedTitle}</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
|
@ -93,13 +99,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">Body</label>
|
<label class="col-sm-2 col-form-label">Body</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
|
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} maxLength={10000} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Cant change a community from an edit */}
|
{/* Cant change a community from an edit */}
|
||||||
{!this.props.post &&
|
{!this.props.post &&
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">Forum</label>
|
<label class="col-sm-2 col-form-label">Community</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
|
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
|
||||||
{this.state.communities.map(community =>
|
{this.state.communities.map(community =>
|
||||||
|
@ -134,8 +140,18 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copySuggestedTitle(i: PostForm) {
|
||||||
|
i.state.postForm.name = i.state.suggestedTitle;
|
||||||
|
i.state.suggestedTitle = undefined;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
handlePostUrlChange(i: PostForm, event: any) {
|
handlePostUrlChange(i: PostForm, event: any) {
|
||||||
i.state.postForm.url = event.target.value;
|
i.state.postForm.url = event.target.value;
|
||||||
|
getPageTitle(i.state.postForm.url).then(d => {
|
||||||
|
i.state.suggestedTitle = d;
|
||||||
|
i.setState(i.state);
|
||||||
|
});
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +186,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
this.state.communities = res.communities;
|
this.state.communities = res.communities;
|
||||||
if (this.props.post) {
|
if (this.props.post) {
|
||||||
this.state.postForm.community_id = this.props.post.community_id;
|
this.state.postForm.community_id = this.props.post.community_id;
|
||||||
|
} else if (this.props.prevCommunityName) {
|
||||||
|
let foundCommunityId = res.communities.find(r => r.name == this.props.prevCommunityName).id;
|
||||||
|
this.state.postForm.community_id = foundCommunityId;
|
||||||
} else {
|
} else {
|
||||||
this.state.postForm.community_id = res.communities[0].id;
|
this.state.postForm.community_id = res.communities[0].id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{post.removed &&
|
{post.removed &&
|
||||||
<small className="ml-2 text-muted font-italic">removed</small>
|
<small className="ml-2 text-muted font-italic">removed</small>
|
||||||
}
|
}
|
||||||
|
{post.deleted &&
|
||||||
|
<small className="ml-2 text-muted font-italic">deleted</small>
|
||||||
|
}
|
||||||
{post.locked &&
|
{post.locked &&
|
||||||
<small className="ml-2 text-muted font-italic">locked</small>
|
<small className="ml-2 text-muted font-italic">locked</small>
|
||||||
}
|
}
|
||||||
|
@ -118,7 +121,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.props.showCommunity &&
|
{this.props.showCommunity &&
|
||||||
<span>
|
<span>
|
||||||
<span> to </span>
|
<span> to </span>
|
||||||
<Link to={`/f/${post.community_name}`}>{post.community_name}</Link>
|
<Link to={`/c/${post.community_name}`}>{post.community_name}</Link>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
|
@ -140,7 +143,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{UserService.Instance.user && this.props.editable &&
|
{UserService.Instance.user && this.props.editable &&
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{this.props.post.saved ? 'unsave' : 'save'}</span>
|
<span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? 'unsave' : 'save'}</span>
|
||||||
</li>
|
</li>
|
||||||
{this.myPost &&
|
{this.myPost &&
|
||||||
<>
|
<>
|
||||||
|
@ -148,7 +151,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
|
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
|
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
||||||
|
{!post.deleted ? 'delete' : 'restore'}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -237,12 +242,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
handleDeleteClick(i: PostListing) {
|
handleDeleteClick(i: PostListing) {
|
||||||
let deleteForm: PostFormI = {
|
let deleteForm: PostFormI = {
|
||||||
body: '',
|
body: i.props.post.body,
|
||||||
community_id: i.props.post.community_id,
|
community_id: i.props.post.community_id,
|
||||||
name: "deleted",
|
name: i.props.post.name,
|
||||||
url: '',
|
url: i.props.post.url,
|
||||||
edit_id: i.props.post.id,
|
edit_id: i.props.post.id,
|
||||||
creator_id: i.props.post.creator_id,
|
creator_id: i.props.post.creator_id,
|
||||||
|
deleted: !i.props.post.deleted,
|
||||||
auth: null
|
auth: null
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPost(deleteForm);
|
WebSocketService.Instance.editPost(deleteForm);
|
||||||
|
|
|
@ -1,183 +1,28 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Post } from '../interfaces';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
|
||||||
import { UserOperation, Post, GetPostsForm, SortType, ListingType, GetPostsResponse, CreatePostLikeResponse, CommunityUser} from '../interfaces';
|
|
||||||
import { WebSocketService, UserService } from '../services';
|
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { msgOp, fetchLimit } from '../utils';
|
|
||||||
|
|
||||||
interface PostListingsProps {
|
interface PostListingsProps {
|
||||||
type?: ListingType;
|
|
||||||
communityId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PostListingsState {
|
|
||||||
moderators: Array<CommunityUser>;
|
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
sortType: SortType;
|
showCommunity?: boolean;
|
||||||
type_: ListingType;
|
|
||||||
page: number;
|
|
||||||
loading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostListings extends Component<PostListingsProps, PostListingsState> {
|
export class PostListings extends Component<PostListingsProps, any> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
|
||||||
private emptyState: PostListingsState = {
|
|
||||||
moderators: [],
|
|
||||||
posts: [],
|
|
||||||
sortType: SortType.Hot,
|
|
||||||
type_: (this.props.type !== undefined && UserService.Instance.user) ? this.props.type :
|
|
||||||
this.props.communityId
|
|
||||||
? ListingType.Community
|
|
||||||
: UserService.Instance.user
|
|
||||||
? ListingType.Subscribed
|
|
||||||
: ListingType.All,
|
|
||||||
page: 1,
|
|
||||||
loading: true
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
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),
|
|
||||||
(err) => console.error(err),
|
|
||||||
() => console.log('complete')
|
|
||||||
);
|
|
||||||
|
|
||||||
this.refetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.subscription.unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.loading ?
|
{this.props.posts.length > 0 ? this.props.posts.map(post =>
|
||||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<PostListing post={post} showCommunity={this.props.showCommunity} />) :
|
||||||
<div>
|
<div>No posts. {this.props.showCommunity !== undefined && <span>Subscribe to some <Link to="/communities">communities</Link>.</span>}
|
||||||
{this.selects()}
|
|
||||||
{this.state.posts.length > 0
|
|
||||||
? this.state.posts.map(post =>
|
|
||||||
<PostListing post={post} showCommunity={!this.props.communityId}/>)
|
|
||||||
: <div>No Listings. {!this.props.communityId && <span>Subscribe to some <Link to="/communities">forums</Link>.</span>}</div>
|
|
||||||
}
|
|
||||||
{this.paginator()}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
selects() {
|
|
||||||
return (
|
|
||||||
<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={SortType.Hot}>Hot</option>
|
|
||||||
<option value={SortType.New}>New</option>
|
|
||||||
<option disabled>──────────</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.user &&
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
paginator() {
|
|
||||||
return (
|
|
||||||
<div class="mt-2">
|
|
||||||
{this.state.page > 1 &&
|
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
|
|
||||||
}
|
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPage(i: PostListings) {
|
|
||||||
i.state.page++;
|
|
||||||
i.setState(i.state);
|
|
||||||
i.refetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
prevPage(i: PostListings) {
|
|
||||||
i.state.page--;
|
|
||||||
i.setState(i.state);
|
|
||||||
i.refetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSortChange(i: PostListings, event: any) {
|
|
||||||
i.state.sortType = Number(event.target.value);
|
|
||||||
i.state.page = 1;
|
|
||||||
i.setState(i.state);
|
|
||||||
i.refetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
refetch() {
|
|
||||||
let getPostsForm: GetPostsForm = {
|
|
||||||
community_id: this.props.communityId,
|
|
||||||
page: this.state.page,
|
|
||||||
limit: fetchLimit,
|
|
||||||
sort: SortType[this.state.sortType],
|
|
||||||
type_: ListingType[this.state.type_]
|
|
||||||
}
|
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTypeChange(i: PostListings, event: any) {
|
|
||||||
i.state.type_ = Number(event.target.value);
|
|
||||||
i.state.page = 1;
|
|
||||||
if (i.state.type_ == ListingType.All) {
|
|
||||||
i.context.router.history.push('/all');
|
|
||||||
} else {
|
|
||||||
i.context.router.history.push('/');
|
|
||||||
}
|
|
||||||
i.setState(i.state);
|
|
||||||
i.refetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
|
||||||
console.log(msg);
|
|
||||||
let op: UserOperation = msgOp(msg);
|
|
||||||
if (msg.error) {
|
|
||||||
alert(msg.error);
|
|
||||||
return;
|
|
||||||
} 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;
|
|
||||||
let found = this.state.posts.find(c => c.id == res.post.id);
|
|
||||||
found.my_vote = res.post.my_vote;
|
|
||||||
found.score = res.post.score;
|
|
||||||
found.upvotes = res.post.upvotes;
|
|
||||||
found.downvotes = res.post.downvotes;
|
|
||||||
this.setState(this.state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -244,6 +244,7 @@ export class Post extends Component<any, PostState> {
|
||||||
found.content = res.comment.content;
|
found.content = res.comment.content;
|
||||||
found.updated = res.comment.updated;
|
found.updated = res.comment.updated;
|
||||||
found.removed = res.comment.removed;
|
found.removed = res.comment.removed;
|
||||||
|
found.deleted = res.comment.deleted;
|
||||||
found.upvotes = res.comment.upvotes;
|
found.upvotes = res.comment.upvotes;
|
||||||
found.downvotes = res.comment.downvotes;
|
found.downvotes = res.comment.downvotes;
|
||||||
found.score = res.comment.score;
|
found.score = res.comment.score;
|
||||||
|
|
|
@ -97,13 +97,13 @@ export class Search extends Component<any, SearchState> {
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select w-auto">
|
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto">
|
||||||
<option disabled>Type</option>
|
<option disabled>Type</option>
|
||||||
<option value={SearchType.Both}>Both</option>
|
<option value={SearchType.Both}>Both</option>
|
||||||
<option value={SearchType.Comments}>Comments</option>
|
<option value={SearchType.Comments}>Comments</option>
|
||||||
<option value={SearchType.Posts}>Posts</option>
|
<option value={SearchType.Posts}>Posts</option>
|
||||||
</select>
|
</select>
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
|
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
||||||
<option disabled>Sort Type</option>
|
<option disabled>Sort Type</option>
|
||||||
<option value={SortType.New}>New</option>
|
<option value={SortType.New}>New</option>
|
||||||
<option value={SortType.TopDay}>Top Day</option>
|
<option value={SortType.TopDay}>Top Day</option>
|
||||||
|
|
|
@ -20,6 +20,7 @@ export class Setup extends Component<any, State> {
|
||||||
username: undefined,
|
username: undefined,
|
||||||
password: undefined,
|
password: undefined,
|
||||||
password_verify: undefined,
|
password_verify: undefined,
|
||||||
|
spam_timeri: 3000,
|
||||||
admin: true,
|
admin: true,
|
||||||
},
|
},
|
||||||
doneRegisteringUser: false,
|
doneRegisteringUser: false,
|
||||||
|
|
|
@ -56,8 +56,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
{community.removed &&
|
{community.removed &&
|
||||||
<small className="ml-2 text-muted font-italic">removed</small>
|
<small className="ml-2 text-muted font-italic">removed</small>
|
||||||
}
|
}
|
||||||
|
{community.deleted &&
|
||||||
|
<small className="ml-2 text-muted font-italic">deleted</small>
|
||||||
|
}
|
||||||
</h5>
|
</h5>
|
||||||
<Link className="text-muted" to={`/f/${community.name}`}>/f/{community.name}</Link>
|
<Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link>
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
{this.canMod &&
|
{this.canMod &&
|
||||||
<>
|
<>
|
||||||
|
@ -66,7 +69,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
</li>
|
</li>
|
||||||
{this.amCreator &&
|
{this.amCreator &&
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */}
|
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
||||||
|
{!community.deleted ? 'delete' : 'restore'}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
@ -142,9 +147,18 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO no deleting communities yet
|
handleDeleteClick(i: Sidebar) {
|
||||||
// handleDeleteClick(i: Sidebar, event) {
|
event.preventDefault();
|
||||||
// }
|
let deleteForm: CommunityFormI = {
|
||||||
|
name: i.props.community.name,
|
||||||
|
title: i.props.community.title,
|
||||||
|
category_id: i.props.community.category_id,
|
||||||
|
edit_id: i.props.community.id,
|
||||||
|
deleted: !i.props.community.deleted,
|
||||||
|
auth: null,
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.editCommunity(deleteForm);
|
||||||
|
}
|
||||||
|
|
||||||
handleUnsubscribe(communityId: number) {
|
handleUnsubscribe(communityId: number) {
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
|
@ -174,9 +188,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
return UserService.Instance.user && this.props.admins.map(a => a.id).includes(UserService.Instance.user.id);
|
return UserService.Instance.user && this.props.admins.map(a => a.id).includes(UserService.Instance.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModRemoveShow(i: Sidebar) {
|
handleModRemoveShow(i: Sidebar) {
|
||||||
i.state.showRemoveDialog = true;
|
i.state.showRemoveDialog = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label">Sidebar</label>
|
<label class="col-12 col-form-label">Sidebar</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} />
|
<textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { Component } from 'inferno';
|
||||||
let general =
|
let general =
|
||||||
[
|
[
|
||||||
"Nathan J. Goode",
|
"Nathan J. Goode",
|
||||||
"Eduardo Cavazos"
|
|
||||||
];
|
];
|
||||||
// let highlighted = [];
|
// let highlighted = [];
|
||||||
// let silver = [];
|
// let silver = [];
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Subscription } from "rxjs";
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces';
|
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { msgOp, fetchLimit } from '../utils';
|
import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
|
@ -25,6 +25,7 @@ interface UserState {
|
||||||
view: View;
|
view: View;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class User extends Component<any, UserState> {
|
export class User extends Component<any, UserState> {
|
||||||
|
@ -47,9 +48,10 @@ export class User extends Component<any, UserState> {
|
||||||
moderates: [],
|
moderates: [],
|
||||||
comments: [],
|
comments: [],
|
||||||
posts: [],
|
posts: [],
|
||||||
view: View.Overview,
|
loading: true,
|
||||||
sort: SortType.New,
|
view: this.getViewFromProps(this.props),
|
||||||
page: 1,
|
sort: this.getSortTypeFromProps(this.props),
|
||||||
|
page: this.getPageFromProps(this.props),
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -71,13 +73,42 @@ export class User extends Component<any, UserState> {
|
||||||
this.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getViewFromProps(props: any): View {
|
||||||
|
return (props.match.params.view) ?
|
||||||
|
View[capitalizeFirstLetter(props.match.params.view)] :
|
||||||
|
View.Overview;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortTypeFromProps(props: any): SortType {
|
||||||
|
return (props.match.params.sort) ?
|
||||||
|
routeSortTypeToEnum(props.match.params.sort) :
|
||||||
|
SortType.New;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageFromProps(props: any): number {
|
||||||
|
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Necessary for back button for some reason
|
||||||
|
componentWillReceiveProps(nextProps: any) {
|
||||||
|
if (nextProps.history.action == 'POP') {
|
||||||
|
this.state = this.emptyState;
|
||||||
|
this.state.view = this.getViewFromProps(nextProps);
|
||||||
|
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||||
|
this.state.page = this.getPageFromProps(nextProps);
|
||||||
|
this.refetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{this.state.loading ?
|
||||||
|
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-9">
|
<div class="col-12 col-md-9">
|
||||||
<h5>/u/{this.state.user.name}</h5>
|
<h5>/u/{this.state.user.name}</h5>
|
||||||
|
@ -102,6 +133,7 @@ export class User extends Component<any, UserState> {
|
||||||
{this.follows()}
|
{this.follows()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -109,14 +141,14 @@ export class User extends Component<any, UserState> {
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select w-auto">
|
<select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto">
|
||||||
<option disabled>View</option>
|
<option disabled>View</option>
|
||||||
<option value={View.Overview}>Overview</option>
|
<option value={View.Overview}>Overview</option>
|
||||||
<option value={View.Comments}>Comments</option>
|
<option value={View.Comments}>Comments</option>
|
||||||
<option value={View.Posts}>Posts</option>
|
<option value={View.Posts}>Posts</option>
|
||||||
<option value={View.Saved}>Saved</option>
|
<option value={View.Saved}>Saved</option>
|
||||||
</select>
|
</select>
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
|
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
||||||
<option disabled>Sort Type</option>
|
<option disabled>Sort Type</option>
|
||||||
<option value={SortType.New}>New</option>
|
<option value={SortType.New}>New</option>
|
||||||
<option value={SortType.TopDay}>Top Day</option>
|
<option value={SortType.TopDay}>Top Day</option>
|
||||||
|
@ -209,7 +241,7 @@ export class User extends Component<any, UserState> {
|
||||||
<h5>Moderates</h5>
|
<h5>Moderates</h5>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
{this.state.moderates.map(community =>
|
{this.state.moderates.map(community =>
|
||||||
<li><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li>
|
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -227,7 +259,7 @@ export class User extends Component<any, UserState> {
|
||||||
<h5>Subscribed</h5>
|
<h5>Subscribed</h5>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
{this.state.follows.map(community =>
|
{this.state.follows.map(community =>
|
||||||
<li><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li>
|
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -247,15 +279,23 @@ export class User extends Component<any, UserState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUrl() {
|
||||||
|
let viewStr = View[this.state.view].toLowerCase();
|
||||||
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
|
this.props.history.push(`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`);
|
||||||
|
}
|
||||||
|
|
||||||
nextPage(i: User) {
|
nextPage(i: User) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: User) {
|
prevPage(i: User) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +315,7 @@ export class User extends Component<any, UserState> {
|
||||||
i.state.sort = Number(event.target.value);
|
i.state.sort = Number(event.target.value);
|
||||||
i.state.page = 1;
|
i.state.page = 1;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,6 +323,7 @@ export class User extends Component<any, UserState> {
|
||||||
i.state.view = Number(event.target.value);
|
i.state.view = Number(event.target.value);
|
||||||
i.state.page = 1;
|
i.state.page = 1;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
i.updateUrl();
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,6 +340,7 @@ export class User extends Component<any, UserState> {
|
||||||
this.state.follows = res.follows;
|
this.state.follows = res.follows;
|
||||||
this.state.moderates = res.moderates;
|
this.state.moderates = res.moderates;
|
||||||
this.state.posts = res.posts;
|
this.state.posts = res.posts;
|
||||||
|
this.state.loading = false;
|
||||||
document.title = `/u/${this.state.user.name} - Lemmy`;
|
document.title = `/u/${this.state.user.name} - Lemmy`;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.EditComment) {
|
} else if (op == UserOperation.EditComment) {
|
||||||
|
@ -307,6 +350,7 @@ export class User extends Component<any, UserState> {
|
||||||
found.content = res.comment.content;
|
found.content = res.comment.content;
|
||||||
found.updated = res.comment.updated;
|
found.updated = res.comment.updated;
|
||||||
found.removed = res.comment.removed;
|
found.removed = res.comment.removed;
|
||||||
|
found.deleted = res.comment.deleted;
|
||||||
found.upvotes = res.comment.upvotes;
|
found.upvotes = res.comment.upvotes;
|
||||||
found.downvotes = res.comment.downvotes;
|
found.downvotes = res.comment.downvotes;
|
||||||
found.score = res.comment.score;
|
found.score = res.comment.score;
|
||||||
|
|
|
@ -87,6 +87,10 @@ blockquote {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mouse-icon {
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
.new-comments {
|
.new-comments {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
|
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
|
||||||
|
<link rel="apple-touch-icon" href="/static/assets/apple-touch-icon.png" />
|
||||||
<title>Lemmy</title>
|
<title>Lemmy</title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { render, Component } from 'inferno';
|
import { render, Component } from 'inferno';
|
||||||
import { HashRouter, Route, Switch } from 'inferno-router';
|
import { HashRouter, BrowserRouter, Route, Switch } from 'inferno-router';
|
||||||
|
import { Main } from './components/main';
|
||||||
import { Navbar } from './components/navbar';
|
import { Navbar } from './components/navbar';
|
||||||
import { Footer } from './components/footer';
|
import { Footer } from './components/footer';
|
||||||
import { Home } from './components/home';
|
|
||||||
import { Login } from './components/login';
|
import { Login } from './components/login';
|
||||||
import { CreatePost } from './components/create-post';
|
import { CreatePost } from './components/create-post';
|
||||||
import { CreateCommunity } from './components/create-community';
|
import { CreateCommunity } from './components/create-community';
|
||||||
|
@ -37,18 +36,21 @@ class Index extends Component<any, any> {
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div class="mt-3 p-0">
|
<div class="mt-1 p-0">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/all" component={Home} />
|
<Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} />
|
||||||
<Route exact path="/" component={Home} />
|
<Route exact path={`/`} component={Main} />
|
||||||
<Route path={`/login`} component={Login} />
|
<Route path={`/login`} component={Login} />
|
||||||
<Route path={`/create_post`} component={CreatePost} />
|
<Route path={`/create_post`} component={CreatePost} />
|
||||||
<Route path={`/create_community`} component={CreateCommunity} />
|
<Route path={`/create_community`} component={CreateCommunity} />
|
||||||
|
<Route path={`/communities/page/:page`} component={Communities} />
|
||||||
<Route path={`/communities`} component={Communities} />
|
<Route path={`/communities`} component={Communities} />
|
||||||
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
||||||
<Route path={`/post/:id`} component={Post} />
|
<Route path={`/post/:id`} component={Post} />
|
||||||
|
<Route path={`/c/:name/sort/:sort/page/:page`} component={Community} />
|
||||||
<Route path={`/community/:id`} component={Community} />
|
<Route path={`/community/:id`} component={Community} />
|
||||||
<Route path={`/f/:name`} component={Community} />
|
<Route path={`/c/:name`} component={Community} />
|
||||||
|
<Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} />
|
||||||
<Route path={`/user/:id`} component={User} />
|
<Route path={`/user/:id`} component={User} />
|
||||||
<Route path={`/u/:username`} component={User} />
|
<Route path={`/u/:username`} component={User} />
|
||||||
<Route path={`/inbox`} component={Inbox} />
|
<Route path={`/inbox`} component={Inbox} />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export enum UserOperation {
|
export enum UserOperation {
|
||||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search
|
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CommentSortType {
|
export enum CommentSortType {
|
||||||
|
@ -52,6 +52,7 @@ export interface Community {
|
||||||
category_id: number;
|
category_id: number;
|
||||||
creator_id: number;
|
creator_id: number;
|
||||||
removed: boolean;
|
removed: boolean;
|
||||||
|
deleted: boolean;
|
||||||
published: string;
|
published: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
@ -71,6 +72,7 @@ export interface Post {
|
||||||
creator_id: number;
|
creator_id: number;
|
||||||
community_id: number;
|
community_id: number;
|
||||||
removed: boolean;
|
removed: boolean;
|
||||||
|
deleted: boolean;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
published: string;
|
published: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
|
@ -96,6 +98,7 @@ export interface Comment {
|
||||||
parent_id?: number;
|
parent_id?: number;
|
||||||
content: string;
|
content: string;
|
||||||
removed: boolean;
|
removed: boolean;
|
||||||
|
deleted: boolean;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
published: string;
|
published: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
|
@ -348,6 +351,7 @@ export interface CommunityForm {
|
||||||
category_id: number,
|
category_id: number,
|
||||||
edit_id?: number;
|
edit_id?: number;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
expires?: number;
|
expires?: number;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
|
@ -392,6 +396,7 @@ export interface PostForm {
|
||||||
edit_id?: number;
|
edit_id?: number;
|
||||||
creator_id: number;
|
creator_id: number;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
auth: string;
|
auth: string;
|
||||||
|
@ -424,6 +429,7 @@ export interface CommentForm {
|
||||||
edit_id?: number;
|
edit_id?: number;
|
||||||
creator_id: number;
|
creator_id: number;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
read?: boolean;
|
read?: boolean;
|
||||||
auth: string;
|
auth: string;
|
||||||
|
|
|
@ -147,6 +147,7 @@ export class WebSocketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserDetails(form: GetUserDetailsForm) {
|
public getUserDetails(form: GetUserDetailsForm) {
|
||||||
|
this.setAuth(form, false);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +178,12 @@ export class WebSocketService {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.Search, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.Search, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public markAllAsRead() {
|
||||||
|
let form = {};
|
||||||
|
this.setAuth(form);
|
||||||
|
this.subject.next(this.wsSendWrapper(UserOperation.MarkAllAsRead, form));
|
||||||
|
}
|
||||||
|
|
||||||
private wsSendWrapper(op: UserOperation, data: any) {
|
private wsSendWrapper(op: UserOperation, data: any) {
|
||||||
let send = { op: UserOperation[op], data: data };
|
let send = { op: UserOperation[op], data: data };
|
||||||
console.log(send);
|
console.log(send);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { UserOperation, Comment, User } from './interfaces';
|
import { UserOperation, Comment, User, SortType, ListingType } from './interfaces';
|
||||||
import * as markdown_it from 'markdown-it';
|
import * as markdown_it from 'markdown-it';
|
||||||
|
import * as markdown_it_container from 'markdown-it-container';
|
||||||
|
|
||||||
export let repoUrl = 'https://github.com/dessalines/lemmy';
|
export let repoUrl = 'https://github.com/dessalines/lemmy';
|
||||||
|
|
||||||
|
@ -12,6 +13,23 @@ var md = new markdown_it({
|
||||||
html: true,
|
html: true,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
typographer: true
|
typographer: true
|
||||||
|
}).use(markdown_it_container, 'spoiler', {
|
||||||
|
validate: function(params: any) {
|
||||||
|
return params.trim().match(/^spoiler\s+(.*)$/);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function (tokens: any, idx: any) {
|
||||||
|
var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
|
||||||
|
|
||||||
|
if (tokens[idx].nesting === 1) {
|
||||||
|
// opening tag
|
||||||
|
return '<details><summary>' + md.utils.escapeHtml(m[1]) + '</summary>\n';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// closing tag
|
||||||
|
return '</details>\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function hotRank(comment: Comment): number {
|
export function hotRank(comment: Comment): number {
|
||||||
|
@ -67,3 +85,35 @@ export function isImage(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export let fetchLimit: number = 20;
|
export let fetchLimit: number = 20;
|
||||||
|
|
||||||
|
export function capitalizeFirstLetter(str: string): string {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function routeSortTypeToEnum(sort: string): SortType {
|
||||||
|
if (sort == 'new') {
|
||||||
|
return SortType.New;
|
||||||
|
} else if (sort == 'hot') {
|
||||||
|
return SortType.Hot;
|
||||||
|
} else if (sort == 'topday') {
|
||||||
|
return SortType.TopDay;
|
||||||
|
} else if (sort == 'topweek') {
|
||||||
|
return SortType.TopWeek;
|
||||||
|
} else if (sort == 'topmonth') {
|
||||||
|
return SortType.TopMonth;
|
||||||
|
} else if (sort == 'topall') {
|
||||||
|
return SortType.TopAll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function routeListingTypeToEnum(type: string): ListingType {
|
||||||
|
return ListingType[capitalizeFirstLetter(type)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPageTitle(url: string) {
|
||||||
|
let res = await fetch(`https://textance.herokuapp.com/title/${url}`);
|
||||||
|
let data = await res.text();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export let version: string = "v0.0.3-37-g5c8e23b";
|
export let version: string = "v0.0.3-91-gb304b48";
|
14
ui/yarn.lock
14
ui/yarn.lock
|
@ -38,7 +38,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
|
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
|
||||||
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
|
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
|
||||||
|
|
||||||
"@types/markdown-it@^0.0.7":
|
"@types/markdown-it-container@^2.0.2":
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/markdown-it-container/-/markdown-it-container-2.0.2.tgz#0e624653415a1c2f088a5ae51f7bfff480c03f49"
|
||||||
|
integrity sha512-T770GL+zJz8Ssh1NpLiOruYhrU96yb8ovPSegLrWY5XIkJc6PVVC7kH/oQaVD0rkePpWMFJK018OgS/pwviOMw==
|
||||||
|
dependencies:
|
||||||
|
"@types/markdown-it" "*"
|
||||||
|
|
||||||
|
"@types/markdown-it@*", "@types/markdown-it@^0.0.7":
|
||||||
version "0.0.7"
|
version "0.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
|
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
|
||||||
integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
|
integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
|
||||||
|
@ -1674,6 +1681,11 @@ map-visit@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
|
markdown-it-container@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-2.0.0.tgz#0019b43fd02eefece2f1960a2895fba81a404695"
|
||||||
|
integrity sha1-ABm0P9Au7+zi8ZYKKJX7qBpARpU=
|
||||||
|
|
||||||
markdown-it@^8.4.2:
|
markdown-it@^8.4.2:
|
||||||
version "8.4.2"
|
version "8.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
|
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
|
||||||
|
|
Loading…
Reference in a new issue