Saving replies, the actual fixes will be in the merge to dev.

This commit is contained in:
Dessalines 2019-04-19 21:06:25 -07:00
parent 25b75de078
commit b25b1b6a0d
42 changed files with 1017 additions and 432 deletions

View file

@ -38,7 +38,7 @@ create table community (
description text,
category_id int references category on update cascade on delete cascade not null,
creator_id int references user_ on update cascade on delete cascade not null,
removed boolean default false,
removed boolean default false not null,
published timestamp not null default now(),
updated timestamp
);

View file

@ -1,2 +1,4 @@
drop table post_read;
drop table post_saved;
drop table post_like;
drop table post;

View file

@ -5,8 +5,8 @@ create table post (
body text,
creator_id int references user_ on update cascade on delete cascade not null,
community_id int references community on update cascade on delete cascade not null,
removed boolean default false,
locked boolean default false,
removed boolean default false not null,
locked boolean default false not null,
published timestamp not null default now(),
updated timestamp
);
@ -20,3 +20,18 @@ create table post_like (
unique(post_id, user_id)
);
create table post_saved (
id serial primary key,
post_id int references post on update cascade on delete cascade not null,
user_id int references user_ on update cascade on delete cascade not null,
published timestamp not null default now(),
unique(post_id, user_id)
);
create table post_read (
id serial primary key,
post_id int references post on update cascade on delete cascade not null,
user_id int references user_ on update cascade on delete cascade not null,
published timestamp not null default now(),
unique(post_id, user_id)
);

View file

@ -1,2 +1,3 @@
drop table comment_saved;
drop table comment_like;
drop table comment;

View file

@ -4,7 +4,8 @@ create table comment (
post_id int references post on update cascade on delete cascade not null,
parent_id int references comment on update cascade on delete cascade,
content text not null,
removed boolean default false,
removed boolean default false not null,
read boolean default false not null,
published timestamp not null default now(),
updated timestamp
);
@ -18,3 +19,11 @@ create table comment_like (
published timestamp not null default now(),
unique(comment_id, user_id)
);
create table comment_saved (
id serial primary key,
comment_id int references comment on update cascade on delete cascade not null,
user_id int references user_ on update cascade on delete cascade not null,
published timestamp not null default now(),
unique(comment_id, user_id)
);

View file

@ -31,7 +31,8 @@ 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,
u.admin or (select cm.id::bool from community_moderator cm where u.id = cm.user_id and cm.community_id = ap.community_id) as am_mod
(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
@ -43,6 +44,7 @@ ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as am_mod
null as read,
null as saved
from all_post ap
;

View file

@ -13,19 +13,16 @@ with all_community as
select
ac.*,
u.id as user_id,
cf.id::boolean as subscribed,
u.admin or (select cm.id::bool from community_moderator cm where u.id = cm.user_id and cm.community_id = ac.id) as am_mod
(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
left join community_follower cf on u.id = cf.user_id and ac.id = cf.community_id
union all
select
ac.*,
null as user_id,
null as subscribed,
null as am_mod
null as subscribed
from all_community ac
;

View file

@ -4,7 +4,8 @@ with all_comment as
select
c.*,
(select community_id from post p where p.id = c.post_id),
(select cb.id::bool from community_user_ban cb where c.creator_id = cb.user_id) as banned,
(select u.banned from user_ u where c.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
(select name from user_ where c.creator_id = user_.id) as creator_name,
coalesce(sum(cl.score), 0) as score,
count (case when cl.score = 1 then 1 else null end) as upvotes,
@ -18,7 +19,7 @@ select
ac.*,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
u.admin or (select cm.id::bool from community_moderator cm, post p where u.id = cm.user_id and ac.post_id = p.id and p.community_id = cm.community_id) as am_mod
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
@ -29,6 +30,6 @@ select
ac.*,
null as user_id,
null as my_vote,
null as am_mod
null as saved
from all_comment ac
;

View file

@ -43,8 +43,7 @@ create view mod_ban_view as
select mb.*,
(select name from user_ u where mb.mod_user_id = u.id) as mod_user_name,
(select name from user_ u where mb.other_user_id = u.id) as other_user_name
from mod_ban_from_community mb;
from mod_ban mb;
create view mod_add_community_view as
select ma.*,
@ -53,7 +52,6 @@ select ma.*,
(select name from community c where ma.community_id = c.id) as community_name
from mod_add_community ma;
create view mod_add_view as
select ma.*,
(select name from user_ u where ma.mod_user_id = u.id) as mod_user_name,

View file

@ -1,9 +1,9 @@
extern crate diesel;
use schema::{comment, comment_like};
use schema::{comment, comment_like, comment_saved};
use diesel::*;
use diesel::result::Error;
use serde::{Deserialize, Serialize};
use {Crud, Likeable};
use {Crud, Likeable, Saveable};
use actions::post::Post;
// WITH RECURSIVE MyTree AS (
@ -22,7 +22,8 @@ pub struct Comment {
pub post_id: i32,
pub parent_id: Option<i32>,
pub content: String,
pub removed: Option<bool>,
pub removed: bool,
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>
}
@ -38,27 +39,6 @@ pub struct CommentForm {
pub updated: Option<chrono::NaiveDateTime>
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
#[belongs_to(Comment)]
#[table_name = "comment_like"]
pub struct CommentLike {
pub id: i32,
pub user_id: i32,
pub comment_id: i32,
pub post_id: i32,
pub score: i16,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="comment_like"]
pub struct CommentLikeForm {
pub user_id: i32,
pub comment_id: i32,
pub post_id: i32,
pub score: i16
}
impl Crud<CommentForm> for Comment {
fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use schema::comment::dsl::*;
@ -87,6 +67,27 @@ impl Crud<CommentForm> for Comment {
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
#[belongs_to(Comment)]
#[table_name = "comment_like"]
pub struct CommentLike {
pub id: i32,
pub user_id: i32,
pub comment_id: i32,
pub post_id: i32,
pub score: i16,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="comment_like"]
pub struct CommentLikeForm {
pub user_id: i32,
pub comment_id: i32,
pub post_id: i32,
pub score: i16
}
impl Likeable <CommentLikeForm> for CommentLike {
fn read(conn: &PgConnection, comment_id_from: i32) -> Result<Vec<Self>, Error> {
use schema::comment_like::dsl::*;
@ -119,6 +120,39 @@ impl CommentLike {
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Comment)]
#[table_name = "comment_saved"]
pub struct CommentSaved {
pub id: i32,
pub comment_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="comment_saved"]
pub struct CommentSavedForm {
pub comment_id: i32,
pub user_id: i32,
}
impl Saveable <CommentSavedForm> for CommentSaved {
fn save(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result<Self, Error> {
use schema::comment_saved::dsl::*;
insert_into(comment_saved)
.values(comment_saved_form)
.get_result::<Self>(conn)
}
fn unsave(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result<usize, Error> {
use schema::comment_saved::dsl::*;
diesel::delete(comment_saved
.filter(comment_id.eq(comment_saved_form.comment_id))
.filter(user_id.eq(comment_saved_form.user_id)))
.execute(conn)
}
}
#[cfg(test)]
mod tests {
use establish_connection;
@ -150,7 +184,7 @@ mod tests {
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
removed: false,
updated: None
};
@ -162,8 +196,8 @@ mod tests {
url: None,
body: None,
community_id: inserted_community.id,
removed: None,
locked: None,
removed: false,
locked: false,
updated: None
};
@ -185,7 +219,8 @@ mod tests {
content: "A test comment".into(),
creator_id: inserted_user.id,
post_id: inserted_post.id,
removed: Some(false),
removed: false,
read: false,
parent_id: None,
published: inserted_comment.published,
updated: None
@ -202,6 +237,7 @@ mod tests {
let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
// Comment Like
let comment_like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
@ -220,9 +256,25 @@ mod tests {
score: 1
};
// Comment Saved
let comment_saved_form = CommentSavedForm {
comment_id: inserted_comment.id,
user_id: inserted_user.id,
};
let inserted_comment_saved = CommentSaved::save(&conn, &comment_saved_form).unwrap();
let expected_comment_saved = CommentSaved {
id: inserted_comment_saved.id,
comment_id: inserted_comment.id,
user_id: inserted_user.id,
published: inserted_comment_saved.published,
};
let read_comment = Comment::read(&conn, inserted_comment.id).unwrap();
let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap();
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
let saved_removed = CommentSaved::unsave(&conn, &comment_saved_form).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Comment::delete(&conn, inserted_child_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
@ -233,8 +285,10 @@ mod tests {
assert_eq!(expected_comment, inserted_comment);
assert_eq!(expected_comment, updated_comment);
assert_eq!(expected_comment_like, inserted_comment_like);
assert_eq!(expected_comment_saved, inserted_comment_saved);
assert_eq!(expected_comment.id, inserted_child_comment.parent_id.unwrap());
assert_eq!(1, like_removed);
assert_eq!(1, saved_removed);
assert_eq!(1, num_deleted);
}

View file

@ -13,18 +13,20 @@ table! {
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Nullable<Bool>,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
community_id -> Int4,
banned -> Nullable<Bool>,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
am_mod -> Nullable<Bool>,
saved -> Nullable<Bool>,
}
}
@ -36,18 +38,20 @@ pub struct CommentView {
pub post_id: i32,
pub parent_id: Option<i32>,
pub content: String,
pub removed: Option<bool>,
pub removed: bool,
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub community_id: i32,
pub banned: Option<bool>,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub am_mod: Option<bool>,
pub saved: Option<bool>,
}
impl CommentView {
@ -57,6 +61,7 @@ impl CommentView {
for_post_id: Option<i32>,
for_creator_id: Option<i32>,
my_user_id: Option<i32>,
saved_only: bool,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
@ -82,6 +87,10 @@ impl CommentView {
query = query.filter(post_id.eq(for_post_id));
};
if saved_only {
query = query.filter(saved.eq(true));
}
query = match sort {
// SortType::Hot => query.order_by(hot_rank.desc()),
SortType::New => query.order_by(published.desc()),
@ -159,7 +168,7 @@ mod tests {
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
removed: false,
updated: None
};
@ -171,8 +180,8 @@ mod tests {
url: None,
body: None,
community_id: inserted_community.id,
removed: None,
locked: None,
removed: false,
locked: false,
updated: None
};
@ -205,8 +214,10 @@ mod tests {
post_id: inserted_post.id,
community_id: inserted_community.id,
parent_id: None,
removed: Some(false),
banned: None,
removed: false,
read: false,
banned: false,
banned_from_community: false,
published: inserted_comment.published,
updated: None,
creator_name: inserted_user.name.to_owned(),
@ -215,7 +226,7 @@ mod tests {
upvotes: 1,
user_id: None,
my_vote: None,
am_mod: None,
saved: None,
};
let expected_comment_view_with_user = CommentView {
@ -225,8 +236,10 @@ mod tests {
post_id: inserted_post.id,
community_id: inserted_community.id,
parent_id: None,
removed: Some(false),
banned: None,
removed: false,
read: false,
banned: false,
banned_from_community: false,
published: inserted_comment.published,
updated: None,
creator_name: inserted_user.name.to_owned(),
@ -235,11 +248,11 @@ mod tests {
upvotes: 1,
user_id: Some(inserted_user.id),
my_vote: Some(1),
am_mod: None,
saved: None,
};
let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, None, None).unwrap();
let read_comment_views_with_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, Some(inserted_user.id), None, None).unwrap();
let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, false, None, None).unwrap();
let read_comment_views_with_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, Some(inserted_user.id), false, None, None).unwrap();
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();

View file

@ -14,7 +14,7 @@ pub struct Community {
pub description: Option<String>,
pub category_id: i32,
pub creator_id: i32,
pub removed: Option<bool>,
pub removed: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>
}
@ -27,7 +27,7 @@ pub struct CommunityForm {
pub description: Option<String>,
pub category_id: i32,
pub creator_id: i32,
pub removed: Option<bool>,
pub removed: bool,
pub updated: Option<chrono::NaiveDateTime>
}
@ -236,7 +236,7 @@ mod tests {
title: "nada".to_owned(),
description: None,
category_id: 1,
removed: None,
removed: false,
updated: None,
};
@ -249,7 +249,7 @@ mod tests {
title: "nada".to_owned(),
description: None,
category_id: 1,
removed: Some(false),
removed: false,
published: inserted_community.published,
updated: None
};

View file

@ -12,7 +12,7 @@ table! {
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
removed -> Nullable<Bool>,
removed -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
creator_name -> Varchar,
@ -22,7 +22,6 @@ table! {
number_of_comments -> BigInt,
user_id -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
am_mod -> Nullable<Bool>,
}
}
@ -83,7 +82,7 @@ pub struct CommunityView {
pub description: Option<String>,
pub category_id: i32,
pub creator_id: i32,
pub removed: Option<bool>,
pub removed: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub creator_name: String,
@ -93,7 +92,6 @@ pub struct CommunityView {
pub number_of_comments: i64,
pub user_id: Option<i32>,
pub subscribed: Option<bool>,
pub am_mod: Option<bool>,
}
impl CommunityView {

View file

@ -441,7 +441,7 @@ mod tests {
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
removed: false,
updated: None
};
@ -453,8 +453,8 @@ mod tests {
body: None,
creator_id: inserted_user.id,
community_id: inserted_community.id,
removed: None,
locked: None,
removed: false,
locked: false,
updated: None
};

View file

@ -1,9 +1,9 @@
extern crate diesel;
use schema::{post, post_like};
use schema::{post, post_like, post_saved, post_read};
use diesel::*;
use diesel::result::Error;
use serde::{Deserialize, Serialize};
use {Crud, Likeable};
use {Crud, Likeable, Saveable, Readable};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="post"]
@ -14,8 +14,8 @@ pub struct Post {
pub body: Option<String>,
pub creator_id: i32,
pub community_id: i32,
pub removed: Option<bool>,
pub locked: Option<bool>,
pub removed: bool,
pub locked: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>
}
@ -28,30 +28,11 @@ pub struct PostForm {
pub body: Option<String>,
pub creator_id: i32,
pub community_id: i32,
pub removed: Option<bool>,
pub locked: Option<bool>,
pub removed: bool,
pub locked: bool,
pub updated: Option<chrono::NaiveDateTime>
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Post)]
#[table_name = "post_like"]
pub struct PostLike {
pub id: i32,
pub post_id: i32,
pub user_id: i32,
pub score: i16,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="post_like"]
pub struct PostLikeForm {
pub post_id: i32,
pub user_id: i32,
pub score: i16
}
impl Crud<PostForm> for Post {
fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
use schema::post::dsl::*;
@ -80,6 +61,25 @@ impl Crud<PostForm> for Post {
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Post)]
#[table_name = "post_like"]
pub struct PostLike {
pub id: i32,
pub post_id: i32,
pub user_id: i32,
pub score: i16,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="post_like"]
pub struct PostLikeForm {
pub post_id: i32,
pub user_id: i32,
pub score: i16
}
impl Likeable <PostLikeForm> for PostLike {
fn read(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
use schema::post_like::dsl::*;
@ -102,6 +102,72 @@ impl Likeable <PostLikeForm> for PostLike {
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Post)]
#[table_name = "post_saved"]
pub struct PostSaved {
pub id: i32,
pub post_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="post_saved"]
pub struct PostSavedForm {
pub post_id: i32,
pub user_id: i32,
}
impl Saveable <PostSavedForm> for PostSaved {
fn save(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result<Self, Error> {
use schema::post_saved::dsl::*;
insert_into(post_saved)
.values(post_saved_form)
.get_result::<Self>(conn)
}
fn unsave(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result<usize, Error> {
use schema::post_saved::dsl::*;
diesel::delete(post_saved
.filter(post_id.eq(post_saved_form.post_id))
.filter(user_id.eq(post_saved_form.user_id)))
.execute(conn)
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Post)]
#[table_name = "post_read"]
pub struct PostRead {
pub id: i32,
pub post_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="post_read"]
pub struct PostReadForm {
pub post_id: i32,
pub user_id: i32,
}
impl Readable <PostReadForm> for PostRead {
fn mark_as_read(conn: &PgConnection, post_read_form: &PostReadForm) -> Result<Self, Error> {
use schema::post_read::dsl::*;
insert_into(post_read)
.values(post_read_form)
.get_result::<Self>(conn)
}
fn mark_as_unread(conn: &PgConnection, post_read_form: &PostReadForm) -> Result<usize, Error> {
use schema::post_read::dsl::*;
diesel::delete(post_read
.filter(post_id.eq(post_read_form.post_id))
.filter(user_id.eq(post_read_form.user_id)))
.execute(conn)
}
}
#[cfg(test)]
mod tests {
use establish_connection;
@ -132,7 +198,7 @@ mod tests {
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
removed: false,
updated: None
};
@ -144,8 +210,8 @@ mod tests {
body: None,
creator_id: inserted_user.id,
community_id: inserted_community.id,
removed: None,
locked: None,
removed: false,
locked: false,
updated: None
};
@ -159,11 +225,12 @@ mod tests {
creator_id: inserted_user.id,
community_id: inserted_community.id,
published: inserted_post.published,
removed: Some(false),
locked: Some(false),
removed: false,
locked: false,
updated: None
};
// Post Like
let post_like_form = PostLikeForm {
post_id: inserted_post.id,
user_id: inserted_user.id,
@ -180,9 +247,41 @@ mod tests {
score: 1
};
// Post Save
let post_saved_form = PostSavedForm {
post_id: inserted_post.id,
user_id: inserted_user.id,
};
let inserted_post_saved = PostSaved::save(&conn, &post_saved_form).unwrap();
let expected_post_saved = PostSaved {
id: inserted_post_saved.id,
post_id: inserted_post.id,
user_id: inserted_user.id,
published: inserted_post_saved.published,
};
// Post Read
let post_read_form = PostReadForm {
post_id: inserted_post.id,
user_id: inserted_user.id,
};
let inserted_post_read = PostRead::mark_as_read(&conn, &post_read_form).unwrap();
let expected_post_read = PostRead {
id: inserted_post_read.id,
post_id: inserted_post.id,
user_id: inserted_user.id,
published: inserted_post_read.published,
};
let read_post = Post::read(&conn, inserted_post.id).unwrap();
let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap();
let like_removed = PostLike::remove(&conn, &post_like_form).unwrap();
let saved_removed = PostSaved::unsave(&conn, &post_saved_form).unwrap();
let read_removed = PostRead::mark_as_unread(&conn, &post_read_form).unwrap();
let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.id).unwrap();
@ -191,7 +290,11 @@ mod tests {
assert_eq!(expected_post, inserted_post);
assert_eq!(expected_post, updated_post);
assert_eq!(expected_post_like, inserted_post_like);
assert_eq!(expected_post_saved, inserted_post_saved);
assert_eq!(expected_post_read, inserted_post_read);
assert_eq!(1, like_removed);
assert_eq!(1, saved_removed);
assert_eq!(1, read_removed);
assert_eq!(1, num_deleted);
}

View file

@ -19,8 +19,8 @@ table! {
body -> Nullable<Text>,
creator_id -> Int4,
community_id -> Int4,
removed -> Nullable<Bool>,
locked -> Nullable<Bool>,
removed -> Bool,
locked -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
creator_name -> Varchar,
@ -33,7 +33,8 @@ table! {
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
am_mod -> Nullable<Bool>,
read -> Nullable<Bool>,
saved -> Nullable<Bool>,
}
}
@ -47,8 +48,8 @@ pub struct PostView {
pub body: Option<String>,
pub creator_id: i32,
pub community_id: i32,
pub removed: Option<bool>,
pub locked: Option<bool>,
pub removed: bool,
pub locked: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub creator_name: String,
@ -61,7 +62,8 @@ pub struct PostView {
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub subscribed: Option<bool>,
pub am_mod: Option<bool>,
pub read: Option<bool>,
pub saved: Option<bool>,
}
impl PostView {
@ -71,6 +73,8 @@ impl PostView {
for_community_id: Option<i32>,
for_creator_id: Option<i32>,
my_user_id: Option<i32>,
saved_only: bool,
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
@ -88,6 +92,15 @@ impl PostView {
query = query.filter(creator_id.eq(for_creator_id));
};
// TODO these are wrong, bc they'll only show saved for your logged in user, not theirs
if saved_only {
query = query.filter(saved.eq(true));
};
if unread_only {
query = query.filter(read.eq(false));
};
match type_ {
PostListingType::Subscribed => {
query = query.filter(subscribed.eq(true));
@ -187,7 +200,7 @@ mod tests {
description: None,
creator_id: inserted_user.id,
category_id: 1,
removed: None,
removed: false,
updated: None
};
@ -199,8 +212,8 @@ mod tests {
body: None,
creator_id: inserted_user.id,
community_id: inserted_community.id,
removed: None,
locked: None,
removed: false,
locked: false,
updated: None
};
@ -239,8 +252,8 @@ mod tests {
creator_id: inserted_user.id,
creator_name: user_name.to_owned(),
community_id: inserted_community.id,
removed: Some(false),
locked: Some(false),
removed: false,
locked: false,
community_name: community_name.to_owned(),
number_of_comments: 0,
score: 1,
@ -250,7 +263,8 @@ mod tests {
published: inserted_post.published,
updated: None,
subscribed: None,
am_mod: None,
read: None,
saved: None,
};
let expected_post_listing_with_user = PostView {
@ -260,8 +274,8 @@ mod tests {
name: post_name.to_owned(),
url: None,
body: None,
removed: Some(false),
locked: Some(false),
removed: false,
locked: false,
creator_id: inserted_user.id,
creator_name: user_name.to_owned(),
community_id: inserted_community.id,
@ -274,12 +288,13 @@ mod tests {
published: inserted_post.published,
updated: None,
subscribed: None,
am_mod: None,
read: None,
saved: None,
};
let read_post_listings_with_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, Some(inserted_user.id), None, None).unwrap();
let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, None, None, None).unwrap();
let read_post_listings_with_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, Some(inserted_user.id), false, false, None, None).unwrap();
let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, None, false, false, None, None).unwrap();
let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
let read_post_listing_with_user = PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();

View file

@ -55,6 +55,16 @@ pub trait Bannable<T> {
fn unban(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
}
pub trait Saveable<T> {
fn save(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
fn unsave(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
}
pub trait Readable<T> {
fn mark_as_read(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
fn mark_as_unread(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
}
pub fn establish_connection() -> PgConnection {
let db_url = Settings::get().db_url;
PgConnection::establish(&db_url)

View file

@ -12,7 +12,8 @@ table! {
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Nullable<Bool>,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
@ -29,6 +30,15 @@ table! {
}
}
table! {
comment_saved (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
community (id) {
id -> Int4,
@ -37,7 +47,7 @@ table! {
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
removed -> Nullable<Bool>,
removed -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
@ -168,8 +178,8 @@ table! {
body -> Nullable<Text>,
creator_id -> Int4,
community_id -> Int4,
removed -> Nullable<Bool>,
locked -> Nullable<Bool>,
removed -> Bool,
locked -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
@ -185,6 +195,24 @@ table! {
}
}
table! {
post_read (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
post_saved (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
site (id) {
id -> Int4,
@ -225,6 +253,8 @@ joinable!(comment -> user_ (creator_id));
joinable!(comment_like -> comment (comment_id));
joinable!(comment_like -> post (post_id));
joinable!(comment_like -> user_ (user_id));
joinable!(comment_saved -> comment (comment_id));
joinable!(comment_saved -> user_ (user_id));
joinable!(community -> category (category_id));
joinable!(community -> user_ (creator_id));
joinable!(community_follower -> community (community_id));
@ -247,6 +277,10 @@ joinable!(post -> community (community_id));
joinable!(post -> user_ (creator_id));
joinable!(post_like -> post (post_id));
joinable!(post_like -> user_ (user_id));
joinable!(post_read -> post (post_id));
joinable!(post_read -> user_ (user_id));
joinable!(post_saved -> post (post_id));
joinable!(post_saved -> user_ (user_id));
joinable!(site -> user_ (creator_id));
joinable!(user_ban -> user_ (user_id));
@ -254,6 +288,7 @@ allow_tables_to_appear_in_same_query!(
category,
comment,
comment_like,
comment_saved,
community,
community_follower,
community_moderator,
@ -268,6 +303,8 @@ allow_tables_to_appear_in_same_query!(
mod_remove_post,
post,
post_like,
post_read,
post_saved,
site,
user_,
user_ban,

View file

@ -11,7 +11,7 @@ use bcrypt::{verify};
use std::str::FromStr;
use diesel::PgConnection;
use {Crud, Joinable, Likeable, Followable, Bannable, establish_connection, naive_now, naive_from_unix, SortType, has_slurs, remove_slurs};
use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, has_slurs, remove_slurs};
use actions::community::*;
use actions::user::*;
use actions::post::*;
@ -26,7 +26,7 @@ use actions::moderator::*;
#[derive(EnumString,ToString,Debug)]
pub enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
}
#[derive(Serialize, Deserialize)]
@ -164,7 +164,8 @@ pub struct GetPostResponse {
post: PostView,
comments: Vec<CommentView>,
community: CommunityView,
moderators: Vec<CommunityModeratorView>
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
@ -217,6 +218,13 @@ pub struct EditComment {
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct SaveComment {
comment_id: i32,
save: bool,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct CommentResponse {
op: String,
@ -253,9 +261,16 @@ pub struct EditPost {
name: String,
url: Option<String>,
body: Option<String>,
removed: Option<bool>,
removed: bool,
locked: bool,
reason: Option<String>,
locked: Option<bool>,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct SavePost {
post_id: i32,
save: bool,
auth: String
}
@ -266,7 +281,7 @@ pub struct EditCommunity {
title: String,
description: Option<String>,
category_id: i32,
removed: Option<bool>,
removed: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String
@ -297,7 +312,7 @@ pub struct GetUserDetails {
page: Option<i64>,
limit: Option<i64>,
community_id: Option<i32>,
auth: Option<String>
saved_only: bool,
}
#[derive(Serialize, Deserialize)]
@ -308,8 +323,6 @@ pub struct GetUserDetailsResponse {
moderates: Vec<CommunityModeratorView>,
comments: Vec<CommentView>,
posts: Vec<PostView>,
saved_posts: Vec<PostView>,
saved_comments: Vec<CommentView>,
}
#[derive(Serialize, Deserialize)]
@ -468,6 +481,8 @@ impl ChatServer {
Some(community_id),
None,
None,
false,
false,
None,
Some(9999))
.unwrap();
@ -491,7 +506,6 @@ impl Handler<Connect> for ChatServer {
type Result = usize;
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
println!("Someone joined");
// notify all users in same room
// self.send_room_message(&"Main".to_owned(), "Someone joined", 0);
@ -513,7 +527,6 @@ impl Handler<Disconnect> for ChatServer {
type Result = ();
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
println!("Someone disconnected");
// let mut rooms: Vec<i32> = Vec::new();
@ -586,6 +599,10 @@ impl Handler<StandardMessage> for ChatServer {
let edit_comment: EditComment = serde_json::from_str(data).unwrap();
edit_comment.perform(self, msg.id)
},
UserOperation::SaveComment => {
let save_post: SaveComment = serde_json::from_str(data).unwrap();
save_post.perform(self, msg.id)
},
UserOperation::CreateCommentLike => {
let create_comment_like: CreateCommentLike = serde_json::from_str(data).unwrap();
create_comment_like.perform(self, msg.id)
@ -602,6 +619,10 @@ impl Handler<StandardMessage> for ChatServer {
let edit_post: EditPost = serde_json::from_str(data).unwrap();
edit_post.perform(self, msg.id)
},
UserOperation::SavePost => {
let save_post: SavePost = serde_json::from_str(data).unwrap();
save_post.perform(self, msg.id)
},
UserOperation::EditCommunity => {
let edit_community: EditCommunity = serde_json::from_str(data).unwrap();
edit_community.perform(self, msg.id)
@ -745,11 +766,11 @@ impl Perform for Register {
}
};
// If its an admin, add them as a mod to main
// If its an admin, add them as a mod and follower to main
if self.admin {
let community_moderator_form = CommunityModeratorForm {
community_id: 1,
user_id: inserted_user.id
user_id: inserted_user.id,
};
let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
@ -758,6 +779,18 @@ impl Perform for Register {
return self.error("Community moderator already exists.");
}
};
let community_follower_form = CommunityFollowerForm {
community_id: 1,
user_id: inserted_user.id,
};
let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => {
return self.error("Community follower already exists.");
}
};
}
@ -797,6 +830,11 @@ impl Perform for CreateCommunity {
let user_id = claims.id;
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
// When you create a community, make sure the user becomes a moderator and a follower
let community_form = CommunityForm {
@ -805,7 +843,7 @@ impl Perform for CreateCommunity {
description: self.description.to_owned(),
category_id: self.category_id,
creator_id: user_id,
removed: None,
removed: false,
updated: None,
};
@ -934,19 +972,24 @@ impl Perform for CreatePost {
let user_id = claims.id;
// Check for a ban
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() {
return self.error("You have been banned from this community");
}
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
let post_form = PostForm {
name: self.name.to_owned(),
url: self.url.to_owned(),
body: self.body.to_owned(),
community_id: self.community_id,
creator_id: user_id,
removed: None,
locked: None,
removed: false,
locked: false,
updated: None
};
@ -1031,12 +1074,14 @@ impl Perform for GetPost {
chat.rooms.get_mut(&self.id).unwrap().insert(addr);
let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, user_id, None, Some(9999)).unwrap();
let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, user_id, false, None, Some(9999)).unwrap();
let community = CommunityView::read(&conn, post_view.community_id, user_id).unwrap();
let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id).unwrap();
let admins = UserView::admins(&conn).unwrap();
// Return the jwt
serde_json::to_string(
&GetPostResponse {
@ -1044,7 +1089,8 @@ impl Perform for GetPost {
post: post_view,
comments: comments,
community: community,
moderators: moderators
moderators: moderators,
admins: admins,
}
)
.unwrap()
@ -1117,12 +1163,17 @@ impl Perform for CreateComment {
let user_id = claims.id;
// Check for a ban
// Check for a community ban
let post = Post::read(&conn, self.post_id).unwrap();
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return self.error("You have been banned from this community");
}
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
let content_slurs_removed = remove_slurs(&self.content.to_owned());
let comment_form = CommentForm {
@ -1202,24 +1253,38 @@ impl Perform for EditComment {
let user_id = claims.id;
// Verify its the creator or a mod
// Verify its the creator or a mod, or an admin
let orig_comment = CommentView::read(&conn, self.edit_id, None).unwrap();
let mut editors: Vec<i32> = CommunityModeratorView::for_community(&conn, orig_comment.community_id)
let mut editors: Vec<i32> = vec![self.creator_id];
editors.append(
&mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)
.unwrap()
.into_iter()
.map(|m| m.user_id)
.collect();
editors.push(self.creator_id);
.collect()
);
editors.append(
&mut UserView::admins(&conn)
.unwrap()
.into_iter()
.map(|a| a.id)
.collect()
);
if !editors.contains(&user_id) {
return self.error("Not allowed to edit comment.");
}
// Check for a ban
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
return self.error("You have been banned from this community");
}
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
let content_slurs_removed = remove_slurs(&self.content.to_owned());
let comment_form = CommentForm {
@ -1278,6 +1343,60 @@ impl Perform for EditComment {
}
}
impl Perform for SaveComment {
fn op_type(&self) -> UserOperation {
UserOperation::SaveComment
}
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
let conn = establish_connection();
let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return self.error("Not logged in.");
}
};
let user_id = claims.id;
let comment_saved_form = CommentSavedForm {
comment_id: self.comment_id,
user_id: user_id,
};
if self.save {
match CommentSaved::save(&conn, &comment_saved_form) {
Ok(comment) => comment,
Err(_e) => {
return self.error("Couldnt do comment save");
}
};
} else {
match CommentSaved::unsave(&conn, &comment_saved_form) {
Ok(comment) => comment,
Err(_e) => {
return self.error("Couldnt do comment save");
}
};
}
let comment_view = CommentView::read(&conn, self.comment_id, Some(user_id)).unwrap();
let comment_out = serde_json::to_string(
&CommentResponse {
op: self.op_type().to_string(),
comment: comment_view
}
)
.unwrap();
comment_out
}
}
impl Perform for CreateCommentLike {
fn op_type(&self) -> UserOperation {
UserOperation::CreateCommentLike
@ -1296,12 +1415,17 @@ impl Perform for CreateCommentLike {
let user_id = claims.id;
// Check for a ban
// Check for a community ban
let post = Post::read(&conn, self.post_id).unwrap();
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return self.error("You have been banned from this community");
}
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
let like_form = CommentLikeForm {
comment_id: self.comment_id,
post_id: self.post_id,
@ -1377,7 +1501,7 @@ impl Perform for GetPosts {
let type_ = PostListingType::from_str(&self.type_).expect("listing type");
let sort = SortType::from_str(&self.sort).expect("listing sort");
let posts = match PostView::list(&conn, type_, &sort, self.community_id, None, user_id, self.page, self.limit) {
let posts = match PostView::list(&conn, type_, &sort, self.community_id, None, user_id, false, false, self.page, self.limit) {
Ok(posts) => posts,
Err(_e) => {
return self.error("Couldn't get posts");
@ -1414,12 +1538,17 @@ impl Perform for CreatePostLike {
let user_id = claims.id;
// Check for a ban
// Check for a community ban
let post = Post::read(&conn, self.post_id).unwrap();
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return self.error("You have been banned from this community");
}
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
let like_form = PostLikeForm {
post_id: self.post_id,
user_id: user_id,
@ -1494,11 +1623,16 @@ impl Perform for EditPost {
return self.error("Not allowed to edit comment.");
}
// Check for a ban
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() {
return self.error("You have been banned from this community");
}
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
let post_form = PostForm {
name: self.name.to_owned(),
url: self.url.to_owned(),
@ -1518,21 +1652,21 @@ impl Perform for EditPost {
};
// Mod tables
if let Some(removed) = self.removed.to_owned() {
if self.removed {
let form = ModRemovePostForm {
mod_user_id: user_id,
post_id: self.edit_id,
removed: Some(removed),
removed: Some(self.removed),
reason: self.reason.to_owned(),
};
ModRemovePost::create(&conn, &form).unwrap();
}
if let Some(locked) = self.locked.to_owned() {
if self.locked {
let form = ModLockPostForm {
mod_user_id: user_id,
post_id: self.edit_id,
locked: Some(locked),
locked: Some(self.locked),
};
ModLockPost::create(&conn, &form).unwrap();
}
@ -1564,6 +1698,59 @@ impl Perform for EditPost {
}
}
impl Perform for SavePost {
fn op_type(&self) -> UserOperation {
UserOperation::SavePost
}
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
let conn = establish_connection();
let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return self.error("Not logged in.");
}
};
let user_id = claims.id;
let post_saved_form = PostSavedForm {
post_id: self.post_id,
user_id: user_id,
};
if self.save {
match PostSaved::save(&conn, &post_saved_form) {
Ok(post) => post,
Err(_e) => {
return self.error("Couldnt do post save");
}
};
} else {
match PostSaved::unsave(&conn, &post_saved_form) {
Ok(post) => post,
Err(_e) => {
return self.error("Couldnt do post save");
}
};
}
let post_view = PostView::read(&conn, self.post_id, Some(user_id)).unwrap();
let post_out = serde_json::to_string(
&PostResponse {
op: self.op_type().to_string(),
post: post_view
}
)
.unwrap();
post_out
}
}
impl Perform for EditCommunity {
fn op_type(&self) -> UserOperation {
UserOperation::EditCommunity
@ -1586,6 +1773,11 @@ impl Perform for EditCommunity {
let user_id = claims.id;
// Check for a site ban
if UserView::read(&conn, user_id).unwrap().banned {
return self.error("You have been banned from the site");
}
// Verify its a mod
let moderator_view = CommunityModeratorView::for_community(&conn, self.edit_id).unwrap();
let mod_ids: Vec<i32> = moderator_view.into_iter().map(|m| m.user_id).collect();
@ -1611,7 +1803,7 @@ impl Perform for EditCommunity {
};
// Mod tables
if let Some(removed) = self.removed.to_owned() {
if self.removed {
let expires = match self.expires {
Some(time) => Some(naive_from_unix(time)),
None => None
@ -1619,7 +1811,7 @@ impl Perform for EditCommunity {
let form = ModRemoveCommunityForm {
mod_user_id: user_id,
community_id: self.edit_id,
removed: Some(removed),
removed: Some(self.removed),
reason: self.reason.to_owned(),
expires: expires
};
@ -1750,26 +1942,21 @@ impl Perform for GetUserDetails {
let conn = establish_connection();
let user_id: Option<i32> = match &self.auth {
Some(auth) => {
match Claims::decode(&auth) {
Ok(claims) => {
let user_id = claims.claims.id;
Some(user_id)
}
Err(_e) => None
}
}
None => None
};
//TODO add save
let sort = SortType::from_str(&self.sort).expect("listing sort");
let user_view = UserView::read(&conn, self.user_id).unwrap();
let posts = PostView::list(&conn, PostListingType::All, &sort, self.community_id, Some(self.user_id), user_id, self.page, self.limit).unwrap();
let comments = CommentView::list(&conn, &sort, None, Some(self.user_id), user_id, self.page, self.limit).unwrap();
let posts = if self.saved_only {
PostView::list(&conn, PostListingType::All, &sort, self.community_id, None, Some(self.user_id), self.saved_only, false, self.page, self.limit).unwrap()
} else {
PostView::list(&conn, PostListingType::All, &sort, self.community_id, Some(self.user_id), None, self.saved_only, false, self.page, self.limit).unwrap()
};
let comments = if self.saved_only {
CommentView::list(&conn, &sort, None, None, Some(self.user_id), self.saved_only, self.page, self.limit).unwrap()
} else {
CommentView::list(&conn, &sort, None, Some(self.user_id), None, self.saved_only, self.page, self.limit).unwrap()
};
let follows = CommunityFollowerView::for_user(&conn, self.user_id).unwrap();
let moderates = CommunityModeratorView::for_user(&conn, self.user_id).unwrap();
@ -1782,8 +1969,6 @@ impl Perform for GetUserDetails {
moderates: moderates,
comments: comments,
posts: posts,
saved_posts: Vec::new(),
saved_comments: Vec::new(),
}
)
.unwrap()

View file

@ -1,12 +1,14 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, BanFromCommunityForm, CommunityUser, AddModToCommunityForm } from '../interfaces';
import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime } from '../utils';
import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes';
enum BanType {Community, Site};
interface CommentNodeState {
showReply: boolean;
showEdit: boolean;
@ -15,6 +17,7 @@ interface CommentNodeState {
showBanDialog: boolean;
banReason: string;
banExpires: string;
banType: BanType;
}
interface CommentNodeProps {
@ -23,6 +26,7 @@ interface CommentNodeProps {
viewOnly?: boolean;
locked?: boolean;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
}
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -35,6 +39,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
showBanDialog: false,
banReason: null,
banExpires: null,
banType: BanType.Community
}
constructor(props: any, context: any) {
@ -60,6 +65,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<li className="list-inline-item">
<Link className="text-info" to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link>
</li>
{this.isMod &&
<li className="list-inline-item badge badge-secondary">mod</li>
}
{this.isAdmin &&
<li className="list-inline-item badge badge-secondary">admin</li>
}
<li className="list-inline-item">
<span>(
<span className="text-info">+{node.comment.upvotes}</span>
@ -77,11 +88,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<div>
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.content)} />
<ul class="list-inline mb-1 text-muted small font-weight-bold">
{!this.props.viewOnly &&
<span class="mr-2">
{UserService.Instance.user && !this.props.viewOnly &&
<>
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
</li>
<li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? 'unsave' : 'save'}</span>
</li>
{this.myComment &&
<>
<li className="list-inline-item">
@ -92,32 +106,52 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</li>
</>
}
{/* Admins and mods can remove comments */}
{this.canMod &&
<>
<li className="list-inline-item">
{!this.props.node.comment.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
}
</li>
{!this.isMod &&
}
{/* Mods can ban from community, and appoint as mods to community */}
{this.canMod &&
<>
{!this.isMod &&
<li className="list-inline-item">
{!this.props.node.comment.banned ?
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban</span>
{!this.props.node.comment.banned_from_community ?
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}>ban</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}>unban</span>
}
</li>
</>
}
{!this.props.node.comment.banned &&
{!this.props.node.comment.banned_from_community &&
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
</li>
}
</>
}
</span>
{/* Admins can ban from all, and appoint other admins */}
{this.canAdmin &&
<>
{!this.isAdmin &&
<li className="list-inline-item">
{!this.props.node.comment.banned ?
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban from site</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban from site</span>
}
</li>
}
{!this.props.node.comment.banned &&
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.addAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span>
</li>
}
</>
}
</>
}
<li className="list-inline-item">
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link>
@ -133,7 +167,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</form>
}
{this.state.showBanDialog &&
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row">
<label class="col-form-label">Reason</label>
<input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
@ -147,8 +181,21 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div>
</form>
}
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
{this.props.node.children && <CommentNodes nodes={this.props.node.children} locked={this.props.locked} moderators={this.props.moderators}/>}
{this.state.showReply &&
<CommentForm
node={node}
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
/>
}
{this.props.node.children &&
<CommentNodes
nodes={this.props.node.children}
locked={this.props.locked}
moderators={this.props.moderators}
admins={this.props.admins}
/>
}
</div>
)
}
@ -158,27 +205,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
get canMod(): boolean {
let adminsThenMods = this.props.admins.map(a => a.id)
.concat(this.props.moderators.map(m => m.user_id));
// You can do moderator actions only on the mods added after you.
if (UserService.Instance.user) {
let modIds = this.props.moderators.map(m => m.user_id);
let yourIndex = modIds.findIndex(id => id == UserService.Instance.user.id);
if (yourIndex == -1) {
return false;
} else {
console.log(modIds);
modIds = modIds.slice(0, yourIndex+1); // +1 cause you cant mod yourself
console.log(modIds);
return !modIds.includes(this.props.node.comment.creator_id);
}
} else {
return false;
}
return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
}
get isMod(): boolean {
return this.props.moderators.map(m => m.user_id).includes(this.props.node.comment.creator_id);
return isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id);
}
get isAdmin(): boolean {
return isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
}
get canAdmin(): boolean {
return canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
}
handleReplyClick(i: CommentNode) {
@ -193,16 +235,27 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleDeleteClick(i: CommentNode) {
let deleteForm: CommentFormI = {
content: "*deleted*",
content: '*deleted*',
edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
removed: i.props.node.comment.removed,
auth: null
};
WebSocketService.Instance.editComment(deleteForm);
}
handleSaveCommentClick(i: CommentNode) {
let saved = (i.props.node.comment.saved == undefined) ? true : !i.props.node.comment.saved;
let form: SaveCommentForm = {
comment_id: i.props.node.comment.id,
save: saved
};
WebSocketService.Instance.saveComment(form);
}
handleReplyCancel() {
this.state.showReply = false;
this.state.showEdit = false;
@ -257,8 +310,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state);
}
handleModBanFromCommunityShow(i: CommentNode) {
i.state.showBanDialog = true;
i.state.banType = BanType.Community;
i.setState(i.state);
}
handleModBanShow(i: CommentNode) {
i.state.showBanDialog = true;
i.state.banType = BanType.Site;
i.setState(i.state);
}
@ -272,16 +332,42 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state);
}
handleModBanFromCommunitySubmit(i: CommentNode) {
i.state.banType = BanType.Community;
i.setState(i.state);
i.handleModBanBothSubmit(i);
}
handleModBanSubmit(i: CommentNode) {
i.state.banType = BanType.Site;
i.setState(i.state);
i.handleModBanBothSubmit(i);
}
handleModBanBothSubmit(i: CommentNode) {
event.preventDefault();
console.log(BanType[i.state.banType]);
console.log(i.props.node.comment.banned);
if (i.state.banType == BanType.Community) {
let form: BanFromCommunityForm = {
user_id: i.props.node.comment.creator_id,
community_id: i.props.node.comment.community_id,
ban: !i.props.node.comment.banned,
ban: !i.props.node.comment.banned_from_community,
reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires),
};
WebSocketService.Instance.banFromCommunity(form);
} else {
let form: BanUserForm = {
user_id: i.props.node.comment.creator_id,
ban: !i.props.node.comment.banned,
reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires),
};
WebSocketService.Instance.banUser(form);
}
i.state.showBanDialog = false;
i.setState(i.state);
@ -296,4 +382,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
WebSocketService.Instance.addModToCommunity(form);
i.setState(i.state);
}
addAdmin(i: CommentNode) {
let form: AddAdminForm = {
user_id: i.props.node.comment.creator_id,
added: !i.isAdmin,
};
WebSocketService.Instance.addAdmin(form);
i.setState(i.state);
}
}

View file

@ -1,5 +1,5 @@
import { Component } from 'inferno';
import { CommentNode as CommentNodeI, CommunityUser } from '../interfaces';
import { CommentNode as CommentNodeI, CommunityUser, UserView } from '../interfaces';
import { CommentNode } from './comment-node';
interface CommentNodesState {
@ -8,6 +8,7 @@ interface CommentNodesState {
interface CommentNodesProps {
nodes: Array<CommentNodeI>;
moderators?: Array<CommunityUser>;
admins?: Array<UserView>;
noIndent?: boolean;
viewOnly?: boolean;
locked?: boolean;
@ -27,7 +28,9 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly}
locked={this.props.locked}
moderators={this.props.moderators}/>
moderators={this.props.moderators}
admins={this.props.admins}
/>
)}
</div>
)

View file

@ -53,9 +53,9 @@ export class Communities extends Component<any, CommunitiesState> {
return (
<div class="container">
{this.state.loading ?
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div>
<h4>Communities</h4>
<h5>Communities</h5>
<div class="table-responsive">
<table id="community_table" class="table table-sm table-hover">
<thead class="pointer">

View file

@ -60,14 +60,14 @@ export class Community extends Component<any, State> {
return (
<div class="container">
{this.state.loading ?
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div class="row">
<div class="col-12 col-md-9">
<h4>{this.state.community.title}
<h5>{this.state.community.title}
{this.state.community.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
</h4>
</h5>
<PostListings communityId={this.state.communityId} />
</div>
<div class="col-12 col-md-3">

View file

@ -13,7 +13,7 @@ export class CreateCommunity extends Component<any, any> {
<div class="container">
<div class="row">
<div class="col-12 col-lg-6 mb-4">
<h4>Create Forum</h4>
<h5>Create Forum</h5>
<CommunityForm onCreate={this.handleCommunityCreate}/>
</div>
</div>

View file

@ -13,7 +13,7 @@ export class CreatePost extends Component<any, any> {
<div class="container">
<div class="row">
<div class="col-12 col-lg-6 mb-4">
<h4>Create a Post</h4>
<h5>Create a Post</h5>
<PostForm onCreate={this.handlePostCreate}/>
</div>
</div>

View file

@ -67,7 +67,7 @@ export class Login extends Component<any, State> {
return (
<div>
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
<h4>Login</h4>
<h5>Login</h5>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Email or Username</label>
<div class="col-sm-10">
@ -94,7 +94,7 @@ export class Login extends Component<any, State> {
registerForm() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h4>Sign Up</h4>
<h5>Sign Up</h5>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">

View file

@ -78,12 +78,12 @@ export class Main extends Component<any, State> {
</div>
<div class="col-12 col-md-4">
{this.state.loading ?
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div>
{this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div>
<h4>Subscribed forums</h4>
<h5>Subscribed forums</h5>
<ul class="list-inline">
{this.state.subscribedCommunities.map(community =>
<li class="list-inline-item"><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
@ -103,7 +103,7 @@ export class Main extends Component<any, State> {
trendingCommunities() {
return (
<div>
<h4>Trending <Link class="text-white" to="/communities">forums</Link></h4>
<h5>Trending <Link class="text-white" to="/communities">forums</Link></h5>
<ul class="list-inline">
{this.state.trendingCommunities.map(community =>
<li class="list-inline-item"><Link to={`/community/${community.id}`}>{community.name}</Link></li>
@ -116,7 +116,7 @@ export class Main extends Component<any, State> {
landing() {
return (
<div>
<h4>{`${this.state.site.site.name}`}</h4>
<h5>{`${this.state.site.site.name}`}</h5>
<ul class="my-1 list-inline">
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_users} Users</li>
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li>
@ -136,10 +136,10 @@ export class Main extends Component<any, State> {
<hr />
</div>
}
<h4>Welcome to
<h5>Welcome to
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
<a href={repoUrl}>Lemmy<sup>Beta</sup></a>
</h4>
</h5>
<p>Lemmy is a <a href="https://en.wikipedia.org/wiki/Link_aggregation">link aggregator</a> / reddit alternative, intended to work in the <a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a>.</p>
<p>Its self-hostable, has live-updating comment threads, and is tiny (<code>~80kB</code>). Federation into the ActivityPub network is on the roadmap.</p>
<p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p>

View file

@ -9,7 +9,7 @@ import { MomentTime } from './moment-time';
import * as moment from 'moment';
interface ModlogState {
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}>,
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity | ModAdd | ModBan}>,
communityId?: number,
communityName?: string,
page: number;
@ -51,6 +51,8 @@ export class Modlog extends Component<any, ModlogState> {
let removed_communities = addTypeInfo(res.removed_communities, "removed_communities");
let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community");
let added_to_community = addTypeInfo(res.added_to_community, "added_to_community");
let added = addTypeInfo(res.added, "added");
let banned = addTypeInfo(res.banned, "banned");
this.state.combined = [];
this.state.combined.push(...removed_posts);
@ -59,9 +61,11 @@ export class Modlog extends Component<any, ModlogState> {
this.state.combined.push(...removed_communities);
this.state.combined.push(...banned_from_community);
this.state.combined.push(...added_to_community);
this.state.combined.push(...added);
this.state.combined.push(...banned);
if (this.state.communityId && this.state.combined.length > 0) {
this.state.communityName = this.state.combined[0].data.community_name;
this.state.communityName = (this.state.combined[0].data as ModRemovePost).community_name;
}
// Sort them by time
@ -95,13 +99,14 @@ export class Modlog extends Component<any, ModlogState> {
<>
{(i.data as ModRemoveComment).removed? 'Removed' : 'Restored'}
<span> Comment <Link to={`/post/${(i.data as ModRemoveComment).post_id}/comment/${(i.data as ModRemoveComment).comment_id}`}>{(i.data as ModRemoveComment).comment_content}</Link></span>
<span> by <Link to={`/user/${(i.data as ModRemoveComment).comment_user_id}`}>{(i.data as ModRemoveComment).comment_user_name}</Link></span>
<div>{(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}</div>
</>
}
{i.type_ == 'removed_communities' &&
<>
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'}
<span> Community <Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
<span> Community <Link to={`/community/${(i.data as ModRemoveCommunity).community_id}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span>
<div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
<div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div>
</>
@ -110,6 +115,8 @@ export class Modlog extends Component<any, ModlogState> {
<>
<span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span>
<span><Link to={`/user/${(i.data as ModBanFromCommunity).other_user_id}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
<span> from the community </span>
<span><Link to={`/community/${(i.data as ModBanFromCommunity).community_id}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span>
<div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
<div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div>
</>
@ -119,7 +126,22 @@ export class Modlog extends Component<any, ModlogState> {
<span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
<span><Link to={`/user/${(i.data as ModAddCommunity).other_user_id}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
<span> as a mod to the community </span>
<span><Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
<span><Link to={`/community/${(i.data as ModAddCommunity).community_id}`}>{(i.data as ModAddCommunity).community_name}</Link></span>
</>
}
{i.type_ == 'banned' &&
<>
<span>{(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '} </span>
<span><Link to={`/user/${(i.data as ModBan).other_user_id}`}>{(i.data as ModBan).other_user_name}</Link></span>
<div>{(i.data as ModBan).reason && ` reason: ${(i.data as ModBan).reason}`}</div>
<div>{(i.data as ModBan).expires && ` expires: ${moment.utc((i.data as ModBan).expires).fromNow()}`}</div>
</>
}
{i.type_ == 'added' &&
<>
<span>{(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '} </span>
<span><Link to={`/user/${(i.data as ModAdd).other_user_id}`}>{(i.data as ModAdd).other_user_name}</Link></span>
<span> as an admin </span>
</>
}
</td>
@ -136,12 +158,12 @@ export class Modlog extends Component<any, ModlogState> {
return (
<div class="container">
{this.state.loading ?
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div>
<h4>
<h5>
{this.state.communityName && <Link className="text-white" to={`/community/${this.state.communityId}`}>/f/{this.state.communityName} </Link>}
<span>Modlog</span>
</h4>
</h5>
<div class="table-responsive">
<table id="modlog_table" class="table table-sm table-hover">
<thead class="pointer">

View file

@ -64,6 +64,10 @@ export class Navbar extends Component<any, NavbarState> {
</ul>
<ul class="navbar-nav ml-auto mr-2">
{this.state.isLoggedIn ?
<>
<li className="nav-item">
<Link class="nav-link" to="/communities">🖂</Link>
</li>
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
{UserService.Instance.user.username}
@ -72,7 +76,9 @@ export class Navbar extends Component<any, NavbarState> {
<a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}>Overview</a>
<a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a>
</div>
</li> :
</li>
</>
:
<Link class="nav-link" to="/login">Login / Sign up</Link>
}
</ul>

View file

@ -1,7 +1,7 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { WebSocketService, UserService } from '../services';
import { Post, CreatePostLikeForm, PostForm as PostFormI } from '../interfaces';
import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm } from '../interfaces';
import { MomentTime } from './moment-time';
import { PostForm } from './post-form';
import { mdToHtml } from '../utils';
@ -60,17 +60,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div>{post.score}</div>
<div className={`pointer downvote ${post.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(this, this.handlePostDisLike)}></div>
</div>
<div className="ml-4">
<div className="pt-1 ml-4">
{post.url
? <div className="mb-0">
<h4 className="d-inline"><a className="text-white" href={post.url} title={post.url}>{post.name}</a>
<h5 className="d-inline"><a className="text-white" href={post.url} title={post.url}>{post.name}</a>
{post.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
{post.locked &&
<small className="ml-2 text-muted font-italic">locked</small>
}
</h4>
</h5>
<small><a className="ml-2 text-muted font-italic" href={post.url} title={post.url}>{(new URL(post.url)).hostname}</a></small>
{ !this.state.iframeExpanded
? <span class="badge badge-light pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
@ -83,14 +83,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</span>
}
</div>
: <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link>
: <h5 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link>
{post.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
{post.locked &&
<small className="ml-2 text-muted font-italic">locked</small>
}
</h4>
</h5>
}
</div>
<div className="details ml-4 mb-1">
@ -120,17 +120,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link>
</li>
</ul>
{this.props.editable &&
{UserService.Instance.user && this.props.editable &&
<ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{this.props.post.saved ? 'unsave' : 'save'}</span>
</li>
{this.myPost &&
<span>
<>
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
</li>
<li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
</li>
</span>
</>
}
{this.props.post.am_mod &&
<span>
@ -204,11 +207,23 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
url: '',
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
locked: !i.props.post.locked,
auth: null
};
WebSocketService.Instance.editPost(deleteForm);
}
handleSavePostClick(i: PostListing) {
let saved = (i.props.post.saved == undefined) ? true : !i.props.post.saved;
let form: SavePostForm = {
post_id: i.props.post.id,
save: saved
};
WebSocketService.Instance.savePost(form);
}
handleModRemoveShow(i: PostListing) {
i.state.showRemoveDialog = true;
i.setState(i.state);
@ -227,6 +242,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
locked: !i.props.post.locked,
reason: i.state.removeReason,
auth: null,
};
@ -242,6 +258,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,
removed: !i.props.post.removed,
locked: !i.props.post.locked,
auth: null,
};

View file

@ -61,7 +61,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
return (
<div>
{this.state.loading ?
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div>
{this.selects()}
{this.state.posts.length > 0

View file

@ -1,7 +1,7 @@
import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, AddModToCommunityResponse } from '../interfaces';
import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView } from '../interfaces';
import { WebSocketService } from '../services';
import { msgOp, hotRank } from '../utils';
import { PostListing } from './post-listing';
@ -17,6 +17,7 @@ interface PostState {
commentSort: CommentSortType;
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
scrolled?: boolean;
scrolled_comment_id?: number;
loading: boolean;
@ -31,6 +32,7 @@ export class Post extends Component<any, PostState> {
commentSort: CommentSortType.Hot,
community: null,
moderators: [],
admins: [],
scrolled: false,
loading: true
}
@ -77,7 +79,7 @@ export class Post extends Component<any, PostState> {
return (
<div class="container">
{this.state.loading ?
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div class="row">
<div class="col-12 col-md-8 col-lg-7 mb-3">
<PostListing post={this.state.post} showBody showCommunity editable />
@ -123,9 +125,15 @@ export class Post extends Component<any, PostState> {
newComments() {
return (
<div class="sticky-top">
<h4>New Comments</h4>
<h5>New Comments</h5>
{this.state.comments.map(comment =>
<CommentNodes nodes={[{comment: comment}]} noIndent locked={this.state.post.locked} moderators={this.state.moderators} />
<CommentNodes
nodes={[{comment: comment}]}
noIndent
locked={this.state.post.locked}
moderators={this.state.moderators}
admins={this.state.admins}
/>
)}
</div>
)
@ -187,8 +195,13 @@ export class Post extends Component<any, PostState> {
commentsTree() {
let nodes = this.buildCommentsTree();
return (
<div className="">
<CommentNodes nodes={nodes} locked={this.state.post.locked} moderators={this.state.moderators} />
<div>
<CommentNodes
nodes={nodes}
locked={this.state.post.locked}
moderators={this.state.moderators}
admins={this.state.admins}
/>
</div>
);
}
@ -202,9 +215,11 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.GetPost) {
let res: GetPostResponse = msg;
this.state.post = res.post;
this.state.post = res.post;
this.state.comments = res.comments;
this.state.community = res.community;
this.state.moderators = res.moderators;
this.state.admins = res.admins;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
@ -222,8 +237,12 @@ export class Post extends Component<any, PostState> {
found.score = res.comment.score;
this.setState(this.state);
}
else if (op == UserOperation.CreateCommentLike) {
} else if (op == UserOperation.SaveComment) {
let res: CommentResponse = msg;
let found = this.state.comments.find(c => c.id == res.comment.id);
found.saved = res.comment.saved;
this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg;
let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
found.score = res.comment.score;
@ -243,6 +262,10 @@ export class Post extends Component<any, PostState> {
let res: PostResponse = msg;
this.state.post = res.post;
this.setState(this.state);
} else if (op == UserOperation.SavePost) {
let res: PostResponse = msg;
this.state.post = res.post;
this.setState(this.state);
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.state.community = res.community;
@ -257,12 +280,21 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.BanFromCommunity) {
let res: BanFromCommunityResponse = msg;
this.state.comments.filter(c => c.creator_id == res.user.id)
.forEach(c => c.banned = res.banned);
.forEach(c => c.banned_from_community = res.banned);
this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) {
let res: AddModToCommunityResponse = msg;
this.state.moderators = res.moderators;
this.setState(this.state);
} else if (op == UserOperation.BanUser) {
let res: BanUserResponse = msg;
this.state.comments.filter(c => c.creator_id == res.user.id)
.forEach(c => c.banned = res.banned);
this.setState(this.state);
} else if (op == UserOperation.AddAdmin) {
let res: AddAdminResponse = msg;
this.state.admins = res.admins;
this.setState(this.state);
}
}

View file

@ -61,7 +61,7 @@ export class Setup extends Component<any, State> {
registerUser() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h4>Set up Site Administrator</h4>
<h5>Set up Site Administrator</h5>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">

View file

@ -48,11 +48,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
let community = this.props.community;
return (
<div>
<h4 className="mb-0">{community.title}
<h5 className="mb-0">{community.title}
{community.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
</h4>
</h5>
<Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
{community.am_mod &&
<ul class="list-inline mb-1 text-muted small font-weight-bold">

View file

@ -33,7 +33,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
render() {
return (
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
<h4>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h4>
<h5>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h5>
<div class="form-group row">
<label class="col-12 col-form-label">Name</label>
<div class="col-12">

View file

@ -77,7 +77,7 @@ export class User extends Component<any, UserState> {
<div class="container">
<div class="row">
<div class="col-12 col-md-9">
<h4>/u/{this.state.user.name}</h4>
<h5>/u/{this.state.user.name}</h5>
{this.selects()}
{this.state.view == View.Overview &&
this.overview()
@ -88,6 +88,9 @@ export class User extends Component<any, UserState> {
{this.state.view == View.Posts &&
this.posts()
}
{this.state.view == View.Saved &&
this.overview()
}
{this.paginator()}
</div>
<div class="col-12 col-md-3">
@ -108,7 +111,7 @@ export class User extends Component<any, UserState> {
<option value={View.Overview}>Overview</option>
<option value={View.Comments}>Comments</option>
<option value={View.Posts}>Posts</option>
{/* <option value={View.Saved}>Saved</option> */}
<option value={View.Saved}>Saved</option>
</select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
<option disabled>Sort Type</option>
@ -178,7 +181,7 @@ export class User extends Component<any, UserState> {
let user = this.state.user;
return (
<div>
<h4>{user.name}</h4>
<h5>{user.name}</h5>
<div>Joined <MomentTime data={user} /></div>
<table class="table table-bordered table-sm mt-2">
<tr>
@ -200,7 +203,7 @@ export class User extends Component<any, UserState> {
<div>
{this.state.moderates.length > 0 &&
<div>
<h4>Moderates</h4>
<h5>Moderates</h5>
<ul class="list-unstyled">
{this.state.moderates.map(community =>
<li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
@ -218,7 +221,7 @@ export class User extends Component<any, UserState> {
{this.state.follows.length > 0 &&
<div>
<hr />
<h4>Subscribed</h4>
<h5>Subscribed</h5>
<ul class="list-unstyled">
{this.state.follows.map(community =>
<li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
@ -257,6 +260,7 @@ export class User extends Component<any, UserState> {
let form: GetUserDetailsForm = {
user_id: this.state.user_id,
sort: SortType[this.state.sort],
saved_only: this.state.view == View.Saved,
page: this.state.page,
limit: fetchLimit,
};

View file

@ -7,9 +7,7 @@
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
<title>Lemmy</title>
<link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,700,800" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
</head>

View file

@ -15,7 +15,8 @@ import { Modlog } from './components/modlog';
import { Setup } from './components/setup';
import { Symbols } from './components/symbols';
import './main.css';
import './css/bootstrap.min.css';
import './css/main.css';
import { WebSocketService, UserService } from './services';

View file

@ -1,5 +1,5 @@
export enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
}
export enum CommentSortType {
@ -41,65 +41,69 @@ export interface CommunityUser {
}
export interface Community {
user_id?: number;
subscribed?: boolean;
am_mod?: boolean;
removed?: boolean;
id: number;
name: string;
title: string;
description?: string;
creator_id: number;
creator_name: string;
category_id: number;
creator_id: number;
removed: boolean;
published: string;
updated?: string;
creator_name: string;
category_name: string;
number_of_subscribers: number;
number_of_posts: number;
number_of_comments: number;
published: string;
updated?: string;
user_id?: number;
subscribed?: boolean;
}
export interface Post {
user_id?: number;
my_vote?: number;
am_mod?: boolean;
removed?: boolean;
locked?: boolean;
id: number;
name: string;
url?: string;
body?: string;
creator_id: number;
creator_name: string;
community_id: number;
removed: boolean;
locked: boolean;
published: string;
updated?: string;
creator_name: string;
community_name: string;
number_of_comments: number;
score: number;
upvotes: number;
downvotes: number;
hot_rank: number;
published: string;
updated?: string;
user_id?: number;
my_vote?: number;
subscribed?: boolean;
read?: boolean;
saved?: boolean;
}
export interface Comment {
id: number;
content: string;
creator_id: number;
creator_name: string;
post_id: number,
community_id: number,
parent_id?: number;
content: string;
removed: boolean;
read: boolean;
published: string;
updated?: string;
community_id: number,
banned: boolean;
banned_from_community: boolean;
creator_name: string;
score: number;
upvotes: number;
downvotes: number;
user_id?: number;
my_vote?: number;
am_mod?: boolean;
removed?: boolean;
banned?: boolean;
saved?: boolean;
}
export interface Category {
@ -137,7 +141,7 @@ export interface GetUserDetailsForm {
page?: number;
limit?: number;
community_id?: number;
auth?: string;
saved_only: boolean;
}
export interface UserDetailsResponse {
@ -147,7 +151,6 @@ export interface UserDetailsResponse {
moderates: Array<CommunityUser>;
comments: Array<Comment>;
posts: Array<Post>;
saved?: Array<Post>;
}
export interface BanFromCommunityForm {
@ -324,7 +327,7 @@ export interface CommunityForm {
description?: string,
category_id: number,
edit_id?: number;
removed?: boolean;
removed: boolean;
reason?: string;
expires?: number;
auth?: string;
@ -367,9 +370,9 @@ export interface PostForm {
updated?: number;
edit_id?: number;
creator_id: number;
removed?: boolean;
removed: boolean;
locked: boolean;
reason?: string;
locked?: boolean;
auth: string;
}
@ -379,6 +382,13 @@ export interface GetPostResponse {
comments: Array<Comment>;
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
}
export interface SavePostForm {
post_id: number;
save: boolean;
auth?: string;
}
export interface PostResponse {
@ -392,11 +402,17 @@ export interface CommentForm {
parent_id?: number;
edit_id?: number;
creator_id: number;
removed?: boolean;
removed: boolean;
reason?: string;
auth: string;
}
export interface SaveCommentForm {
comment_id: number;
save: boolean;
auth?: string;
}
export interface CommentResponse {
op: string;
comment: Comment;

View file

@ -1,87 +0,0 @@
body {
font-family: 'Open Sans', sans-serif;
}
.pointer {
cursor: pointer;
}
.no-click {
pointer-events:none;
opacity: 0.65;
}
.upvote:hover {
color: var(--info);
}
.downvote:hover {
color: var(--danger);
}
.form-control, .form-control:focus {
background-color: var(--secondary);
color: #fff;
}
.form-control:disabled {
background-color: var(--secondary);
opacity: .5;
}
.custom-select {
color: #fff;
background-color: var(--secondary);
}
.mark {
background-color: #322a00;
}
.md-div p {
margin-bottom: 0px;
}
.md-div img {
max-width: 100%;
height: auto;
}
.listing {
min-height: 61px;
}
.icon {
display: inline-flex;
width: 1em;
height: 1em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
vertical-align: middle;
align-self: center;
}
.spin {
animation: spins 2s linear infinite;
}
@keyframes spins {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}
.dropdown-menu {
z-index: 2000;
}
.navbar-bg {
background-color: #222;
}
blockquote {
border-left: 3px solid #ccc;
margin: 0.5em 5px;
padding: 0.1em 5px;
}

View file

@ -1,5 +1,5 @@
import { wsUri } from '../env';
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, SiteForm, Site, UserView } 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 } from '../interfaces';
import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
@ -96,6 +96,11 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form));
}
public saveComment(form: SaveCommentForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.SaveComment, form));
}
public getPosts(form: GetPostsForm) {
this.setAuth(form, false);
this.subject.next(this.wsSendWrapper(UserOperation.GetPosts, form));
@ -111,6 +116,11 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm));
}
public savePost(form: SavePostForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.SavePost, form));
}
public banFromCommunity(form: BanFromCommunityForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.BanFromCommunity, form));
@ -121,8 +131,17 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
}
public banUser(form: BanUserForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.BanUser, form));
}
public addAdmin(form: AddAdminForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.AddAdmin, form));
}
public getUserDetails(form: GetUserDetailsForm) {
this.setAuth(form, false);
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
}

View file

@ -1,4 +1,4 @@
import { UserOperation, Comment } from './interfaces';
import { UserOperation, Comment, User } from './interfaces';
import * as markdown_it from 'markdown-it';
export let repoUrl = 'https://github.com/dessalines/lemmy';
@ -40,4 +40,23 @@ export function addTypeInfo<T>(arr: Array<T>, name: string): Array<{type_: strin
return arr.map(e => {return {type_: name, data: e}});
}
export function canMod(user: User, modIds: Array<number>, creator_id: number): boolean {
// You can do moderator actions only on the mods added after you.
if (user) {
let yourIndex = modIds.findIndex(id => id == user.id);
if (yourIndex == -1) {
return false;
} else {
modIds = modIds.slice(0, yourIndex+1); // +1 cause you cant mod yourself
return !modIds.includes(creator_id);
}
} else {
return false;
}
}
export function isMod(modIds: Array<number>, creator_id: number): boolean {
return modIds.includes(creator_id);
}
export let fetchLimit: number = 20;