- 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.
-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
## 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.
posts: Vec<PostView>,
}
```
-
+#### Save User Settings
+##### Request
+```rust
+{
+ show_nsfw: bool,
+ auth: String,
+}
+```
+##### Response
+```rust
+{
+ op: String,
+ jwt: String
+}
+```
#### Get Replies / Inbox
##### Request
```rust
--- /dev/null
+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
+;
+
--- /dev/null
+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
+;
name: String,
title: String,
description: Option<String>,
- category_id: i32 ,
+ category_id: i32,
+ nsfw: bool,
auth: String
}
category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
+ nsfw: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String
creator_id: user_id,
removed: None,
deleted: None,
+ nsfw: data.nsfw,
updated: None,
};
creator_id: user_id,
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
+ nsfw: data.nsfw,
updated: Some(naive_now())
};
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
}
}
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(
#[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)]
name: String,
url: Option<String>,
body: Option<String>,
+ nsfw: bool,
community_id: i32,
auth: String
}
body: Option<String>,
removed: Option<bool>,
deleted: Option<bool>,
+ nsfw: bool,
locked: Option<bool>,
reason: Option<String>,
auth: String
creator_id: user_id,
removed: None,
deleted: None,
+ nsfw: data.nsfw,
locked: None,
updated: None
};
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
}
}
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,
- type_,
- &sort,
- data.community_id,
- None,
- None,
- user_id,
- false,
- false,
- data.page,
- data.limit) {
+ let posts = match PostView::list(
+ &conn,
+ type_,
+ &sort,
+ data.community_id,
+ None,
+ None,
+ user_id,
+ show_nsfw,
+ false,
+ false,
+ data.page,
+ data.limit) {
Ok(posts) => posts,
Err(_e) => {
return Err(APIError::err(&self.op, "couldnt_get_posts"))?
}
};
- // Return the jwt
Ok(
GetPostsResponse {
op: self.op.to_string(),
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())
};
let mut communities = Vec::new();
let mut users = Vec::new();
+ // TODO no clean / non-nsfw searching rn
+
match type_ {
SearchType::Posts => {
posts = PostView::list(
None,
Some(data.q.to_owned()),
None,
+ true,
false,
false,
data.page,
&conn,
&sort,
None,
+ true,
Some(data.q.to_owned()),
data.page,
data.limit)?;
None,
Some(data.q.to_owned()),
None,
+ true,
false,
false,
data.page,
&conn,
&sort,
None,
+ true,
Some(data.q.to_owned()),
data.page,
data.limit)?;
password: String,
password_verify: String,
admin: bool,
+ show_nsfw: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct SaveUserSettings {
+ show_nsfw: bool,
+ auth: String,
}
#[derive(Serialize, Deserialize)]
updated: None,
admin: data.admin,
banned: false,
+ show_nsfw: data.show_nsfw,
};
// Create the user
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,
}
}
+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
}
}
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)?;
// If its saved only, you don't care what creator it was
let posts = if data.saved_only {
- PostView::list(&conn,
- PostListingType::All,
- &sort,
- data.community_id,
- None,
- None,
- Some(user_details_id),
- data.saved_only,
- false,
- data.page,
- data.limit)?
+ 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,
- PostListingType::All,
- &sort,
- data.community_id,
- Some(user_details_id),
- None,
- user_id,
- data.saved_only,
- false,
- data.page,
- data.limit)?
+ 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,
- &sort,
- None,
- None,
- None,
- Some(user_details_id),
- data.saved_only,
- data.page,
- data.limit)?
+ CommentView::list(
+ &conn,
+ &sort,
+ None,
+ None,
+ None,
+ Some(user_details_id),
+ data.saved_only,
+ data.page,
+ data.limit)?
} else {
- CommentView::list(&conn,
- &sort,
- None,
- Some(user_details_id),
- None,
- user_id,
- data.saved_only,
- data.page,
- data.limit)?
+ CommentView::list(
+ &conn,
+ &sort,
+ None,
+ Some(user_details_id),
+ None,
+ user_id,
+ data.saved_only,
+ data.page,
+ data.limit)?
};
let follows = CommunityFollowerView::for_user(&conn, user_details_id)?;
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) {
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) {
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
+ pub nsfw: bool,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
pub removed: Option<bool>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
+ pub nsfw: bool,
}
impl Crud<CommunityForm> for Community {
title: "nada".to_owned(),
description: None,
category_id: 1,
+ nsfw: false,
removed: None,
deleted: None,
updated: None,
title: "nada".to_owned(),
description: None,
category_id: 1,
+ nsfw: false,
removed: false,
deleted: false,
published: inserted_community.published,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
+ nsfw -> Bool,
creator_name -> Varchar,
category_name -> Varchar,
number_of_subscribers -> BigInt,
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,
query.first::<Self>(conn)
}
- pub fn list(conn: &PgConnection,
- sort: &SortType,
- from_user_id: Option<i32>,
- search_term: Option<String>,
- page: Option<i64>,
- limit: Option<i64>,
- ) -> Result<Vec<Self>, Error> {
+ 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>,
+ ) -> Result<Vec<Self>, Error> {
use super::community_view::community_view::dsl::*;
let mut query = community_view.into_boxed();
_ => ()
};
+ if !show_nsfw {
+ query = query.filter(nsfw.eq(false));
+ };
+
query
.limit(limit)
.offset(offset)
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
+ pub nsfw: bool,
}
#[derive(Insertable, AsChangeset, Clone)]
pub locked: Option<bool>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
+ pub nsfw: bool,
}
impl Crud<PostForm> for Post {
removed: None,
deleted: None,
locked: None,
+ nsfw: false,
updated: None
};
published: inserted_post.published,
removed: false,
locked: false,
+ nsfw: false,
deleted: false,
updated: None
};
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,
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,
}
impl PostView {
- 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>,
- saved_only: bool,
- unread_only: bool,
- page: Option<i64>,
- limit: Option<i64>,
- ) -> Result<Vec<Self>, Error> {
+ 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>,
+ limit: Option<i64>,
+ ) -> Result<Vec<Self>, Error> {
use super::post_view::post_view::dsl::*;
let (limit, offset) = limit_and_offset(page, limit);
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()),
community_name: community_name.to_owned(),
community_removed: false,
community_deleted: false,
+ community_nsfw: false,
number_of_comments: 0,
score: 1,
upvotes: 1,
community_name: community_name.to_owned(),
community_removed: false,
community_deleted: false,
+ community_nsfw: false,
number_of_comments: 0,
score: 1,
upvotes: 1,
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)]
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_ {
pub id: i32,
pub username: String,
pub iss: String,
+ pub show_nsfw: bool,
}
impl Claims {
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()
}
email: None,
admin: false,
banned: false,
- updated: None
+ updated: None,
+ show_nsfw: false,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
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();
+#![recursion_limit = "512"]
#[macro_use] pub extern crate strum_macros;
#[macro_use] pub extern crate lazy_static;
#[macro_use] pub extern crate failure;
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
+ nsfw -> Bool,
}
}
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
+ nsfw -> Bool,
}
}
banned -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
+ show_nsfw -> Bool,
}
}
use crate::db::*;
use crate::db::post_view::*;
let conn = establish_connection();
- let posts = PostView::list(&conn,
- PostListingType::Community,
- &SortType::New,
- Some(*community_id),
- None,
- None,
- None,
- false,
- false,
- None,
- Some(9999))?;
+ let posts = PostView::list(
+ &conn,
+ PostListingType::Community,
+ &SortType::New,
+ Some(*community_id),
+ None,
+ None,
+ None,
+ false,
+ false,
+ false,
+ None,
+ Some(9999))?;
for post in posts {
self.send_room_message(&post.id, message, skip_id);
}
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()?;
"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"
}
}
communityForm: {
name: null,
title: null,
- category_id: null
+ category_id: null,
+ nsfw: false,
},
categories: [],
loading: false
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
}
}
</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">
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();
}
number_of_comments: null,
published: null,
removed: null,
+ nsfw: false,
deleted: null,
},
moderators: [],
{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} />
password: undefined,
password_verify: undefined,
admin: false,
+ show_nsfw: false,
},
loginLoading: false,
registerLoading: false,
<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>
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) {
private emptyState: PostFormState = {
postForm: {
name: null,
+ nsfw: false,
auth: null,
community_id: null,
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
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
}
}
</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">
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();
}
{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
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);
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);
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,
};
password: undefined,
password_verify: undefined,
admin: true,
+ show_nsfw: true,
},
doneRegisteringUser: false,
userLoading: false,
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';
sort: SortType;
page: number;
loading: boolean;
+ userSettingsForm: UserSettingsForm;
+ userSettingsLoading: boolean;
}
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) {
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)] :
</div>
<div class="col-12 col-md-3">
{this.userInfo()}
+ {this.isCurrentUser &&
+ this.userSettings()
+ }
{this.moderates()}
{this.follows()}
</div>
return (
<div>
<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">
<tr>
<td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td>
)
}
+ 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>
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);
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);
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);
}
}
}
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 {
id: number;
iss: string;
username: string;
+ show_nsfw: boolean;
}
export interface UserView {
creator_id: number;
removed: boolean;
deleted: boolean;
+ nsfw: boolean;
published: string;
updated?: string;
creator_name: string;
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;
password: string;
password_verify: string;
admin: boolean;
+ show_nsfw: boolean;
}
export interface LoginResponse {
jwt: string;
}
-
+export interface UserSettingsForm {
+ show_nsfw: boolean;
+ auth: string;
+}
export interface CommunityForm {
name: string;
edit_id?: number;
removed?: boolean;
deleted?: boolean;
+ nsfw: boolean;
reason?: string;
expires?: number;
auth?: string;
creator_id: number;
removed?: boolean;
deleted?: boolean;
+ nsfw: boolean;
locked?: boolean;
reason?: string;
auth: string;
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';
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);
mod: 'mod',
mods: 'mods',
moderates: 'Moderates',
+ settings: 'Settings',
remove_as_mod: 'remove as mod',
appoint_as_mod: 'appoint as mod',
modlog: 'Modlog',
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:',
"extends": "tslint:recommended",
"rules": {
"forin": false,
- "indent": [ true, "tabs" ],
+ "indent": [ true, "spaces" ],
"interface-name": false,
"ban-types": true,
"max-classes-per-file": true,
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==