mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-27 06:41:18 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
1328dfea5f
49 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).
|
||||
- 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/).
|
||||
|
||||
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 read: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
|
@ -37,7 +38,8 @@ pub struct CommentForm {
|
|||
pub content: String,
|
||||
pub removed: 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 {
|
||||
|
@ -186,6 +188,7 @@ mod tests {
|
|||
category_id: 1,
|
||||
creator_id: inserted_user.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
updated: None
|
||||
};
|
||||
|
||||
|
@ -198,6 +201,7 @@ mod tests {
|
|||
body: None,
|
||||
community_id: inserted_community.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
locked: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -209,6 +213,7 @@ mod tests {
|
|||
creator_id: inserted_user.id,
|
||||
post_id: inserted_post.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
read: None,
|
||||
parent_id: None,
|
||||
updated: None
|
||||
|
@ -222,6 +227,7 @@ mod tests {
|
|||
creator_id: inserted_user.id,
|
||||
post_id: inserted_post.id,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
read: false,
|
||||
parent_id: None,
|
||||
published: inserted_comment.published,
|
||||
|
@ -234,6 +240,7 @@ mod tests {
|
|||
post_id: inserted_post.id,
|
||||
parent_id: Some(inserted_comment.id),
|
||||
removed: None,
|
||||
deleted: None,
|
||||
read: None,
|
||||
updated: None
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ table! {
|
|||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
community_id -> Int4,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
|
@ -42,6 +43,7 @@ pub struct CommentView {
|
|||
pub read: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
pub community_id: i32,
|
||||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
|
@ -115,6 +117,7 @@ impl CommentView {
|
|||
_ => query.order_by(published.desc())
|
||||
};
|
||||
|
||||
// Note: deleted and removed comments are done on the front side
|
||||
query
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
@ -153,6 +156,7 @@ table! {
|
|||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
community_id -> Int4,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
|
@ -179,6 +183,7 @@ pub struct ReplyView {
|
|||
pub read: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
pub community_id: i32,
|
||||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
|
@ -275,6 +280,7 @@ mod tests {
|
|||
category_id: 1,
|
||||
creator_id: inserted_user.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
updated: None
|
||||
};
|
||||
|
||||
|
@ -287,6 +293,7 @@ mod tests {
|
|||
body: None,
|
||||
community_id: inserted_community.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
locked: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -299,6 +306,7 @@ mod tests {
|
|||
post_id: inserted_post.id,
|
||||
parent_id: None,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
read: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -322,6 +330,7 @@ mod tests {
|
|||
community_id: inserted_community.id,
|
||||
parent_id: None,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
read: false,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
|
@ -344,6 +353,7 @@ mod tests {
|
|||
community_id: inserted_community.id,
|
||||
parent_id: None,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
read: false,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
|
|
|
@ -16,7 +16,8 @@ pub struct Community {
|
|||
pub creator_id: i32,
|
||||
pub removed: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
||||
|
@ -28,7 +29,8 @@ pub struct CommunityForm {
|
|||
pub category_id: i32,
|
||||
pub creator_id: i32,
|
||||
pub removed: Option<bool>,
|
||||
pub updated: Option<chrono::NaiveDateTime>
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: Option<bool>,
|
||||
}
|
||||
|
||||
impl Crud<CommunityForm> for Community {
|
||||
|
@ -245,6 +247,7 @@ mod tests {
|
|||
description: None,
|
||||
category_id: 1,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
|
@ -258,6 +261,7 @@ mod tests {
|
|||
description: None,
|
||||
category_id: 1,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
published: inserted_community.published,
|
||||
updated: None
|
||||
};
|
||||
|
|
|
@ -15,11 +15,13 @@ table! {
|
|||
removed -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
creator_name -> Varchar,
|
||||
category_name -> Varchar,
|
||||
number_of_subscribers -> BigInt,
|
||||
number_of_posts -> BigInt,
|
||||
number_of_comments -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
}
|
||||
|
@ -85,11 +87,13 @@ pub struct CommunityView {
|
|||
pub removed: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
pub creator_name: String,
|
||||
pub category_name: String,
|
||||
pub number_of_subscribers: i64,
|
||||
pub number_of_posts: i64,
|
||||
pub number_of_comments: i64,
|
||||
pub hot_rank: i32,
|
||||
pub user_id: Option<i32>,
|
||||
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
|
||||
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::TopAll => {
|
||||
match from_user_id {
|
||||
|
@ -139,6 +144,7 @@ impl CommunityView {
|
|||
.limit(limit)
|
||||
.offset(offset)
|
||||
.filter(removed.eq(false))
|
||||
.filter(deleted.eq(false))
|
||||
.load::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -442,6 +442,7 @@ mod tests {
|
|||
category_id: 1,
|
||||
creator_id: inserted_user.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
updated: None
|
||||
};
|
||||
|
||||
|
@ -454,6 +455,7 @@ mod tests {
|
|||
creator_id: inserted_user.id,
|
||||
community_id: inserted_community.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
locked: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -465,6 +467,7 @@ mod tests {
|
|||
creator_id: inserted_user.id,
|
||||
post_id: inserted_post.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
read: None,
|
||||
parent_id: None,
|
||||
updated: None
|
||||
|
|
|
@ -17,7 +17,8 @@ pub struct Post {
|
|||
pub removed: bool,
|
||||
pub locked: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
|
@ -30,7 +31,8 @@ pub struct PostForm {
|
|||
pub community_id: i32,
|
||||
pub removed: 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 {
|
||||
|
@ -199,6 +201,7 @@ mod tests {
|
|||
category_id: 1,
|
||||
creator_id: inserted_user.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
updated: None
|
||||
};
|
||||
|
||||
|
@ -211,6 +214,7 @@ mod tests {
|
|||
creator_id: inserted_user.id,
|
||||
community_id: inserted_community.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
locked: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -227,6 +231,7 @@ mod tests {
|
|||
published: inserted_post.published,
|
||||
removed: false,
|
||||
locked: false,
|
||||
deleted: false,
|
||||
updated: None
|
||||
};
|
||||
|
||||
|
|
|
@ -23,9 +23,11 @@ table! {
|
|||
locked -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
creator_name -> Varchar,
|
||||
community_name -> Varchar,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
number_of_comments -> BigInt,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
|
@ -53,9 +55,11 @@ pub struct PostView {
|
|||
pub locked: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub deleted: bool,
|
||||
pub creator_name: String,
|
||||
pub community_name: String,
|
||||
pub community_removed: bool,
|
||||
pub community_deleted: bool,
|
||||
pub number_of_comments: i64,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
|
@ -144,7 +148,9 @@ impl PostView {
|
|||
.limit(limit)
|
||||
.offset(offset)
|
||||
.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)
|
||||
}
|
||||
|
@ -206,6 +212,7 @@ mod tests {
|
|||
creator_id: inserted_user.id,
|
||||
category_id: 1,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
updated: None
|
||||
};
|
||||
|
||||
|
@ -218,6 +225,7 @@ mod tests {
|
|||
creator_id: inserted_user.id,
|
||||
community_id: inserted_community.id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
locked: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -258,9 +266,11 @@ mod tests {
|
|||
creator_name: user_name.to_owned(),
|
||||
community_id: inserted_community.id,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
locked: false,
|
||||
community_name: community_name.to_owned(),
|
||||
community_removed: false,
|
||||
community_deleted: false,
|
||||
number_of_comments: 0,
|
||||
score: 1,
|
||||
upvotes: 1,
|
||||
|
@ -281,12 +291,14 @@ mod tests {
|
|||
url: None,
|
||||
body: None,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
locked: false,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name.to_owned(),
|
||||
community_id: inserted_community.id,
|
||||
community_name: community_name.to_owned(),
|
||||
community_removed: false,
|
||||
community_deleted: false,
|
||||
number_of_comments: 0,
|
||||
score: 1,
|
||||
upvotes: 1,
|
||||
|
|
|
@ -29,7 +29,8 @@ fn chat_route(req: &HttpRequest<WsChatSessionState>) -> Result<HttpResponse, Err
|
|||
req,
|
||||
WSSession {
|
||||
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 {
|
||||
/// unique session id
|
||||
id: usize,
|
||||
ip: String,
|
||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||
/// otherwise we drop connection.
|
||||
hb: Instant
|
||||
|
@ -61,6 +63,7 @@ impl Actor for WSSession {
|
|||
.addr
|
||||
.send(Connect {
|
||||
addr: addr.recipient(),
|
||||
ip: self.ip.to_owned(),
|
||||
})
|
||||
.into_actor(self)
|
||||
.then(|res, act, ctx| {
|
||||
|
@ -76,7 +79,10 @@ impl Actor for WSSession {
|
|||
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +117,7 @@ impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
|||
.addr
|
||||
.send(StandardMessage {
|
||||
id: self.id,
|
||||
msg: m
|
||||
msg: m,
|
||||
})
|
||||
.into_actor(self)
|
||||
.then(|res, _, ctx| {
|
||||
|
@ -215,7 +221,7 @@ impl WSSession {
|
|||
// notify chat server
|
||||
ctx.state()
|
||||
.addr
|
||||
.do_send(Disconnect { id: act.id });
|
||||
.do_send(Disconnect { id: act.id, ip: act.ip.to_owned() });
|
||||
|
||||
// stop actor
|
||||
ctx.stop();
|
||||
|
|
|
@ -16,6 +16,7 @@ table! {
|
|||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +51,7 @@ table! {
|
|||
removed -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,6 +184,7 @@ table! {
|
|||
locked -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ use bcrypt::{verify};
|
|||
use std::str::FromStr;
|
||||
use diesel::PgConnection;
|
||||
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 actions::community::*;
|
||||
|
@ -25,9 +26,14 @@ use actions::user_view::*;
|
|||
use actions::moderator_views::*;
|
||||
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)]
|
||||
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)]
|
||||
|
@ -48,12 +54,14 @@ pub struct WSMessage(pub String);
|
|||
#[rtype(usize)]
|
||||
pub struct Connect {
|
||||
pub addr: Recipient<WSMessage>,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
/// Session is disconnected
|
||||
#[derive(Message)]
|
||||
pub struct Disconnect {
|
||||
pub id: usize,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
/// Send message to specific room
|
||||
|
@ -219,6 +227,7 @@ pub struct EditComment {
|
|||
creator_id: i32,
|
||||
post_id: i32,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
reason: Option<String>,
|
||||
read: Option<bool>,
|
||||
auth: String
|
||||
|
@ -268,6 +277,7 @@ pub struct EditPost {
|
|||
url: Option<String>,
|
||||
body: Option<String>,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
locked: Option<bool>,
|
||||
reason: Option<String>,
|
||||
auth: String
|
||||
|
@ -288,6 +298,7 @@ pub struct EditCommunity {
|
|||
description: Option<String>,
|
||||
category_id: i32,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
reason: Option<String>,
|
||||
expires: Option<i64>,
|
||||
auth: String
|
||||
|
@ -320,6 +331,7 @@ pub struct GetUserDetails {
|
|||
limit: Option<i64>,
|
||||
community_id: Option<i32>,
|
||||
saved_only: bool,
|
||||
auth: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -478,10 +490,27 @@ pub struct SearchResponse {
|
|||
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
|
||||
/// session. implementation is super primitive
|
||||
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
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
@ -493,6 +522,7 @@ impl Default for ChatServer {
|
|||
|
||||
ChatServer {
|
||||
sessions: HashMap::new(),
|
||||
rate_limits: HashMap::new(),
|
||||
rooms: rooms,
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
|
@ -505,8 +535,8 @@ impl ChatServer {
|
|||
if let Some(sessions) = self.rooms.get(&room) {
|
||||
for id in sessions {
|
||||
if *id != skip_id {
|
||||
if let Some(addr) = self.sessions.get(id) {
|
||||
let _ = addr.do_send(WSMessage(message.to_owned()));
|
||||
if let Some(info) = self.sessions.get(id) {
|
||||
let _ = info.addr.do_send(WSMessage(message.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -531,8 +561,51 @@ impl ChatServer {
|
|||
|
||||
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`
|
||||
impl Actor for ChatServer {
|
||||
/// 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 {
|
||||
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
|
||||
// self.send_room_message(&"Main".to_owned(), "Someone joined", 0);
|
||||
|
||||
// register session with random id
|
||||
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
|
||||
// self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id);
|
||||
|
@ -563,6 +652,7 @@ impl Handler<Connect> for ChatServer {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Handler for Disconnect message.
|
||||
impl Handler<Disconnect> for ChatServer {
|
||||
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)?;
|
||||
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 {
|
||||
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();
|
||||
|
||||
chat.check_rate_limit_register(addr)?;
|
||||
|
||||
// Make sure passwords match
|
||||
if &self.password != &self.password_verify {
|
||||
return Err(self.error("Passwords do not match."))?
|
||||
|
@ -871,10 +967,12 @@ impl Perform for 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();
|
||||
|
||||
chat.check_rate_limit_register(addr)?;
|
||||
|
||||
let claims = match Claims::decode(&self.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => {
|
||||
|
@ -903,6 +1001,7 @@ impl Perform for CreateCommunity {
|
|||
category_id: self.category_id,
|
||||
creator_id: user_id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
|
@ -1016,10 +1115,12 @@ impl Perform for 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();
|
||||
|
||||
chat.check_rate_limit_register(addr)?;
|
||||
|
||||
let claims = match Claims::decode(&self.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => {
|
||||
|
@ -1051,6 +1152,7 @@ impl Perform for CreatePost {
|
|||
community_id: self.community_id,
|
||||
creator_id: user_id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
locked: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -1227,6 +1329,8 @@ impl Perform for CreateComment {
|
|||
|
||||
let conn = establish_connection();
|
||||
|
||||
chat.check_rate_limit(addr)?;
|
||||
|
||||
let claims = match Claims::decode(&self.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => {
|
||||
|
@ -1255,6 +1359,7 @@ impl Perform for CreateComment {
|
|||
post_id: self.post_id,
|
||||
creator_id: user_id,
|
||||
removed: None,
|
||||
deleted: None,
|
||||
read: None,
|
||||
updated: None
|
||||
};
|
||||
|
@ -1371,9 +1476,10 @@ impl Perform for EditComment {
|
|||
post_id: self.post_id,
|
||||
creator_id: self.creator_id,
|
||||
removed: self.removed.to_owned(),
|
||||
deleted: self.deleted.to_owned(),
|
||||
read: self.read.to_owned(),
|
||||
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) {
|
||||
Ok(comment) => comment,
|
||||
|
@ -1483,6 +1589,8 @@ impl Perform for CreateCommentLike {
|
|||
|
||||
let conn = establish_connection();
|
||||
|
||||
chat.check_rate_limit(addr)?;
|
||||
|
||||
let claims = match Claims::decode(&self.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => {
|
||||
|
@ -1611,10 +1719,12 @@ impl Perform for 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();
|
||||
|
||||
chat.check_rate_limit(addr)?;
|
||||
|
||||
let claims = match Claims::decode(&self.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => {
|
||||
|
@ -1734,6 +1844,7 @@ impl Perform for EditPost {
|
|||
creator_id: self.creator_id.to_owned(),
|
||||
community_id: self.community_id,
|
||||
removed: self.removed.to_owned(),
|
||||
deleted: self.deleted.to_owned(),
|
||||
locked: self.locked.to_owned(),
|
||||
updated: Some(naive_now())
|
||||
};
|
||||
|
@ -1899,6 +2010,7 @@ impl Perform for EditCommunity {
|
|||
category_id: self.category_id.to_owned(),
|
||||
creator_id: user_id,
|
||||
removed: self.removed.to_owned(),
|
||||
deleted: self.deleted.to_owned(),
|
||||
updated: Some(naive_now())
|
||||
};
|
||||
|
||||
|
@ -2051,6 +2163,19 @@ impl Perform for GetUserDetails {
|
|||
|
||||
let conn = establish_connection();
|
||||
|
||||
let user_id: Option<i32> = match &self.auth {
|
||||
Some(auth) => {
|
||||
match Claims::decode(&auth) {
|
||||
Ok(claims) => {
|
||||
let user_id = claims.claims.id;
|
||||
Some(user_id)
|
||||
}
|
||||
Err(_e) => None
|
||||
}
|
||||
}
|
||||
None => None
|
||||
};
|
||||
|
||||
//TODO add save
|
||||
let sort = SortType::from_str(&self.sort)?;
|
||||
|
||||
|
@ -2081,7 +2206,7 @@ impl Perform for GetUserDetails {
|
|||
self.community_id,
|
||||
Some(user_details_id),
|
||||
None,
|
||||
None,
|
||||
user_id,
|
||||
self.saved_only,
|
||||
false,
|
||||
self.page,
|
||||
|
@ -2103,7 +2228,7 @@ impl Perform for GetUserDetails {
|
|||
None,
|
||||
Some(user_details_id),
|
||||
None,
|
||||
None,
|
||||
user_id,
|
||||
self.saved_only,
|
||||
self.page,
|
||||
self.limit)?
|
||||
|
@ -2663,7 +2788,7 @@ impl Perform for Search {
|
|||
},
|
||||
SearchType::Comments => {
|
||||
comments = CommentView::list(&conn,
|
||||
&sort,
|
||||
&sort,
|
||||
None,
|
||||
None,
|
||||
Some(self.q.to_owned()),
|
||||
|
@ -2685,7 +2810,7 @@ impl Perform for Search {
|
|||
self.page,
|
||||
self.limit)?;
|
||||
comments = CommentView::list(&conn,
|
||||
&sort,
|
||||
&sort,
|
||||
None,
|
||||
None,
|
||||
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,
|
||||
}
|
||||
)?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
BIN
ui/assets/apple-touch-icon.png
Normal file
BIN
ui/assets/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
|
@ -45,7 +45,7 @@ Sparky.task('config', _ => {
|
|||
// Sparky.task('version', _ => setVersion());
|
||||
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
||||
Sparky.task('env', _ => (isProduction = true));
|
||||
Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/'));
|
||||
Sparky.task('copy-assets', () => Sparky.src('assets/*.*').dest('dist/'));
|
||||
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
|
||||
fuse.dev();
|
||||
app.hmr().watch();
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"@types/js-cookie": "^2.2.1",
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
"@types/markdown-it": "^0.0.7",
|
||||
"@types/markdown-it-container": "^2.0.2",
|
||||
"autosize": "^4.0.2",
|
||||
"classcat": "^1.1.3",
|
||||
"dotenv": "^6.1.0",
|
||||
|
@ -27,6 +28,7 @@
|
|||
"js-cookie": "^2.2.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"rxjs": "^6.4.0"
|
||||
},
|
||||
|
|
|
@ -56,7 +56,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} 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 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 &&
|
||||
<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">
|
||||
{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>
|
||||
</li>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
|
@ -252,11 +254,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
|
||||
handleDeleteClick(i: CommentNode) {
|
||||
let deleteForm: CommentFormI = {
|
||||
content: '*deleted*',
|
||||
content: i.props.node.comment.content,
|
||||
edit_id: i.props.node.comment.id,
|
||||
creator_id: i.props.node.comment.creator_id,
|
||||
post_id: i.props.node.comment.post_id,
|
||||
parent_id: i.props.node.comment.parent_id,
|
||||
deleted: !i.props.node.comment.deleted,
|
||||
auth: null
|
||||
};
|
||||
WebSocketService.Instance.editComment(deleteForm);
|
||||
|
|
|
@ -10,6 +10,7 @@ declare const Sortable: any;
|
|||
|
||||
interface CommunitiesState {
|
||||
communities: Array<Community>;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
|
@ -17,7 +18,8 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
private subscription: Subscription;
|
||||
private emptyState: CommunitiesState = {
|
||||
communities: [],
|
||||
loading: true
|
||||
loading: true,
|
||||
page: this.getPageFromProps(this.props),
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -31,13 +33,12 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
() => console.log('complete')
|
||||
);
|
||||
|
||||
let listCommunitiesForm: ListCommunitiesForm = {
|
||||
sort: SortType[SortType.TopAll],
|
||||
limit: 100,
|
||||
}
|
||||
this.refetch();
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
}
|
||||
|
||||
getPageFromProps(props: any): number {
|
||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -45,40 +46,49 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = "Forums - Lemmy";
|
||||
document.title = "Communities - Lemmy";
|
||||
let table = document.querySelector('#community_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() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
<div>
|
||||
<h5>Forums</h5>
|
||||
<h5>List of communities</h5>
|
||||
<div class="table-responsive">
|
||||
<table id="community_table" class="table table-sm table-hover">
|
||||
<thead class="pointer">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th class="d-none d-lg-table-cell">Title</th>
|
||||
<th>Category</th>
|
||||
<th class="text-right d-none d-md-table-cell">Subscribers</th>
|
||||
<th class="text-right d-none d-md-table-cell">Posts</th>
|
||||
<th class="text-right d-none d-md-table-cell">Comments</th>
|
||||
<th class="text-right">Subscribers</th>
|
||||
<th class="text-right d-none d-lg-table-cell">Posts</th>
|
||||
<th class="text-right d-none d-lg-table-cell">Comments</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.communities.map(community =>
|
||||
<tr>
|
||||
<td><Link to={`/f/${community.name}`}>{community.name}</Link></td>
|
||||
<td>{community.title}</td>
|
||||
<td><Link to={`/c/${community.name}`}>{community.name}</Link></td>
|
||||
<td class="d-none d-lg-table-cell">{community.title}</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 d-none d-md-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">{community.number_of_subscribers}</td>
|
||||
<td class="text-right d-none d-lg-table-cell">{community.number_of_posts}</td>
|
||||
<td class="text-right d-none d-lg-table-cell">{community.number_of_comments}</td>
|
||||
<td class="text-right">
|
||||
{community.subscribed ?
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
{this.paginator()}
|
||||
</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) {
|
||||
let form: FollowCommunityForm = {
|
||||
community_id: communityId,
|
||||
|
@ -112,6 +152,17 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
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) {
|
||||
console.log(msg);
|
||||
let op: UserOperation = msgOp(msg);
|
||||
|
|
|
@ -88,7 +88,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
|||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label">Sidebar</label>
|
||||
<div class="col-12">
|
||||
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} />
|
||||
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
|
@ -120,10 +120,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
|||
if (i.props.community) {
|
||||
WebSocketService.Instance.editCommunity(i.state.communityForm);
|
||||
} else {
|
||||
|
||||
setTimeout(function(){
|
||||
WebSocketService.Instance.createCommunity(i.state.communityForm);
|
||||
}, 10000);
|
||||
WebSocketService.Instance.createCommunity(i.state.communityForm);
|
||||
}
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Component } from 'inferno';
|
||||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from "rxjs";
|
||||
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 { PostListings } from './post-listings';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { msgOp } from '../utils';
|
||||
import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils';
|
||||
|
||||
interface State {
|
||||
community: CommunityI;
|
||||
|
@ -14,6 +14,9 @@ interface State {
|
|||
moderators: Array<CommunityUser>;
|
||||
admins: Array<UserView>;
|
||||
loading: boolean;
|
||||
posts: Array<Post>;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export class Community extends Component<any, State> {
|
||||
|
@ -38,7 +41,20 @@ export class Community extends Component<any, State> {
|
|||
admins: [],
|
||||
communityId: Number(this.props.match.params.id),
|
||||
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) {
|
||||
|
@ -66,6 +82,16 @@ export class Community extends Component<any, State> {
|
|||
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() {
|
||||
return (
|
||||
<div class="container">
|
||||
|
@ -78,7 +104,9 @@ export class Community extends Component<any, State> {
|
|||
<small className="ml-2 text-muted font-italic">removed</small>
|
||||
}
|
||||
</h5>
|
||||
{this.state.community && <PostListings communityId={this.state.community.id} />}
|
||||
{this.selects()}
|
||||
<PostListings posts={this.state.posts} />
|
||||
{this.paginator()}
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<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) {
|
||||
console.log(msg);
|
||||
|
@ -105,9 +199,9 @@ export class Community extends Component<any, State> {
|
|||
this.state.community = res.community;
|
||||
this.state.moderators = res.moderators;
|
||||
this.state.admins = res.admins;
|
||||
this.state.loading = false;
|
||||
document.title = `/f/${this.state.community.name} - Lemmy`;
|
||||
document.title = `/c/${this.state.community.name} - Lemmy`;
|
||||
this.setState(this.state);
|
||||
this.fetchPosts();
|
||||
} else if (op == UserOperation.EditCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
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.number_of_subscribers = res.community.number_of_subscribers;
|
||||
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() {
|
||||
document.title = "Create Forum - Lemmy";
|
||||
document.title = "Create Community - Lemmy";
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -18,7 +18,7 @@ export class CreateCommunity extends Component<any, any> {
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 mb-4">
|
||||
<h5>Create Forum</h5>
|
||||
<h5>Create Community</h5>
|
||||
<CommunityForm onCreate={this.handleCommunityCreate}/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -27,7 +27,7 @@ export class CreateCommunity extends Component<any, any> {
|
|||
}
|
||||
|
||||
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="col-12 col-lg-6 mb-4">
|
||||
<h5>Create a Post</h5>
|
||||
<PostForm onCreate={this.handlePostCreate}/>
|
||||
<PostForm onCreate={this.handlePostCreate} prevCommunityName={this.prevCommunityName} />
|
||||
</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) {
|
||||
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="row">
|
||||
<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.replies()}
|
||||
{this.paginator()}
|
||||
|
@ -71,12 +80,12 @@ export class Inbox extends Component<any, InboxState> {
|
|||
selects() {
|
||||
return (
|
||||
<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 value={UnreadType.Unread}>Unread</option>
|
||||
<option value={UnreadType.All}>All</option>
|
||||
</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 value={SortType.New}>New</option>
|
||||
<option value={SortType.TopDay}>Top Day</option>
|
||||
|
@ -147,13 +156,17 @@ export class Inbox extends Component<any, InboxState> {
|
|||
i.refetch();
|
||||
}
|
||||
|
||||
markAllAsRead() {
|
||||
WebSocketService.Instance.markAllAsRead();
|
||||
}
|
||||
|
||||
parseMessage(msg: any) {
|
||||
console.log(msg);
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (msg.error) {
|
||||
alert(msg.error);
|
||||
return;
|
||||
} else if (op == UserOperation.GetReplies) {
|
||||
} else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) {
|
||||
let res: GetRepliesResponse = msg;
|
||||
this.state.replies = res.replies;
|
||||
this.sendRepliesCount();
|
||||
|
@ -165,6 +178,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
found.content = res.comment.content;
|
||||
found.updated = res.comment.updated;
|
||||
found.removed = res.comment.removed;
|
||||
found.deleted = res.comment.deleted;
|
||||
found.upvotes = res.comment.upvotes;
|
||||
found.downvotes = res.comment.downvotes;
|
||||
found.score = res.comment.score;
|
||||
|
|
|
@ -95,7 +95,7 @@ export class Login extends Component<any, State> {
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
Forgot your password or deleted your account? Reset your password. TODO
|
||||
{/* Forgot your password or deleted your account? Reset your password. TODO */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -161,7 +161,6 @@ export class Login extends Component<any, State> {
|
|||
event.preventDefault();
|
||||
i.state.registerLoading = true;
|
||||
i.setState(i.state);
|
||||
event.preventDefault();
|
||||
|
||||
let endTimer = new Date().getTime();
|
||||
let elapsed = endTimer - i.state.registerForm.spam_timeri;
|
||||
|
@ -209,14 +208,14 @@ export class Login extends Component<any, State> {
|
|||
return;
|
||||
} else {
|
||||
if (op == UserOperation.Login) {
|
||||
this.state.loginLoading = false;
|
||||
this.state.registerLoading = false;
|
||||
this.state = this.emptyState;
|
||||
this.setState(this.state);
|
||||
let res: LoginResponse = msg;
|
||||
UserService.Instance.login(res);
|
||||
this.props.history.push('/');
|
||||
} else if (op == UserOperation.Register) {
|
||||
this.state.loginLoading = false;
|
||||
this.state.registerLoading = false;
|
||||
this.state = this.emptyState;
|
||||
this.setState(this.state);
|
||||
let res: LoginResponse = msg;
|
||||
UserService.Instance.login(res);
|
||||
this.props.history.push('/communities');
|
||||
|
|
|
@ -2,16 +2,11 @@ import { Component, linkEvent } from 'inferno';
|
|||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
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 { PostListings } from './post-listings';
|
||||
import { SiteForm } from './site-form';
|
||||
import { msgOp, repoUrl, mdToHtml } from '../utils';
|
||||
|
||||
|
||||
interface MainProps {
|
||||
type: ListingType;
|
||||
}
|
||||
import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils';
|
||||
|
||||
interface MainState {
|
||||
subscribedCommunities: Array<CommunityUser>;
|
||||
|
@ -19,9 +14,13 @@ interface MainState {
|
|||
site: GetSiteResponse;
|
||||
showEditSite: 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 emptyState: MainState = {
|
||||
|
@ -43,7 +42,29 @@ export class Main extends Component<MainProps, MainState> {
|
|||
banned: [],
|
||||
},
|
||||
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) {
|
||||
|
@ -66,43 +87,57 @@ export class Main extends Component<MainProps, MainState> {
|
|||
}
|
||||
|
||||
let listCommunitiesForm: ListCommunitiesForm = {
|
||||
sort: SortType[SortType.New],
|
||||
sort: SortType[SortType.Hot],
|
||||
limit: 6
|
||||
}
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
|
||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||
this.fetchPosts();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
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() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<PostListings type={this.props.type} />
|
||||
{this.posts()}
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
{this.state.loading ?
|
||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
<div>
|
||||
{this.trendingCommunities()}
|
||||
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
|
||||
<div>
|
||||
<h5>Subscribed forums</h5>
|
||||
<ul class="list-inline">
|
||||
{this.state.subscribedCommunities.map(community =>
|
||||
<li class="list-inline-item"><Link to={`/f/${community.community_name}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
{this.sidebar()}
|
||||
</div>
|
||||
{!this.state.loading &&
|
||||
<div>
|
||||
{this.trendingCommunities()}
|
||||
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
|
||||
<div>
|
||||
<h5>Subscribed <Link class="text-white" to="/communities">communities</Link></h5>
|
||||
<ul class="list-inline">
|
||||
{this.state.subscribedCommunities.map(community =>
|
||||
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
{this.sidebar()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -113,10 +148,10 @@ export class Main extends Component<MainProps, MainState> {
|
|||
trendingCommunities() {
|
||||
return (
|
||||
<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">
|
||||
{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>
|
||||
</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() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -175,7 +216,7 @@ export class Main extends Component<MainProps, MainState> {
|
|||
landing() {
|
||||
return (
|
||||
<div>
|
||||
<h5>Welcome to
|
||||
<h5>Powered by
|
||||
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
|
||||
<a href={repoUrl}>Lemmy<sup>Beta</sup></a>
|
||||
</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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
console.log(msg);
|
||||
let op: UserOperation = msgOp(msg);
|
||||
|
@ -211,12 +358,10 @@ export class Main extends Component<MainProps, MainState> {
|
|||
} else if (op == UserOperation.GetFollowedCommunities) {
|
||||
let res: GetFollowedCommunitiesResponse = msg;
|
||||
this.state.subscribedCommunities = res.communities;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.ListCommunities) {
|
||||
let res: ListCommunitiesResponse = msg;
|
||||
this.state.trendingCommunities = res.communities;
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.GetSite) {
|
||||
let res: GetSiteResponse = msg;
|
||||
|
@ -234,6 +379,19 @@ export class Main extends Component<MainProps, MainState> {
|
|||
this.state.site.site = res.site;
|
||||
this.state.showEditSite = false;
|
||||
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.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).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><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><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).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><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><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' &&
|
||||
|
@ -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> :
|
||||
<div>
|
||||
<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>
|
||||
</h5>
|
||||
<div class="table-responsive">
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Link } from 'inferno-router';
|
|||
import { Subscription } from "rxjs";
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
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 { version } from '../version';
|
||||
|
||||
|
@ -12,6 +12,7 @@ interface NavbarState {
|
|||
expanded: boolean;
|
||||
expandUserDropdown: boolean;
|
||||
unreadCount: number;
|
||||
siteName: string;
|
||||
}
|
||||
|
||||
export class Navbar extends Component<any, NavbarState> {
|
||||
|
@ -21,7 +22,8 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
isLoggedIn: (UserService.Instance.user !== undefined),
|
||||
unreadCount: 0,
|
||||
expanded: false,
|
||||
expandUserDropdown: false
|
||||
expandUserDropdown: false,
|
||||
siteName: undefined
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -45,6 +47,8 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
(err) => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
WebSocketService.Instance.getSite();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -59,30 +63,29 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
|
||||
// TODO class active corresponding to current page
|
||||
// TODO toggle css collapse
|
||||
navbar() {
|
||||
return (
|
||||
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3">
|
||||
<a title={version} class="navbar-brand" href="#">
|
||||
<svg class="icon mr-2"><use xlinkHref="#icon-mouse"></use></svg>
|
||||
Lemmy
|
||||
</a>
|
||||
<Link title={version} class="navbar-brand" to="/">
|
||||
<svg class="icon mr-2 mouse-icon"><use xlinkHref="#icon-mouse"></use></svg>
|
||||
{this.state.siteName}
|
||||
</Link>
|
||||
<button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<Link class="nav-link" to="/communities">Forums</Link>
|
||||
<Link class="nav-link" to="/communities">Communities</Link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link class="nav-link" to="/search">Search</Link>
|
||||
</li>
|
||||
<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 class="nav-item">
|
||||
<Link class="nav-link" to="/create_community">Create Forum</Link>
|
||||
<Link class="nav-link" to="/create_community">Create Community</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto mr-2">
|
||||
|
@ -145,6 +148,14 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
} else if (op == UserOperation.GetReplies) {
|
||||
let res: GetRepliesResponse = msg;
|
||||
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) {
|
||||
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 { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType } from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { msgOp } from '../utils';
|
||||
import { msgOp, getPageTitle } from '../utils';
|
||||
import * as autosize from 'autosize';
|
||||
|
||||
interface PostFormProps {
|
||||
post?: Post; // If a post is given, that means this is an edit
|
||||
prevCommunityName?: string;
|
||||
onCancel?(): any;
|
||||
onCreate?(id: number): any;
|
||||
onEdit?(post: Post): any;
|
||||
|
@ -17,6 +18,7 @@ interface PostFormState {
|
|||
postForm: PostFormI;
|
||||
communities: Array<Community>;
|
||||
loading: boolean;
|
||||
suggestedTitle: string;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
communities: [],
|
||||
loading: false
|
||||
loading: false,
|
||||
suggestedTitle: undefined,
|
||||
}
|
||||
|
||||
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>
|
||||
<div class="col-sm-10">
|
||||
<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 class="form-group row">
|
||||
|
@ -93,13 +99,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Body</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
|
||||
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} maxLength={10000} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Cant change a community from an edit */}
|
||||
{!this.props.post &&
|
||||
<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">
|
||||
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
|
||||
{this.state.communities.map(community =>
|
||||
|
@ -134,8 +140,18 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -170,6 +186,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.state.communities = res.communities;
|
||||
if (this.props.post) {
|
||||
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 {
|
||||
this.state.postForm.community_id = res.communities[0].id;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{post.removed &&
|
||||
<small className="ml-2 text-muted font-italic">removed</small>
|
||||
}
|
||||
{post.deleted &&
|
||||
<small className="ml-2 text-muted font-italic">deleted</small>
|
||||
}
|
||||
{post.locked &&
|
||||
<small className="ml-2 text-muted font-italic">locked</small>
|
||||
}
|
||||
|
@ -118,7 +121,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{this.props.showCommunity &&
|
||||
<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>
|
||||
}
|
||||
</li>
|
||||
|
@ -140,7 +143,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{UserService.Instance.user && this.props.editable &&
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<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>
|
||||
{this.myPost &&
|
||||
<>
|
||||
|
@ -148,7 +151,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
|
||||
</li>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
|
@ -237,12 +242,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
handleDeleteClick(i: PostListing) {
|
||||
let deleteForm: PostFormI = {
|
||||
body: '',
|
||||
body: i.props.post.body,
|
||||
community_id: i.props.post.community_id,
|
||||
name: "deleted",
|
||||
url: '',
|
||||
name: i.props.post.name,
|
||||
url: i.props.post.url,
|
||||
edit_id: i.props.post.id,
|
||||
creator_id: i.props.post.creator_id,
|
||||
deleted: !i.props.post.deleted,
|
||||
auth: null
|
||||
};
|
||||
WebSocketService.Instance.editPost(deleteForm);
|
||||
|
|
|
@ -1,183 +1,28 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { UserOperation, Post, GetPostsForm, SortType, ListingType, GetPostsResponse, CreatePostLikeResponse, CommunityUser} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { Post } from '../interfaces';
|
||||
import { PostListing } from './post-listing';
|
||||
import { msgOp, fetchLimit } from '../utils';
|
||||
|
||||
interface PostListingsProps {
|
||||
type?: ListingType;
|
||||
communityId?: number;
|
||||
}
|
||||
|
||||
interface PostListingsState {
|
||||
moderators: Array<CommunityUser>;
|
||||
posts: Array<Post>;
|
||||
sortType: SortType;
|
||||
type_: ListingType;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
showCommunity?: boolean;
|
||||
}
|
||||
|
||||
export class PostListings extends Component<PostListingsProps, PostListingsState> {
|
||||
|
||||
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
|
||||
}
|
||||
export class PostListings extends Component<PostListingsProps, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
this.refetch();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.loading ?
|
||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
<div>
|
||||
{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()}
|
||||
{this.props.posts.length > 0 ? this.props.posts.map(post =>
|
||||
<PostListing post={post} showCommunity={this.props.showCommunity} />) :
|
||||
<div>No posts. {this.props.showCommunity !== undefined && <span>Subscribe to some <Link to="/communities">communities</Link>.</span>}
|
||||
</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.updated = res.comment.updated;
|
||||
found.removed = res.comment.removed;
|
||||
found.deleted = res.comment.deleted;
|
||||
found.upvotes = res.comment.upvotes;
|
||||
found.downvotes = res.comment.downvotes;
|
||||
found.score = res.comment.score;
|
||||
|
|
|
@ -97,13 +97,13 @@ export class Search extends Component<any, SearchState> {
|
|||
selects() {
|
||||
return (
|
||||
<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 value={SearchType.Both}>Both</option>
|
||||
<option value={SearchType.Comments}>Comments</option>
|
||||
<option value={SearchType.Posts}>Posts</option>
|
||||
</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 value={SortType.New}>New</option>
|
||||
<option value={SortType.TopDay}>Top Day</option>
|
||||
|
|
|
@ -20,6 +20,7 @@ export class Setup extends Component<any, State> {
|
|||
username: undefined,
|
||||
password: undefined,
|
||||
password_verify: undefined,
|
||||
spam_timeri: 3000,
|
||||
admin: true,
|
||||
},
|
||||
doneRegisteringUser: false,
|
||||
|
|
|
@ -56,8 +56,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
{community.removed &&
|
||||
<small className="ml-2 text-muted font-italic">removed</small>
|
||||
}
|
||||
{community.deleted &&
|
||||
<small className="ml-2 text-muted font-italic">deleted</small>
|
||||
}
|
||||
</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">
|
||||
{this.canMod &&
|
||||
<>
|
||||
|
@ -66,7 +69,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
</li>
|
||||
{this.amCreator &&
|
||||
<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>
|
||||
}
|
||||
</>
|
||||
|
@ -142,9 +147,18 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
this.setState(this.state);
|
||||
}
|
||||
|
||||
// TODO no deleting communities yet
|
||||
// handleDeleteClick(i: Sidebar, event) {
|
||||
// }
|
||||
handleDeleteClick(i: Sidebar) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
handleDeleteClick() {
|
||||
}
|
||||
|
||||
handleModRemoveShow(i: Sidebar) {
|
||||
i.state.showRemoveDialog = true;
|
||||
i.setState(i.state);
|
||||
|
|
|
@ -49,7 +49,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label">Sidebar</label>
|
||||
<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 class="form-group row">
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Component } from 'inferno';
|
|||
let general =
|
||||
[
|
||||
"Nathan J. Goode",
|
||||
"Eduardo Cavazos"
|
||||
];
|
||||
// let highlighted = [];
|
||||
// let silver = [];
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Subscription } from "rxjs";
|
|||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { msgOp, fetchLimit } from '../utils';
|
||||
import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { MomentTime } from './moment-time';
|
||||
|
@ -25,6 +25,7 @@ interface UserState {
|
|||
view: View;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class User extends Component<any, UserState> {
|
||||
|
@ -47,9 +48,10 @@ export class User extends Component<any, UserState> {
|
|||
moderates: [],
|
||||
comments: [],
|
||||
posts: [],
|
||||
view: View.Overview,
|
||||
sort: SortType.New,
|
||||
page: 1,
|
||||
loading: true,
|
||||
view: this.getViewFromProps(this.props),
|
||||
sort: this.getSortTypeFromProps(this.props),
|
||||
page: this.getPageFromProps(this.props),
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -71,13 +73,42 @@ export class User extends Component<any, UserState> {
|
|||
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() {
|
||||
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() {
|
||||
return (
|
||||
<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="col-12 col-md-9">
|
||||
<h5>/u/{this.state.user.name}</h5>
|
||||
|
@ -102,6 +133,7 @@ export class User extends Component<any, UserState> {
|
|||
{this.follows()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -109,14 +141,14 @@ export class User extends Component<any, UserState> {
|
|||
selects() {
|
||||
return (
|
||||
<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 value={View.Overview}>Overview</option>
|
||||
<option value={View.Comments}>Comments</option>
|
||||
<option value={View.Posts}>Posts</option>
|
||||
<option value={View.Saved}>Saved</option>
|
||||
</select>
|
||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
|
||||
<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 value={SortType.New}>New</option>
|
||||
<option value={SortType.TopDay}>Top Day</option>
|
||||
|
@ -209,7 +241,7 @@ export class User extends Component<any, UserState> {
|
|||
<h5>Moderates</h5>
|
||||
<ul class="list-unstyled">
|
||||
{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>
|
||||
</div>
|
||||
|
@ -227,7 +259,7 @@ export class User extends Component<any, UserState> {
|
|||
<h5>Subscribed</h5>
|
||||
<ul class="list-unstyled">
|
||||
{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>
|
||||
</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) {
|
||||
i.state.page++;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
}
|
||||
|
||||
prevPage(i: User) {
|
||||
i.state.page--;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
}
|
||||
|
||||
|
@ -275,6 +315,7 @@ export class User extends Component<any, UserState> {
|
|||
i.state.sort = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
}
|
||||
|
||||
|
@ -282,6 +323,7 @@ export class User extends Component<any, UserState> {
|
|||
i.state.view = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
}
|
||||
|
||||
|
@ -298,6 +340,7 @@ export class User extends Component<any, UserState> {
|
|||
this.state.follows = res.follows;
|
||||
this.state.moderates = res.moderates;
|
||||
this.state.posts = res.posts;
|
||||
this.state.loading = false;
|
||||
document.title = `/u/${this.state.user.name} - Lemmy`;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.EditComment) {
|
||||
|
@ -307,6 +350,7 @@ export class User extends Component<any, UserState> {
|
|||
found.content = res.comment.content;
|
||||
found.updated = res.comment.updated;
|
||||
found.removed = res.comment.removed;
|
||||
found.deleted = res.comment.deleted;
|
||||
found.upvotes = res.comment.upvotes;
|
||||
found.downvotes = res.comment.downvotes;
|
||||
found.score = res.comment.score;
|
||||
|
|
|
@ -87,6 +87,10 @@ blockquote {
|
|||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mouse-icon {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.new-comments {
|
||||
max-height: 100vh;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="utf-8">
|
||||
<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="apple-touch-icon" href="/static/assets/apple-touch-icon.png" />
|
||||
<title>Lemmy</title>
|
||||
<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>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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 { Footer } from './components/footer';
|
||||
import { Home } from './components/home';
|
||||
import { Login } from './components/login';
|
||||
import { CreatePost } from './components/create-post';
|
||||
import { CreateCommunity } from './components/create-community';
|
||||
|
@ -37,18 +36,21 @@ class Index extends Component<any, any> {
|
|||
return (
|
||||
<HashRouter>
|
||||
<Navbar />
|
||||
<div class="mt-3 p-0">
|
||||
<div class="mt-1 p-0">
|
||||
<Switch>
|
||||
<Route exact path="/all" component={Home} />
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} />
|
||||
<Route exact path={`/`} component={Main} />
|
||||
<Route path={`/login`} component={Login} />
|
||||
<Route path={`/create_post`} component={CreatePost} />
|
||||
<Route path={`/create_community`} component={CreateCommunity} />
|
||||
<Route path={`/communities/page/:page`} component={Communities} />
|
||||
<Route path={`/communities`} component={Communities} />
|
||||
<Route path={`/post/:id/comment/:comment_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={`/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={`/u/:username`} component={User} />
|
||||
<Route path={`/inbox`} component={Inbox} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 {
|
||||
|
@ -52,6 +52,7 @@ export interface Community {
|
|||
category_id: number;
|
||||
creator_id: number;
|
||||
removed: boolean;
|
||||
deleted: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
creator_name: string;
|
||||
|
@ -71,6 +72,7 @@ export interface Post {
|
|||
creator_id: number;
|
||||
community_id: number;
|
||||
removed: boolean;
|
||||
deleted: boolean;
|
||||
locked: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
|
@ -96,6 +98,7 @@ export interface Comment {
|
|||
parent_id?: number;
|
||||
content: string;
|
||||
removed: boolean;
|
||||
deleted: boolean;
|
||||
read: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
|
@ -348,6 +351,7 @@ export interface CommunityForm {
|
|||
category_id: number,
|
||||
edit_id?: number;
|
||||
removed?: boolean;
|
||||
deleted?: boolean;
|
||||
reason?: string;
|
||||
expires?: number;
|
||||
auth?: string;
|
||||
|
@ -392,6 +396,7 @@ export interface PostForm {
|
|||
edit_id?: number;
|
||||
creator_id: number;
|
||||
removed?: boolean;
|
||||
deleted?: boolean;
|
||||
locked?: boolean;
|
||||
reason?: string;
|
||||
auth: string;
|
||||
|
@ -424,6 +429,7 @@ export interface CommentForm {
|
|||
edit_id?: number;
|
||||
creator_id: number;
|
||||
removed?: boolean;
|
||||
deleted?: boolean;
|
||||
reason?: string;
|
||||
read?: boolean;
|
||||
auth: string;
|
||||
|
|
|
@ -147,6 +147,7 @@ export class WebSocketService {
|
|||
}
|
||||
|
||||
public getUserDetails(form: GetUserDetailsForm) {
|
||||
this.setAuth(form, false);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
|
||||
}
|
||||
|
||||
|
@ -177,6 +178,12 @@ export class WebSocketService {
|
|||
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) {
|
||||
let send = { op: UserOperation[op], data: data };
|
||||
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_container from 'markdown-it-container';
|
||||
|
||||
export let repoUrl = 'https://github.com/dessalines/lemmy';
|
||||
|
||||
|
@ -12,6 +13,23 @@ var md = new markdown_it({
|
|||
html: true,
|
||||
linkify: 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 {
|
||||
|
@ -67,3 +85,35 @@ export function isImage(url: string) {
|
|||
}
|
||||
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
|
||||
integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
|
||||
|
@ -1674,6 +1681,11 @@ map-visit@^1.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "8.4.2"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
|
||||
|
|
Loading…
Reference in a new issue