Merge branch 'dev'

This commit is contained in:
Dessalines 2019-08-15 16:53:59 -07:00
commit 22008cf7fc
37 changed files with 623 additions and 130 deletions

View file

@ -37,6 +37,7 @@ Front Page|Post
- Can ban and unban users from communities and the site. - Can ban and unban users from communities and the site.
- Clean, mobile-friendly interface. - Clean, mobile-friendly interface.
- i18n / internationalization support. - i18n / internationalization support.
- NSFW post / community support.
- High performance. - High performance.
- Server is written in rust. - Server is written in rust.
- Front end is `~80kB` gzipped. - Front end is `~80kB` gzipped.

View file

@ -2,12 +2,14 @@ version: '2.4'
services: services:
db: db:
image: postgres image: postgres:12-alpine
restart: always restart: always
environment: environment:
POSTGRES_USER: rrr POSTGRES_USER: rrr
POSTGRES_PASSWORD: rrr POSTGRES_PASSWORD: rrr
POSTGRES_DB: rrr POSTGRES_DB: rrr
volumes:
- db:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U rrr"] test: ["CMD-SHELL", "pg_isready -U rrr"]
interval: 5s interval: 5s
@ -27,3 +29,5 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
volumes:
db:

View file

@ -2,12 +2,14 @@ version: '2.4'
services: services:
db: db:
image: postgres image: postgres:12-alpine
restart: always restart: always
environment: environment:
POSTGRES_USER: rrr POSTGRES_USER: rrr
POSTGRES_PASSWORD: rrr POSTGRES_PASSWORD: rrr
POSTGRES_DB: rrr POSTGRES_DB: rrr
volumes:
- db:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U rrr"] test: ["CMD-SHELL", "pg_isready -U rrr"]
interval: 5s interval: 5s
@ -26,3 +28,5 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
volumes:
db:

View file

@ -1 +1 @@
docker exec -it lemmy_db_1 pg_dumpall -c -U rrr > dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql docker exec -it lemmy_db_1 pg_dumpall -c -U rrr > dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql

View file

@ -28,7 +28,7 @@ A simple test command:
## API ## API
### List ### List
`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` `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, SaveUserSettings`
### Sort Types ### Sort Types
These go wherever there is a `sort` field. These go wherever there is a `sort` field.
@ -109,7 +109,21 @@ Only the first user will be able to be the admin.
posts: Vec<PostView>, posts: Vec<PostView>,
} }
``` ```
#### Save User Settings
##### Request
```rust
{
show_nsfw: bool,
auth: String,
}
```
##### Response
```rust
{
op: String,
jwt: String
}
```
#### Get Replies / Inbox #### Get Replies / Inbox
##### Request ##### Request
```rust ```rust

View file

@ -0,0 +1,80 @@
drop view community_view;
drop view post_view;
alter table community drop column nsfw;
alter table post drop column nsfw;
alter table user_ drop column show_nsfw;
-- the views
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
;
-- 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
;

View file

@ -0,0 +1,79 @@
alter table community add column nsfw boolean default false not null;
alter table post add column nsfw boolean default false not null;
alter table user_ add column show_nsfw 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,
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
;
-- Post view
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 nsfw from community c where p.community_id = c.id) as community_nsfw,
(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
;

View file

@ -22,7 +22,8 @@ pub struct CreateCommunity {
name: String, name: String,
title: String, title: String,
description: Option<String>, description: Option<String>,
category_id: i32 , category_id: i32,
nsfw: bool,
auth: String auth: String
} }
@ -86,6 +87,7 @@ pub struct EditCommunity {
category_id: i32, category_id: i32,
removed: Option<bool>, removed: Option<bool>,
deleted: Option<bool>, deleted: Option<bool>,
nsfw: bool,
reason: Option<String>, reason: Option<String>,
expires: Option<i64>, expires: Option<i64>,
auth: String auth: String
@ -194,6 +196,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
creator_id: user_id, creator_id: user_id,
removed: None, removed: None,
deleted: None, deleted: None,
nsfw: data.nsfw,
updated: None, updated: None,
}; };
@ -291,6 +294,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
creator_id: user_id, creator_id: user_id,
removed: data.removed.to_owned(), removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(), deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
updated: Some(naive_now()) updated: Some(naive_now())
}; };
@ -333,12 +337,11 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
let data: &ListCommunities = &self.data; let data: &ListCommunities = &self.data;
let conn = establish_connection(); let conn = establish_connection();
let user_id: Option<i32> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
Some(auth) => { Some(auth) => {
match Claims::decode(&auth) { match Claims::decode(&auth) {
Ok(claims) => { Ok(claims) => {
let user_id = claims.claims.id; Some(claims.claims)
Some(user_id)
} }
Err(_e) => None Err(_e) => None
} }
@ -346,9 +349,26 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
None => None None => None
}; };
let user_id = match &user_claims {
Some(claims) => Some(claims.id),
None => None
};
let show_nsfw = match &user_claims {
Some(claims) => claims.show_nsfw,
None => false
};
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let communities: Vec<CommunityView> = CommunityView::list(&conn, &sort, user_id, None, data.page, data.limit)?; let communities: Vec<CommunityView> = CommunityView::list(
&conn,
&sort,
user_id,
show_nsfw,
None,
data.page,
data.limit)?;
// Return the jwt // Return the jwt
Ok( Ok(

View file

@ -22,7 +22,7 @@ pub mod site;
#[derive(EnumString,ToString,Debug)] #[derive(EnumString,ToString,Debug)]
pub enum UserOperation { pub enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead 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, SaveUserSettings
} }
#[derive(Fail, Debug)] #[derive(Fail, Debug)]

View file

@ -6,6 +6,7 @@ pub struct CreatePost {
name: String, name: String,
url: Option<String>, url: Option<String>,
body: Option<String>, body: Option<String>,
nsfw: bool,
community_id: i32, community_id: i32,
auth: String auth: String
} }
@ -73,6 +74,7 @@ pub struct EditPost {
body: Option<String>, body: Option<String>,
removed: Option<bool>, removed: Option<bool>,
deleted: Option<bool>, deleted: Option<bool>,
nsfw: bool,
locked: Option<bool>, locked: Option<bool>,
reason: Option<String>, reason: Option<String>,
auth: String auth: String
@ -123,6 +125,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
creator_id: user_id, creator_id: user_id,
removed: None, removed: None,
deleted: None, deleted: None,
nsfw: data.nsfw,
locked: None, locked: None,
updated: None updated: None
}; };
@ -219,12 +222,11 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
let data: &GetPosts = &self.data; let data: &GetPosts = &self.data;
let conn = establish_connection(); let conn = establish_connection();
let user_id: Option<i32> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
Some(auth) => { Some(auth) => {
match Claims::decode(&auth) { match Claims::decode(&auth) {
Ok(claims) => { Ok(claims) => {
let user_id = claims.claims.id; Some(claims.claims)
Some(user_id)
} }
Err(_e) => None Err(_e) => None
} }
@ -232,27 +234,38 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
None => None None => None
}; };
let user_id = match &user_claims {
Some(claims) => Some(claims.id),
None => None
};
let show_nsfw = match &user_claims {
Some(claims) => claims.show_nsfw,
None => false
};
let type_ = PostListingType::from_str(&data.type_)?; let type_ = PostListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let posts = match PostView::list(&conn, let posts = match PostView::list(
type_, &conn,
&sort, type_,
data.community_id, &sort,
None, data.community_id,
None, None,
user_id, None,
false, user_id,
false, show_nsfw,
data.page, false,
data.limit) { false,
data.page,
data.limit) {
Ok(posts) => posts, Ok(posts) => posts,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "couldnt_get_posts"))? return Err(APIError::err(&self.op, "couldnt_get_posts"))?
} }
}; };
// Return the jwt
Ok( Ok(
GetPostsResponse { GetPostsResponse {
op: self.op.to_string(), op: self.op.to_string(),
@ -381,6 +394,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
community_id: data.community_id, community_id: data.community_id,
removed: data.removed.to_owned(), removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(), deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
locked: data.locked.to_owned(), locked: data.locked.to_owned(),
updated: Some(naive_now()) updated: Some(naive_now())
}; };

View file

@ -277,6 +277,8 @@ impl Perform<SearchResponse> for Oper<Search> {
let mut communities = Vec::new(); let mut communities = Vec::new();
let mut users = Vec::new(); let mut users = Vec::new();
// TODO no clean / non-nsfw searching rn
match type_ { match type_ {
SearchType::Posts => { SearchType::Posts => {
posts = PostView::list( posts = PostView::list(
@ -287,6 +289,7 @@ impl Perform<SearchResponse> for Oper<Search> {
None, None,
Some(data.q.to_owned()), Some(data.q.to_owned()),
None, None,
true,
false, false,
false, false,
data.page, data.page,
@ -309,6 +312,7 @@ impl Perform<SearchResponse> for Oper<Search> {
&conn, &conn,
&sort, &sort,
None, None,
true,
Some(data.q.to_owned()), Some(data.q.to_owned()),
data.page, data.page,
data.limit)?; data.limit)?;
@ -330,6 +334,7 @@ impl Perform<SearchResponse> for Oper<Search> {
None, None,
Some(data.q.to_owned()), Some(data.q.to_owned()),
None, None,
true,
false, false,
false, false,
data.page, data.page,
@ -348,6 +353,7 @@ impl Perform<SearchResponse> for Oper<Search> {
&conn, &conn,
&sort, &sort,
None, None,
true,
Some(data.q.to_owned()), Some(data.q.to_owned()),
data.page, data.page,
data.limit)?; data.limit)?;

View file

@ -15,6 +15,13 @@ pub struct Register {
password: String, password: String,
password_verify: String, password_verify: String,
admin: bool, admin: bool,
show_nsfw: bool,
}
#[derive(Serialize, Deserialize)]
pub struct SaveUserSettings {
show_nsfw: bool,
auth: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -151,6 +158,7 @@ impl Perform<LoginResponse> for Oper<Register> {
updated: None, updated: None,
admin: data.admin, admin: data.admin,
banned: false, banned: false,
show_nsfw: data.show_nsfw,
}; };
// Create the user // Create the user
@ -170,6 +178,7 @@ impl Perform<LoginResponse> for Oper<Register> {
title: "The Default Community".to_string(), title: "The Default Community".to_string(),
description: Some("The Default Community".to_string()), description: Some("The Default Community".to_string()),
category_id: 1, category_id: 1,
nsfw: false,
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None, removed: None,
deleted: None, deleted: None,
@ -218,18 +227,61 @@ impl Perform<LoginResponse> for Oper<Register> {
} }
} }
impl Perform<LoginResponse> for Oper<SaveUserSettings> {
fn perform(&self) -> Result<LoginResponse, Error> {
let data: &SaveUserSettings = &self.data;
let conn = establish_connection();
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return Err(APIError::err(&self.op, "not_logged_in"))?
}
};
let user_id = claims.id;
let read_user = User_::read(&conn, user_id)?;
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
password_encrypted: read_user.password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: read_user.admin,
banned: read_user.banned,
show_nsfw: data.show_nsfw,
};
let updated_user = match User_::update(&conn, user_id, &user_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(&self.op, "couldnt_update_user"))?
}
};
// Return the jwt
Ok(
LoginResponse {
op: self.op.to_string(),
jwt: updated_user.jwt()
}
)
}
}
impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> { impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
fn perform(&self) -> Result<GetUserDetailsResponse, Error> { fn perform(&self) -> Result<GetUserDetailsResponse, Error> {
let data: &GetUserDetails = &self.data; let data: &GetUserDetails = &self.data;
let conn = establish_connection(); let conn = establish_connection();
let user_id: Option<i32> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
Some(auth) => { Some(auth) => {
match Claims::decode(&auth) { match Claims::decode(&auth) {
Ok(claims) => { Ok(claims) => {
let user_id = claims.claims.id; Some(claims.claims)
Some(user_id)
} }
Err(_e) => None Err(_e) => None
} }
@ -237,6 +289,16 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
None => None None => None
}; };
let user_id = match &user_claims {
Some(claims) => Some(claims.id),
None => None
};
let show_nsfw = match &user_claims {
Some(claims) => claims.show_nsfw,
None => false
};
//TODO add save //TODO add save
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
@ -249,50 +311,56 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
// If its saved only, you don't care what creator it was // If its saved only, you don't care what creator it was
let posts = if data.saved_only { let posts = if data.saved_only {
PostView::list(&conn, PostView::list(
PostListingType::All, &conn,
&sort, PostListingType::All,
data.community_id, &sort,
None, data.community_id,
None, None,
Some(user_details_id), None,
data.saved_only, Some(user_details_id),
false, show_nsfw,
data.page, data.saved_only,
data.limit)? false,
data.page,
data.limit)?
} else { } else {
PostView::list(&conn, PostView::list(
PostListingType::All, &conn,
&sort, PostListingType::All,
data.community_id, &sort,
Some(user_details_id), data.community_id,
None, Some(user_details_id),
user_id, None,
data.saved_only, user_id,
false, show_nsfw,
data.page, data.saved_only,
data.limit)? false,
data.page,
data.limit)?
}; };
let comments = if data.saved_only { let comments = if data.saved_only {
CommentView::list(&conn, CommentView::list(
&sort, &conn,
None, &sort,
None, None,
None, None,
Some(user_details_id), None,
data.saved_only, Some(user_details_id),
data.page, data.saved_only,
data.limit)? data.page,
data.limit)?
} else { } else {
CommentView::list(&conn, CommentView::list(
&sort, &conn,
None, &sort,
Some(user_details_id), None,
None, Some(user_details_id),
user_id, None,
data.saved_only, user_id,
data.page, data.saved_only,
data.limit)? data.page,
data.limit)?
}; };
let follows = CommunityFollowerView::for_user(&conn, user_details_id)?; let follows = CommunityFollowerView::for_user(&conn, user_details_id)?;
@ -343,6 +411,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
updated: Some(naive_now()), updated: Some(naive_now()),
admin: data.added, admin: data.added,
banned: read_user.banned, banned: read_user.banned,
show_nsfw: read_user.show_nsfw,
}; };
match User_::update(&conn, data.user_id, &user_form) { match User_::update(&conn, data.user_id, &user_form) {
@ -402,6 +471,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
updated: Some(naive_now()), updated: Some(naive_now()),
admin: read_user.admin, admin: read_user.admin,
banned: data.ban, banned: data.ban,
show_nsfw: read_user.show_nsfw,
}; };
match User_::update(&conn, data.user_id, &user_form) { match User_::update(&conn, data.user_id, &user_form) {

View file

@ -46,7 +46,8 @@ mod tests {
published: naive_now(), published: naive_now(),
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let person = expected_user.person(); let person = expected_user.person();

View file

@ -170,7 +170,8 @@ mod tests {
email: None, email: None,
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -183,7 +184,8 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None, removed: None,
deleted: None, deleted: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_community = Community::create(&conn, &new_community).unwrap(); let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -197,7 +199,8 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
locked: None, locked: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_post = Post::create(&conn, &new_post).unwrap(); let inserted_post = Post::create(&conn, &new_post).unwrap();

View file

@ -261,7 +261,8 @@ mod tests {
email: None, email: None,
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -274,7 +275,8 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None, removed: None,
deleted: None, deleted: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_community = Community::create(&conn, &new_community).unwrap(); let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -288,7 +290,8 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
locked: None, locked: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_post = Post::create(&conn, &new_post).unwrap(); let inserted_post = Post::create(&conn, &new_post).unwrap();

View file

@ -14,6 +14,7 @@ pub struct Community {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool, pub deleted: bool,
pub nsfw: bool,
} }
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)] #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
@ -27,6 +28,7 @@ pub struct CommunityForm {
pub removed: Option<bool>, pub removed: Option<bool>,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>, pub deleted: Option<bool>,
pub nsfw: bool,
} }
impl Crud<CommunityForm> for Community { impl Crud<CommunityForm> for Community {
@ -229,7 +231,8 @@ mod tests {
email: None, email: None,
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -240,6 +243,7 @@ mod tests {
title: "nada".to_owned(), title: "nada".to_owned(),
description: None, description: None,
category_id: 1, category_id: 1,
nsfw: false,
removed: None, removed: None,
deleted: None, deleted: None,
updated: None, updated: None,
@ -254,6 +258,7 @@ mod tests {
title: "nada".to_owned(), title: "nada".to_owned(),
description: None, description: None,
category_id: 1, category_id: 1,
nsfw: false,
removed: false, removed: false,
deleted: false, deleted: false,
published: inserted_community.published, published: inserted_community.published,

View file

@ -12,6 +12,7 @@ table! {
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
deleted -> Bool, deleted -> Bool,
nsfw -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
category_name -> Varchar, category_name -> Varchar,
number_of_subscribers -> BigInt, number_of_subscribers -> BigInt,
@ -84,6 +85,7 @@ pub struct CommunityView {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool, pub deleted: bool,
pub nsfw: bool,
pub creator_name: String, pub creator_name: String,
pub category_name: String, pub category_name: String,
pub number_of_subscribers: i64, pub number_of_subscribers: i64,
@ -112,13 +114,15 @@ impl CommunityView {
query.first::<Self>(conn) query.first::<Self>(conn)
} }
pub fn list(conn: &PgConnection, pub fn list(
sort: &SortType, conn: &PgConnection,
from_user_id: Option<i32>, sort: &SortType,
search_term: Option<String>, from_user_id: Option<i32>,
page: Option<i64>, show_nsfw: bool,
limit: Option<i64>, search_term: Option<String>,
) -> Result<Vec<Self>, Error> { page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
use super::community_view::community_view::dsl::*; use super::community_view::community_view::dsl::*;
let mut query = community_view.into_boxed(); let mut query = community_view.into_boxed();
@ -143,6 +147,10 @@ impl CommunityView {
_ => () _ => ()
}; };
if !show_nsfw {
query = query.filter(nsfw.eq(false));
};
query query
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)

View file

@ -412,7 +412,8 @@ mod tests {
email: None, email: None,
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let inserted_mod = User_::create(&conn, &new_mod).unwrap(); let inserted_mod = User_::create(&conn, &new_mod).unwrap();
@ -425,7 +426,8 @@ mod tests {
email: None, email: None,
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -438,7 +440,8 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None, removed: None,
deleted: None, deleted: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_community = Community::create(&conn, &new_community).unwrap(); let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -452,7 +455,8 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
locked: None, locked: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_post = Post::create(&conn, &new_post).unwrap(); let inserted_post = Post::create(&conn, &new_post).unwrap();

View file

@ -15,6 +15,7 @@ pub struct Post {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool, pub deleted: bool,
pub nsfw: bool,
} }
#[derive(Insertable, AsChangeset, Clone)] #[derive(Insertable, AsChangeset, Clone)]
@ -29,6 +30,7 @@ pub struct PostForm {
pub locked: Option<bool>, pub locked: Option<bool>,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>, pub deleted: Option<bool>,
pub nsfw: bool,
} }
impl Crud<PostForm> for Post { impl Crud<PostForm> for Post {
@ -183,7 +185,8 @@ mod tests {
email: None, email: None,
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -196,7 +199,8 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None, removed: None,
deleted: None, deleted: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_community = Community::create(&conn, &new_community).unwrap(); let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -210,6 +214,7 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
locked: None, locked: None,
nsfw: false,
updated: None updated: None
}; };
@ -225,6 +230,7 @@ mod tests {
published: inserted_post.published, published: inserted_post.published,
removed: false, removed: false,
locked: false, locked: false,
nsfw: false,
deleted: false, deleted: false,
updated: None updated: None
}; };

View file

@ -19,10 +19,12 @@ table! {
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
deleted -> Bool, deleted -> Bool,
nsfw -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
community_name -> Varchar, community_name -> Varchar,
community_removed -> Bool, community_removed -> Bool,
community_deleted -> Bool, community_deleted -> Bool,
community_nsfw -> Bool,
number_of_comments -> BigInt, number_of_comments -> BigInt,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
@ -51,10 +53,12 @@ pub struct PostView {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool, pub deleted: bool,
pub nsfw: bool,
pub creator_name: String, pub creator_name: String,
pub community_name: String, pub community_name: String,
pub community_removed: bool, pub community_removed: bool,
pub community_deleted: bool, pub community_deleted: bool,
pub community_nsfw: bool,
pub number_of_comments: i64, pub number_of_comments: i64,
pub score: i64, pub score: i64,
pub upvotes: i64, pub upvotes: i64,
@ -68,18 +72,20 @@ pub struct PostView {
} }
impl PostView { impl PostView {
pub fn list(conn: &PgConnection, pub fn list(
type_: PostListingType, conn: &PgConnection,
sort: &SortType, type_: PostListingType,
for_community_id: Option<i32>, sort: &SortType,
for_creator_id: Option<i32>, for_community_id: Option<i32>,
search_term: Option<String>, for_creator_id: Option<i32>,
my_user_id: Option<i32>, search_term: Option<String>,
saved_only: bool, my_user_id: Option<i32>,
unread_only: bool, show_nsfw: bool,
page: Option<i64>, saved_only: bool,
limit: Option<i64>, unread_only: bool,
) -> Result<Vec<Self>, Error> { page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
use super::post_view::post_view::dsl::*; use super::post_view::post_view::dsl::*;
let (limit, offset) = limit_and_offset(page, limit); let (limit, offset) = limit_and_offset(page, limit);
@ -121,6 +127,12 @@ impl PostView {
query = query.filter(user_id.is_null()); query = query.filter(user_id.is_null());
} }
if !show_nsfw {
query = query
.filter(nsfw.eq(false))
.filter(community_nsfw.eq(false));
};
query = match sort { query = match sort {
SortType::Hot => query.order_by(hot_rank.desc()) SortType::Hot => query.order_by(hot_rank.desc())
.then_order_by(published.desc()), .then_order_by(published.desc()),
@ -196,6 +208,7 @@ mod tests {
updated: None, updated: None,
admin: false, admin: false,
banned: false, banned: false,
show_nsfw: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -208,7 +221,8 @@ mod tests {
category_id: 1, category_id: 1,
removed: None, removed: None,
deleted: None, deleted: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_community = Community::create(&conn, &new_community).unwrap(); let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -222,7 +236,8 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
locked: None, locked: None,
updated: None updated: None,
nsfw: false,
}; };
let inserted_post = Post::create(&conn, &new_post).unwrap(); let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -266,6 +281,7 @@ mod tests {
community_name: community_name.to_owned(), community_name: community_name.to_owned(),
community_removed: false, community_removed: false,
community_deleted: false, community_deleted: false,
community_nsfw: false,
number_of_comments: 0, number_of_comments: 0,
score: 1, score: 1,
upvotes: 1, upvotes: 1,
@ -276,6 +292,7 @@ mod tests {
subscribed: None, subscribed: None,
read: None, read: None,
saved: None, saved: None,
nsfw: false,
}; };
let expected_post_listing_with_user = PostView { let expected_post_listing_with_user = PostView {
@ -294,6 +311,7 @@ mod tests {
community_name: community_name.to_owned(), community_name: community_name.to_owned(),
community_removed: false, community_removed: false,
community_deleted: false, community_deleted: false,
community_nsfw: false,
number_of_comments: 0, number_of_comments: 0,
score: 1, score: 1,
upvotes: 1, upvotes: 1,
@ -304,6 +322,7 @@ mod tests {
subscribed: None, subscribed: None,
read: None, read: None,
saved: None, saved: None,
nsfw: false,
}; };
@ -315,6 +334,7 @@ mod tests {
Some(inserted_user.id), Some(inserted_user.id),
false, false,
false, false,
false,
None, None,
None).unwrap(); None).unwrap();
let read_post_listings_no_user = PostView::list(&conn, let read_post_listings_no_user = PostView::list(&conn,
@ -326,6 +346,7 @@ mod tests {
None, None,
false, false,
false, false,
false,
None, None,
None).unwrap(); None).unwrap();
let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap(); let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();

View file

@ -18,7 +18,8 @@ pub struct User_ {
pub admin: bool, pub admin: bool,
pub banned: bool, pub banned: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>,
pub show_nsfw: bool,
} }
#[derive(Insertable, AsChangeset, Clone)] #[derive(Insertable, AsChangeset, Clone)]
@ -31,7 +32,8 @@ pub struct UserForm {
pub admin: bool, pub admin: bool,
pub banned: bool, pub banned: bool,
pub email: Option<String>, pub email: Option<String>,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>,
pub show_nsfw: bool,
} }
impl Crud<UserForm> for User_ { impl Crud<UserForm> for User_ {
@ -77,6 +79,7 @@ pub struct Claims {
pub id: i32, pub id: i32,
pub username: String, pub username: String,
pub iss: String, pub iss: String,
pub show_nsfw: bool,
} }
impl Claims { impl Claims {
@ -96,6 +99,7 @@ impl User_ {
id: self.id, id: self.id,
username: self.name.to_owned(), username: self.name.to_owned(),
iss: self.fedi_name.to_owned(), iss: self.fedi_name.to_owned(),
show_nsfw: self.show_nsfw,
}; };
encode(&Header::default(), &my_claims, Settings::get().jwt_secret.as_ref()).unwrap() encode(&Header::default(), &my_claims, Settings::get().jwt_secret.as_ref()).unwrap()
} }
@ -133,7 +137,8 @@ mod tests {
email: None, email: None,
admin: false, admin: false,
banned: false, banned: false,
updated: None updated: None,
show_nsfw: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -149,7 +154,8 @@ mod tests {
admin: false, admin: false,
banned: false, banned: false,
published: inserted_user.published, published: inserted_user.published,
updated: None updated: None,
show_nsfw: false,
}; };
let read_user = User_::read(&conn, inserted_user.id).unwrap(); let read_user = User_::read(&conn, inserted_user.id).unwrap();

View file

@ -1,3 +1,4 @@
#![recursion_limit = "512"]
#[macro_use] pub extern crate strum_macros; #[macro_use] pub extern crate strum_macros;
#[macro_use] pub extern crate lazy_static; #[macro_use] pub extern crate lazy_static;
#[macro_use] pub extern crate failure; #[macro_use] pub extern crate failure;

View file

@ -52,6 +52,7 @@ table! {
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
deleted -> Bool, deleted -> Bool,
nsfw -> Bool,
} }
} }
@ -185,6 +186,7 @@ table! {
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
deleted -> Bool, deleted -> Bool,
nsfw -> Bool,
} }
} }
@ -240,6 +242,7 @@ table! {
banned -> Bool, banned -> Bool,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
show_nsfw -> Bool,
} }
} }

View file

@ -134,17 +134,19 @@ impl ChatServer {
use crate::db::*; use crate::db::*;
use crate::db::post_view::*; use crate::db::post_view::*;
let conn = establish_connection(); let conn = establish_connection();
let posts = PostView::list(&conn, let posts = PostView::list(
PostListingType::Community, &conn,
&SortType::New, PostListingType::Community,
Some(*community_id), &SortType::New,
None, Some(*community_id),
None, None,
None, None,
false, None,
false, false,
None, false,
Some(9999))?; false,
None,
Some(9999))?;
for post in posts { for post in posts {
self.send_room_message(&post.id, message, skip_id); self.send_room_message(&post.id, message, skip_id);
} }
@ -303,6 +305,11 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
let res = Oper::new(user_operation, get_user_details).perform()?; let res = Oper::new(user_operation, get_user_details).perform()?;
Ok(serde_json::to_string(&res)?) Ok(serde_json::to_string(&res)?)
}, },
UserOperation::SaveUserSettings => {
let save_user_settings: SaveUserSettings = serde_json::from_str(data)?;
let res = Oper::new(user_operation, save_user_settings).perform()?;
Ok(serde_json::to_string(&res)?)
},
UserOperation::AddAdmin => { UserOperation::AddAdmin => {
let add_admin: AddAdmin = serde_json::from_str(data)?; let add_admin: AddAdmin = serde_json::from_str(data)?;
let res = Oper::new(user_operation, add_admin).perform()?; let res = Oper::new(user_operation, add_admin).perform()?;

View file

@ -41,6 +41,6 @@
"fuse-box": "^3.1.3", "fuse-box": "^3.1.3",
"ts-transform-classcat": "^0.0.2", "ts-transform-classcat": "^0.0.2",
"ts-transform-inferno": "^4.0.2", "ts-transform-inferno": "^4.0.2",
"typescript": "^3.3.3333" "typescript": "^3.5.3"
} }
} }

View file

@ -30,7 +30,8 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
communityForm: { communityForm: {
name: null, name: null,
title: null, title: null,
category_id: null category_id: null,
nsfw: false,
}, },
categories: [], categories: [],
loading: false loading: false
@ -48,6 +49,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
category_id: this.props.community.category_id, category_id: this.props.community.category_id,
description: this.props.community.description, description: this.props.community.description,
edit_id: this.props.community.id, edit_id: this.props.community.id,
nsfw: this.props.community.nsfw,
auth: null auth: null
} }
} }
@ -103,6 +105,14 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row">
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.communityForm.nsfw} onChange={linkEvent(this, this.handleCommunityNsfwChange)}/>
<label class="form-check-label"><T i18nKey="nsfw">#</T></label>
</div>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
@ -147,6 +157,11 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
i.setState(i.state); i.setState(i.state);
} }
handleCommunityNsfwChange(i: CommunityForm, event: any) {
i.state.communityForm.nsfw = event.target.checked;
i.setState(i.state);
}
handleCancel(i: CommunityForm) { handleCancel(i: CommunityForm) {
i.props.onCancel(); i.props.onCancel();
} }

View file

@ -37,6 +37,7 @@ export class Community extends Component<any, State> {
number_of_comments: null, number_of_comments: null,
published: null, published: null,
removed: null, removed: null,
nsfw: false,
deleted: null, deleted: null,
}, },
moderators: [], moderators: [],
@ -105,6 +106,9 @@ export class Community extends Component<any, State> {
{this.state.community.removed && {this.state.community.removed &&
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small> <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
} }
{this.state.community.nsfw &&
<small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
}
</h5> </h5>
{this.selects()} {this.selects()}
<PostListings posts={this.state.posts} /> <PostListings posts={this.state.posts} />

View file

@ -28,6 +28,7 @@ export class Login extends Component<any, State> {
password: undefined, password: undefined,
password_verify: undefined, password_verify: undefined,
admin: false, admin: false,
show_nsfw: false,
}, },
loginLoading: false, loginLoading: false,
registerLoading: false, registerLoading: false,
@ -125,11 +126,18 @@ export class Login extends Component<any, State> {
<input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> <input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
</div> </div>
</div> </div>
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.registerForm.show_nsfw} onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}/>
<label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
</div>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.registerLoading ? <button type="submit" class="btn btn-secondary">{this.state.registerLoading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button> <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
</div> </div>
</div> </div>
</form> </form>
@ -181,6 +189,11 @@ export class Login extends Component<any, State> {
i.setState(i.state); i.setState(i.state);
} }
handleRegisterShowNsfwChange(i: Login, event: any) {
i.state.registerForm.show_nsfw = event.target.checked;
i.setState(i.state);
}
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {

View file

@ -31,6 +31,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
private emptyState: PostFormState = { private emptyState: PostFormState = {
postForm: { postForm: {
name: null, name: null,
nsfw: false,
auth: null, auth: null,
community_id: null, community_id: null,
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null, creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
@ -54,6 +55,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
edit_id: this.props.post.id, edit_id: this.props.post.id,
creator_id: this.props.post.creator_id, creator_id: this.props.post.creator_id,
url: this.props.post.url, url: this.props.post.url,
nsfw: this.props.post.nsfw,
auth: null auth: null
} }
} }
@ -126,6 +128,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</div> </div>
</div> </div>
} }
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.postForm.nsfw} onChange={linkEvent(this, this.handlePostNsfwChange)}/>
<label class="form-check-label"><T i18nKey="nsfw">#</T></label>
</div>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
@ -196,6 +206,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
i.setState(i.state); i.setState(i.state);
} }
handlePostNsfwChange(i: PostForm, event: any) {
i.state.postForm.nsfw = event.target.checked;
i.setState(i.state);
}
handleCancel(i: PostForm) { handleCancel(i: PostForm) {
i.props.onCancel(); i.props.onCancel();
} }

View file

@ -93,6 +93,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{post.locked && {post.locked &&
<small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small> <small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small>
} }
{post.nsfw &&
<small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
}
{ post.url && isImage(post.url) && { post.url && isImage(post.url) &&
<> <>
{ !this.state.imageExpanded { !this.state.imageExpanded
@ -251,6 +254,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
edit_id: i.props.post.id, edit_id: i.props.post.id,
creator_id: i.props.post.creator_id, creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted, deleted: !i.props.post.deleted,
nsfw: i.props.post.nsfw,
auth: null auth: null
}; };
WebSocketService.Instance.editPost(deleteForm); WebSocketService.Instance.editPost(deleteForm);
@ -285,6 +289,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
creator_id: i.props.post.creator_id, creator_id: i.props.post.creator_id,
removed: !i.props.post.removed, removed: !i.props.post.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
nsfw: i.props.post.nsfw,
auth: null, auth: null,
}; };
WebSocketService.Instance.editPost(form); WebSocketService.Instance.editPost(form);
@ -299,6 +304,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
community_id: i.props.post.community_id, community_id: i.props.post.community_id,
edit_id: i.props.post.id, edit_id: i.props.post.id,
creator_id: i.props.post.creator_id, creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
locked: !i.props.post.locked, locked: !i.props.post.locked,
auth: null, auth: null,
}; };

View file

@ -23,6 +23,7 @@ export class Setup extends Component<any, State> {
password: undefined, password: undefined,
password_verify: undefined, password_verify: undefined,
admin: true, admin: true,
show_nsfw: true,
}, },
doneRegisteringUser: false, doneRegisteringUser: false,
userLoading: false, userLoading: false,

View file

@ -2,8 +2,8 @@ import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces'; import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils'; import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
@ -28,6 +28,8 @@ interface UserState {
sort: SortType; sort: SortType;
page: number; page: number;
loading: boolean; loading: boolean;
userSettingsForm: UserSettingsForm;
userSettingsLoading: boolean;
} }
export class User extends Component<any, UserState> { export class User extends Component<any, UserState> {
@ -54,6 +56,11 @@ export class User extends Component<any, UserState> {
view: this.getViewFromProps(this.props), view: this.getViewFromProps(this.props),
sort: this.getSortTypeFromProps(this.props), sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props), page: this.getPageFromProps(this.props),
userSettingsForm: {
show_nsfw: null,
auth: null,
},
userSettingsLoading: null,
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -75,6 +82,10 @@ export class User extends Component<any, UserState> {
this.refetch(); this.refetch();
} }
get isCurrentUser() {
return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id;
}
getViewFromProps(props: any): View { getViewFromProps(props: any): View {
return (props.match.params.view) ? return (props.match.params.view) ?
View[capitalizeFirstLetter(props.match.params.view)] : View[capitalizeFirstLetter(props.match.params.view)] :
@ -131,6 +142,9 @@ export class User extends Component<any, UserState> {
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
{this.userInfo()} {this.userInfo()}
{this.isCurrentUser &&
this.userSettings()
}
{this.moderates()} {this.moderates()}
{this.follows()} {this.follows()}
</div> </div>
@ -219,7 +233,7 @@ export class User extends Component<any, UserState> {
return ( return (
<div> <div>
<h5>{user.name}</h5> <h5>{user.name}</h5>
<div>{i18n.t('joined')}<MomentTime data={user} /></div> <div>{i18n.t('joined')} <MomentTime data={user} /></div>
<table class="table table-bordered table-sm mt-2"> <table class="table table-bordered table-sm mt-2">
<tr> <tr>
<td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td> <td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td>
@ -235,6 +249,30 @@ export class User extends Component<any, UserState> {
) )
} }
userSettings() {
return (
<div>
<h5><T i18nKey="settings">#</T></h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group row">
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.userSettingsForm.show_nsfw} onChange={linkEvent(this, this.handleUserSettingsShowNsfwChange)}/>
<label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button type="submit" class="btn btn-secondary">{this.state.userSettingsLoading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('save'))}</button>
</div>
</div>
</form>
</div>
)
}
moderates() { moderates() {
return ( return (
<div> <div>
@ -329,6 +367,19 @@ export class User extends Component<any, UserState> {
i.refetch(); i.refetch();
} }
handleUserSettingsShowNsfwChange(i: User, event: any) {
i.state.userSettingsForm.show_nsfw = event.target.checked;
i.setState(i.state);
}
handleUserSettingsSubmit(i: User, event: any) {
event.preventDefault();
i.state.userSettingsLoading = true;
i.setState(i.state);
WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
}
parseMessage(msg: any) { parseMessage(msg: any) {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
@ -343,6 +394,9 @@ export class User extends Component<any, UserState> {
this.state.moderates = res.moderates; this.state.moderates = res.moderates;
this.state.posts = res.posts; this.state.posts = res.posts;
this.state.loading = false; this.state.loading = false;
if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw;
}
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`; document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0,0); window.scrollTo(0,0);
this.setState(this.state); this.setState(this.state);
@ -378,6 +432,12 @@ export class User extends Component<any, UserState> {
if (res.comment.my_vote !== null) if (res.comment.my_vote !== null)
found.my_vote = res.comment.my_vote; found.my_vote = res.comment.my_vote;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.SaveUserSettings) {
this.state = this.emptyState;
this.state.userSettingsLoading = false;
this.setState(this.state);
let res: LoginResponse = msg;
UserService.Instance.login(res);
} }
} }
} }

View file

@ -1,5 +1,5 @@
export enum UserOperation { export enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead 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, SaveUserSettings
} }
export enum CommentSortType { export enum CommentSortType {
@ -22,6 +22,7 @@ export interface User {
id: number; id: number;
iss: string; iss: string;
username: string; username: string;
show_nsfw: boolean;
} }
export interface UserView { export interface UserView {
@ -53,6 +54,7 @@ export interface Community {
creator_id: number; creator_id: number;
removed: boolean; removed: boolean;
deleted: boolean; deleted: boolean;
nsfw: boolean;
published: string; published: string;
updated?: string; updated?: string;
creator_name: string; creator_name: string;
@ -74,11 +76,14 @@ export interface Post {
removed: boolean; removed: boolean;
deleted: boolean; deleted: boolean;
locked: boolean; locked: boolean;
nsfw: boolean;
published: string; published: string;
updated?: string; updated?: string;
creator_name: string; creator_name: string;
community_name: string; community_name: string;
community_removed: boolean; community_removed: boolean;
community_deleted: boolean;
community_nsfw: boolean;
number_of_comments: number; number_of_comments: number;
score: number; score: number;
upvotes: number; upvotes: number;
@ -334,6 +339,7 @@ export interface RegisterForm {
password: string; password: string;
password_verify: string; password_verify: string;
admin: boolean; admin: boolean;
show_nsfw: boolean;
} }
export interface LoginResponse { export interface LoginResponse {
@ -341,7 +347,10 @@ export interface LoginResponse {
jwt: string; jwt: string;
} }
export interface UserSettingsForm {
show_nsfw: boolean;
auth: string;
}
export interface CommunityForm { export interface CommunityForm {
name: string; name: string;
@ -351,6 +360,7 @@ export interface CommunityForm {
edit_id?: number; edit_id?: number;
removed?: boolean; removed?: boolean;
deleted?: boolean; deleted?: boolean;
nsfw: boolean;
reason?: string; reason?: string;
expires?: number; expires?: number;
auth?: string; auth?: string;
@ -396,6 +406,7 @@ export interface PostForm {
creator_id: number; creator_id: number;
removed?: boolean; removed?: boolean;
deleted?: boolean; deleted?: boolean;
nsfw: boolean;
locked?: boolean; locked?: boolean;
reason?: string; reason?: string;
auth: string; auth: string;

View file

@ -1,5 +1,5 @@
import { wsUri } from '../env'; import { wsUri } from '../env';
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, AddAdminForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm } from '../interfaces'; import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, AddAdminForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm, UserSettingsForm } from '../interfaces';
import { webSocket } from 'rxjs/webSocket'; import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
@ -184,6 +184,11 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.MarkAllAsRead, form)); this.subject.next(this.wsSendWrapper(UserOperation.MarkAllAsRead, form));
} }
public saveUserSettings(userSettingsForm: UserSettingsForm) {
this.setAuth(userSettingsForm);
this.subject.next(this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm));
}
private wsSendWrapper(op: UserOperation, data: any) { private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data }; let send = { op: UserOperation[op], data: data };
console.log(send); console.log(send);

View file

@ -29,6 +29,7 @@ export const en = {
mod: 'mod', mod: 'mod',
mods: 'mods', mods: 'mods',
moderates: 'Moderates', moderates: 'Moderates',
settings: 'Settings',
remove_as_mod: 'remove as mod', remove_as_mod: 'remove as mod',
appoint_as_mod: 'appoint as mod', appoint_as_mod: 'appoint as mod',
modlog: 'Modlog', modlog: 'Modlog',
@ -112,6 +113,8 @@ export const en = {
setup_admin: 'Set Up Site Administrator', setup_admin: 'Set Up Site Administrator',
your_site: 'your site', your_site: 'your site',
modified: 'modified', modified: 'modified',
nsfw: 'NSFW',
show_nsfw: 'Show NSFW content',
sponsors: 'Sponsors', sponsors: 'Sponsors',
sponsors_of_lemmy: 'Sponsors of Lemmy', sponsors_of_lemmy: 'Sponsors of Lemmy',
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:', sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',

View file

@ -2,7 +2,7 @@
"extends": "tslint:recommended", "extends": "tslint:recommended",
"rules": { "rules": {
"forin": false, "forin": false,
"indent": [ true, "tabs" ], "indent": [ true, "spaces" ],
"interface-name": false, "interface-name": false,
"ban-types": true, "ban-types": true,
"max-classes-per-file": true, "max-classes-per-file": true,

View file

@ -2773,7 +2773,7 @@ typescript@^2.6.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
typescript@^3.3.3333: typescript@^3.5.3:
version "3.5.3" version "3.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977"
integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==