Merge branch 'replies' into dev

- Adding reply notifications. Fixes #13.
- Adding Saving posts and comments, and read. Fixes #47.
- Adding proper removed support for comments, communities, posts. Fixes
- Removing reliance on google fonts. Fixes #78.
- Mod related bugs. Fixes #68.
This commit is contained in:
Dessalines 2019-04-20 11:19:58 -07:00
commit bc20155de0
45 changed files with 1479 additions and 417 deletions

View file

@ -38,7 +38,7 @@ create table community (
description text, description text,
category_id int references category on update cascade on delete cascade not null, 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, 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(), published timestamp not null default now(),
updated timestamp updated timestamp
); );

View file

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

View file

@ -5,8 +5,8 @@ create table post (
body text, body text,
creator_id int references user_ on update cascade on delete cascade not null, 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, community_id int references community on update cascade on delete cascade not null,
removed boolean default false, removed boolean default false not null,
locked boolean default false, locked boolean default false not null,
published timestamp not null default now(), published timestamp not null default now(),
updated timestamp updated timestamp
); );
@ -20,3 +20,18 @@ create table post_like (
unique(post_id, user_id) 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_like;
drop table comment; 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, post_id int references post on update cascade on delete cascade not null,
parent_id int references comment on update cascade on delete cascade, parent_id int references comment on update cascade on delete cascade,
content text not null, 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(), published timestamp not null default now(),
updated timestamp updated timestamp
); );
@ -18,3 +19,11 @@ create table comment_like (
published timestamp not null default now(), published timestamp not null default now(),
unique(comment_id, user_id) 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, u.id as user_id,
coalesce(pl.score, 0) as my_vote, coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed, (select 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 from user_ u
cross join all_post ap cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id 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 user_id,
null as my_vote, null as my_vote,
null as subscribed, null as subscribed,
null as am_mod null as read,
null as saved
from all_post ap from all_post ap
; ;

View file

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

View file

@ -1 +1,2 @@
drop view reply_view;
drop view comment_view; drop view comment_view;

View file

@ -4,7 +4,8 @@ with all_comment as
select select
c.*, c.*,
(select community_id from post p where p.id = c.post_id), (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, (select name from user_ where c.creator_id = user_.id) as creator_name,
coalesce(sum(cl.score), 0) as score, coalesce(sum(cl.score), 0) as score,
count (case when cl.score = 1 then 1 else null end) as upvotes, count (case when cl.score = 1 then 1 else null end) as upvotes,
@ -18,7 +19,7 @@ select
ac.*, ac.*,
u.id as user_id, u.id as user_id,
coalesce(cl.score, 0) as my_vote, 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 from user_ u
cross join all_comment ac cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
@ -29,6 +30,31 @@ select
ac.*, ac.*,
null as user_id, null as user_id,
null as my_vote, null as my_vote,
null as am_mod null as saved
from all_comment ac from all_comment ac
; ;
create view reply_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_view cv, closereply
where closereply.id = cv.id
;

View file

@ -43,8 +43,7 @@ create view mod_ban_view as
select mb.*, 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.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 (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 create view mod_add_community_view as
select ma.*, select ma.*,
@ -53,7 +52,6 @@ select ma.*,
(select name from community c where ma.community_id = c.id) as community_name (select name from community c where ma.community_id = c.id) as community_name
from mod_add_community ma; from mod_add_community ma;
create view mod_add_view as create view mod_add_view as
select ma.*, select ma.*,
(select name from user_ u where ma.mod_user_id = u.id) as mod_user_name, (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; extern crate diesel;
use schema::{comment, comment_like}; use schema::{comment, comment_like, comment_saved};
use diesel::*; use diesel::*;
use diesel::result::Error; use diesel::result::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use {Crud, Likeable}; use {Crud, Likeable, Saveable};
use actions::post::Post; use actions::post::Post;
// WITH RECURSIVE MyTree AS ( // WITH RECURSIVE MyTree AS (
@ -22,7 +22,8 @@ pub struct Comment {
pub post_id: i32, pub post_id: i32,
pub parent_id: Option<i32>, pub parent_id: Option<i32>,
pub content: String, pub content: String,
pub removed: Option<bool>, pub removed: bool,
pub read: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -35,30 +36,10 @@ pub struct CommentForm {
pub parent_id: Option<i32>, pub parent_id: Option<i32>,
pub content: String, pub content: String,
pub removed: Option<bool>, pub removed: Option<bool>,
pub read: Option<bool>,
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 { impl Crud<CommentForm> for Comment {
fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> { fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use schema::comment::dsl::*; use schema::comment::dsl::*;
@ -87,6 +68,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 { impl Likeable <CommentLikeForm> for CommentLike {
fn read(conn: &PgConnection, comment_id_from: i32) -> Result<Vec<Self>, Error> { fn read(conn: &PgConnection, comment_id_from: i32) -> Result<Vec<Self>, Error> {
use schema::comment_like::dsl::*; use schema::comment_like::dsl::*;
@ -119,6 +121,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)] #[cfg(test)]
mod tests { mod tests {
use establish_connection; use establish_connection;
@ -174,6 +209,7 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
removed: None, removed: None,
read: None,
parent_id: None, parent_id: None,
updated: None updated: None
}; };
@ -185,7 +221,8 @@ mod tests {
content: "A test comment".into(), content: "A test comment".into(),
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
removed: Some(false), removed: false,
read: false,
parent_id: None, parent_id: None,
published: inserted_comment.published, published: inserted_comment.published,
updated: None updated: None
@ -197,11 +234,13 @@ mod tests {
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id), parent_id: Some(inserted_comment.id),
removed: None, removed: None,
read: None,
updated: None updated: None
}; };
let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
// Comment Like
let comment_like_form = CommentLikeForm { let comment_like_form = CommentLikeForm {
comment_id: inserted_comment.id, comment_id: inserted_comment.id,
post_id: inserted_post.id, post_id: inserted_post.id,
@ -220,9 +259,25 @@ mod tests {
score: 1 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 read_comment = Comment::read(&conn, inserted_comment.id).unwrap();
let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap(); let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap();
let like_removed = CommentLike::remove(&conn, &comment_like_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(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Comment::delete(&conn, inserted_child_comment.id).unwrap(); Comment::delete(&conn, inserted_child_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap();
@ -233,8 +288,10 @@ mod tests {
assert_eq!(expected_comment, inserted_comment); assert_eq!(expected_comment, inserted_comment);
assert_eq!(expected_comment, updated_comment); assert_eq!(expected_comment, updated_comment);
assert_eq!(expected_comment_like, inserted_comment_like); 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!(expected_comment.id, inserted_child_comment.parent_id.unwrap());
assert_eq!(1, like_removed); assert_eq!(1, like_removed);
assert_eq!(1, saved_removed);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
} }

View file

@ -13,18 +13,20 @@ table! {
post_id -> Int4, post_id -> Int4,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Nullable<Bool>, removed -> Bool,
read -> Bool,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
community_id -> Int4, community_id -> Int4,
banned -> Nullable<Bool>, banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
downvotes -> BigInt, downvotes -> BigInt,
user_id -> Nullable<Int4>, user_id -> Nullable<Int4>,
my_vote -> 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 post_id: i32,
pub parent_id: Option<i32>, pub parent_id: Option<i32>,
pub content: String, pub content: String,
pub removed: Option<bool>, pub removed: bool,
pub read: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub community_id: i32, pub community_id: i32,
pub banned: Option<bool>, pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String, pub creator_name: String,
pub score: i64, pub score: i64,
pub upvotes: i64, pub upvotes: i64,
pub downvotes: i64, pub downvotes: i64,
pub user_id: Option<i32>, pub user_id: Option<i32>,
pub my_vote: Option<i32>, pub my_vote: Option<i32>,
pub am_mod: Option<bool>, pub saved: Option<bool>,
} }
impl CommentView { impl CommentView {
@ -57,6 +61,7 @@ impl CommentView {
for_post_id: Option<i32>, for_post_id: Option<i32>,
for_creator_id: Option<i32>, for_creator_id: Option<i32>,
my_user_id: Option<i32>, my_user_id: Option<i32>,
saved_only: bool,
page: Option<i64>, page: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
@ -82,6 +87,10 @@ impl CommentView {
query = query.filter(post_id.eq(for_post_id)); query = query.filter(post_id.eq(for_post_id));
}; };
if saved_only {
query = query.filter(saved.eq(true));
}
query = match sort { query = match sort {
// SortType::Hot => query.order_by(hot_rank.desc()), // SortType::Hot => query.order_by(hot_rank.desc()),
SortType::New => query.order_by(published.desc()), SortType::New => query.order_by(published.desc()),
@ -127,6 +136,107 @@ impl CommentView {
} }
// The faked schema since diesel doesn't do views
table! {
reply_view (id) {
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
community_id -> Int4,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
recipient_id -> Int4,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="reply_view"]
pub struct ReplyView {
pub id: i32,
pub creator_id: i32,
pub post_id: i32,
pub parent_id: Option<i32>,
pub content: String,
pub removed: bool,
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub community_id: i32,
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 saved: Option<bool>,
pub recipient_id: i32,
}
impl ReplyView {
pub fn get_replies(conn: &PgConnection,
for_user_id: i32,
sort: &SortType,
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
use actions::comment_view::reply_view::dsl::*;
let (limit, offset) = limit_and_offset(page, limit);
let mut query = reply_view.into_boxed();
query = query
.filter(user_id.eq(for_user_id))
.filter(recipient_id.eq(for_user_id));
if unread_only {
query = query.filter(read.eq(false));
}
query = match sort {
// SortType::Hot => query.order_by(hot_rank.desc()),
SortType::New => query.order_by(published.desc()),
SortType::TopAll => query.order_by(score.desc()),
SortType::TopYear => query
.filter(published.gt(now - 1.years()))
.order_by(score.desc()),
SortType::TopMonth => query
.filter(published.gt(now - 1.months()))
.order_by(score.desc()),
SortType::TopWeek => query
.filter(published.gt(now - 1.weeks()))
.order_by(score.desc()),
SortType::TopDay => query
.filter(published.gt(now - 1.days()))
.order_by(score.desc()),
_ => query.order_by(published.desc())
};
query
.limit(limit)
.offset(offset)
.load::<Self>(conn)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use establish_connection; use establish_connection;
@ -205,8 +315,10 @@ mod tests {
post_id: inserted_post.id, post_id: inserted_post.id,
community_id: inserted_community.id, community_id: inserted_community.id,
parent_id: None, parent_id: None,
removed: Some(false), removed: false,
banned: None, read: false,
banned: false,
banned_from_community: false,
published: inserted_comment.published, published: inserted_comment.published,
updated: None, updated: None,
creator_name: inserted_user.name.to_owned(), creator_name: inserted_user.name.to_owned(),
@ -215,7 +327,7 @@ mod tests {
upvotes: 1, upvotes: 1,
user_id: None, user_id: None,
my_vote: None, my_vote: None,
am_mod: None, saved: None,
}; };
let expected_comment_view_with_user = CommentView { let expected_comment_view_with_user = CommentView {
@ -225,8 +337,10 @@ mod tests {
post_id: inserted_post.id, post_id: inserted_post.id,
community_id: inserted_community.id, community_id: inserted_community.id,
parent_id: None, parent_id: None,
removed: Some(false), removed: false,
banned: None, read: false,
banned: false,
banned_from_community: false,
published: inserted_comment.published, published: inserted_comment.published,
updated: None, updated: None,
creator_name: inserted_user.name.to_owned(), creator_name: inserted_user.name.to_owned(),
@ -235,11 +349,11 @@ mod tests {
upvotes: 1, upvotes: 1,
user_id: Some(inserted_user.id), user_id: Some(inserted_user.id),
my_vote: Some(1), 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_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), 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 like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap();

View file

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

View file

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

View file

@ -1,9 +1,9 @@
extern crate diesel; extern crate diesel;
use schema::{post, post_like}; use schema::{post, post_like, post_saved, post_read};
use diesel::*; use diesel::*;
use diesel::result::Error; use diesel::result::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use {Crud, Likeable}; use {Crud, Likeable, Saveable, Readable};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="post"] #[table_name="post"]
@ -14,8 +14,8 @@ pub struct Post {
pub body: Option<String>, pub body: Option<String>,
pub creator_id: i32, pub creator_id: i32,
pub community_id: i32, pub community_id: i32,
pub removed: Option<bool>, pub removed: bool,
pub locked: Option<bool>, pub locked: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -33,25 +33,6 @@ pub struct PostForm {
pub updated: Option<chrono::NaiveDateTime> 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 { impl Crud<PostForm> for Post {
fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> { fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
use schema::post::dsl::*; 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 { impl Likeable <PostLikeForm> for PostLike {
fn read(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> { fn read(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
use schema::post_like::dsl::*; 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)] #[cfg(test)]
mod tests { mod tests {
use establish_connection; use establish_connection;
@ -159,11 +225,12 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
community_id: inserted_community.id, community_id: inserted_community.id,
published: inserted_post.published, published: inserted_post.published,
removed: Some(false), removed: false,
locked: Some(false), locked: false,
updated: None updated: None
}; };
// Post Like
let post_like_form = PostLikeForm { let post_like_form = PostLikeForm {
post_id: inserted_post.id, post_id: inserted_post.id,
user_id: inserted_user.id, user_id: inserted_user.id,
@ -180,9 +247,41 @@ mod tests {
score: 1 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 read_post = Post::read(&conn, inserted_post.id).unwrap();
let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap(); let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap();
let like_removed = PostLike::remove(&conn, &post_like_form).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(); let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap(); Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.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, inserted_post);
assert_eq!(expected_post, updated_post); assert_eq!(expected_post, updated_post);
assert_eq!(expected_post_like, inserted_post_like); 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, like_removed);
assert_eq!(1, saved_removed);
assert_eq!(1, read_removed);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
} }

View file

@ -19,8 +19,8 @@ table! {
body -> Nullable<Text>, body -> Nullable<Text>,
creator_id -> Int4, creator_id -> Int4,
community_id -> Int4, community_id -> Int4,
removed -> Nullable<Bool>, removed -> Bool,
locked -> Nullable<Bool>, locked -> Bool,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
creator_name -> Varchar, creator_name -> Varchar,
@ -33,7 +33,8 @@ table! {
user_id -> Nullable<Int4>, user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>, my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>, 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 body: Option<String>,
pub creator_id: i32, pub creator_id: i32,
pub community_id: i32, pub community_id: i32,
pub removed: Option<bool>, pub removed: bool,
pub locked: Option<bool>, pub locked: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub creator_name: String, pub creator_name: String,
@ -61,7 +62,8 @@ pub struct PostView {
pub user_id: Option<i32>, pub user_id: Option<i32>,
pub my_vote: Option<i32>, pub my_vote: Option<i32>,
pub subscribed: Option<bool>, pub subscribed: Option<bool>,
pub am_mod: Option<bool>, pub read: Option<bool>,
pub saved: Option<bool>,
} }
impl PostView { impl PostView {
@ -71,6 +73,8 @@ impl PostView {
for_community_id: Option<i32>, for_community_id: Option<i32>,
for_creator_id: Option<i32>, for_creator_id: Option<i32>,
my_user_id: Option<i32>, my_user_id: Option<i32>,
saved_only: bool,
unread_only: bool,
page: Option<i64>, page: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
@ -88,6 +92,15 @@ impl PostView {
query = query.filter(creator_id.eq(for_creator_id)); 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_ { match type_ {
PostListingType::Subscribed => { PostListingType::Subscribed => {
query = query.filter(subscribed.eq(true)); query = query.filter(subscribed.eq(true));
@ -239,8 +252,8 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
creator_name: user_name.to_owned(), creator_name: user_name.to_owned(),
community_id: inserted_community.id, community_id: inserted_community.id,
removed: Some(false), removed: false,
locked: Some(false), locked: false,
community_name: community_name.to_owned(), community_name: community_name.to_owned(),
number_of_comments: 0, number_of_comments: 0,
score: 1, score: 1,
@ -250,7 +263,8 @@ mod tests {
published: inserted_post.published, published: inserted_post.published,
updated: None, updated: None,
subscribed: None, subscribed: None,
am_mod: None, read: None,
saved: None,
}; };
let expected_post_listing_with_user = PostView { let expected_post_listing_with_user = PostView {
@ -260,8 +274,8 @@ mod tests {
name: post_name.to_owned(), name: post_name.to_owned(),
url: None, url: None,
body: None, body: None,
removed: Some(false), removed: false,
locked: Some(false), locked: false,
creator_id: inserted_user.id, creator_id: inserted_user.id,
creator_name: user_name.to_owned(), creator_name: user_name.to_owned(),
community_id: inserted_community.id, community_id: inserted_community.id,
@ -274,12 +288,13 @@ mod tests {
published: inserted_post.published, published: inserted_post.published,
updated: None, updated: None,
subscribed: 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_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, 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_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(); 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; 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 { pub fn establish_connection() -> PgConnection {
let db_url = Settings::get().db_url; let db_url = Settings::get().db_url;
PgConnection::establish(&db_url) PgConnection::establish(&db_url)

View file

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

View file

@ -11,7 +11,7 @@ use bcrypt::{verify};
use std::str::FromStr; use std::str::FromStr;
use diesel::PgConnection; 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::community::*;
use actions::user::*; use actions::user::*;
use actions::post::*; use actions::post::*;
@ -26,7 +26,7 @@ use actions::moderator::*;
#[derive(EnumString,ToString,Debug)] #[derive(EnumString,ToString,Debug)]
pub enum UserOperation { 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, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -164,7 +164,8 @@ pub struct GetPostResponse {
post: PostView, post: PostView,
comments: Vec<CommentView>, comments: Vec<CommentView>,
community: CommunityView, community: CommunityView,
moderators: Vec<CommunityModeratorView> moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -214,6 +215,14 @@ pub struct EditComment {
post_id: i32, post_id: i32,
removed: Option<bool>, removed: Option<bool>,
reason: Option<String>, reason: Option<String>,
read: Option<bool>,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct SaveComment {
comment_id: i32,
save: bool,
auth: String auth: String
} }
@ -254,8 +263,15 @@ pub struct EditPost {
url: Option<String>, url: Option<String>,
body: Option<String>, body: Option<String>,
removed: Option<bool>, removed: Option<bool>,
reason: Option<String>,
locked: Option<bool>, locked: Option<bool>,
reason: Option<String>,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct SavePost {
post_id: i32,
save: bool,
auth: String auth: String
} }
@ -297,7 +313,7 @@ pub struct GetUserDetails {
page: Option<i64>, page: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
community_id: Option<i32>, community_id: Option<i32>,
auth: Option<String> saved_only: bool,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -308,8 +324,6 @@ pub struct GetUserDetailsResponse {
moderates: Vec<CommunityModeratorView>, moderates: Vec<CommunityModeratorView>,
comments: Vec<CommentView>, comments: Vec<CommentView>,
posts: Vec<PostView>, posts: Vec<PostView>,
saved_posts: Vec<PostView>,
saved_comments: Vec<CommentView>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -426,6 +440,21 @@ pub struct BanUserResponse {
banned: bool, banned: bool,
} }
#[derive(Serialize, Deserialize)]
pub struct GetReplies {
sort: String,
page: Option<i64>,
limit: Option<i64>,
unread_only: bool,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct GetRepliesResponse {
op: String,
replies: Vec<ReplyView>,
}
/// `ChatServer` manages chat rooms and responsible for coordinating chat /// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session. implementation is super primitive /// session. implementation is super primitive
pub struct ChatServer { pub struct ChatServer {
@ -468,6 +497,8 @@ impl ChatServer {
Some(community_id), Some(community_id),
None, None,
None, None,
false,
false,
None, None,
Some(9999)) Some(9999))
.unwrap(); .unwrap();
@ -491,7 +522,6 @@ impl Handler<Connect> for ChatServer {
type Result = usize; type Result = usize;
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result { fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
println!("Someone joined");
// notify all users in same room // notify all users in same room
// self.send_room_message(&"Main".to_owned(), "Someone joined", 0); // self.send_room_message(&"Main".to_owned(), "Someone joined", 0);
@ -513,7 +543,6 @@ impl Handler<Disconnect> for ChatServer {
type Result = (); type Result = ();
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) { fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
println!("Someone disconnected");
// let mut rooms: Vec<i32> = Vec::new(); // let mut rooms: Vec<i32> = Vec::new();
@ -586,6 +615,10 @@ impl Handler<StandardMessage> for ChatServer {
let edit_comment: EditComment = serde_json::from_str(data).unwrap(); let edit_comment: EditComment = serde_json::from_str(data).unwrap();
edit_comment.perform(self, msg.id) 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 => { UserOperation::CreateCommentLike => {
let create_comment_like: CreateCommentLike = serde_json::from_str(data).unwrap(); let create_comment_like: CreateCommentLike = serde_json::from_str(data).unwrap();
create_comment_like.perform(self, msg.id) create_comment_like.perform(self, msg.id)
@ -602,6 +635,10 @@ impl Handler<StandardMessage> for ChatServer {
let edit_post: EditPost = serde_json::from_str(data).unwrap(); let edit_post: EditPost = serde_json::from_str(data).unwrap();
edit_post.perform(self, msg.id) 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 => { UserOperation::EditCommunity => {
let edit_community: EditCommunity = serde_json::from_str(data).unwrap(); let edit_community: EditCommunity = serde_json::from_str(data).unwrap();
edit_community.perform(self, msg.id) edit_community.perform(self, msg.id)
@ -650,6 +687,10 @@ impl Handler<StandardMessage> for ChatServer {
let ban_user: BanUser = serde_json::from_str(data).unwrap(); let ban_user: BanUser = serde_json::from_str(data).unwrap();
ban_user.perform(self, msg.id) ban_user.perform(self, msg.id)
}, },
UserOperation::GetReplies => {
let get_replies: GetReplies = serde_json::from_str(data).unwrap();
get_replies.perform(self, msg.id)
},
}; };
MessageResult(res) MessageResult(res)
@ -745,11 +786,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 { if self.admin {
let community_moderator_form = CommunityModeratorForm { let community_moderator_form = CommunityModeratorForm {
community_id: 1, community_id: 1,
user_id: inserted_user.id user_id: inserted_user.id,
}; };
let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
@ -758,6 +799,18 @@ impl Perform for Register {
return self.error("Community moderator already exists."); 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,8 +850,12 @@ impl Perform for CreateCommunity {
let user_id = claims.id; let user_id = claims.id;
// When you create a community, make sure the user becomes a moderator and a follower // 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 { let community_form = CommunityForm {
name: self.name.to_owned(), name: self.name.to_owned(),
title: self.title.to_owned(), title: self.title.to_owned(),
@ -934,11 +991,16 @@ impl Perform for CreatePost {
let user_id = claims.id; 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() { if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() {
return self.error("You have been banned from this community"); 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 { let post_form = PostForm {
name: self.name.to_owned(), name: self.name.to_owned(),
url: self.url.to_owned(), url: self.url.to_owned(),
@ -1031,12 +1093,14 @@ impl Perform for GetPost {
chat.rooms.get_mut(&self.id).unwrap().insert(addr); 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 community = CommunityView::read(&conn, post_view.community_id, user_id).unwrap();
let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id).unwrap(); let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id).unwrap();
let admins = UserView::admins(&conn).unwrap();
// Return the jwt // Return the jwt
serde_json::to_string( serde_json::to_string(
&GetPostResponse { &GetPostResponse {
@ -1044,7 +1108,8 @@ impl Perform for GetPost {
post: post_view, post: post_view,
comments: comments, comments: comments,
community: community, community: community,
moderators: moderators moderators: moderators,
admins: admins,
} }
) )
.unwrap() .unwrap()
@ -1117,12 +1182,17 @@ impl Perform for CreateComment {
let user_id = claims.id; let user_id = claims.id;
// Check for a ban // Check for a community ban
let post = Post::read(&conn, self.post_id).unwrap(); let post = Post::read(&conn, self.post_id).unwrap();
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return self.error("You have been banned from this community"); 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 content_slurs_removed = remove_slurs(&self.content.to_owned());
let comment_form = CommentForm { let comment_form = CommentForm {
@ -1131,6 +1201,7 @@ impl Perform for CreateComment {
post_id: self.post_id, post_id: self.post_id,
creator_id: user_id, creator_id: user_id,
removed: None, removed: None,
read: None,
updated: None updated: None
}; };
@ -1202,24 +1273,38 @@ impl Perform for EditComment {
let user_id = claims.id; let user_id = claims.id;
// Verify its the creator or a mod, or an admin
// Verify its the creator or a mod
let orig_comment = CommentView::read(&conn, self.edit_id, None).unwrap(); 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() .unwrap()
.into_iter() .into_iter()
.map(|m| m.user_id) .map(|m| m.user_id)
.collect(); .collect()
editors.push(self.creator_id); );
editors.append(
&mut UserView::admins(&conn)
.unwrap()
.into_iter()
.map(|a| a.id)
.collect()
);
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return self.error("Not allowed to edit comment."); 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() { if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
return self.error("You have been banned from this community"); 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 content_slurs_removed = remove_slurs(&self.content.to_owned());
let comment_form = CommentForm { let comment_form = CommentForm {
@ -1228,6 +1313,7 @@ impl Perform for EditComment {
post_id: self.post_id, post_id: self.post_id,
creator_id: self.creator_id, creator_id: self.creator_id,
removed: self.removed.to_owned(), removed: self.removed.to_owned(),
read: self.read.to_owned(),
updated: Some(naive_now()) updated: Some(naive_now())
}; };
@ -1278,6 +1364,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 { impl Perform for CreateCommentLike {
fn op_type(&self) -> UserOperation { fn op_type(&self) -> UserOperation {
UserOperation::CreateCommentLike UserOperation::CreateCommentLike
@ -1296,12 +1436,17 @@ impl Perform for CreateCommentLike {
let user_id = claims.id; let user_id = claims.id;
// Check for a ban // Check for a community ban
let post = Post::read(&conn, self.post_id).unwrap(); let post = Post::read(&conn, self.post_id).unwrap();
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return self.error("You have been banned from this community"); 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 { let like_form = CommentLikeForm {
comment_id: self.comment_id, comment_id: self.comment_id,
post_id: self.post_id, post_id: self.post_id,
@ -1377,7 +1522,7 @@ impl Perform for GetPosts {
let type_ = PostListingType::from_str(&self.type_).expect("listing type"); let type_ = PostListingType::from_str(&self.type_).expect("listing type");
let sort = SortType::from_str(&self.sort).expect("listing sort"); 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, Ok(posts) => posts,
Err(_e) => { Err(_e) => {
return self.error("Couldn't get posts"); return self.error("Couldn't get posts");
@ -1414,12 +1559,17 @@ impl Perform for CreatePostLike {
let user_id = claims.id; let user_id = claims.id;
// Check for a ban // Check for a community ban
let post = Post::read(&conn, self.post_id).unwrap(); let post = Post::read(&conn, self.post_id).unwrap();
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return self.error("You have been banned from this community"); 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 { let like_form = PostLikeForm {
post_id: self.post_id, post_id: self.post_id,
user_id: user_id, user_id: user_id,
@ -1483,22 +1633,36 @@ impl Perform for EditPost {
let user_id = claims.id; let user_id = claims.id;
// Verify its the creator or a mod // Verify its the creator or a mod or admin
let mut editors: Vec<i32> = CommunityModeratorView::for_community(&conn, self.community_id) let mut editors: Vec<i32> = vec![self.creator_id];
editors.append(
&mut CommunityModeratorView::for_community(&conn, self.community_id)
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|m| m.user_id) .map(|m| m.user_id)
.collect(); .collect()
editors.push(self.creator_id); );
editors.append(
&mut UserView::admins(&conn)
.unwrap()
.into_iter()
.map(|a| a.id)
.collect()
);
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return self.error("Not allowed to edit comment."); return self.error("Not allowed to edit post.");
} }
// Check for a ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() {
return self.error("You have been banned from this community"); 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 { let post_form = PostForm {
name: self.name.to_owned(), name: self.name.to_owned(),
url: self.url.to_owned(), url: self.url.to_owned(),
@ -1564,6 +1728,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 { impl Perform for EditCommunity {
fn op_type(&self) -> UserOperation { fn op_type(&self) -> UserOperation {
UserOperation::EditCommunity UserOperation::EditCommunity
@ -1586,6 +1803,11 @@ impl Perform for EditCommunity {
let user_id = claims.id; 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 // Verify its a mod
let moderator_view = CommunityModeratorView::for_community(&conn, self.edit_id).unwrap(); 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(); let mod_ids: Vec<i32> = moderator_view.into_iter().map(|m| m.user_id).collect();
@ -1750,26 +1972,21 @@ impl Perform for GetUserDetails {
let conn = establish_connection(); 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 //TODO add save
let sort = SortType::from_str(&self.sort).expect("listing sort"); let sort = SortType::from_str(&self.sort).expect("listing sort");
let user_view = UserView::read(&conn, self.user_id).unwrap(); 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 posts = if self.saved_only {
let comments = CommentView::list(&conn, &sort, None, Some(self.user_id), user_id, self.page, self.limit).unwrap(); 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 follows = CommunityFollowerView::for_user(&conn, self.user_id).unwrap();
let moderates = CommunityModeratorView::for_user(&conn, self.user_id).unwrap(); let moderates = CommunityModeratorView::for_user(&conn, self.user_id).unwrap();
@ -1782,8 +1999,6 @@ impl Perform for GetUserDetails {
moderates: moderates, moderates: moderates,
comments: comments, comments: comments,
posts: posts, posts: posts,
saved_posts: Vec::new(),
saved_comments: Vec::new(),
} }
) )
.unwrap() .unwrap()
@ -1834,6 +2049,39 @@ impl Perform for GetModlog {
} }
} }
impl Perform for GetReplies {
fn op_type(&self) -> UserOperation {
UserOperation::GetReplies
}
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 sort = SortType::from_str(&self.sort).expect("listing sort");
let replies = ReplyView::get_replies(&conn, user_id, &sort, self.unread_only, self.page, self.limit).unwrap();
// Return the jwt
serde_json::to_string(
&GetRepliesResponse {
op: self.op_type().to_string(),
replies: replies,
}
)
.unwrap()
}
}
impl Perform for BanFromCommunity { impl Perform for BanFromCommunity {
fn op_type(&self) -> UserOperation { fn op_type(&self) -> UserOperation {
UserOperation::BanFromCommunity UserOperation::BanFromCommunity

View file

@ -1,12 +1,14 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; 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 { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime } from '../utils'; import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form'; import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
enum BanType {Community, Site};
interface CommentNodeState { interface CommentNodeState {
showReply: boolean; showReply: boolean;
showEdit: boolean; showEdit: boolean;
@ -15,6 +17,7 @@ interface CommentNodeState {
showBanDialog: boolean; showBanDialog: boolean;
banReason: string; banReason: string;
banExpires: string; banExpires: string;
banType: BanType;
} }
interface CommentNodeProps { interface CommentNodeProps {
@ -22,7 +25,9 @@ interface CommentNodeProps {
noIndent?: boolean; noIndent?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
locked?: boolean; locked?: boolean;
markable?: boolean;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -35,6 +40,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
showBanDialog: false, showBanDialog: false,
banReason: null, banReason: null,
banExpires: null, banExpires: null,
banType: BanType.Community
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -60,6 +66,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="text-info" to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link> <Link className="text-info" to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link>
</li> </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"> <li className="list-inline-item">
<span>( <span>(
<span className="text-info">+{node.comment.upvotes}</span> <span className="text-info">+{node.comment.upvotes}</span>
@ -77,11 +89,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<div> <div>
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.content)} /> <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"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{!this.props.viewOnly && {UserService.Instance.user && !this.props.viewOnly &&
<span class="mr-2"> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span> <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
</li> </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 && {this.myComment &&
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
@ -92,36 +107,61 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</li> </li>
</> </>
} }
{/* Admins and mods can remove comments */}
{this.canMod && {this.canMod &&
<>
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.node.comment.removed ? {!this.props.node.comment.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
} }
</li> </li>
{!this.isMod && }
{/* Mods can ban from community, and appoint as mods to community */}
{this.canMod &&
<> <>
{!this.isMod &&
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.node.comment.banned ? {!this.props.node.comment.banned_from_community ?
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban</span> : <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}>ban</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban</span> <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}>unban</span>
} }
</li> </li>
</>
} }
{!this.props.node.comment.banned && {!this.props.node.comment.banned_from_community &&
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span> <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
</li> </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.handleAddAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span>
</li>
}
</>
}
</>
} }
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link> <Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link>
</li> </li>
{this.props.markable &&
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{`mark as ${node.comment.read ? 'unread' : 'read'}`}</span>
</li>
}
</ul> </ul>
</div> </div>
} }
@ -133,7 +173,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</form> </form>
} }
{this.state.showBanDialog && {this.state.showBanDialog &&
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-form-label">Reason</label> <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)} /> <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
@ -147,8 +187,21 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div> </div>
</form> </form>
} }
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />} {this.state.showReply &&
{this.props.node.children && <CommentNodes nodes={this.props.node.children} locked={this.props.locked} moderators={this.props.moderators}/>} <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> </div>
) )
} }
@ -158,27 +211,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
get canMod(): boolean { 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. return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
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;
}
} }
get isMod(): boolean { get isMod(): boolean {
return this.props.moderators.map(m => m.user_id).includes(this.props.node.comment.creator_id); return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id);
}
get isAdmin(): boolean {
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
}
get canAdmin(): boolean {
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
} }
handleReplyClick(i: CommentNode) { handleReplyClick(i: CommentNode) {
@ -193,7 +241,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleDeleteClick(i: CommentNode) { handleDeleteClick(i: CommentNode) {
let deleteForm: CommentFormI = { let deleteForm: CommentFormI = {
content: "*deleted*", content: '*deleted*',
edit_id: i.props.node.comment.id, edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id, creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id, post_id: i.props.node.comment.post_id,
@ -203,6 +251,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
WebSocketService.Instance.editComment(deleteForm); 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() { handleReplyCancel() {
this.state.showReply = false; this.state.showReply = false;
this.state.showEdit = false; this.state.showEdit = false;
@ -257,8 +315,29 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state); i.setState(i.state);
} }
handleMarkRead(i: CommentNode) {
let form: CommentFormI = {
content: i.props.node.comment.content,
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,
read: !i.props.node.comment.read,
auth: null
};
WebSocketService.Instance.editComment(form);
}
handleModBanFromCommunityShow(i: CommentNode) {
i.state.showBanDialog = true;
i.state.banType = BanType.Community;
i.setState(i.state);
}
handleModBanShow(i: CommentNode) { handleModBanShow(i: CommentNode) {
i.state.showBanDialog = true; i.state.showBanDialog = true;
i.state.banType = BanType.Site;
i.setState(i.state); i.setState(i.state);
} }
@ -272,16 +351,42 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
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) { handleModBanSubmit(i: CommentNode) {
i.state.banType = BanType.Site;
i.setState(i.state);
i.handleModBanBothSubmit(i);
}
handleModBanBothSubmit(i: CommentNode) {
event.preventDefault(); event.preventDefault();
console.log(BanType[i.state.banType]);
console.log(i.props.node.comment.banned);
if (i.state.banType == BanType.Community) {
let form: BanFromCommunityForm = { let form: BanFromCommunityForm = {
user_id: i.props.node.comment.creator_id, user_id: i.props.node.comment.creator_id,
community_id: i.props.node.comment.community_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, reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires), expires: getUnixTime(i.state.banExpires),
}; };
WebSocketService.Instance.banFromCommunity(form); 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.state.showBanDialog = false;
i.setState(i.state); i.setState(i.state);
@ -296,4 +401,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
WebSocketService.Instance.addModToCommunity(form); WebSocketService.Instance.addModToCommunity(form);
i.setState(i.state); i.setState(i.state);
} }
handleAddAdmin(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 { Component } from 'inferno';
import { CommentNode as CommentNodeI, CommunityUser } from '../interfaces'; import { CommentNode as CommentNodeI, CommunityUser, UserView } from '../interfaces';
import { CommentNode } from './comment-node'; import { CommentNode } from './comment-node';
interface CommentNodesState { interface CommentNodesState {
@ -8,9 +8,11 @@ interface CommentNodesState {
interface CommentNodesProps { interface CommentNodesProps {
nodes: Array<CommentNodeI>; nodes: Array<CommentNodeI>;
moderators?: Array<CommunityUser>; moderators?: Array<CommunityUser>;
admins?: Array<UserView>;
noIndent?: boolean; noIndent?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
locked?: boolean; locked?: boolean;
markable?: boolean;
} }
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> { export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
@ -27,7 +29,10 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
noIndent={this.props.noIndent} noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly} viewOnly={this.props.viewOnly}
locked={this.props.locked} locked={this.props.locked}
moderators={this.props.moderators}/> moderators={this.props.moderators}
admins={this.props.admins}
markable={this.props.markable}
/>
)} )}
</div> </div>
) )

View file

@ -53,9 +53,9 @@ export class Communities extends Component<any, CommunitiesState> {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {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> <div>
<h4>Communities</h4> <h5>Communities</h5>
<div class="table-responsive"> <div class="table-responsive">
<table id="community_table" class="table table-sm table-hover"> <table id="community_table" class="table table-sm table-hover">
<thead class="pointer"> <thead class="pointer">

View file

@ -60,14 +60,14 @@ export class Community extends Component<any, State> {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {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="row">
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<h4>{this.state.community.title} <h5>{this.state.community.title}
{this.state.community.removed && {this.state.community.removed &&
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic">removed</small>
} }
</h4> </h5>
<PostListings communityId={this.state.communityId} /> <PostListings communityId={this.state.communityId} />
</div> </div>
<div class="col-12 col-md-3"> <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="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 mb-4"> <div class="col-12 col-lg-6 mb-4">
<h4>Create Forum</h4> <h5>Create Forum</h5>
<CommunityForm onCreate={this.handleCommunityCreate}/> <CommunityForm onCreate={this.handleCommunityCreate}/>
</div> </div>
</div> </div>

View file

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

177
ui/src/components/inbox.tsx Normal file
View file

@ -0,0 +1,177 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, CommentResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
import { CommentNodes } from './comment-nodes';
enum UnreadType {
Unread, All
}
interface InboxState {
unreadType: UnreadType;
replies: Array<Comment>;
sort: SortType;
page: number;
}
export class Inbox extends Component<any, InboxState> {
private subscription: Subscription;
private emptyState: InboxState = {
unreadType: UnreadType.Unread,
replies: [],
sort: SortType.New,
page: 1,
}
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
(msg) => this.parseMessage(msg),
(err) => console.error(err),
() => console.log('complete')
);
this.refetch();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
let user = UserService.Instance.user;
return (
<div class="container">
<div class="row">
<div class="col-12">
<h5>Inbox for <Link to={`/user/${user.id}`}>{user.username}</Link></h5>
{this.selects()}
{this.replies()}
{this.paginator()}
</div>
</div>
</div>
)
}
selects() {
return (
<div className="mb-2">
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select w-auto">
<option disabled>Type</option>
<option value={UnreadType.Unread}>Unread</option>
<option value={UnreadType.All}>All</option>
</select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
<option disabled>Sort Type</option>
<option value={SortType.New}>New</option>
<option value={SortType.TopDay}>Top Day</option>
<option value={SortType.TopWeek}>Week</option>
<option value={SortType.TopMonth}>Month</option>
<option value={SortType.TopYear}>Year</option>
<option value={SortType.TopAll}>All</option>
</select>
</div>
)
}
replies() {
return (
<div>
{this.state.replies.map(reply =>
<CommentNodes nodes={[{comment: reply}]} noIndent viewOnly markable />
)}
</div>
);
}
paginator() {
return (
<div class="mt-2">
{this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
}
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
</div>
);
}
nextPage(i: Inbox) {
i.state.page++;
i.setState(i.state);
i.refetch();
}
prevPage(i: Inbox) {
i.state.page--;
i.setState(i.state);
i.refetch();
}
handleUnreadTypeChange(i: Inbox, event: any) {
i.state.unreadType = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.refetch();
}
refetch() {
let form: GetRepliesForm = {
sort: SortType[this.state.sort],
unread_only: (this.state.unreadType == UnreadType.Unread),
page: this.state.page,
limit: 9999,
};
WebSocketService.Instance.getReplies(form);
}
handleSortChange(i: Inbox, event: any) {
i.state.sort = Number(event.target.value);
i.state.page = 1;
i.setState(i.state);
i.refetch();
}
parseMessage(msg: any) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(msg.error);
return;
} else if (op == UserOperation.GetReplies) {
let res: GetRepliesResponse = msg;
this.state.replies = res.replies;
this.sendRepliesCount();
this.setState(this.state);
} else if (op == UserOperation.EditComment) {
let res: CommentResponse = msg;
// If youre in the unread view, just remove it from the list
if (this.state.unreadType == UnreadType.Unread && res.comment.read) {
this.state.replies = this.state.replies.filter(r => r.id !== res.comment.id);
} else {
let found = this.state.replies.find(c => c.id == res.comment.id);
found.read = res.comment.read;
}
this.sendRepliesCount();
this.setState(this.state);
}
}
sendRepliesCount() {
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: this.state.replies.filter(r => !r.read).length});
}
}

View file

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

View file

@ -2,7 +2,7 @@ import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse } from '../interfaces'; import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, GetRepliesResponse, GetRepliesForm } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { msgOp, repoUrl, mdToHtml } from '../utils'; import { msgOp, repoUrl, mdToHtml } from '../utils';
@ -55,6 +55,15 @@ export class Main extends Component<any, State> {
if (UserService.Instance.user) { if (UserService.Instance.user) {
WebSocketService.Instance.getFollowedCommunities(); WebSocketService.Instance.getFollowedCommunities();
// Get replies for the count
let repliesForm: GetRepliesForm = {
sort: SortType[SortType.New],
unread_only: true,
page: 1,
limit: 9999,
};
WebSocketService.Instance.getReplies(repliesForm);
} }
let listCommunitiesForm: ListCommunitiesForm = { let listCommunitiesForm: ListCommunitiesForm = {
@ -78,12 +87,12 @@ export class Main extends Component<any, State> {
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
{this.state.loading ? {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> <div>
{this.trendingCommunities()} {this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 && {UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div> <div>
<h4>Subscribed forums</h4> <h5>Subscribed forums</h5>
<ul class="list-inline"> <ul class="list-inline">
{this.state.subscribedCommunities.map(community => {this.state.subscribedCommunities.map(community =>
<li class="list-inline-item"><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li> <li class="list-inline-item"><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
@ -103,7 +112,7 @@ export class Main extends Component<any, State> {
trendingCommunities() { trendingCommunities() {
return ( return (
<div> <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"> <ul class="list-inline">
{this.state.trendingCommunities.map(community => {this.state.trendingCommunities.map(community =>
<li class="list-inline-item"><Link to={`/community/${community.id}`}>{community.name}</Link></li> <li class="list-inline-item"><Link to={`/community/${community.id}`}>{community.name}</Link></li>
@ -116,7 +125,7 @@ export class Main extends Component<any, State> {
landing() { landing() {
return ( return (
<div> <div>
<h4>{`${this.state.site.site.name}`}</h4> <h5>{`${this.state.site.site.name}`}</h5>
<ul class="my-1 list-inline"> <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_users} Users</li>
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li> <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li>
@ -136,10 +145,10 @@ export class Main extends Component<any, State> {
<hr /> <hr />
</div> </div>
} }
<h4>Welcome to <h5>Welcome to
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg> <svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
<a href={repoUrl}>Lemmy<sup>Beta</sup></a> <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>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>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> <p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p>
@ -176,7 +185,14 @@ export class Main extends Component<any, State> {
this.state.site.site = res.site; this.state.site.site = res.site;
this.state.site.banned = res.banned; this.state.site.banned = res.banned;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetReplies) {
let res: GetRepliesResponse = msg;
this.sendRepliesCount(res);
} }
} }
sendRepliesCount(res: GetRepliesResponse) {
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length});
}
} }

View file

@ -9,7 +9,7 @@ import { MomentTime } from './moment-time';
import * as moment from 'moment'; import * as moment from 'moment';
interface ModlogState { interface ModlogState {
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}>, combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity | ModAdd | ModBan}>,
communityId?: number, communityId?: number,
communityName?: string, communityName?: string,
page: number; page: number;
@ -51,6 +51,8 @@ export class Modlog extends Component<any, ModlogState> {
let removed_communities = addTypeInfo(res.removed_communities, "removed_communities"); let removed_communities = addTypeInfo(res.removed_communities, "removed_communities");
let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community"); 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_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 = [];
this.state.combined.push(...removed_posts); 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(...removed_communities);
this.state.combined.push(...banned_from_community); this.state.combined.push(...banned_from_community);
this.state.combined.push(...added_to_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) { 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 // Sort them by time
@ -95,13 +99,14 @@ export class Modlog extends Component<any, ModlogState> {
<> <>
{(i.data as ModRemoveComment).removed? 'Removed' : 'Restored'} {(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> 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> <div>{(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}</div>
</> </>
} }
{i.type_ == 'removed_communities' && {i.type_ == 'removed_communities' &&
<> <>
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'} {(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).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
<div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</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>{(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><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).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
<div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</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>{(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><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> 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> </td>
@ -136,12 +158,12 @@ export class Modlog extends Component<any, ModlogState> {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {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> <div>
<h4> <h5>
{this.state.communityName && <Link className="text-white" to={`/community/${this.state.communityId}`}>/f/{this.state.communityName} </Link>} {this.state.communityName && <Link className="text-white" to={`/community/${this.state.communityId}`}>/f/{this.state.communityName} </Link>}
<span>Modlog</span> <span>Modlog</span>
</h4> </h5>
<div class="table-responsive"> <div class="table-responsive">
<table id="modlog_table" class="table table-sm table-hover"> <table id="modlog_table" class="table table-sm table-hover">
<thead class="pointer"> <thead class="pointer">

View file

@ -7,12 +7,14 @@ interface NavbarState {
isLoggedIn: boolean; isLoggedIn: boolean;
expanded: boolean; expanded: boolean;
expandUserDropdown: boolean; expandUserDropdown: boolean;
unreadCount: number;
} }
export class Navbar extends Component<any, NavbarState> { export class Navbar extends Component<any, NavbarState> {
emptyState: NavbarState = { emptyState: NavbarState = {
isLoggedIn: UserService.Instance.user !== undefined, isLoggedIn: (UserService.Instance.user !== undefined),
unreadCount: 0,
expanded: false, expanded: false,
expandUserDropdown: false expandUserDropdown: false
} }
@ -24,8 +26,9 @@ export class Navbar extends Component<any, NavbarState> {
// Subscribe to user changes // Subscribe to user changes
UserService.Instance.sub.subscribe(user => { UserService.Instance.sub.subscribe(user => {
let loggedIn: boolean = user !== undefined; this.state.isLoggedIn = user.user !== undefined;
this.setState({isLoggedIn: loggedIn}); this.state.unreadCount = user.unreadCount;
this.setState(this.state);
}); });
} }
@ -64,6 +67,14 @@ export class Navbar extends Component<any, NavbarState> {
</ul> </ul>
<ul class="navbar-nav ml-auto mr-2"> <ul class="navbar-nav ml-auto mr-2">
{this.state.isLoggedIn ? {this.state.isLoggedIn ?
<>
{
<li className="nav-item">
<Link class="nav-link" to="/inbox">🖂
{this.state.unreadCount> 0 && <span class="badge badge-light">{this.state.unreadCount}</span>}
</Link>
</li>
}
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}> <li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button"> <a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
{UserService.Instance.user.username} {UserService.Instance.user.username}
@ -72,7 +83,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.handleOverviewClick)}>Overview</a>
<a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a> <a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a>
</div> </div>
</li> : </li>
</>
:
<Link class="nav-link" to="/login">Login / Sign up</Link> <Link class="nav-link" to="/login">Login / Sign up</Link>
} }
</ul> </ul>
@ -89,6 +102,7 @@ export class Navbar extends Component<any, NavbarState> {
handleLogoutClick(i: Navbar) { handleLogoutClick(i: Navbar) {
i.state.expandUserDropdown = false; i.state.expandUserDropdown = false;
UserService.Instance.logout(); UserService.Instance.logout();
i.context.router.history.push('/');
} }
handleOverviewClick(i: Navbar) { handleOverviewClick(i: Navbar) {

View file

@ -1,10 +1,10 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { Post, CreatePostLikeForm, PostForm as PostFormI } from '../interfaces'; import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, CommunityUser, UserView } from '../interfaces';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { mdToHtml } from '../utils'; import { mdToHtml, canMod, isMod } from '../utils';
interface PostListingState { interface PostListingState {
showEdit: boolean; showEdit: boolean;
@ -19,6 +19,8 @@ interface PostListingProps {
showCommunity?: boolean; showCommunity?: boolean;
showBody?: boolean; showBody?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
moderators?: Array<CommunityUser>;
admins?: Array<UserView>;
} }
export class PostListing extends Component<PostListingProps, PostListingState> { export class PostListing extends Component<PostListingProps, PostListingState> {
@ -60,17 +62,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div>{post.score}</div> <div>{post.score}</div>
<div className={`pointer downvote ${post.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(this, this.handlePostDisLike)}></div> <div className={`pointer downvote ${post.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(this, this.handlePostDisLike)}></div>
</div> </div>
<div className="ml-4"> <div className="pt-1 ml-4">
{post.url {post.url
? <div className="mb-0"> ? <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 && {post.removed &&
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic">removed</small>
} }
{post.locked && {post.locked &&
<small className="ml-2 text-muted font-italic">locked</small> <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> <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 { !this.state.iframeExpanded
? <span class="badge badge-light pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span> ? <span class="badge badge-light pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
@ -83,14 +85,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</span> </span>
} }
</div> </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 && {post.removed &&
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic">removed</small>
} }
{post.locked && {post.locked &&
<small className="ml-2 text-muted font-italic">locked</small> <small className="ml-2 text-muted font-italic">locked</small>
} }
</h4> </h5>
} }
</div> </div>
<div className="details ml-4 mb-1"> <div className="details ml-4 mb-1">
@ -98,6 +100,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<li className="list-inline-item"> <li className="list-inline-item">
<span>by </span> <span>by </span>
<Link className="text-info" to={`/user/${post.creator_id}`}>{post.creator_name}</Link> <Link className="text-info" to={`/user/${post.creator_id}`}>{post.creator_name}</Link>
{this.isMod &&
<span className="mx-1 badge badge-secondary">mod</span>
}
{this.isAdmin &&
<span className="mx-1 badge badge-secondary">admin</span>
}
{this.props.showCommunity && {this.props.showCommunity &&
<span> <span>
<span> to </span> <span> to </span>
@ -120,19 +128,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link> <Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link>
</li> </li>
</ul> </ul>
{this.props.editable && {UserService.Instance.user && this.props.editable &&
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <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 && {this.myPost &&
<span> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span> <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
</li> </li>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
</li> </li>
</span> </>
} }
{this.props.post.am_mod && {this.canMod &&
<span> <span>
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.post.removed ? {!this.props.post.removed ?
@ -163,6 +174,29 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id; return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id;
} }
get canMod(): boolean {
if (this.props.editable) {
let adminsThenMods = this.props.admins.map(a => a.id)
.concat(this.props.moderators.map(m => m.user_id));
return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id);
} else return false;
}
get isMod(): boolean {
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id);
}
get isAdmin(): boolean {
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id);
}
get canAdmin(): boolean {
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.post.creator_id);
}
handlePostLike(i: PostListing) { handlePostLike(i: PostListing) {
let form: CreatePostLikeForm = { let form: CreatePostLikeForm = {
@ -209,6 +243,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
WebSocketService.Instance.editPost(deleteForm); 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) { handleModRemoveShow(i: PostListing) {
i.state.showRemoveDialog = true; i.state.showRemoveDialog = true;
i.setState(i.state); i.setState(i.state);

View file

@ -61,7 +61,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
return ( return (
<div> <div>
{this.state.loading ? {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> <div>
{this.selects()} {this.selects()}
{this.state.posts.length > 0 {this.state.posts.length > 0

View file

@ -1,7 +1,7 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; 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 { WebSocketService } from '../services';
import { msgOp, hotRank } from '../utils'; import { msgOp, hotRank } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
@ -10,13 +10,13 @@ import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
interface PostState { interface PostState {
post: PostI; post: PostI;
comments: Array<Comment>; comments: Array<Comment>;
commentSort: CommentSortType; commentSort: CommentSortType;
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
scrolled?: boolean; scrolled?: boolean;
scrolled_comment_id?: number; scrolled_comment_id?: number;
loading: boolean; loading: boolean;
@ -31,6 +31,7 @@ export class Post extends Component<any, PostState> {
commentSort: CommentSortType.Hot, commentSort: CommentSortType.Hot,
community: null, community: null,
moderators: [], moderators: [],
admins: [],
scrolled: false, scrolled: false,
loading: true loading: true
} }
@ -77,10 +78,17 @@ export class Post extends Component<any, PostState> {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {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="row">
<div class="col-12 col-md-8 col-lg-7 mb-3"> <div class="col-12 col-md-8 col-lg-7 mb-3">
<PostListing post={this.state.post} showBody showCommunity editable /> <PostListing
post={this.state.post}
showBody
showCommunity
editable
moderators={this.state.moderators}
admins={this.state.admins}
/>
<div className="mb-2" /> <div className="mb-2" />
<CommentForm postId={this.state.post.id} disabled={this.state.post.locked} /> <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
{this.sortRadios()} {this.sortRadios()}
@ -123,9 +131,15 @@ export class Post extends Component<any, PostState> {
newComments() { newComments() {
return ( return (
<div class="sticky-top"> <div class="sticky-top">
<h4>New Comments</h4> <h5>New Comments</h5>
{this.state.comments.map(comment => {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> </div>
) )
@ -187,8 +201,13 @@ export class Post extends Component<any, PostState> {
commentsTree() { commentsTree() {
let nodes = this.buildCommentsTree(); let nodes = this.buildCommentsTree();
return ( return (
<div className=""> <div>
<CommentNodes nodes={nodes} locked={this.state.post.locked} moderators={this.state.moderators} /> <CommentNodes
nodes={nodes}
locked={this.state.post.locked}
moderators={this.state.moderators}
admins={this.state.admins}
/>
</div> </div>
); );
} }
@ -202,9 +221,11 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.GetPost) { } else if (op == UserOperation.GetPost) {
let res: GetPostResponse = msg; let res: GetPostResponse = msg;
this.state.post = res.post; this.state.post = res.post;
this.state.post = res.post;
this.state.comments = res.comments; this.state.comments = res.comments;
this.state.community = res.community; this.state.community = res.community;
this.state.moderators = res.moderators; this.state.moderators = res.moderators;
this.state.admins = res.admins;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
@ -222,8 +243,12 @@ export class Post extends Component<any, PostState> {
found.score = res.comment.score; found.score = res.comment.score;
this.setState(this.state); this.setState(this.state);
} } else if (op == UserOperation.SaveComment) {
else if (op == UserOperation.CreateCommentLike) { 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 res: CommentResponse = msg;
let found: Comment = this.state.comments.find(c => c.id === res.comment.id); let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
found.score = res.comment.score; found.score = res.comment.score;
@ -243,6 +268,10 @@ export class Post extends Component<any, PostState> {
let res: PostResponse = msg; let res: PostResponse = msg;
this.state.post = res.post; this.state.post = res.post;
this.setState(this.state); 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) { } else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg; let res: CommunityResponse = msg;
this.state.community = res.community; this.state.community = res.community;
@ -257,12 +286,21 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.BanFromCommunity) { } else if (op == UserOperation.BanFromCommunity) {
let res: BanFromCommunityResponse = msg; let res: BanFromCommunityResponse = msg;
this.state.comments.filter(c => c.creator_id == res.user.id) 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); this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) { } else if (op == UserOperation.AddModToCommunity) {
let res: AddModToCommunityResponse = msg; let res: AddModToCommunityResponse = msg;
this.state.moderators = res.moderators; this.state.moderators = res.moderators;
this.setState(this.state); 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() { registerUser() {
return ( return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h4>Set up Site Administrator</h4> <h5>Set up Site Administrator</h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Username</label> <label class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10"> <div class="col-sm-10">

View file

@ -48,11 +48,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
let community = this.props.community; let community = this.props.community;
return ( return (
<div> <div>
<h4 className="mb-0">{community.title} <h5 className="mb-0">{community.title}
{community.removed && {community.removed &&
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic">removed</small>
} }
</h4> </h5>
<Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link> <Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
{community.am_mod && {community.am_mod &&
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <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() { render() {
return ( return (
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}> <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"> <div class="form-group row">
<label class="col-12 col-form-label">Name</label> <label class="col-12 col-form-label">Name</label>
<div class="col-12"> <div class="col-12">

View file

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

View file

@ -82,3 +82,10 @@ blockquote {
margin: 0.5em 5px; margin: 0.5em 5px;
padding: 0.1em 5px; padding: 0.1em 5px;
} }
.badge-notify{
/* background:red; */
position:relative;
top: -20px;
left: -35px;
}

View file

@ -7,9 +7,7 @@
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" /> <link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
<title>Lemmy</title> <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 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> <script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
</head> </head>

View file

@ -13,9 +13,11 @@ import { Communities } from './components/communities';
import { User } from './components/user'; import { User } from './components/user';
import { Modlog } from './components/modlog'; import { Modlog } from './components/modlog';
import { Setup } from './components/setup'; import { Setup } from './components/setup';
import { Inbox } from './components/inbox';
import { Symbols } from './components/symbols'; import { Symbols } from './components/symbols';
import './main.css'; import './css/bootstrap.min.css';
import './css/main.css';
import { WebSocketService, UserService } from './services'; import { WebSocketService, UserService } from './services';
@ -45,6 +47,7 @@ class Index extends Component<any, any> {
<Route path={`/community/:id`} component={Community} /> <Route path={`/community/:id`} component={Community} />
<Route path={`/user/:id/:heading`} component={User} /> <Route path={`/user/:id/:heading`} component={User} />
<Route path={`/user/:id`} component={User} /> <Route path={`/user/:id`} component={User} />
<Route path={`/inbox`} component={Inbox} />
<Route path={`/modlog/community/:community_id`} component={Modlog} /> <Route path={`/modlog/community/:community_id`} component={Modlog} />
<Route path={`/modlog`} component={Modlog} /> <Route path={`/modlog`} component={Modlog} />
<Route path={`/setup`} component={Setup} /> <Route path={`/setup`} component={Setup} />

View file

@ -1,5 +1,5 @@
export enum UserOperation { 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, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
} }
export enum CommentSortType { export enum CommentSortType {
@ -41,65 +41,69 @@ export interface CommunityUser {
} }
export interface Community { export interface Community {
user_id?: number;
subscribed?: boolean;
am_mod?: boolean;
removed?: boolean;
id: number; id: number;
name: string; name: string;
title: string; title: string;
description?: string; description?: string;
creator_id: number;
creator_name: string;
category_id: number; category_id: number;
creator_id: number;
removed: boolean;
published: string;
updated?: string;
creator_name: string;
category_name: string; category_name: string;
number_of_subscribers: number; number_of_subscribers: number;
number_of_posts: number; number_of_posts: number;
number_of_comments: number; number_of_comments: number;
published: string; user_id?: number;
updated?: string; subscribed?: boolean;
} }
export interface Post { export interface Post {
user_id?: number;
my_vote?: number;
am_mod?: boolean;
removed?: boolean;
locked?: boolean;
id: number; id: number;
name: string; name: string;
url?: string; url?: string;
body?: string; body?: string;
creator_id: number; creator_id: number;
creator_name: string;
community_id: number; community_id: number;
removed: boolean;
locked: boolean;
published: string;
updated?: string;
creator_name: string;
community_name: string; community_name: string;
number_of_comments: number; number_of_comments: number;
score: number; score: number;
upvotes: number; upvotes: number;
downvotes: number; downvotes: number;
hot_rank: number; hot_rank: number;
published: string; user_id?: number;
updated?: string; my_vote?: number;
subscribed?: boolean;
read?: boolean;
saved?: boolean;
} }
export interface Comment { export interface Comment {
id: number; id: number;
content: string;
creator_id: number; creator_id: number;
creator_name: string;
post_id: number, post_id: number,
community_id: number,
parent_id?: number; parent_id?: number;
content: string;
removed: boolean;
read: boolean;
published: string; published: string;
updated?: string; updated?: string;
community_id: number,
banned: boolean;
banned_from_community: boolean;
creator_name: string;
score: number; score: number;
upvotes: number; upvotes: number;
downvotes: number; downvotes: number;
user_id?: number;
my_vote?: number; my_vote?: number;
am_mod?: boolean; saved?: boolean;
removed?: boolean;
banned?: boolean;
} }
export interface Category { export interface Category {
@ -137,7 +141,7 @@ export interface GetUserDetailsForm {
page?: number; page?: number;
limit?: number; limit?: number;
community_id?: number; community_id?: number;
auth?: string; saved_only: boolean;
} }
export interface UserDetailsResponse { export interface UserDetailsResponse {
@ -147,7 +151,19 @@ export interface UserDetailsResponse {
moderates: Array<CommunityUser>; moderates: Array<CommunityUser>;
comments: Array<Comment>; comments: Array<Comment>;
posts: Array<Post>; posts: Array<Post>;
saved?: Array<Post>; }
export interface GetRepliesForm {
sort: string; // TODO figure this one out
page?: number;
limit?: number;
unread_only: boolean;
auth?: string;
}
export interface GetRepliesResponse {
op: string;
replies: Array<Comment>;
} }
export interface BanFromCommunityForm { export interface BanFromCommunityForm {
@ -324,7 +340,7 @@ export interface CommunityForm {
description?: string, description?: string,
category_id: number, category_id: number,
edit_id?: number; edit_id?: number;
removed?: boolean; removed: boolean;
reason?: string; reason?: string;
expires?: number; expires?: number;
auth?: string; auth?: string;
@ -368,8 +384,8 @@ export interface PostForm {
edit_id?: number; edit_id?: number;
creator_id: number; creator_id: number;
removed?: boolean; removed?: boolean;
reason?: string;
locked?: boolean; locked?: boolean;
reason?: string;
auth: string; auth: string;
} }
@ -379,6 +395,13 @@ export interface GetPostResponse {
comments: Array<Comment>; comments: Array<Comment>;
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
}
export interface SavePostForm {
post_id: number;
save: boolean;
auth?: string;
} }
export interface PostResponse { export interface PostResponse {
@ -394,9 +417,16 @@ export interface CommentForm {
creator_id: number; creator_id: number;
removed?: boolean; removed?: boolean;
reason?: string; reason?: string;
read?: boolean;
auth: string; auth: string;
} }
export interface SaveCommentForm {
comment_id: number;
save: boolean;
auth?: string;
}
export interface CommentResponse { export interface CommentResponse {
op: string; op: string;
comment: Comment; 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

@ -4,9 +4,10 @@ import * as jwt_decode from 'jwt-decode';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
export class UserService { export class UserService {
private static _instance: UserService; private static _instance: UserService;
public user: User; public user: User;
public sub: Subject<User> = new Subject<User>(); public sub: Subject<{user: User, unreadCount: number}> = new Subject<{user: User, unreadCount: number}>();
private constructor() { private constructor() {
let jwt = Cookies.get("jwt"); let jwt = Cookies.get("jwt");
@ -28,7 +29,7 @@ export class UserService {
this.user = undefined; this.user = undefined;
Cookies.remove("jwt"); Cookies.remove("jwt");
console.log("Logged out."); console.log("Logged out.");
this.sub.next(undefined); this.sub.next({user: undefined, unreadCount: 0});
} }
public get auth(): string { public get auth(): string {
@ -37,7 +38,7 @@ export class UserService {
private setUser(jwt: string) { private setUser(jwt: string) {
this.user = jwt_decode(jwt); this.user = jwt_decode(jwt);
this.sub.next(this.user); this.sub.next({user: this.user, unreadCount: 0});
console.log(this.user); console.log(this.user);
} }

View file

@ -1,5 +1,5 @@
import { wsUri } from '../env'; 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, GetRepliesForm } from '../interfaces';
import { webSocket } from 'rxjs/webSocket'; import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
@ -96,6 +96,11 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form)); 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) { public getPosts(form: GetPostsForm) {
this.setAuth(form, false); this.setAuth(form, false);
this.subject.next(this.wsSendWrapper(UserOperation.GetPosts, form)); this.subject.next(this.wsSendWrapper(UserOperation.GetPosts, form));
@ -111,6 +116,11 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm)); 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) { public banFromCommunity(form: BanFromCommunityForm) {
this.setAuth(form); this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.BanFromCommunity, form)); this.subject.next(this.wsSendWrapper(UserOperation.BanFromCommunity, form));
@ -121,11 +131,25 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, 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) { public getUserDetails(form: GetUserDetailsForm) {
this.setAuth(form, false);
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form)); this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
} }
public getReplies(form: GetRepliesForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.GetReplies, form));
}
public getModlog(form: GetModlogForm) { public getModlog(form: GetModlogForm) {
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form)); this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, 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'; import * as markdown_it from 'markdown-it';
export let repoUrl = 'https://github.com/dessalines/lemmy'; 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}}); 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; export let fetchLimit: number = 20;