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
);
+drop table post_read;
+drop table post_saved;
drop table post_like;
drop 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
);
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)
+);
+drop table comment_saved;
drop table comment_like;
drop 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
);
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)
+);
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
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
;
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
;
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,
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
ac.*,
null as user_id,
null as my_vote,
- null as am_mod
+ null as saved
from all_comment ac
;
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.*,
(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,
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 (
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 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::*;
}
}
+#[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::*;
}
}
+#[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;
description: None,
category_id: 1,
creator_id: inserted_user.id,
- removed: None,
+ removed: false,
updated: None
};
url: None,
body: None,
community_id: inserted_community.id,
- removed: None,
- locked: None,
+ removed: false,
+ locked: false,
updated: None
};
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
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,
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();
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);
}
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>,
}
}
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 {
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> {
if let Some(for_post_id) = for_post_id {
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()),
description: None,
category_id: 1,
creator_id: inserted_user.id,
- removed: None,
+ removed: false,
updated: None
};
url: None,
body: None,
community_id: inserted_community.id,
- removed: None,
- locked: None,
+ removed: false,
+ locked: false,
updated: None
};
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(),
upvotes: 1,
user_id: None,
my_vote: None,
- am_mod: None,
+ saved: None,
};
let expected_comment_view_with_user = CommentView {
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(),
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();
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 description: Option<String>,
pub category_id: i32,
pub creator_id: i32,
- pub removed: Option<bool>,
+ pub removed: bool,
pub updated: Option<chrono::NaiveDateTime>
}
title: "nada".to_owned(),
description: None,
category_id: 1,
- removed: None,
+ removed: false,
updated: None,
};
title: "nada".to_owned(),
description: None,
category_id: 1,
- removed: Some(false),
+ removed: false,
published: inserted_community.published,
updated: None
};
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
- removed -> Nullable<Bool>,
+ removed -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
creator_name -> Varchar,
number_of_comments -> BigInt,
user_id -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
- am_mod -> Nullable<Bool>,
}
}
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,
pub number_of_comments: i64,
pub user_id: Option<i32>,
pub subscribed: Option<bool>,
- pub am_mod: Option<bool>,
}
impl CommunityView {
description: None,
category_id: 1,
creator_id: inserted_user.id,
- removed: None,
+ removed: false,
updated: None
};
body: None,
creator_id: inserted_user.id,
community_id: inserted_community.id,
- removed: None,
- locked: None,
+ removed: false,
+ locked: false,
updated: None
};
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"]
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 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::*;
}
}
+#[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::*;
}
}
+#[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;
description: None,
category_id: 1,
creator_id: inserted_user.id,
- removed: None,
+ removed: false,
updated: None
};
body: None,
creator_id: inserted_user.id,
community_id: inserted_community.id,
- removed: None,
- locked: None,
+ removed: false,
+ locked: false,
updated: None
};
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,
published: inserted_post_like.published,
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();
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);
}
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,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
- am_mod -> Nullable<Bool>,
+ read -> Nullable<Bool>,
+ saved -> Nullable<Bool>,
}
}
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,
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 {
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> {
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));
description: None,
creator_id: inserted_user.id,
category_id: 1,
- removed: None,
+ removed: false,
updated: None
};
body: None,
creator_id: inserted_user.id,
community_id: inserted_community.id,
- removed: None,
- locked: None,
+ removed: false,
+ locked: false,
updated: None
};
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,
published: inserted_post.published,
updated: None,
subscribed: None,
- am_mod: None,
+ read: None,
+ saved: None,
};
let expected_post_listing_with_user = PostView {
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,
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();
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)
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
- removed -> Nullable<Bool>,
+ removed -> Bool,
+ read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
}
}
+table! {
+ comment_saved (id) {
+ id -> Int4,
+ comment_id -> Int4,
+ user_id -> Int4,
+ published -> Timestamp,
+ }
+}
+
table! {
community (id) {
id -> Int4,
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
- removed -> Nullable<Bool>,
+ removed -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
body -> Nullable<Text>,
creator_id -> Int4,
community_id -> Int4,
- removed -> Nullable<Bool>,
- locked -> Nullable<Bool>,
+ removed -> Bool,
+ locked -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
}
}
+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,
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));
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));
category,
comment,
comment_like,
+ comment_saved,
community,
community_follower,
community_moderator,
mod_remove_post,
post,
post_like,
+ post_read,
+ post_saved,
site,
user_,
user_ban,
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::*;
#[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)]
post: PostView,
comments: Vec<CommentView>,
community: CommunityView,
- moderators: Vec<CommunityModeratorView>
+ moderators: Vec<CommunityModeratorView>,
+ admins: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
auth: String
}
+#[derive(Serialize, Deserialize)]
+pub struct SaveComment {
+ comment_id: i32,
+ save: bool,
+ auth: String
+}
+
#[derive(Serialize, Deserialize)]
pub struct CommentResponse {
op: String,
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
}
title: String,
description: Option<String>,
category_id: i32,
- removed: Option<bool>,
+ removed: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String
page: Option<i64>,
limit: Option<i64>,
community_id: Option<i32>,
- auth: Option<String>
+ saved_only: bool,
}
#[derive(Serialize, Deserialize)]
moderates: Vec<CommunityModeratorView>,
comments: Vec<CommentView>,
posts: Vec<PostView>,
- saved_posts: Vec<PostView>,
- saved_comments: Vec<CommentView>,
}
#[derive(Serialize, Deserialize)]
Some(community_id),
None,
None,
+ false,
+ false,
None,
Some(9999))
.unwrap();
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);
type Result = ();
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
- println!("Someone disconnected");
// let mut rooms: Vec<i32> = Vec::new();
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)
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)
}
};
- // 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) {
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.");
+ }
+ };
}
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 {
description: self.description.to_owned(),
category_id: self.category_id,
creator_id: user_id,
- removed: None,
+ removed: false,
updated: None,
};
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
};
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 {
post: post_view,
comments: comments,
community: community,
- moderators: moderators
+ moderators: moderators,
+ admins: admins,
}
)
.unwrap()
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 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 {
}
}
+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
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,
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");
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,
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(),
};
// 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();
}
}
}
+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
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();
};
// 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
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
};
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();
moderates: moderates,
comments: comments,
posts: posts,
- saved_posts: Vec::new(),
- saved_comments: Vec::new(),
}
)
.unwrap()
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;
showBanDialog: boolean;
banReason: string;
banExpires: string;
+ banType: BanType;
}
interface CommentNodeProps {
viewOnly?: boolean;
locked?: boolean;
moderators: Array<CommunityUser>;
+ admins: Array<UserView>;
}
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
showBanDialog: false,
banReason: null,
banExpires: null,
+ banType: BanType.Community
}
constructor(props: any, context: any) {
<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>
<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">
- <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
- </li>
- <li className="list-inline-item">
- <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
- </li>
- </>
+ <li className="list-inline-item">
+ <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
+ </li>
+ <li className="list-inline-item">
+ <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
+ </li>
+ </>
}
- {this.canMod &&
- <>
+ {/* 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_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_from_community &&
+ <li className="list-inline-item">
+ <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
+ </li>
+ }
+ </>
+ }
+ {/* 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</span> :
- <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban</span>
+ <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.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</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>
+ }
+ </>
}
- </span>
+ </>
}
<li className="list-inline-item">
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link>
</form>
}
{this.state.showBanDialog &&
- <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
- <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)} />
- </div>
- <div class="form-group row">
- <label class="col-form-label">Expires</label>
- <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} />
- </div>
- <div class="form-group row">
- <button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
- </div>
- </form>
+ <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)} />
+ </div>
+ <div class="form-group row">
+ <label class="col-form-label">Expires</label>
+ <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} />
+ </div>
+ <div class="form-group row">
+ <button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
+ </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}
+ admins={this.props.admins}
+ />
}
- {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}/>}
</div>
)
}
}
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) {
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;
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);
}
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();
- 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,
- reason: i.state.banReason,
- expires: getUnixTime(i.state.banExpires),
- };
- WebSocketService.Instance.banFromCommunity(form);
+
+ 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_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);
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);
+ }
}
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 {
interface CommentNodesProps {
nodes: Array<CommentNodeI>;
moderators?: Array<CommunityUser>;
+ admins?: Array<UserView>;
noIndent?: boolean;
viewOnly?: boolean;
locked?: boolean;
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>
)
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">
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">
<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>
<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>
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">
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">
</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>
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>
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>
<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>
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;
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);
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
<>
{(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>
</>
<>
<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>
</>
<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>
</tr>
- )
+ )
}
</tbody>
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">
i.setState(i.state);
i.refetch();
}
-
+
refetch(){
let modlogForm: GetModlogForm = {
community_id: this.state.communityId,
</ul>
<ul class="navbar-nav ml-auto mr-2">
{this.state.isLoggedIn ?
- <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}
- </a>
- <div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
- <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> :
- <Link class="nav-link" to="/login">Login / Sign up</Link>
+ <>
+ <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}
+ </a>
+ <div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
+ <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>
+ </>
+ :
+ <Link class="nav-link" to="/login">Login / Sign up</Link>
}
</ul>
</div>
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';
<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>
</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">
<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>
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);
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,
};
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,
};
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
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';
commentSort: CommentSortType;
community: Community;
moderators: Array<CommunityUser>;
+ admins: Array<UserView>;
scrolled?: boolean;
scrolled_comment_id?: number;
loading: boolean;
commentSort: CommentSortType.Hot,
community: null,
moderators: [],
+ admins: [],
scrolled: false,
loading: true
}
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 />
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>
)
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>
);
}
} 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) {
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;
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;
} 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);
}
}
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">
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">
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">
<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()
{this.state.view == View.Posts &&
this.posts()
}
+ {this.state.view == View.Saved &&
+ this.overview()
+ }
{this.paginator()}
</div>
<div class="col-12 col-md-3">
<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>
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>
<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>
{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>
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,
};
<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>
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';
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 {
}
export interface Community {
- user_id?: number;
- subscribed?: boolean;
- am_mod?: boolean;
- removed?: boolean;
id: number;
name: string;
title: string;
description?: string;
+ category_id: number;
creator_id: number;
+ removed: boolean;
+ published: string;
+ updated?: string;
creator_name: string;
- category_id: number;
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 {
page?: number;
limit?: number;
community_id?: number;
- auth?: string;
+ saved_only: boolean;
}
export interface UserDetailsResponse {
moderates: Array<CommunityUser>;
comments: Array<Comment>;
posts: Array<Post>;
- saved?: Array<Post>;
}
export interface BanFromCommunityForm {
description?: string,
category_id: number,
edit_id?: number;
- removed?: boolean;
+ removed: boolean;
reason?: string;
expires?: number;
auth?: string;
updated?: number;
edit_id?: number;
creator_id: number;
- removed?: boolean;
+ removed: boolean;
+ locked: boolean;
reason?: string;
- locked?: boolean;
auth: string;
}
comments: Array<Comment>;
community: Community;
moderators: Array<CommunityUser>;
+ admins: Array<UserView>;
+}
+
+export interface SavePostForm {
+ post_id: number;
+ save: boolean;
+ auth?: string;
}
export interface PostResponse {
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;
+++ /dev/null
-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;
-}
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';
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));
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));
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));
}
-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';
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;