Merge branch 'dev'

This commit is contained in:
Dessalines 2019-08-15 16:53:59 -07:00
commit ebfdd385ef
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.
- Clean, mobile-friendly interface.
- i18n / internationalization support.
- NSFW post / community support.
- High performance.
- Server is written in rust.
- Front end is `~80kB` gzipped.

View File

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

View File

@ -2,12 +2,14 @@ version: '2.4'
services:
db:
image: postgres
image: postgres:12-alpine
restart: always
environment:
POSTGRES_USER: rrr
POSTGRES_PASSWORD: rrr
POSTGRES_DB: rrr
volumes:
- db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rrr"]
interval: 5s
@ -26,3 +28,5 @@ services:
depends_on:
db:
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
### 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
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>,
}
```
#### Save User Settings
##### Request
```rust
{
show_nsfw: bool,
auth: String,
}
```
##### Response
```rust
{
op: String,
jwt: String
}
```
#### Get Replies / Inbox
##### Request
```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

@ -23,6 +23,7 @@ pub struct CreateCommunity {
title: String,
description: Option<String>,
category_id: i32,
nsfw: bool,
auth: String
}
@ -86,6 +87,7 @@ pub struct EditCommunity {
category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
nsfw: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String
@ -194,6 +196,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
creator_id: user_id,
removed: None,
deleted: None,
nsfw: data.nsfw,
updated: None,
};
@ -291,6 +294,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
creator_id: user_id,
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
updated: Some(naive_now())
};
@ -333,12 +337,11 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
let data: &ListCommunities = &self.data;
let conn = establish_connection();
let user_id: Option<i32> = match &data.auth {
let user_claims: Option<Claims> = match &data.auth {
Some(auth) => {
match Claims::decode(&auth) {
Ok(claims) => {
let user_id = claims.claims.id;
Some(user_id)
Some(claims.claims)
}
Err(_e) => None
}
@ -346,9 +349,26 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
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 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
Ok(

View File

@ -22,7 +22,7 @@ pub mod site;
#[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, 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)]

View File

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

View File

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

View File

@ -15,6 +15,13 @@ pub struct Register {
password: String,
password_verify: String,
admin: bool,
show_nsfw: bool,
}
#[derive(Serialize, Deserialize)]
pub struct SaveUserSettings {
show_nsfw: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
@ -151,6 +158,7 @@ impl Perform<LoginResponse> for Oper<Register> {
updated: None,
admin: data.admin,
banned: false,
show_nsfw: data.show_nsfw,
};
// Create the user
@ -170,6 +178,7 @@ impl Perform<LoginResponse> for Oper<Register> {
title: "The Default Community".to_string(),
description: Some("The Default Community".to_string()),
category_id: 1,
nsfw: false,
creator_id: inserted_user.id,
removed: 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> {
fn perform(&self) -> Result<GetUserDetailsResponse, Error> {
let data: &GetUserDetails = &self.data;
let conn = establish_connection();
let user_id: Option<i32> = match &data.auth {
let user_claims: Option<Claims> = match &data.auth {
Some(auth) => {
match Claims::decode(&auth) {
Ok(claims) => {
let user_id = claims.claims.id;
Some(user_id)
Some(claims.claims)
}
Err(_e) => None
}
@ -237,6 +289,16 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
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
let sort = SortType::from_str(&data.sort)?;
@ -249,32 +311,37 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
// If its saved only, you don't care what creator it was
let posts = if data.saved_only {
PostView::list(&conn,
PostView::list(
&conn,
PostListingType::All,
&sort,
data.community_id,
None,
None,
Some(user_details_id),
show_nsfw,
data.saved_only,
false,
data.page,
data.limit)?
} else {
PostView::list(&conn,
PostView::list(
&conn,
PostListingType::All,
&sort,
data.community_id,
Some(user_details_id),
None,
user_id,
show_nsfw,
data.saved_only,
false,
data.page,
data.limit)?
};
let comments = if data.saved_only {
CommentView::list(&conn,
CommentView::list(
&conn,
&sort,
None,
None,
@ -284,7 +351,8 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
data.page,
data.limit)?
} else {
CommentView::list(&conn,
CommentView::list(
&conn,
&sort,
None,
Some(user_details_id),
@ -343,6 +411,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
updated: Some(naive_now()),
admin: data.added,
banned: read_user.banned,
show_nsfw: read_user.show_nsfw,
};
match User_::update(&conn, data.user_id, &user_form) {
@ -402,6 +471,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
updated: Some(naive_now()),
admin: read_user.admin,
banned: data.ban,
show_nsfw: read_user.show_nsfw,
};
match User_::update(&conn, data.user_id, &user_form) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,10 +19,12 @@ table! {
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
creator_name -> Varchar,
community_name -> Varchar,
community_removed -> Bool,
community_deleted -> Bool,
community_nsfw -> Bool,
number_of_comments -> BigInt,
score -> BigInt,
upvotes -> BigInt,
@ -51,10 +53,12 @@ pub struct PostView {
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub nsfw: bool,
pub creator_name: String,
pub community_name: String,
pub community_removed: bool,
pub community_deleted: bool,
pub community_nsfw: bool,
pub number_of_comments: i64,
pub score: i64,
pub upvotes: i64,
@ -68,13 +72,15 @@ pub struct PostView {
}
impl PostView {
pub fn list(conn: &PgConnection,
pub fn list(
conn: &PgConnection,
type_: PostListingType,
sort: &SortType,
for_community_id: Option<i32>,
for_creator_id: Option<i32>,
search_term: Option<String>,
my_user_id: Option<i32>,
show_nsfw: bool,
saved_only: bool,
unread_only: bool,
page: Option<i64>,
@ -121,6 +127,12 @@ impl PostView {
query = query.filter(user_id.is_null());
}
if !show_nsfw {
query = query
.filter(nsfw.eq(false))
.filter(community_nsfw.eq(false));
};
query = match sort {
SortType::Hot => query.order_by(hot_rank.desc())
.then_order_by(published.desc()),
@ -196,6 +208,7 @@ mod tests {
updated: None,
admin: false,
banned: false,
show_nsfw: false,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -208,7 +221,8 @@ mod tests {
category_id: 1,
removed: None,
deleted: None,
updated: None
updated: None,
nsfw: false,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -222,7 +236,8 @@ mod tests {
removed: None,
deleted: None,
locked: None,
updated: None
updated: None,
nsfw: false,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -266,6 +281,7 @@ mod tests {
community_name: community_name.to_owned(),
community_removed: false,
community_deleted: false,
community_nsfw: false,
number_of_comments: 0,
score: 1,
upvotes: 1,
@ -276,6 +292,7 @@ mod tests {
subscribed: None,
read: None,
saved: None,
nsfw: false,
};
let expected_post_listing_with_user = PostView {
@ -294,6 +311,7 @@ mod tests {
community_name: community_name.to_owned(),
community_removed: false,
community_deleted: false,
community_nsfw: false,
number_of_comments: 0,
score: 1,
upvotes: 1,
@ -304,6 +322,7 @@ mod tests {
subscribed: None,
read: None,
saved: None,
nsfw: false,
};
@ -315,6 +334,7 @@ mod tests {
Some(inserted_user.id),
false,
false,
false,
None,
None).unwrap();
let read_post_listings_no_user = PostView::list(&conn,
@ -326,6 +346,7 @@ mod tests {
None,
false,
false,
false,
None,
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 banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>
pub updated: Option<chrono::NaiveDateTime>,
pub show_nsfw: bool,
}
#[derive(Insertable, AsChangeset, Clone)]
@ -31,7 +32,8 @@ pub struct UserForm {
pub admin: bool,
pub banned: bool,
pub email: Option<String>,
pub updated: Option<chrono::NaiveDateTime>
pub updated: Option<chrono::NaiveDateTime>,
pub show_nsfw: bool,
}
impl Crud<UserForm> for User_ {
@ -77,6 +79,7 @@ pub struct Claims {
pub id: i32,
pub username: String,
pub iss: String,
pub show_nsfw: bool,
}
impl Claims {
@ -96,6 +99,7 @@ impl User_ {
id: self.id,
username: self.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()
}
@ -133,7 +137,8 @@ mod tests {
email: None,
admin: false,
banned: false,
updated: None
updated: None,
show_nsfw: false,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -149,7 +154,8 @@ mod tests {
admin: false,
banned: false,
published: inserted_user.published,
updated: None
updated: None,
show_nsfw: false,
};
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 lazy_static;
#[macro_use] pub extern crate failure;

View File

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

View File

@ -134,7 +134,8 @@ impl ChatServer {
use crate::db::*;
use crate::db::post_view::*;
let conn = establish_connection();
let posts = PostView::list(&conn,
let posts = PostView::list(
&conn,
PostListingType::Community,
&SortType::New,
Some(*community_id),
@ -143,6 +144,7 @@ impl ChatServer {
None,
false,
false,
false,
None,
Some(9999))?;
for post in posts {
@ -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()?;
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 => {
let add_admin: AddAdmin = serde_json::from_str(data)?;
let res = Oper::new(user_operation, add_admin).perform()?;

View File

@ -41,6 +41,6 @@
"fuse-box": "^3.1.3",
"ts-transform-classcat": "^0.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: {
name: null,
title: null,
category_id: null
category_id: null,
nsfw: false,
},
categories: [],
loading: false
@ -48,6 +49,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
category_id: this.props.community.category_id,
description: this.props.community.description,
edit_id: this.props.community.id,
nsfw: this.props.community.nsfw,
auth: null
}
}
@ -103,6 +105,14 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
</select>
</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="col-12">
<button type="submit" class="btn btn-secondary mr-2">
@ -147,6 +157,11 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
i.setState(i.state);
}
handleCommunityNsfwChange(i: CommunityForm, event: any) {
i.state.communityForm.nsfw = event.target.checked;
i.setState(i.state);
}
handleCancel(i: CommunityForm) {
i.props.onCancel();
}

View File

@ -37,6 +37,7 @@ export class Community extends Component<any, State> {
number_of_comments: null,
published: null,
removed: null,
nsfw: false,
deleted: null,
},
moderators: [],
@ -105,6 +106,9 @@ export class Community extends Component<any, State> {
{this.state.community.removed &&
<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>
{this.selects()}
<PostListings posts={this.state.posts} />

View File

@ -28,6 +28,7 @@ export class Login extends Component<any, State> {
password: undefined,
password_verify: undefined,
admin: false,
show_nsfw: false,
},
loginLoading: 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 />
</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="col-sm-10">
<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>
</div>
</div>
</form>
@ -181,6 +189,11 @@ export class Login extends Component<any, 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) {
let op: UserOperation = msgOp(msg);
if (msg.error) {

View File

@ -31,6 +31,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
private emptyState: PostFormState = {
postForm: {
name: null,
nsfw: false,
auth: null,
community_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,
creator_id: this.props.post.creator_id,
url: this.props.post.url,
nsfw: this.props.post.nsfw,
auth: null
}
}
@ -126,6 +128,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</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="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2">
@ -196,6 +206,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
i.setState(i.state);
}
handlePostNsfwChange(i: PostForm, event: any) {
i.state.postForm.nsfw = event.target.checked;
i.setState(i.state);
}
handleCancel(i: PostForm) {
i.props.onCancel();
}

View File

@ -93,6 +93,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{post.locked &&
<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) &&
<>
{ !this.state.imageExpanded
@ -251,6 +254,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted,
nsfw: i.props.post.nsfw,
auth: null
};
WebSocketService.Instance.editPost(deleteForm);
@ -285,6 +289,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
reason: i.state.removeReason,
nsfw: i.props.post.nsfw,
auth: null,
};
WebSocketService.Instance.editPost(form);
@ -299,6 +304,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
community_id: i.props.post.community_id,
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
locked: !i.props.post.locked,
auth: null,
};

View File

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

View File

@ -2,8 +2,8 @@ import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces';
import { WebSocketService } from '../services';
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils';
import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes';
@ -28,6 +28,8 @@ interface UserState {
sort: SortType;
page: number;
loading: boolean;
userSettingsForm: UserSettingsForm;
userSettingsLoading: boolean;
}
export class User extends Component<any, UserState> {
@ -54,6 +56,11 @@ export class User extends Component<any, UserState> {
view: this.getViewFromProps(this.props),
sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props),
userSettingsForm: {
show_nsfw: null,
auth: null,
},
userSettingsLoading: null,
}
constructor(props: any, context: any) {
@ -75,6 +82,10 @@ export class User extends Component<any, UserState> {
this.refetch();
}
get isCurrentUser() {
return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id;
}
getViewFromProps(props: any): View {
return (props.match.params.view) ?
View[capitalizeFirstLetter(props.match.params.view)] :
@ -131,6 +142,9 @@ export class User extends Component<any, UserState> {
</div>
<div class="col-12 col-md-3">
{this.userInfo()}
{this.isCurrentUser &&
this.userSettings()
}
{this.moderates()}
{this.follows()}
</div>
@ -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() {
return (
<div>
@ -329,6 +367,19 @@ export class User extends Component<any, UserState> {
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) {
console.log(msg);
let op: UserOperation = msgOp(msg);
@ -343,6 +394,9 @@ export class User extends Component<any, UserState> {
this.state.moderates = res.moderates;
this.state.posts = res.posts;
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}`;
window.scrollTo(0,0);
this.setState(this.state);
@ -378,6 +432,12 @@ export class User extends Component<any, UserState> {
if (res.comment.my_vote !== null)
found.my_vote = res.comment.my_vote;
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 {
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 {
@ -22,6 +22,7 @@ export interface User {
id: number;
iss: string;
username: string;
show_nsfw: boolean;
}
export interface UserView {
@ -53,6 +54,7 @@ export interface Community {
creator_id: number;
removed: boolean;
deleted: boolean;
nsfw: boolean;
published: string;
updated?: string;
creator_name: string;
@ -74,11 +76,14 @@ export interface Post {
removed: boolean;
deleted: boolean;
locked: boolean;
nsfw: boolean;
published: string;
updated?: string;
creator_name: string;
community_name: string;
community_removed: boolean;
community_deleted: boolean;
community_nsfw: boolean;
number_of_comments: number;
score: number;
upvotes: number;
@ -334,6 +339,7 @@ export interface RegisterForm {
password: string;
password_verify: string;
admin: boolean;
show_nsfw: boolean;
}
export interface LoginResponse {
@ -341,7 +347,10 @@ export interface LoginResponse {
jwt: string;
}
export interface UserSettingsForm {
show_nsfw: boolean;
auth: string;
}
export interface CommunityForm {
name: string;
@ -351,6 +360,7 @@ export interface CommunityForm {
edit_id?: number;
removed?: boolean;
deleted?: boolean;
nsfw: boolean;
reason?: string;
expires?: number;
auth?: string;
@ -396,6 +406,7 @@ export interface PostForm {
creator_id: number;
removed?: boolean;
deleted?: boolean;
nsfw: boolean;
locked?: boolean;
reason?: string;
auth: string;

View File

@ -1,5 +1,5 @@
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 { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
@ -184,6 +184,11 @@ export class WebSocketService {
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) {
let send = { op: UserOperation[op], data: data };
console.log(send);

View File

@ -29,6 +29,7 @@ export const en = {
mod: 'mod',
mods: 'mods',
moderates: 'Moderates',
settings: 'Settings',
remove_as_mod: 'remove as mod',
appoint_as_mod: 'appoint as mod',
modlog: 'Modlog',
@ -112,6 +113,8 @@ export const en = {
setup_admin: 'Set Up Site Administrator',
your_site: 'your site',
modified: 'modified',
nsfw: 'NSFW',
show_nsfw: 'Show NSFW content',
sponsors: 'Sponsors',
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:',

View File

@ -2,7 +2,7 @@
"extends": "tslint:recommended",
"rules": {
"forin": false,
"indent": [ true, "tabs" ],
"indent": [ true, "spaces" ],
"interface-name": false,
"ban-types": 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"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
typescript@^3.3.3333:
typescript@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977"
integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==