Merge branch 'moderation'

- Fixes #63
- Fixes #62
- Fixes #61
- Fixes #60
- Fixes #59
- Fixes #57
- Fixes #56
- Fixes #50
- Fixes #49
- Some of #48
- Fixes #45
- Some of #35
- Fixes #29
- Fixes #14
This commit is contained in:
Dessalines 2019-04-16 16:09:08 -07:00
commit e6542b55d2
66 changed files with 4189 additions and 465 deletions

View file

@ -1,12 +1,39 @@
FROM node:10-jessie as node FROM node:10-jessie as node
#If encounter Invalid cross-device error -run on host 'echo N | sudo tee /sys/module/overlay/parameters/metacopy' #If encounter Invalid cross-device error -run on host 'echo N | sudo tee /sys/module/overlay/parameters/metacopy'
COPY ui /app/ui COPY ui /app/ui
RUN cd /app/ui && yarn && yarn build WORKDIR /app/ui
RUN yarn
RUN yarn build
FROM rust:1.33 as rust FROM rust:1.33 as rust
COPY server /app/server
# create a new empty shell project
WORKDIR /app
RUN USER=root cargo new server
WORKDIR /app/server
# copy over your manifests
COPY server/Cargo.toml server/Cargo.lock ./
# this build step will cache your dependencies
RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN cargo build --release --bin lemmy
RUN ls ./target/release/.fingerprint/
RUN rm -r ./target/release/.fingerprint/server-*
# copy your source tree
# RUN rm -rf ./src/
COPY server/src ./src/
COPY server/migrations ./migrations/
# build for release
RUN cargo build --frozen --release --bin lemmy
RUN mv /app/server/target/release/lemmy /app/lemmy
# The output image
# FROM debian:stable-slim
# RUN apt-get -y update && apt-get install -y postgresql-client
# COPY --from=rust /app/server/target/release/lemmy /app/lemmy
COPY --from=node /app/ui/dist /app/dist COPY --from=node /app/ui/dist /app/dist
RUN cd /app/server && cargo build --release
RUN mv /app/server/target/release/lemmy /app/
WORKDIR /app/
EXPOSE 8536 EXPOSE 8536

View file

@ -19,12 +19,11 @@ Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Infern
## Features ## Features
- TBD - TBD
-
the name
Lead singer from motorhead. ## Why's it called Lemmy?
The old school video game. - Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
The furry rodents. - The old school [video game](https://en.wikipedia.org/wiki/Lemmings_(video_game)).
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
Goals r/ censorship Goals r/ censorship

View file

@ -10,9 +10,9 @@ services:
POSTGRES_DB: rrr POSTGRES_DB: rrr
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U rrr"] test: ["CMD-SHELL", "pg_isready -U rrr"]
interval: 30s interval: 5s
timeout: 30s timeout: 5s
retries: 3 retries: 20
lemmy: lemmy:
build: build:
context: . context: .
@ -22,6 +22,7 @@ services:
environment: environment:
LEMMY_FRONT_END_DIR: /app/dist LEMMY_FRONT_END_DIR: /app/dist
DATABASE_URL: postgres://rrr:rrr@db:5432/rrr DATABASE_URL: postgres://rrr:rrr@db:5432/rrr
restart: always
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy

1
server/Cargo.lock generated
View file

@ -1359,6 +1359,7 @@ dependencies = [
"env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonwebtoken 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "jsonwebtoken 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -24,4 +24,5 @@ rand = "0.6.5"
strum = "0.14.0" strum = "0.14.0"
strum_macros = "0.14.0" strum_macros = "0.14.0"
jsonwebtoken = "*" jsonwebtoken = "*"
regex = "1" regex = "*"
lazy_static = "*"

View file

@ -1 +1,2 @@
drop table user_ drop table user_ban;
drop table user_;

View file

@ -6,9 +6,18 @@ create table user_ (
password_encrypted text not null, password_encrypted text not null,
email text unique, email text unique,
icon bytea, icon bytea,
admin boolean default false not null,
banned boolean default false not null,
published timestamp not null default now(), published timestamp not null default now(),
updated timestamp, updated timestamp,
unique(name, fedi_name) unique(name, fedi_name)
); );
create table user_ban (
id serial primary key,
user_id int references user_ on update cascade on delete cascade not null,
published timestamp not null default now(),
unique (user_id)
);
insert into user_ (name, fedi_name, password_encrypted) values ('admin', 'TBD', 'TBD'); insert into user_ (name, fedi_name, password_encrypted) values ('admin', 'TBD', 'TBD');

View file

@ -1,3 +1,5 @@
drop table site;
drop table community_user_ban;;
drop table community_moderator; drop table community_moderator;
drop table community_follower; drop table community_follower;
drop table community; drop table community;

View file

@ -38,6 +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,
published timestamp not null default now(), published timestamp not null default now(),
updated timestamp updated timestamp
); );
@ -46,14 +47,33 @@ create table community_moderator (
id serial primary key, id serial primary key,
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,
user_id int references user_ 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() published timestamp not null default now(),
unique (community_id, user_id)
); );
create table community_follower ( create table community_follower (
id serial primary key, id serial primary key,
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,
user_id int references user_ 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() published timestamp not null default now(),
unique (community_id, user_id)
);
create table community_user_ban (
id serial primary key,
community_id int references community 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 (community_id, user_id)
); );
insert into community (name, title, category_id, creator_id) values ('main', 'The Default Community', 1, 1); insert into community (name, title, category_id, creator_id) values ('main', 'The Default Community', 1, 1);
create table site (
id serial primary key,
name varchar(20) not null unique,
description text,
creator_id int references user_ on update cascade on delete cascade not null,
published timestamp not null default now(),
updated timestamp
);

View file

@ -5,6 +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,
locked boolean default false,
published timestamp not null default now(), published timestamp not null default now(),
updated timestamp updated timestamp
); );

View file

@ -4,6 +4,7 @@ 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,
published timestamp not null default now(), published timestamp not null default now(),
updated timestamp updated timestamp
); );

View file

@ -30,7 +30,8 @@ select
ap.*, 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
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
@ -41,33 +42,7 @@ select
ap.*, 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
from all_post ap from all_post ap
; ;
/* The old post view */
/* create view post_view as */
/* select */
/* u.id as user_id, */
/* pl.score as my_vote, */
/* p.id as id, */
/* p.name as name, */
/* p.url, */
/* p.body, */
/* p.creator_id, */
/* (select name from user_ where p.creator_id = user_.id) creator_name, */
/* p.community_id, */
/* (select name from community where p.community_id = community.id) as community_name, */
/* (select count(*) from comment where comment.post_id = p.id) as number_of_comments, */
/* coalesce(sum(pl.score) over (partition by p.id), 0) as score, */
/* count (case when pl.score = 1 then 1 else null end) over (partition by p.id) as upvotes, */
/* count (case when pl.score = -1 then 1 else null end) over (partition by p.id) as downvotes, */
/* hot_rank(coalesce(sum(pl.score) over (partition by p.id) , 0), p.published) as hot_rank, */
/* p.published, */
/* p.updated */
/* from user_ u */
/* cross join post p */
/* left join post_like pl on u.id = pl.user_id and p.id = pl.post_id; */

View file

@ -1,3 +1,5 @@
drop view community_view; drop view community_view;
drop view community_moderator_view; drop view community_moderator_view;
drop view community_follower_view; drop view community_follower_view;
drop view community_user_ban_view;
drop view site_view;

View file

@ -13,7 +13,8 @@ with all_community as
select select
ac.*, ac.*,
u.id as user_id, u.id as user_id,
cf.id::boolean as subscribed cf.id::boolean as subscribed,
u.admin or (select cm.id::bool from community_moderator cm where u.id = cm.user_id and cm.community_id = ac.id) as am_mod
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 left join community_follower cf on u.id = cf.user_id and ac.id = cf.community_id
@ -23,7 +24,8 @@ 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
; ;
@ -38,3 +40,17 @@ select *,
(select name from user_ u where cf.user_id = u.id) as user_name, (select name from user_ u where cf.user_id = u.id) as user_name,
(select name from community c where cf.community_id = c.id) as community_name (select name from community c where cf.community_id = c.id) as community_name
from community_follower cf; from community_follower cf;
create view community_user_ban_view as
select *,
(select name from user_ u where cm.user_id = u.id) as user_name,
(select name from community c where cm.community_id = c.id) as community_name
from community_user_ban cm;
create view site_view as
select *,
(select name from user_ u where s.creator_id = u.id) as creator_name,
(select count(*) from user_) as number_of_users,
(select count(*) from post) as number_of_posts,
(select count(*) from comment) as number_of_comments
from site s;

View file

@ -3,7 +3,9 @@ with all_comment as
( (
select select
c.*, c.*,
(select name from user_ where c.creator_id = user_.id) creator_name, (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 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,
count (case when cl.score = -1 then 1 else null end) as downvotes count (case when cl.score = -1 then 1 else null end) as downvotes
@ -15,7 +17,8 @@ with all_comment as
select 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
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
@ -25,6 +28,7 @@ union all
select select
ac.*, ac.*,
null as user_id, null as user_id,
null as my_vote null as my_vote,
null as am_mod
from all_comment ac from all_comment ac
; ;

View file

@ -0,0 +1,8 @@
drop table mod_remove_post;
drop table mod_lock_post;
drop table mod_remove_comment;
drop table mod_remove_community;
drop table mod_ban;
drop table mod_ban_from_community;
drop table mod_add;
drop table mod_add_community;

View file

@ -0,0 +1,76 @@
create table mod_remove_post (
id serial primary key,
mod_user_id int references user_ on update cascade on delete cascade not null,
post_id int references post on update cascade on delete cascade not null,
reason text,
removed boolean default true,
when_ timestamp not null default now()
);
create table mod_lock_post (
id serial primary key,
mod_user_id int references user_ on update cascade on delete cascade not null,
post_id int references post on update cascade on delete cascade not null,
locked boolean default true,
when_ timestamp not null default now()
);
create table mod_remove_comment (
id serial primary key,
mod_user_id int references user_ on update cascade on delete cascade not null,
comment_id int references comment on update cascade on delete cascade not null,
reason text,
removed boolean default true,
when_ timestamp not null default now()
);
create table mod_remove_community (
id serial primary key,
mod_user_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,
reason text,
removed boolean default true,
expires timestamp,
when_ timestamp not null default now()
);
-- TODO make sure you can't ban other mods
create table mod_ban_from_community (
id serial primary key,
mod_user_id int references user_ on update cascade on delete cascade not null,
other_user_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,
reason text,
banned boolean default true,
expires timestamp,
when_ timestamp not null default now()
);
create table mod_ban (
id serial primary key,
mod_user_id int references user_ on update cascade on delete cascade not null,
other_user_id int references user_ on update cascade on delete cascade not null,
reason text,
banned boolean default true,
expires timestamp,
when_ timestamp not null default now()
);
create table mod_add_community (
id serial primary key,
mod_user_id int references user_ on update cascade on delete cascade not null,
other_user_id int references user_ on update cascade on delete cascade not null,
community_id int references community on update cascade on delete cascade not null,
removed boolean default false,
when_ timestamp not null default now()
);
-- When removed is false that means kicked
create table mod_add (
id serial primary key,
mod_user_id int references user_ on update cascade on delete cascade not null,
other_user_id int references user_ on update cascade on delete cascade not null,
removed boolean default false,
when_ timestamp not null default now()
);

View file

@ -2,10 +2,11 @@ create view user_view as
select id, select id,
name, name,
fedi_name, fedi_name,
admin,
banned,
published, published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts, (select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score, (select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments, (select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score (select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u; from user_ u;

View file

@ -0,0 +1,8 @@
drop view mod_remove_post_view;
drop view mod_lock_post_view;
drop view mod_remove_comment_view;
drop view mod_remove_community_view;
drop view mod_ban_from_community_view;
drop view mod_ban_view;
drop view mod_add_community_view;
drop view mod_add_view;

View file

@ -0,0 +1,61 @@
create view mod_remove_post_view as
select mrp.*,
(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name,
(select name from post p where mrp.post_id = p.id) as post_name,
(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name
from mod_remove_post mrp;
create view mod_lock_post_view as
select mlp.*,
(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name,
(select name from post p where mlp.post_id = p.id) as post_name,
(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name
from mod_lock_post mlp;
create view mod_remove_comment_view as
select mrc.*,
(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id,
(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name,
(select content from comment c where mrc.comment_id = c.id) as comment_content,
(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id,
(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name,
(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id,
(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name
from mod_remove_comment mrc;
create view mod_remove_community_view as
select mrc.*,
(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
(select c.name from community c where mrc.community_id = c.id) as community_name
from mod_remove_community mrc;
create view mod_ban_from_community_view as
select mb.*,
(select name from user_ u where mb.mod_user_id = u.id) as mod_user_name,
(select name from user_ u where mb.other_user_id = u.id) as other_user_name,
(select name from community c where mb.community_id = c.id) as community_name
from mod_ban_from_community mb;
create view mod_ban_view as
select mb.*,
(select name from user_ u where mb.mod_user_id = u.id) as mod_user_name,
(select name from user_ u where mb.other_user_id = u.id) as other_user_name
from mod_ban_from_community mb;
create view mod_add_community_view as
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.other_user_id = u.id) as other_user_name,
(select name from community c where ma.community_id = c.id) as community_name
from mod_add_community ma;
create view mod_add_view as
select ma.*,
(select name from user_ u where ma.mod_user_id = u.id) as mod_user_name,
(select name from user_ u where ma.other_user_id = u.id) as other_user_name
from mod_add ma;

View file

@ -22,6 +22,7 @@ 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 published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -33,6 +34,7 @@ pub struct CommentForm {
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 updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -135,6 +137,8 @@ mod tests {
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: false,
banned: false,
updated: None updated: None
}; };
@ -146,6 +150,7 @@ mod tests {
description: None, description: None,
category_id: 1, category_id: 1,
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None,
updated: None updated: None
}; };
@ -157,6 +162,8 @@ mod tests {
url: None, url: None,
body: None, body: None,
community_id: inserted_community.id, community_id: inserted_community.id,
removed: None,
locked: None,
updated: None updated: None
}; };
@ -166,6 +173,7 @@ 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: None,
parent_id: None, parent_id: None,
updated: None updated: None
}; };
@ -177,6 +185,7 @@ 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),
parent_id: None, parent_id: None,
published: inserted_comment.published, published: inserted_comment.published,
updated: None updated: None
@ -187,6 +196,7 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id), parent_id: Some(inserted_comment.id),
removed: None,
updated: None updated: None
}; };

View file

@ -13,14 +13,18 @@ table! {
post_id -> Int4, post_id -> Int4,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Nullable<Bool>,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
community_id -> Int4,
banned -> Nullable<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>,
} }
} }
@ -32,14 +36,18 @@ 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 published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub community_id: i32,
pub banned: Option<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>,
} }
impl CommentView { impl CommentView {
@ -130,6 +138,8 @@ mod tests {
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: false,
banned: false,
updated: None updated: None
}; };
@ -141,6 +151,7 @@ mod tests {
description: None, description: None,
category_id: 1, category_id: 1,
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None,
updated: None updated: None
}; };
@ -152,6 +163,8 @@ mod tests {
url: None, url: None,
body: None, body: None,
community_id: inserted_community.id, community_id: inserted_community.id,
removed: None,
locked: None,
updated: None updated: None
}; };
@ -162,6 +175,7 @@ mod tests {
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: None, parent_id: None,
removed: None,
updated: None updated: None
}; };
@ -181,7 +195,10 @@ mod tests {
content: "A test comment 32".into(), content: "A test comment 32".into(),
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
community_id: inserted_community.id,
parent_id: None, parent_id: None,
removed: Some(false),
banned: None,
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(),
@ -189,7 +206,8 @@ mod tests {
downvotes: 0, downvotes: 0,
upvotes: 1, upvotes: 1,
user_id: None, user_id: None,
my_vote: None my_vote: None,
am_mod: None,
}; };
let expected_comment_view_with_user = CommentView { let expected_comment_view_with_user = CommentView {
@ -197,7 +215,10 @@ mod tests {
content: "A test comment 32".into(), content: "A test comment 32".into(),
creator_id: inserted_user.id, creator_id: inserted_user.id,
post_id: inserted_post.id, post_id: inserted_post.id,
community_id: inserted_community.id,
parent_id: None, parent_id: None,
removed: Some(false),
banned: None,
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(),
@ -206,6 +227,7 @@ 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,
}; };
let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, 999).unwrap(); let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, 999).unwrap();

View file

@ -1,9 +1,9 @@
extern crate diesel; extern crate diesel;
use schema::{community, community_moderator, community_follower}; use schema::{community, community_moderator, community_follower, community_user_ban, site};
use diesel::*; use diesel::*;
use diesel::result::Error; use diesel::result::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use {Crud, Followable, Joinable}; use {Crud, Followable, Joinable, Bannable};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="community"] #[table_name="community"]
@ -14,6 +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 published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -26,44 +27,10 @@ pub struct CommunityForm {
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 updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_moderator"]
pub struct CommunityModerator {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_moderator"]
pub struct CommunityModeratorForm {
pub community_id: i32,
pub user_id: i32,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_follower"]
pub struct CommunityFollower {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_follower"]
pub struct CommunityFollowerForm {
pub community_id: i32,
pub user_id: i32,
}
impl Crud<CommunityForm> for Community { impl Crud<CommunityForm> for Community {
fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> { fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
use schema::community::dsl::*; use schema::community::dsl::*;
@ -92,20 +59,21 @@ impl Crud<CommunityForm> for Community {
} }
} }
impl Followable<CommunityFollowerForm> for CommunityFollower { #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> { #[belongs_to(Community)]
use schema::community_follower::dsl::*; #[table_name = "community_moderator"]
insert_into(community_follower) pub struct CommunityModerator {
.values(community_follower_form) pub id: i32,
.get_result::<Self>(conn) pub community_id: i32,
} pub user_id: i32,
fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<usize, Error> { pub published: chrono::NaiveDateTime,
use schema::community_follower::dsl::*;
diesel::delete(community_follower
.filter(community_id.eq(&community_follower_form.community_id))
.filter(user_id.eq(&community_follower_form.user_id)))
.execute(conn)
} }
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_moderator"]
pub struct CommunityModeratorForm {
pub community_id: i32,
pub user_id: i32,
} }
impl Joinable<CommunityModeratorForm> for CommunityModerator { impl Joinable<CommunityModeratorForm> for CommunityModerator {
@ -125,6 +93,120 @@ impl Joinable<CommunityModeratorForm> for CommunityModerator {
} }
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_user_ban"]
pub struct CommunityUserBan {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_user_ban"]
pub struct CommunityUserBanForm {
pub community_id: i32,
pub user_id: i32,
}
impl Bannable<CommunityUserBanForm> for CommunityUserBan {
fn ban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<Self, Error> {
use schema::community_user_ban::dsl::*;
insert_into(community_user_ban)
.values(community_user_ban_form)
.get_result::<Self>(conn)
}
fn unban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<usize, Error> {
use schema::community_user_ban::dsl::*;
diesel::delete(community_user_ban
.filter(community_id.eq(community_user_ban_form.community_id))
.filter(user_id.eq(community_user_ban_form.user_id)))
.execute(conn)
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_follower"]
pub struct CommunityFollower {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_follower"]
pub struct CommunityFollowerForm {
pub community_id: i32,
pub user_id: i32,
}
impl Followable<CommunityFollowerForm> for CommunityFollower {
fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> {
use schema::community_follower::dsl::*;
insert_into(community_follower)
.values(community_follower_form)
.get_result::<Self>(conn)
}
fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<usize, Error> {
use schema::community_follower::dsl::*;
diesel::delete(community_follower
.filter(community_id.eq(&community_follower_form.community_id))
.filter(user_id.eq(&community_follower_form.user_id)))
.execute(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="site"]
pub struct Site {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="site"]
pub struct SiteForm {
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub updated: Option<chrono::NaiveDateTime>
}
impl Crud<SiteForm> for Site {
fn read(conn: &PgConnection, _site_id: i32) -> Result<Self, Error> {
use schema::site::dsl::*;
site.first::<Self>(conn)
}
fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
use schema::site::dsl::*;
diesel::delete(site.find(site_id))
.execute(conn)
}
fn create(conn: &PgConnection, new_site: &SiteForm) -> Result<Self, Error> {
use schema::site::dsl::*;
insert_into(site)
.values(new_site)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, site_id: i32, new_site: &SiteForm) -> Result<Self, Error> {
use schema::site::dsl::*;
diesel::update(site.find(site_id))
.set(new_site)
.get_result::<Self>(conn)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use establish_connection; use establish_connection;
@ -136,11 +218,13 @@ mod tests {
let conn = establish_connection(); let conn = establish_connection();
let new_user = UserForm { let new_user = UserForm {
name: "bob".into(), name: "bobbee".into(),
fedi_name: "rrf".into(), fedi_name: "rrf".into(),
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: false,
banned: false,
updated: None updated: None
}; };
@ -152,6 +236,7 @@ mod tests {
title: "nada".to_owned(), title: "nada".to_owned(),
description: None, description: None,
category_id: 1, category_id: 1,
removed: None,
updated: None, updated: None,
}; };
@ -164,11 +249,11 @@ mod tests {
title: "nada".to_owned(), title: "nada".to_owned(),
description: None, description: None,
category_id: 1, category_id: 1,
removed: Some(false),
published: inserted_community.published, published: inserted_community.published,
updated: None updated: None
}; };
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id, community_id: inserted_community.id,
user_id: inserted_user.id user_id: inserted_user.id
@ -176,6 +261,7 @@ mod tests {
let inserted_community_follower = CommunityFollower::follow(&conn, &community_follower_form).unwrap(); let inserted_community_follower = CommunityFollower::follow(&conn, &community_follower_form).unwrap();
let expected_community_follower = CommunityFollower { let expected_community_follower = CommunityFollower {
id: inserted_community_follower.id, id: inserted_community_follower.id,
community_id: inserted_community.id, community_id: inserted_community.id,
@ -197,10 +283,25 @@ mod tests {
published: inserted_community_user.published published: inserted_community_user.published
}; };
let community_user_ban_form = CommunityUserBanForm {
community_id: inserted_community.id,
user_id: inserted_user.id
};
let inserted_community_user_ban = CommunityUserBan::ban(&conn, &community_user_ban_form).unwrap();
let expected_community_user_ban = CommunityUserBan {
id: inserted_community_user_ban.id,
community_id: inserted_community.id,
user_id: inserted_user.id,
published: inserted_community_user_ban.published
};
let read_community = Community::read(&conn, inserted_community.id).unwrap(); let read_community = Community::read(&conn, inserted_community.id).unwrap();
let updated_community = Community::update(&conn, inserted_community.id, &new_community).unwrap(); let updated_community = Community::update(&conn, inserted_community.id, &new_community).unwrap();
let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap(); let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap();
let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap(); let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap();
let unban = CommunityUserBan::unban(&conn, &community_user_ban_form).unwrap();
let num_deleted = Community::delete(&conn, inserted_community.id).unwrap(); let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.id).unwrap(); User_::delete(&conn, inserted_user.id).unwrap();
@ -209,8 +310,10 @@ mod tests {
assert_eq!(expected_community, updated_community); assert_eq!(expected_community, updated_community);
assert_eq!(expected_community_follower, inserted_community_follower); assert_eq!(expected_community_follower, inserted_community_follower);
assert_eq!(expected_community_user, inserted_community_user); assert_eq!(expected_community_user, inserted_community_user);
assert_eq!(expected_community_user_ban, inserted_community_user_ban);
assert_eq!(1, ignored_community); assert_eq!(1, ignored_community);
assert_eq!(1, left_community); assert_eq!(1, left_community);
assert_eq!(1, unban);
// assert_eq!(2, loaded_count); // assert_eq!(2, loaded_count);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);

View file

@ -2,6 +2,7 @@ extern crate diesel;
use diesel::*; use diesel::*;
use diesel::result::Error; use diesel::result::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use {SortType};
table! { table! {
community_view (id) { community_view (id) {
@ -11,6 +12,7 @@ table! {
description -> Nullable<Text>, description -> Nullable<Text>,
category_id -> Int4, category_id -> Int4,
creator_id -> Int4, creator_id -> Int4,
removed -> Nullable<Bool>,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
creator_name -> Varchar, creator_name -> Varchar,
@ -20,6 +22,7 @@ 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>,
} }
} }
@ -45,6 +48,32 @@ table! {
} }
} }
table! {
community_user_ban_view (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
user_name -> Varchar,
community_name -> Varchar,
}
}
table! {
site_view (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
creator_name -> Varchar,
number_of_users -> BigInt,
number_of_posts -> BigInt,
number_of_comments -> BigInt,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="community_view"] #[table_name="community_view"]
pub struct CommunityView { pub struct CommunityView {
@ -54,6 +83,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 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,
@ -63,6 +93,7 @@ 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 {
@ -83,20 +114,30 @@ impl CommunityView {
query.first::<Self>(conn) query.first::<Self>(conn)
} }
pub fn list_all(conn: &PgConnection, from_user_id: Option<i32>) -> Result<Vec<Self>, Error> { pub fn list(conn: &PgConnection, from_user_id: Option<i32>, sort: SortType, limit: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::community_view::community_view::dsl::*; use actions::community_view::community_view::dsl::*;
let mut query = community_view.into_boxed(); let mut query = community_view.into_boxed();
// The view lets you pass a null user_id, if you're not logged in // The view lets you pass a null user_id, if you're not logged in
if let Some(from_user_id) = from_user_id {
query = query.filter(user_id.eq(from_user_id)) match sort {
.order_by((subscribed.desc(), number_of_subscribers.desc())); SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
} else { SortType::TopAll => {
query = query.filter(user_id.is_null()) match from_user_id {
.order_by(number_of_subscribers.desc()); Some(from_user_id) => query = query.filter(user_id.eq(from_user_id)).order_by((subscribed.desc(), number_of_subscribers.desc())),
None => query = query.order_by(number_of_subscribers.desc()).filter(user_id.is_null())
}
}
_ => ()
}; };
query.load::<Self>(conn) if let Some(limit) = limit {
query = query.limit(limit);
};
query.filter(removed.eq(false)).load::<Self>(conn)
} }
} }
@ -146,3 +187,58 @@ impl CommunityFollowerView {
community_follower_view.filter(user_id.eq(from_user_id)).load::<Self>(conn) community_follower_view.filter(user_id.eq(from_user_id)).load::<Self>(conn)
} }
} }
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="community_user_ban_view"]
pub struct CommunityUserBanView {
pub id: i32,
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
pub user_name : String,
pub community_name: String,
}
impl CommunityUserBanView {
pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
use actions::community_view::community_user_ban_view::dsl::*;
community_user_ban_view.filter(community_id.eq(from_community_id)).load::<Self>(conn)
}
pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
use actions::community_view::community_user_ban_view::dsl::*;
community_user_ban_view.filter(user_id.eq(from_user_id)).load::<Self>(conn)
}
pub fn get(conn: &PgConnection, from_user_id: i32, from_community_id: i32) -> Result<Self, Error> {
use actions::community_view::community_user_ban_view::dsl::*;
community_user_ban_view
.filter(user_id.eq(from_user_id))
.filter(community_id.eq(from_community_id))
.first::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="site_view"]
pub struct SiteView {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub creator_name: String,
pub number_of_users: i64,
pub number_of_posts: i64,
pub number_of_comments: i64,
}
impl SiteView {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
use actions::community_view::site_view::dsl::*;
site_view.first::<Self>(conn)
}
}

View file

@ -7,3 +7,5 @@ pub mod comment_view;
pub mod category; pub mod category;
pub mod community_view; pub mod community_view;
pub mod user_view; pub mod user_view;
pub mod moderator;
pub mod moderator_views;

View file

@ -0,0 +1,655 @@
extern crate diesel;
use schema::{mod_remove_post, mod_lock_post, mod_remove_comment, mod_remove_community, mod_ban_from_community, mod_ban, mod_add_community, mod_add};
use diesel::*;
use diesel::result::Error;
use serde::{Deserialize, Serialize};
use {Crud};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_remove_post"]
pub struct ModRemovePost {
pub id: i32,
pub mod_user_id: i32,
pub post_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_remove_post"]
pub struct ModRemovePostForm {
pub mod_user_id: i32,
pub post_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
}
impl Crud<ModRemovePostForm> for ModRemovePost {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_remove_post::dsl::*;
mod_remove_post.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_remove_post::dsl::*;
diesel::delete(mod_remove_post.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModRemovePostForm) -> Result<Self, Error> {
use schema::mod_remove_post::dsl::*;
insert_into(mod_remove_post)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModRemovePostForm) -> Result<Self, Error> {
use schema::mod_remove_post::dsl::*;
diesel::update(mod_remove_post.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_lock_post"]
pub struct ModLockPost {
pub id: i32,
pub mod_user_id: i32,
pub post_id: i32,
pub locked: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_lock_post"]
pub struct ModLockPostForm {
pub mod_user_id: i32,
pub post_id: i32,
pub locked: Option<bool>,
}
impl Crud<ModLockPostForm> for ModLockPost {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_lock_post::dsl::*;
mod_lock_post.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_lock_post::dsl::*;
diesel::delete(mod_lock_post.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModLockPostForm) -> Result<Self, Error> {
use schema::mod_lock_post::dsl::*;
insert_into(mod_lock_post)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModLockPostForm) -> Result<Self, Error> {
use schema::mod_lock_post::dsl::*;
diesel::update(mod_lock_post.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_remove_comment"]
pub struct ModRemoveComment {
pub id: i32,
pub mod_user_id: i32,
pub comment_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_remove_comment"]
pub struct ModRemoveCommentForm {
pub mod_user_id: i32,
pub comment_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
}
impl Crud<ModRemoveCommentForm> for ModRemoveComment {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_remove_comment::dsl::*;
mod_remove_comment.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_remove_comment::dsl::*;
diesel::delete(mod_remove_comment.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModRemoveCommentForm) -> Result<Self, Error> {
use schema::mod_remove_comment::dsl::*;
insert_into(mod_remove_comment)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModRemoveCommentForm) -> Result<Self, Error> {
use schema::mod_remove_comment::dsl::*;
diesel::update(mod_remove_comment.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_remove_community"]
pub struct ModRemoveCommunity {
pub id: i32,
pub mod_user_id: i32,
pub community_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_remove_community"]
pub struct ModRemoveCommunityForm {
pub mod_user_id: i32,
pub community_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
}
impl Crud<ModRemoveCommunityForm> for ModRemoveCommunity {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_remove_community::dsl::*;
mod_remove_community.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_remove_community::dsl::*;
diesel::delete(mod_remove_community.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModRemoveCommunityForm) -> Result<Self, Error> {
use schema::mod_remove_community::dsl::*;
insert_into(mod_remove_community)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModRemoveCommunityForm) -> Result<Self, Error> {
use schema::mod_remove_community::dsl::*;
diesel::update(mod_remove_community.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_ban_from_community"]
pub struct ModBanFromCommunity {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub community_id: i32,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_ban_from_community"]
pub struct ModBanFromCommunityForm {
pub mod_user_id: i32,
pub other_user_id: i32,
pub community_id: i32,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
}
impl Crud<ModBanFromCommunityForm> for ModBanFromCommunity {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_ban_from_community::dsl::*;
mod_ban_from_community.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_ban_from_community::dsl::*;
diesel::delete(mod_ban_from_community.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModBanFromCommunityForm) -> Result<Self, Error> {
use schema::mod_ban_from_community::dsl::*;
insert_into(mod_ban_from_community)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModBanFromCommunityForm) -> Result<Self, Error> {
use schema::mod_ban_from_community::dsl::*;
diesel::update(mod_ban_from_community.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_ban"]
pub struct ModBan {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_ban"]
pub struct ModBanForm {
pub mod_user_id: i32,
pub other_user_id: i32,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
}
impl Crud<ModBanForm> for ModBan {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_ban::dsl::*;
mod_ban.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_ban::dsl::*;
diesel::delete(mod_ban.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModBanForm) -> Result<Self, Error> {
use schema::mod_ban::dsl::*;
insert_into(mod_ban)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModBanForm) -> Result<Self, Error> {
use schema::mod_ban::dsl::*;
diesel::update(mod_ban.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_add_community"]
pub struct ModAddCommunity {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub community_id: i32,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_add_community"]
pub struct ModAddCommunityForm {
pub mod_user_id: i32,
pub other_user_id: i32,
pub community_id: i32,
pub removed: Option<bool>,
}
impl Crud<ModAddCommunityForm> for ModAddCommunity {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_add_community::dsl::*;
mod_add_community.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_add_community::dsl::*;
diesel::delete(mod_add_community.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModAddCommunityForm) -> Result<Self, Error> {
use schema::mod_add_community::dsl::*;
insert_into(mod_add_community)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModAddCommunityForm) -> Result<Self, Error> {
use schema::mod_add_community::dsl::*;
diesel::update(mod_add_community.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="mod_add"]
pub struct ModAdd {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="mod_add"]
pub struct ModAddForm {
pub mod_user_id: i32,
pub other_user_id: i32,
pub removed: Option<bool>,
}
impl Crud<ModAddForm> for ModAdd {
fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
use schema::mod_add::dsl::*;
mod_add.find(from_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
use schema::mod_add::dsl::*;
diesel::delete(mod_add.find(from_id))
.execute(conn)
}
fn create(conn: &PgConnection, form: &ModAddForm) -> Result<Self, Error> {
use schema::mod_add::dsl::*;
insert_into(mod_add)
.values(form)
.get_result::<Self>(conn)
}
fn update(conn: &PgConnection, from_id: i32, form: &ModAddForm) -> Result<Self, Error> {
use schema::mod_add::dsl::*;
diesel::update(mod_add.find(from_id))
.set(form)
.get_result::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use establish_connection;
use super::*;
use actions::user::*;
use actions::post::*;
use actions::community::*;
use actions::comment::*;
// use Crud;
#[test]
fn test_crud() {
let conn = establish_connection();
let new_mod = UserForm {
name: "the mod".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: false,
banned: false,
updated: None
};
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
let new_user = UserForm {
name: "jim2".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: false,
banned: false,
updated: None
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let new_community = CommunityForm {
name: "mod_community".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
updated: None
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post thweep".into(),
url: None,
body: None,
creator_id: inserted_user.id,
community_id: inserted_community.id,
removed: None,
locked: None,
updated: None
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
post_id: inserted_post.id,
removed: None,
parent_id: None,
updated: None
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
// Now the actual tests
// remove post
let mod_remove_post_form = ModRemovePostForm {
mod_user_id: inserted_mod.id,
post_id: inserted_post.id,
reason: None,
removed: None,
};
let inserted_mod_remove_post = ModRemovePost::create(&conn, &mod_remove_post_form).unwrap();
let read_moderator_remove_post = ModRemovePost::read(&conn, inserted_mod_remove_post.id).unwrap();
let expected_moderator_remove_post = ModRemovePost {
id: inserted_mod_remove_post.id,
post_id: inserted_post.id,
mod_user_id: inserted_mod.id,
reason: None,
removed: Some(true),
when_: inserted_mod_remove_post.when_,
};
// lock post
let mod_lock_post_form = ModLockPostForm {
mod_user_id: inserted_mod.id,
post_id: inserted_post.id,
locked: None,
};
let inserted_mod_lock_post = ModLockPost::create(&conn, &mod_lock_post_form).unwrap();
let read_moderator_lock_post = ModLockPost::read(&conn, inserted_mod_lock_post.id).unwrap();
let expected_moderator_lock_post = ModLockPost {
id: inserted_mod_lock_post.id,
post_id: inserted_post.id,
mod_user_id: inserted_mod.id,
locked: Some(true),
when_: inserted_mod_lock_post.when_,
};
// comment
let mod_remove_comment_form = ModRemoveCommentForm {
mod_user_id: inserted_mod.id,
comment_id: inserted_comment.id,
reason: None,
removed: None,
};
let inserted_mod_remove_comment = ModRemoveComment::create(&conn, &mod_remove_comment_form).unwrap();
let read_moderator_remove_comment = ModRemoveComment::read(&conn, inserted_mod_remove_comment.id).unwrap();
let expected_moderator_remove_comment = ModRemoveComment {
id: inserted_mod_remove_comment.id,
comment_id: inserted_comment.id,
mod_user_id: inserted_mod.id,
reason: None,
removed: Some(true),
when_: inserted_mod_remove_comment.when_,
};
// community
let mod_remove_community_form = ModRemoveCommunityForm {
mod_user_id: inserted_mod.id,
community_id: inserted_community.id,
reason: None,
removed: None,
expires: None,
};
let inserted_mod_remove_community = ModRemoveCommunity::create(&conn, &mod_remove_community_form).unwrap();
let read_moderator_remove_community = ModRemoveCommunity::read(&conn, inserted_mod_remove_community.id).unwrap();
let expected_moderator_remove_community = ModRemoveCommunity {
id: inserted_mod_remove_community.id,
community_id: inserted_community.id,
mod_user_id: inserted_mod.id,
reason: None,
removed: Some(true),
expires: None,
when_: inserted_mod_remove_community.when_,
};
// ban from community
let mod_ban_from_community_form = ModBanFromCommunityForm {
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
community_id: inserted_community.id,
reason: None,
banned: None,
expires: None,
};
let inserted_mod_ban_from_community = ModBanFromCommunity::create(&conn, &mod_ban_from_community_form).unwrap();
let read_moderator_ban_from_community = ModBanFromCommunity::read(&conn, inserted_mod_ban_from_community.id).unwrap();
let expected_moderator_ban_from_community = ModBanFromCommunity {
id: inserted_mod_ban_from_community.id,
community_id: inserted_community.id,
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
reason: None,
banned: Some(true),
expires: None,
when_: inserted_mod_ban_from_community.when_,
};
// ban
let mod_ban_form = ModBanForm {
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
reason: None,
banned: None,
expires: None,
};
let inserted_mod_ban = ModBan::create(&conn, &mod_ban_form).unwrap();
let read_moderator_ban = ModBan::read(&conn, inserted_mod_ban.id).unwrap();
let expected_moderator_ban = ModBan {
id: inserted_mod_ban.id,
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
reason: None,
banned: Some(true),
expires: None,
when_: inserted_mod_ban.when_,
};
// mod add community
let mod_add_community_form = ModAddCommunityForm {
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
community_id: inserted_community.id,
removed: None,
};
let inserted_mod_add_community = ModAddCommunity::create(&conn, &mod_add_community_form).unwrap();
let read_moderator_add_community = ModAddCommunity::read(&conn, inserted_mod_add_community.id).unwrap();
let expected_moderator_add_community = ModAddCommunity {
id: inserted_mod_add_community.id,
community_id: inserted_community.id,
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
removed: Some(false),
when_: inserted_mod_add_community.when_,
};
// mod add
let mod_add_form = ModAddForm {
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
removed: None,
};
let inserted_mod_add = ModAdd::create(&conn, &mod_add_form).unwrap();
let read_moderator_add = ModAdd::read(&conn, inserted_mod_add.id).unwrap();
let expected_moderator_add = ModAdd {
id: inserted_mod_add.id,
mod_user_id: inserted_mod.id,
other_user_id: inserted_user.id,
removed: Some(false),
when_: inserted_mod_add.when_,
};
ModRemovePost::delete(&conn, inserted_mod_remove_post.id).unwrap();
ModLockPost::delete(&conn, inserted_mod_lock_post.id).unwrap();
ModRemoveComment::delete(&conn, inserted_mod_remove_comment.id).unwrap();
ModRemoveCommunity::delete(&conn, inserted_mod_remove_community.id).unwrap();
ModBanFromCommunity::delete(&conn, inserted_mod_ban_from_community.id).unwrap();
ModBan::delete(&conn, inserted_mod_ban.id).unwrap();
ModAddCommunity::delete(&conn, inserted_mod_add_community.id).unwrap();
ModAdd::delete(&conn, inserted_mod_add.id).unwrap();
Comment::delete(&conn, inserted_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.id).unwrap();
User_::delete(&conn, inserted_mod.id).unwrap();
assert_eq!(expected_moderator_remove_post, read_moderator_remove_post);
assert_eq!(expected_moderator_lock_post, read_moderator_lock_post);
assert_eq!(expected_moderator_remove_comment, read_moderator_remove_comment);
assert_eq!(expected_moderator_remove_community, read_moderator_remove_community);
assert_eq!(expected_moderator_ban_from_community, read_moderator_ban_from_community);
assert_eq!(expected_moderator_ban, read_moderator_ban);
assert_eq!(expected_moderator_add_community, read_moderator_add_community);
assert_eq!(expected_moderator_add, read_moderator_add);
}
}

View file

@ -0,0 +1,427 @@
extern crate diesel;
use diesel::*;
use diesel::result::Error;
use serde::{Deserialize, Serialize};
table! {
mod_remove_post_view (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
mod_user_name -> Varchar,
post_name -> Varchar,
community_id -> Int4,
community_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_remove_post_view"]
pub struct ModRemovePostView {
pub id: i32,
pub mod_user_id: i32,
pub post_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub post_name: String,
pub community_id: i32,
pub community_name: String,
}
impl ModRemovePostView {
pub fn list(conn: &PgConnection,
from_community_id: Option<i32>,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_remove_post_view::dsl::*;
let mut query = mod_remove_post_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_community_id) = from_community_id {
query = query.filter(community_id.eq(from_community_id));
};
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}
table! {
mod_lock_post_view (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
locked -> Nullable<Bool>,
when_ -> Timestamp,
mod_user_name -> Varchar,
post_name -> Varchar,
community_id -> Int4,
community_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_lock_post_view"]
pub struct ModLockPostView {
pub id: i32,
pub mod_user_id: i32,
pub post_id: i32,
pub locked: Option<bool>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub post_name: String,
pub community_id: i32,
pub community_name: String,
}
impl ModLockPostView {
pub fn list(conn: &PgConnection,
from_community_id: Option<i32>,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_lock_post_view::dsl::*;
let mut query = mod_lock_post_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_community_id) = from_community_id {
query = query.filter(community_id.eq(from_community_id));
};
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}
table! {
mod_remove_comment_view (id) {
id -> Int4,
mod_user_id -> Int4,
comment_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
mod_user_name -> Varchar,
comment_user_id -> Int4,
comment_user_name -> Varchar,
comment_content -> Text,
post_id -> Int4,
post_name -> Varchar,
community_id -> Int4,
community_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_remove_comment_view"]
pub struct ModRemoveCommentView {
pub id: i32,
pub mod_user_id: i32,
pub comment_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub comment_user_id: i32,
pub comment_user_name: String,
pub comment_content: String,
pub post_id: i32,
pub post_name: String,
pub community_id: i32,
pub community_name: String,
}
impl ModRemoveCommentView {
pub fn list(conn: &PgConnection,
from_community_id: Option<i32>,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_remove_comment_view::dsl::*;
let mut query = mod_remove_comment_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_community_id) = from_community_id {
query = query.filter(community_id.eq(from_community_id));
};
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}
table! {
mod_remove_community_view (id) {
id -> Int4,
mod_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
mod_user_name -> Varchar,
community_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_remove_community_view"]
pub struct ModRemoveCommunityView {
pub id: i32,
pub mod_user_id: i32,
pub community_id: i32,
pub reason: Option<String>,
pub removed: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub community_name: String,
}
impl ModRemoveCommunityView {
pub fn list(conn: &PgConnection,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_remove_community_view::dsl::*;
let mut query = mod_remove_community_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}
table! {
mod_ban_from_community_view (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
mod_user_name -> Varchar,
other_user_name -> Varchar,
community_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_ban_from_community_view"]
pub struct ModBanFromCommunityView {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub community_id: i32,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub other_user_name: String,
pub community_name: String,
}
impl ModBanFromCommunityView {
pub fn list(conn: &PgConnection,
from_community_id: Option<i32>,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_ban_from_community_view::dsl::*;
let mut query = mod_ban_from_community_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_community_id) = from_community_id {
query = query.filter(community_id.eq(from_community_id));
};
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}
table! {
mod_ban_view (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
mod_user_name -> Varchar,
other_user_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_ban_view"]
pub struct ModBanView {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub other_user_name: String,
}
impl ModBanView {
pub fn list(conn: &PgConnection,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_ban_view::dsl::*;
let mut query = mod_ban_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}
table! {
mod_add_community_view (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
mod_user_name -> Varchar,
other_user_name -> Varchar,
community_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_add_community_view"]
pub struct ModAddCommunityView {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub community_id: i32,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub other_user_name: String,
pub community_name: String,
}
impl ModAddCommunityView {
pub fn list(conn: &PgConnection,
from_community_id: Option<i32>,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_add_community_view::dsl::*;
let mut query = mod_add_community_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_community_id) = from_community_id {
query = query.filter(community_id.eq(from_community_id));
};
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}
table! {
mod_add_view (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
mod_user_name -> Varchar,
other_user_name -> Varchar,
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
#[table_name="mod_add_view"]
pub struct ModAddView {
pub id: i32,
pub mod_user_id: i32,
pub other_user_id: i32,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
pub mod_user_name: String,
pub other_user_name: String,
}
impl ModAddView {
pub fn list(conn: &PgConnection,
from_mod_user_id: Option<i32>,
limit: Option<i64>,
page: Option<i64>) -> Result<Vec<Self>, Error> {
use actions::moderator_views::mod_add_view::dsl::*;
let mut query = mod_add_view.into_boxed();
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
if let Some(from_mod_user_id) = from_mod_user_id {
query = query.filter(mod_user_id.eq(from_mod_user_id));
};
query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn)
}
}

View file

@ -14,6 +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 locked: Option<bool>,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -26,6 +28,8 @@ pub struct PostForm {
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 locked: Option<bool>,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -115,17 +119,20 @@ mod tests {
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: false,
banned: false,
updated: None updated: None
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
let new_community = CommunityForm { let new_community = CommunityForm {
name: "test community_2".to_string(), name: "test community_3".to_string(),
title: "nada".to_owned(), title: "nada".to_owned(),
description: None, description: None,
category_id: 1, category_id: 1,
creator_id: inserted_user.id, creator_id: inserted_user.id,
removed: None,
updated: None updated: None
}; };
@ -137,6 +144,8 @@ mod tests {
body: None, body: None,
creator_id: inserted_user.id, creator_id: inserted_user.id,
community_id: inserted_community.id, community_id: inserted_community.id,
removed: None,
locked: None,
updated: None updated: None
}; };
@ -150,6 +159,8 @@ 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),
locked: Some(false),
updated: None updated: None
}; };

View file

@ -19,6 +19,8 @@ table! {
body -> Nullable<Text>, body -> Nullable<Text>,
creator_id -> Int4, creator_id -> Int4,
community_id -> Int4, community_id -> Int4,
removed -> Nullable<Bool>,
locked -> Nullable<Bool>,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
creator_name -> Varchar, creator_name -> Varchar,
@ -31,6 +33,7 @@ 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>,
} }
} }
@ -44,6 +47,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 locked: Option<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,
@ -56,6 +61,7 @@ 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>,
} }
impl PostView { impl PostView {
@ -110,6 +116,8 @@ impl PostView {
.order_by(score.desc()) .order_by(score.desc())
}; };
query = query.filter(removed.eq(false));
query.load::<Self>(conn) query.load::<Self>(conn)
} }
@ -156,7 +164,9 @@ mod tests {
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
updated: None updated: None,
admin: false,
banned: false,
}; };
let inserted_user = User_::create(&conn, &new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -167,6 +177,7 @@ mod tests {
description: None, description: None,
creator_id: inserted_user.id, creator_id: inserted_user.id,
category_id: 1, category_id: 1,
removed: None,
updated: None updated: None
}; };
@ -178,6 +189,8 @@ mod tests {
body: None, body: None,
creator_id: inserted_user.id, creator_id: inserted_user.id,
community_id: inserted_community.id, community_id: inserted_community.id,
removed: None,
locked: None,
updated: None updated: None
}; };
@ -216,6 +229,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),
locked: Some(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,
@ -224,7 +239,8 @@ mod tests {
hot_rank: 864, hot_rank: 864,
published: inserted_post.published, published: inserted_post.published,
updated: None, updated: None,
subscribed: None subscribed: None,
am_mod: None,
}; };
let expected_post_listing_with_user = PostView { let expected_post_listing_with_user = PostView {
@ -234,6 +250,8 @@ mod tests {
name: post_name.to_owned(), name: post_name.to_owned(),
url: None, url: None,
body: None, body: None,
removed: Some(false),
locked: Some(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,
@ -245,7 +263,8 @@ mod tests {
hot_rank: 864, hot_rank: 864,
published: inserted_post.published, published: inserted_post.published,
updated: None, updated: None,
subscribed: None subscribed: None,
am_mod: None,
}; };
@ -274,6 +293,5 @@ mod tests {
assert_eq!(expected_post_like, inserted_post_like); assert_eq!(expected_post_like, inserted_post_like);
assert_eq!(1, like_removed); assert_eq!(1, like_removed);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
} }
} }

View file

@ -17,6 +17,8 @@ pub struct User_ {
pub password_encrypted: String, pub password_encrypted: String,
pub email: Option<String>, pub email: Option<String>,
pub icon: Option<Vec<u8>>, pub icon: Option<Vec<u8>>,
pub admin: bool,
pub banned: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -28,6 +30,8 @@ pub struct UserForm {
pub fedi_name: String, pub fedi_name: String,
pub preferred_username: Option<String>, pub preferred_username: Option<String>,
pub password_encrypted: String, pub password_encrypted: String,
pub admin: bool,
pub banned: bool,
pub email: Option<String>, pub email: Option<String>,
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
@ -42,22 +46,26 @@ impl Crud<UserForm> for User_ {
.execute(conn) .execute(conn)
} }
fn create(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> { fn create(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
let mut edited_user = form.clone();
let password_hash = hash(&form.password_encrypted, DEFAULT_COST)
.expect("Couldn't hash password");
edited_user.password_encrypted = password_hash;
insert_into(user_) insert_into(user_)
.values(edited_user) .values(form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
fn update(conn: &PgConnection, user_id: i32, form: &UserForm) -> Result<Self, Error> { fn update(conn: &PgConnection, user_id: i32, form: &UserForm) -> Result<Self, Error> {
diesel::update(user_.find(user_id))
.set(form)
.get_result::<Self>(conn)
}
}
impl User_ {
pub fn register(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
let mut edited_user = form.clone(); let mut edited_user = form.clone();
let password_hash = hash(&form.password_encrypted, DEFAULT_COST) let password_hash = hash(&form.password_encrypted, DEFAULT_COST)
.expect("Couldn't hash password"); .expect("Couldn't hash password");
edited_user.password_encrypted = password_hash; edited_user.password_encrypted = password_hash;
diesel::update(user_.find(user_id))
.set(edited_user) Self::create(&conn, &edited_user)
.get_result::<Self>(conn)
} }
} }
@ -122,6 +130,8 @@ mod tests {
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
admin: false,
banned: false,
updated: None updated: None
}; };
@ -132,9 +142,11 @@ mod tests {
name: "thommy".into(), name: "thommy".into(),
fedi_name: "rrf".into(), fedi_name: "rrf".into(),
preferred_username: None, preferred_username: None,
password_encrypted: "$2y$12$YXpNpYsdfjmed.QlYLvw4OfTCgyKUnKHc/V8Dgcf9YcVKHPaYXYYy".into(), password_encrypted: "nope".into(),
email: None, email: None,
icon: None, icon: None,
admin: false,
banned: false,
published: inserted_user.published, published: inserted_user.published,
updated: None updated: None
}; };
@ -143,9 +155,9 @@ mod tests {
let updated_user = User_::update(&conn, inserted_user.id, &new_user).unwrap(); let updated_user = User_::update(&conn, inserted_user.id, &new_user).unwrap();
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap(); let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(expected_user.id, read_user.id); assert_eq!(expected_user, read_user);
assert_eq!(expected_user.id, inserted_user.id); assert_eq!(expected_user, inserted_user);
assert_eq!(expected_user.id, updated_user.id); assert_eq!(expected_user, updated_user);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
} }
} }

View file

@ -8,6 +8,8 @@ table! {
id -> Int4, id -> Int4,
name -> Varchar, name -> Varchar,
fedi_name -> Varchar, fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
published -> Timestamp, published -> Timestamp,
number_of_posts -> BigInt, number_of_posts -> BigInt,
post_score -> BigInt, post_score -> BigInt,
@ -22,6 +24,8 @@ pub struct UserView {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub fedi_name: String, pub fedi_name: String,
pub admin: bool,
pub banned: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub number_of_posts: i64, pub number_of_posts: i64,
pub post_score: i64, pub post_score: i64,
@ -36,5 +40,17 @@ impl UserView {
user_view.find(from_user_id) user_view.find(from_user_id)
.first::<Self>(conn) .first::<Self>(conn)
} }
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use actions::user_view::user_view::dsl::*;
user_view.filter(admin.eq(true))
.load::<Self>(conn)
}
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use actions::user_view::user_view::dsl::*;
user_view.filter(banned.eq(true))
.load::<Self>(conn)
}
} }

View file

@ -44,6 +44,8 @@ mod tests {
email: None, email: None,
icon: None, icon: None,
published: naive_now(), published: naive_now(),
admin: false,
banned: false,
updated: None updated: None
}; };

View file

@ -12,7 +12,7 @@ pub extern crate jsonwebtoken;
pub extern crate bcrypt; pub extern crate bcrypt;
pub extern crate regex; pub extern crate regex;
#[macro_use] pub extern crate strum_macros; #[macro_use] pub extern crate strum_macros;
#[macro_use] pub extern crate lazy_static;
pub mod schema; pub mod schema;
pub mod apub; pub mod apub;
pub mod actions; pub mod actions;
@ -50,6 +50,11 @@ pub trait Likeable<T> {
fn remove(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized; fn remove(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
} }
pub trait Bannable<T> {
fn ban(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
fn unban(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)
@ -88,22 +93,47 @@ pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc() chrono::prelude::Utc::now().naive_utc()
} }
pub fn naive_from_unix(time: i64) -> NaiveDateTime {
NaiveDateTime::from_timestamp(time, 0)
}
pub fn is_email_regex(test: &str) -> bool { pub fn is_email_regex(test: &str) -> bool {
let re = Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); EMAIL_REGEX.is_match(test)
re.is_match(test) }
pub fn remove_slurs(test: &str) -> String {
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
pub fn has_slurs(test: &str) -> bool {
SLUR_REGEX.is_match(test)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use {Settings, is_email_regex}; use {Settings, is_email_regex, remove_slurs, has_slurs};
#[test] #[test]
fn test_api() { fn test_api() {
assert_eq!(Settings::get().api_endpoint(), "http://0.0.0.0/api/v1"); assert_eq!(Settings::get().api_endpoint(), "http://0.0.0.0/api/v1");
} }
#[test] #[test] fn test_email() {
fn test_email() {
assert!(is_email_regex("gush@gmail.com")); assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho")); assert!(!is_email_regex("nada_neutho"));
} }
#[test] fn test_slur_filter() {
let test = "coons test dindu ladyboy tranny. This is a bunch of other safe text.".to_string();
let slur_free = "No slurs here";
assert_eq!(remove_slurs(&test), "*removed* test *removed* *removed* *removed*. This is a bunch of other safe text.".to_string());
assert!(has_slurs(&test));
assert!(!has_slurs(slur_free));
} }
}
lazy_static! {
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bnig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?))").unwrap();
}

View file

@ -12,6 +12,7 @@ table! {
post_id -> Int4, post_id -> Int4,
parent_id -> Nullable<Int4>, parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Nullable<Bool>,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
} }
@ -36,6 +37,7 @@ table! {
description -> Nullable<Text>, description -> Nullable<Text>,
category_id -> Int4, category_id -> Int4,
creator_id -> Int4, creator_id -> Int4,
removed -> Nullable<Bool>,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
} }
@ -59,6 +61,105 @@ table! {
} }
} }
table! {
community_user_ban (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
mod_add (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_add_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_ban (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_ban_from_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_lock_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
locked -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_remove_comment (id) {
id -> Int4,
mod_user_id -> Int4,
comment_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_remove_community (id) {
id -> Int4,
mod_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_remove_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! { table! {
post (id) { post (id) {
id -> Int4, id -> Int4,
@ -67,6 +168,8 @@ table! {
body -> Nullable<Text>, body -> Nullable<Text>,
creator_id -> Int4, creator_id -> Int4,
community_id -> Int4, community_id -> Int4,
removed -> Nullable<Bool>,
locked -> Nullable<Bool>,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
} }
@ -82,6 +185,17 @@ table! {
} }
} }
table! {
site (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
}
table! { table! {
user_ (id) { user_ (id) {
id -> Int4, id -> Int4,
@ -91,11 +205,21 @@ table! {
password_encrypted -> Text, password_encrypted -> Text,
email -> Nullable<Text>, email -> Nullable<Text>,
icon -> Nullable<Bytea>, icon -> Nullable<Bytea>,
admin -> Bool,
banned -> Bool,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
} }
} }
table! {
user_ban (id) {
id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
joinable!(comment -> post (post_id)); joinable!(comment -> post (post_id));
joinable!(comment -> user_ (creator_id)); joinable!(comment -> user_ (creator_id));
joinable!(comment_like -> comment (comment_id)); joinable!(comment_like -> comment (comment_id));
@ -107,10 +231,24 @@ joinable!(community_follower -> community (community_id));
joinable!(community_follower -> user_ (user_id)); joinable!(community_follower -> user_ (user_id));
joinable!(community_moderator -> community (community_id)); joinable!(community_moderator -> community (community_id));
joinable!(community_moderator -> user_ (user_id)); joinable!(community_moderator -> user_ (user_id));
joinable!(community_user_ban -> community (community_id));
joinable!(community_user_ban -> user_ (user_id));
joinable!(mod_add_community -> community (community_id));
joinable!(mod_ban_from_community -> community (community_id));
joinable!(mod_lock_post -> post (post_id));
joinable!(mod_lock_post -> user_ (mod_user_id));
joinable!(mod_remove_comment -> comment (comment_id));
joinable!(mod_remove_comment -> user_ (mod_user_id));
joinable!(mod_remove_community -> community (community_id));
joinable!(mod_remove_community -> user_ (mod_user_id));
joinable!(mod_remove_post -> post (post_id));
joinable!(mod_remove_post -> user_ (mod_user_id));
joinable!(post -> community (community_id)); 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!(site -> user_ (creator_id));
joinable!(user_ban -> user_ (user_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
category, category,
@ -119,7 +257,18 @@ allow_tables_to_appear_in_same_query!(
community, community,
community_follower, community_follower,
community_moderator, community_moderator,
community_user_ban,
mod_add,
mod_add_community,
mod_ban,
mod_ban_from_community,
mod_lock_post,
mod_remove_comment,
mod_remove_community,
mod_remove_post,
post, post,
post_like, post_like,
site,
user_, user_,
user_ban,
); );

File diff suppressed because it is too large Load diff

1
ui/.gitignore vendored
View file

@ -1,4 +1,3 @@
src/version.ts
dist dist
.fusebox .fusebox
_site _site

View file

@ -11,7 +11,7 @@ const transformInferno = require('ts-transform-inferno').default;
const transformClasscat = require('ts-transform-classcat').default; const transformClasscat = require('ts-transform-classcat').default;
let fuse, app; let fuse, app;
let isProduction = false; let isProduction = false;
var setVersion = require('./set_version.js').setVersion; // var setVersion = require('./set_version.js').setVersion;
Sparky.task('config', _ => { Sparky.task('config', _ => {
fuse = new FuseBox({ fuse = new FuseBox({
@ -42,16 +42,16 @@ Sparky.task('config', _ => {
}); });
app = fuse.bundle('app').instructions('>index.tsx'); app = fuse.bundle('app').instructions('>index.tsx');
}); });
Sparky.task('version', _ => setVersion()); // Sparky.task('version', _ => setVersion());
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
Sparky.task('env', _ => (isProduction = true)); Sparky.task('env', _ => (isProduction = true));
Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/')); Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/'));
Sparky.task('dev', ['clean', 'config', 'copy-assets', 'version'], _ => { Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
fuse.dev(); fuse.dev();
app.hmr().watch(); app.hmr().watch();
return fuse.run(); return fuse.run();
}); });
Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets', 'version'], _ => { Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => {
// fuse.dev({ reload: true }); // remove after demo // fuse.dev({ reload: true }); // remove after demo
return fuse.run(); return fuse.run();
}); });

2
ui/set_version.js Normal file → Executable file
View file

@ -7,3 +7,5 @@ exports.setVersion = function() {
let line = `export let version: string = "${revision}";`; let line = `export let version: string = "${revision}";`;
fs.writeFileSync("./src/version.ts", line); fs.writeFileSync("./src/version.ts", line);
} }
this.setVersion()

View file

@ -1,6 +1,6 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces'; import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService, UserService } from '../services';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
interface CommentFormProps { interface CommentFormProps {
@ -8,6 +8,7 @@ interface CommentFormProps {
node?: CommentNodeI; node?: CommentNodeI;
onReplyCancel?(): any; onReplyCancel?(): any;
edit?: boolean; edit?: boolean;
disabled?: boolean;
} }
interface CommentFormState { interface CommentFormState {
@ -21,9 +22,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
commentForm: { commentForm: {
auth: null, auth: null,
content: null, content: null,
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
}, },
buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply" buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply",
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -36,6 +38,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
this.state.commentForm.edit_id = this.props.node.comment.id; this.state.commentForm.edit_id = this.props.node.comment.id;
this.state.commentForm.parent_id = this.props.node.comment.parent_id; this.state.commentForm.parent_id = this.props.node.comment.parent_id;
this.state.commentForm.content = this.props.node.comment.content; this.state.commentForm.content = this.props.node.comment.content;
this.state.commentForm.creator_id = this.props.node.comment.creator_id;
} else { } else {
// A reply gets a new parent id // A reply gets a new parent id
this.state.commentForm.parent_id = this.props.node.comment.id; this.state.commentForm.parent_id = this.props.node.comment.id;
@ -53,12 +56,12 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}> <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-12"> <div class="col-sm-12">
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required /> <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required disabled={this.props.disabled}/>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button> <button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
{this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>} {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
</div> </div>
</div> </div>
@ -68,6 +71,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
} }
handleCommentSubmit(i: CommentForm, event: any) { handleCommentSubmit(i: CommentForm, event: any) {
event.preventDefault();
if (i.props.edit) { if (i.props.edit) {
WebSocketService.Instance.editComment(i.state.commentForm); WebSocketService.Instance.editComment(i.state.commentForm);
} else { } else {

View file

@ -1,8 +1,8 @@
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 } from '../interfaces'; import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, BanFromCommunityForm, CommunityUser, AddModToCommunityForm } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { mdToHtml } from '../utils'; import { mdToHtml, getUnixTime } 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';
@ -10,19 +10,31 @@ import { CommentNodes } from './comment-nodes';
interface CommentNodeState { interface CommentNodeState {
showReply: boolean; showReply: boolean;
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean;
removeReason: string;
showBanDialog: boolean;
banReason: string;
banExpires: string;
} }
interface CommentNodeProps { interface CommentNodeProps {
node: CommentNodeI; node: CommentNodeI;
noIndent?: boolean; noIndent?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
locked?: boolean;
moderators: Array<CommunityUser>;
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
private emptyState: CommentNodeState = { private emptyState: CommentNodeState = {
showReply: false, showReply: false,
showEdit: false showEdit: false,
showRemoveDialog: false,
removeReason: null,
showBanDialog: false,
banReason: null,
banExpires: null,
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -60,10 +72,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<span><MomentTime data={node.comment} /></span> <span><MomentTime data={node.comment} /></span>
</li> </li>
</ul> </ul>
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />} {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
{!this.state.showEdit && {!this.state.showEdit &&
<div> <div>
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(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 && {!this.props.viewOnly &&
<span class="mr-2"> <span class="mr-2">
@ -71,14 +83,39 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span> <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
</li> </li>
{this.myComment && {this.myComment &&
<>
<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>
}
{this.myComment &&
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
</li> </li>
</>
}
{this.canMod &&
<>
<li className="list-inline-item">
{!this.props.node.comment.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
}
</li>
{!this.isMod &&
<>
<li className="list-inline-item">
{!this.props.node.comment.banned ?
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban</span>
}
</li>
</>
}
{!this.props.node.comment.banned &&
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
</li>
}
</>
} }
</span> </span>
} }
@ -89,14 +126,59 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div> </div>
} }
</div> </div>
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />} {this.state.showRemoveDialog &&
{this.props.node.children && <CommentNodes nodes={this.props.node.children} />} <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
<button type="submit" class="btn btn-secondary">Remove Comment</button>
</form>
}
{this.state.showBanDialog &&
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
<div class="form-group row">
<label class="col-form-label">Reason</label>
<input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
</div>
<div class="form-group row">
<label class="col-form-label">Expires</label>
<input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} />
</div>
<div class="form-group row">
<button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
</div>
</form>
}
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
{this.props.node.children && <CommentNodes nodes={this.props.node.children} locked={this.props.locked} moderators={this.props.moderators}/>}
</div> </div>
) )
} }
private get myComment(): boolean { get myComment(): boolean {
return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id; return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id;
}
get canMod(): boolean {
// You can do moderator actions only on the mods added after you.
if (UserService.Instance.user) {
let modIds = this.props.moderators.map(m => m.user_id);
let yourIndex = modIds.findIndex(id => id == UserService.Instance.user.id);
if (yourIndex == -1) {
return false;
} else {
console.log(modIds);
modIds = modIds.slice(0, yourIndex+1); // +1 cause you cant mod yourself
console.log(modIds);
return !modIds.includes(this.props.node.comment.creator_id);
}
} else {
return false;
}
}
get isMod(): boolean {
return this.props.moderators.map(m => m.user_id).includes(this.props.node.comment.creator_id);
} }
handleReplyClick(i: CommentNode) { handleReplyClick(i: CommentNode) {
@ -113,6 +195,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
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,
post_id: i.props.node.comment.post_id, post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id, parent_id: i.props.node.comment.parent_id,
auth: null auth: null
@ -145,4 +228,72 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}; };
WebSocketService.Instance.likeComment(form); WebSocketService.Instance.likeComment(form);
} }
handleModRemoveShow(i: CommentNode) {
i.state.showRemoveDialog = true;
i.setState(i.state);
}
handleModRemoveReasonChange(i: CommentNode, event: any) {
i.state.removeReason = event.target.value;
i.setState(i.state);
}
handleModRemoveSubmit(i: CommentNode) {
event.preventDefault();
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,
removed: !i.props.node.comment.removed,
reason: i.state.removeReason,
auth: null
};
WebSocketService.Instance.editComment(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handleModBanShow(i: CommentNode) {
i.state.showBanDialog = true;
i.setState(i.state);
}
handleModBanReasonChange(i: CommentNode, event: any) {
i.state.banReason = event.target.value;
i.setState(i.state);
}
handleModBanExpiresChange(i: CommentNode, event: any) {
i.state.banExpires = event.target.value;
i.setState(i.state);
}
handleModBanSubmit(i: CommentNode) {
event.preventDefault();
let form: BanFromCommunityForm = {
user_id: i.props.node.comment.creator_id,
community_id: i.props.node.comment.community_id,
ban: !i.props.node.comment.banned,
reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires),
};
WebSocketService.Instance.banFromCommunity(form);
i.state.showBanDialog = false;
i.setState(i.state);
}
handleAddModToCommunity(i: CommentNode) {
let form: AddModToCommunityForm = {
user_id: i.props.node.comment.creator_id,
community_id: i.props.node.comment.community_id,
added: !i.isMod,
};
WebSocketService.Instance.addModToCommunity(form);
i.setState(i.state);
}
} }

View file

@ -1,5 +1,5 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { CommentNode as CommentNodeI } from '../interfaces'; import { CommentNode as CommentNodeI, CommunityUser } from '../interfaces';
import { CommentNode } from './comment-node'; import { CommentNode } from './comment-node';
interface CommentNodesState { interface CommentNodesState {
@ -7,8 +7,10 @@ interface CommentNodesState {
interface CommentNodesProps { interface CommentNodesProps {
nodes: Array<CommentNodeI>; nodes: Array<CommentNodeI>;
moderators?: Array<CommunityUser>;
noIndent?: boolean; noIndent?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
locked?: boolean;
} }
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> { export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
@ -21,10 +23,15 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
return ( return (
<div className="comments"> <div className="comments">
{this.props.nodes.map(node => {this.props.nodes.map(node =>
<CommentNode node={node} noIndent={this.props.noIndent} viewOnly={this.props.viewOnly}/> <CommentNode node={node}
noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly}
locked={this.props.locked}
moderators={this.props.moderators}/>
)} )}
</div> </div>
) )
} }
} }

View file

@ -2,7 +2,7 @@ import { Component, linkEvent } 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, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm } from '../interfaces'; import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
@ -30,7 +30,12 @@ export class Communities extends Component<any, CommunitiesState> {
(err) => console.error(err), (err) => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
WebSocketService.Instance.listCommunities();
let listCommunitiesForm: ListCommunitiesForm = {
sort: SortType[SortType.TopAll]
}
WebSocketService.Instance.listCommunities(listCommunitiesForm);
} }
@ -45,7 +50,7 @@ export class Communities extends Component<any, CommunitiesState> {
render() { render() {
return ( return (
<div class="container-fluid"> <div class="container">
{this.state.loading ? {this.state.loading ?
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : <h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div> <div>
@ -57,9 +62,9 @@ export class Communities extends Component<any, CommunitiesState> {
<th>Name</th> <th>Name</th>
<th>Title</th> <th>Title</th>
<th>Category</th> <th>Category</th>
<th class="text-right">Subscribers</th> <th class="text-right d-none d-md-table-cell">Subscribers</th>
<th class="text-right">Posts</th> <th class="text-right d-none d-md-table-cell">Posts</th>
<th class="text-right">Comments</th> <th class="text-right d-none d-md-table-cell">Comments</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -69,13 +74,13 @@ export class Communities extends Component<any, CommunitiesState> {
<td><Link to={`/community/${community.id}`}>{community.name}</Link></td> <td><Link to={`/community/${community.id}`}>{community.name}</Link></td>
<td>{community.title}</td> <td>{community.title}</td>
<td>{community.category_name}</td> <td>{community.category_name}</td>
<td class="text-right">{community.number_of_subscribers}</td> <td class="text-right d-none d-md-table-cell">{community.number_of_subscribers}</td>
<td class="text-right">{community.number_of_posts}</td> <td class="text-right d-none d-md-table-cell">{community.number_of_posts}</td>
<td class="text-right">{community.number_of_comments}</td> <td class="text-right d-none d-md-table-cell">{community.number_of_comments}</td>
<td class="text-right"> <td class="text-right">
{community.subscribed ? {community.subscribed ?
<button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button> : <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</span> :
<button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button> <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</span>
} }
</td> </td>
</tr> </tr>
@ -97,7 +102,6 @@ export class Communities extends Component<any, CommunitiesState> {
WebSocketService.Instance.followCommunity(form); WebSocketService.Instance.followCommunity(form);
} }
handleSubscribe(communityId: number) { handleSubscribe(communityId: number) {
let form: FollowCommunityForm = { let form: FollowCommunityForm = {
community_id: communityId, community_id: communityId,

View file

@ -155,6 +155,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
if (msg.error) { if (msg.error) {
alert(msg.error); alert(msg.error);
this.state.loading = false; this.state.loading = false;
this.setState(this.state);
return; return;
} else if (op == UserOperation.ListCategories){ } else if (op == UserOperation.ListCategories){
let res: ListCategoriesResponse = msg; let res: ListCategoriesResponse = msg;

View file

@ -63,7 +63,11 @@ export class Community extends Component<any, State> {
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<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}</h4> <h4>{this.state.community.title}
{this.state.community.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
</h4>
<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

@ -0,0 +1,36 @@
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { repoUrl } from '../utils';
import { version } from '../version';
export class Footer extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<nav title={version} class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3 my-2">
<div className="navbar-collapse">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<Link class="nav-link" to="/modlog">Modlog</Link>
</li>
<li class="nav-item">
<a class="nav-link" href={repoUrl}>Contribute</a>
</li>
<li class="nav-item">
<a class="nav-link" href={repoUrl}>Code</a>
</li>
<li class="nav-item">
<a class="nav-link" href={repoUrl}>About</a>
</li>
</ul>
</div>
</nav>
);
}
}

View file

@ -20,7 +20,8 @@ let emptyState: State = {
registerForm: { registerForm: {
username: undefined, username: undefined,
password: undefined, password: undefined,
password_verify: undefined password_verify: undefined,
admin: false,
}, },
loginLoading: false, loginLoading: false,
registerLoading: false registerLoading: false
@ -97,13 +98,13 @@ export class Login extends Component<any, State> {
<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">
<input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} /> <input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} pattern="[a-zA-Z0-9_]+" />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Email</label> <label class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="email" class="form-control" value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> <input type="email" class="form-control" placeholder="Optional" value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -147,6 +148,7 @@ export class Login extends Component<any, State> {
} }
handleRegisterSubmit(i: Login, event: any) { handleRegisterSubmit(i: Login, event: any) {
event.preventDefault();
i.state.registerLoading = true; i.state.registerLoading = true;
i.setState(i.state); i.setState(i.state);
event.preventDefault(); event.preventDefault();

View file

@ -2,13 +2,15 @@ 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 } from '../interfaces'; import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { msgOp } from '../utils'; import { msgOp, repoUrl, mdToHtml } from '../utils';
interface State { interface State {
subscribedCommunities: Array<CommunityUser>; subscribedCommunities: Array<CommunityUser>;
trendingCommunities: Array<Community>;
site: GetSiteResponse;
loading: boolean; loading: boolean;
} }
@ -17,6 +19,22 @@ export class Main extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: State = { private emptyState: State = {
subscribedCommunities: [], subscribedCommunities: [],
trendingCommunities: [],
site: {
op: null,
site: {
id: null,
name: null,
creator_id: null,
creator_name: null,
published: null,
number_of_users: null,
number_of_posts: null,
number_of_comments: null,
},
admins: [],
banned: [],
},
loading: true loading: true
} }
@ -33,9 +51,18 @@ export class Main extends Component<any, State> {
() => console.log('complete') () => console.log('complete')
); );
if (UserService.Instance.loggedIn) { WebSocketService.Instance.getSite();
if (UserService.Instance.user) {
WebSocketService.Instance.getFollowedCommunities(); WebSocketService.Instance.getFollowedCommunities();
} }
let listCommunitiesForm: ListCommunitiesForm = {
sort: SortType[SortType.New],
limit: 8
}
WebSocketService.Instance.listCommunities(listCommunitiesForm);
} }
componentWillUnmount() { componentWillUnmount() {
@ -46,25 +73,25 @@ export class Main extends Component<any, State> {
return ( return (
<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-8">
<PostListings /> <PostListings />
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-4">
<h4>A Landing message</h4>
{UserService.Instance.loggedIn &&
<div>
{this.state.loading ? {this.state.loading ?
<h4 class="mt-3"><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div>
{this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div> <div>
<hr />
<h4>Subscribed forums</h4> <h4>Subscribed forums</h4>
<ul class="list-unstyled"> <ul class="list-inline">
{this.state.subscribedCommunities.map(community => {this.state.subscribedCommunities.map(community =>
<li><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>
)} )}
</ul> </ul>
</div> </div>
} }
{this.landing()}
</div> </div>
} }
</div> </div>
@ -73,6 +100,54 @@ export class Main extends Component<any, State> {
) )
} }
trendingCommunities() {
return (
<div>
<h4>Trending <Link class="text-white" to="/communities">forums</Link></h4>
<ul class="list-inline">
{this.state.trendingCommunities.map(community =>
<li class="list-inline-item"><Link to={`/community/${community.id}`}>{community.name}</Link></li>
)}
</ul>
</div>
)
}
landing() {
return (
<div>
<h4>{`${this.state.site.site.name}`}</h4>
<ul class="my-1 list-inline">
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_users} Users</li>
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li>
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_comments} Comments</li>
<li className="list-inline-item"><Link className="badge badge-light" to="/modlog">Modlog</Link></li>
</ul>
<ul class="list-inline small">
<li class="list-inline-item">admins: </li>
{this.state.site.admins.map(admin =>
<li class="list-inline-item"><Link class="text-info" to={`/user/${admin.id}`}>{admin.name}</Link></li>
)}
</ul>
{this.state.site.site.description &&
<div>
<hr />
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
<hr />
</div>
}
<h4>Welcome to
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
<a href={repoUrl}>Lemmy<sup>Beta</sup></a>
</h4>
<p>Lemmy is a <a href="https://en.wikipedia.org/wiki/Link_aggregation">link aggregator</a> / reddit alternative, intended to work in the <a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a>.</p>
<p>Its self-hostable, has live-updating comment threads, and is tiny (<code>~80kB</code>). Federation into the ActivityPub network is on the roadmap.</p>
<p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p>
<p>Suggest new features or report bugs <a href={repoUrl}>here.</a></p>
<p>Made with <a href="https://www.rust-lang.org">Rust</a>, <a href="https://actix.rs/">Actix</a>, <a href="https://www.infernojs.org">Inferno</a>, <a href="https://www.typescriptlang.org/">Typescript</a>.</p>
</div>
)
}
parseMessage(msg: any) { parseMessage(msg: any) {
console.log(msg); console.log(msg);
@ -85,6 +160,22 @@ export class Main extends Component<any, State> {
this.state.subscribedCommunities = res.communities; this.state.subscribedCommunities = res.communities;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg;
this.state.trendingCommunities = res.communities;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
// This means it hasn't been set up yet
if (!res.site) {
this.context.router.history.push("/setup");
}
this.state.site.admins = res.admins;
this.state.site.site = res.site;
this.state.site.banned = res.banned;
this.setState(this.state);
} }
} }
} }

View file

@ -0,0 +1,175 @@
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, GetModlogForm, GetModlogResponse, ModRemovePost, ModLockPost, ModRemoveComment, ModRemoveCommunity, ModBanFromCommunity, ModBan, ModAddCommunity, ModAdd } from '../interfaces';
import { WebSocketService } from '../services';
import { msgOp, addTypeInfo } from '../utils';
import { MomentTime } from './moment-time';
import * as moment from 'moment';
interface ModlogState {
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}>,
communityId?: number,
communityName?: string,
loading: boolean;
}
export class Modlog extends Component<any, ModlogState> {
private subscription: Subscription;
private emptyState: ModlogState = {
combined: [],
loading: true,
}
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.state.communityId = this.props.match.params.community_id ? Number(this.props.match.params.community_id) : undefined;
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')
);
let modlogForm: GetModlogForm = {
community_id: this.state.communityId
};
WebSocketService.Instance.getModlog(modlogForm);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
setCombined(res: GetModlogResponse) {
let removed_posts = addTypeInfo(res.removed_posts, "removed_posts");
let locked_posts = addTypeInfo(res.locked_posts, "locked_posts");
let removed_comments = addTypeInfo(res.removed_comments, "removed_comments");
let removed_communities = addTypeInfo(res.removed_communities, "removed_communities");
let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community");
let added_to_community = addTypeInfo(res.added_to_community, "added_to_community");
this.state.combined.push(...removed_posts);
this.state.combined.push(...locked_posts);
this.state.combined.push(...removed_comments);
this.state.combined.push(...removed_communities);
this.state.combined.push(...banned_from_community);
this.state.combined.push(...added_to_community);
if (this.state.communityId && this.state.combined.length > 0) {
this.state.communityName = this.state.combined[0].data.community_name;
}
// Sort them by time
this.state.combined.sort((a, b) => b.data.when_.localeCompare(a.data.when_));
this.setState(this.state);
}
combined() {
return (
<tbody>
{this.state.combined.map(i =>
<tr>
<td><MomentTime data={i.data} /></td>
<td><Link to={`/user/${i.data.mod_user_id}`}>{i.data.mod_user_name}</Link></td>
<td>
{i.type_ == 'removed_posts' &&
<>
{(i.data as ModRemovePost).removed? 'Removed' : 'Restored'}
<span> Post <Link to={`/post/${(i.data as ModRemovePost).post_id}`}>{(i.data as ModRemovePost).post_name}</Link></span>
<div>{(i.data as ModRemovePost).reason && ` reason: ${(i.data as ModRemovePost).reason}`}</div>
</>
}
{i.type_ == 'locked_posts' &&
<>
{(i.data as ModLockPost).locked? 'Locked' : 'Unlocked'}
<span> Post <Link to={`/post/${(i.data as ModLockPost).post_id}`}>{(i.data as ModLockPost).post_name}</Link></span>
</>
}
{i.type_ == 'removed_comments' &&
<>
{(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>
<div>{(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}</div>
</>
}
{i.type_ == 'removed_communities' &&
<>
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'}
<span> Community <Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
<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>
</>
}
{i.type_ == 'banned_from_community' &&
<>
<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>
<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>
</>
}
{i.type_ == 'added_to_community' &&
<>
<span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
<span><Link to={`/user/${(i.data as ModAddCommunity).other_user_id}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
<span> as a mod to the community </span>
<span><Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
</>
}
</td>
</tr>
)
}
</tbody>
);
}
render() {
return (
<div class="container">
{this.state.loading ?
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div>
<h4>
{this.state.communityName && <Link className="text-white" to={`/community/${this.state.communityId}`}>/f/{this.state.communityName} </Link>}
<span>Modlog</span>
</h4>
<div class="table-responsive">
<table id="modlog_table" class="table table-sm table-hover">
<thead class="pointer">
<tr>
<th>Time</th>
<th>Mod</th>
<th>Action</th>
</tr>
</thead>
{this.combined()}
</table>
</div>
</div>
}
</div>
);
}
parseMessage(msg: any) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(msg.error);
return;
} else if (op == UserOperation.GetModlog) {
let res: GetModlogResponse = msg;
this.state.loading = false;
this.setCombined(res);
}
}
}

View file

@ -3,7 +3,8 @@ import * as moment from 'moment';
interface MomentTimeProps { interface MomentTimeProps {
data: { data: {
published: string; published?: string;
when_?: string;
updated?: string; updated?: string;
} }
} }
@ -20,8 +21,9 @@ export class MomentTime extends Component<MomentTimeProps, any> {
<span title={this.props.data.updated} className="font-italics">modified {moment.utc(this.props.data.updated).fromNow()}</span> <span title={this.props.data.updated} className="font-italics">modified {moment.utc(this.props.data.updated).fromNow()}</span>
) )
} else { } else {
let str = this.props.data.published || this.props.data.when_;
return ( return (
<span title={this.props.data.published}>{moment.utc(this.props.data.published).fromNow()}</span> <span title={str}>{moment.utc(str).fromNow()}</span>
) )
} }
} }

View file

@ -1,6 +1,5 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { repoUrl } from '../utils';
import { UserService } from '../services'; import { UserService } from '../services';
import { version } from '../version'; import { version } from '../version';
@ -13,7 +12,7 @@ interface NavbarState {
export class Navbar extends Component<any, NavbarState> { export class Navbar extends Component<any, NavbarState> {
emptyState: NavbarState = { emptyState: NavbarState = {
isLoggedIn: UserService.Instance.loggedIn, isLoggedIn: UserService.Instance.user !== undefined,
expanded: false, expanded: false,
expandUserDropdown: false expandUserDropdown: false
} }
@ -25,7 +24,7 @@ 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 !== null; let loggedIn: boolean = user !== undefined;
this.setState({isLoggedIn: loggedIn}); this.setState({isLoggedIn: loggedIn});
}); });
} }
@ -40,7 +39,7 @@ export class Navbar extends Component<any, NavbarState> {
// TODO toggle css collapse // TODO toggle css collapse
navbar() { navbar() {
return ( return (
<nav class="navbar navbar-expand-sm navbar-light bg-light p-0 px-3 shadow"> <nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3">
<a title={version} class="navbar-brand" href="#"> <a title={version} class="navbar-brand" href="#">
<svg class="icon mr-2"><use xlinkHref="#icon-mouse"></use></svg> <svg class="icon mr-2"><use xlinkHref="#icon-mouse"></use></svg>
Lemmy Lemmy
@ -51,10 +50,10 @@ export class Navbar extends Component<any, NavbarState> {
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}> <div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href={repoUrl}>About</a> <Link class="nav-link" to="/communities">Forums</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/communities">Forums</Link> <Link class="nav-link" to="/modlog">Modlog</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/create_post">Create Post</Link> <Link class="nav-link" to="/create_post">Create Post</Link>
@ -74,7 +73,7 @@ export class Navbar extends Component<any, NavbarState> {
<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</Link> <Link class="nav-link" to="/login">Login / Sign up</Link>
} }
</ul> </ul>
</div> </div>

View file

@ -1,8 +1,8 @@
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 { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse } from '../interfaces'; import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
@ -26,7 +26,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
postForm: { postForm: {
name: null, name: null,
auth: null, auth: null,
community_id: null community_id: null,
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
}, },
communities: [], communities: [],
loading: false loading: false
@ -43,6 +44,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
name: this.props.post.name, name: this.props.post.name,
community_id: this.props.post.community_id, community_id: this.props.post.community_id,
edit_id: this.props.post.id, edit_id: this.props.post.id,
creator_id: this.props.post.creator_id,
url: this.props.post.url, url: this.props.post.url,
auth: null auth: null
} }
@ -56,7 +58,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
() => console.log('complete') () => console.log('complete')
); );
WebSocketService.Instance.listCommunities(); let listCommunitiesForm: ListCommunitiesForm = {
sort: SortType[SortType.TopAll]
}
WebSocketService.Instance.listCommunities(listCommunitiesForm);
} }
componentDidMount() { componentDidMount() {
@ -89,6 +95,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} /> <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} />
</div> </div>
</div> </div>
{/* Cant change a community from an edit */}
{!this.props.post &&
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Forum</label> <label class="col-sm-2 col-form-label">Forum</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -99,6 +107,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</select> </select>
</div> </div>
</div> </div>
}
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
@ -151,7 +160,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error);
this.state.loading = false; this.state.loading = false;
this.setState(this.state);
return; return;
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg; let res: ListCommunitiesResponse = msg;

View file

@ -8,6 +8,8 @@ import { mdToHtml } from '../utils';
interface PostListingState { interface PostListingState {
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean;
removeReason: string;
iframeExpanded: boolean; iframeExpanded: boolean;
} }
@ -23,6 +25,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
private emptyState: PostListingState = { private emptyState: PostListingState = {
showEdit: false, showEdit: false,
showRemoveDialog: false,
removeReason: null,
iframeExpanded: false iframeExpanded: false
} }
@ -59,7 +63,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div className="ml-4"> <div className="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}>{post.name}</a></h4> <h4 className="d-inline"><a className="text-white" href={post.url}>{post.name}</a>
{post.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
{post.locked &&
<small className="ml-2 text-muted font-italic">locked</small>
}
</h4>
<small><a className="ml-2 text-muted font-italic" href={post.url}>{(new URL(post.url)).hostname}</a></small> <small><a className="ml-2 text-muted font-italic" href={post.url}>{(new URL(post.url)).hostname}</a></small>
{ !this.state.iframeExpanded { !this.state.iframeExpanded
? <span class="pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span> ? <span class="pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
@ -72,7 +83,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></h4> : <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link>
{post.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
{post.locked &&
<small className="ml-2 text-muted font-italic">locked</small>
}
</h4>
} }
</div> </div>
<div className="details ml-4 mb-1"> <div className="details ml-4 mb-1">
@ -102,16 +120,39 @@ 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.myPost && {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">
{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"> <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 &&
<span>
<li className="list-inline-item">
{!this.props.post.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
}
</li>
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{this.props.post.locked ? 'unlock' : 'lock'}</span>
</li>
</span>
}
</ul> </ul>
} }
{this.state.showRemoveDialog &&
<form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
<button type="submit" class="btn btn-secondary">Remove Post</button>
</form>
}
{this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />} {this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />}
</div> </div>
</div> </div>
@ -119,7 +160,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
private get myPost(): boolean { private get myPost(): boolean {
return this.props.editable && UserService.Instance.loggedIn && this.props.post.creator_id == UserService.Instance.user.id; return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id;
} }
handlePostLike(i: PostListing) { handlePostLike(i: PostListing) {
@ -162,11 +203,51 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
name: "deleted", name: "deleted",
url: '', url: '',
edit_id: i.props.post.id, edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
auth: null auth: null
}; };
WebSocketService.Instance.editPost(deleteForm); WebSocketService.Instance.editPost(deleteForm);
} }
handleModRemoveShow(i: PostListing) {
i.state.showRemoveDialog = true;
i.setState(i.state);
}
handleModRemoveReasonChange(i: PostListing, event: any) {
i.state.removeReason = event.target.value;
i.setState(i.state);
}
handleModRemoveSubmit(i: PostListing) {
event.preventDefault();
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
reason: i.state.removeReason,
auth: null,
};
WebSocketService.Instance.editPost(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handleModLock(i: PostListing) {
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
locked: !i.props.post.locked,
auth: null,
};
WebSocketService.Instance.editPost(form);
}
handleIframeExpandClick(i: PostListing) { handleIframeExpandClick(i: PostListing) {
i.state.iframeExpanded = !i.state.iframeExpanded; i.state.iframeExpanded = !i.state.iframeExpanded;
i.setState(i.state); i.setState(i.state);

View file

@ -43,7 +43,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
sortType: SortType.Hot, sortType: SortType.Hot,
type_: this.props.communityId type_: this.props.communityId
? ListingType.Community ? ListingType.Community
: UserService.Instance.loggedIn : UserService.Instance.user
? ListingType.Subscribed ? ListingType.Subscribed
: ListingType.All, : ListingType.All,
loading: true loading: true
@ -86,7 +86,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
{this.state.posts.length > 0 {this.state.posts.length > 0
? this.state.posts.map(post => ? this.state.posts.map(post =>
<PostListing post={post} showCommunity={!this.props.communityId}/>) <PostListing post={post} showCommunity={!this.props.communityId}/>)
: <div>No Listings. Subscribe to some <Link to="/communities">forums</Link>.</div> : <div>No Listings. {!this.props.communityId && <span>Subscribe to some <Link to="/communities">forums</Link>.</span>}</div>
} }
</div> </div>
} }
@ -109,7 +109,7 @@ export class PostListings extends Component<PostListingsProps, PostListingsState
<option value={SortType.TopAll}>All</option> <option value={SortType.TopAll}>All</option>
</select> </select>
{!this.props.communityId && {!this.props.communityId &&
UserService.Instance.loggedIn && UserService.Instance.user &&
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto"> <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
<option disabled>Type</option> <option disabled>Type</option>
<option value={ListingType.All}>All</option> <option value={ListingType.All}>All</option>

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 } from '../interfaces'; import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, AddModToCommunityResponse } 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';
@ -79,14 +79,14 @@ export class Post extends Component<any, PostState> {
{this.state.loading ? {this.state.loading ?
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
<div class="row"> <div class="row">
<div class="col-12 col-sm-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 />
<div className="mb-2" /> <div className="mb-2" />
<CommentForm postId={this.state.post.id} /> <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
{this.sortRadios()} {this.sortRadios()}
{this.commentsTree()} {this.commentsTree()}
</div> </div>
<div class="col-12 col-sm-4 col-lg-3 mb-3"> <div class="col-12 col-md-4 col-lg-3 mb-3 d-none d-md-block">
{this.state.comments.length > 0 && this.newComments()} {this.state.comments.length > 0 && this.newComments()}
</div> </div>
<div class="col-12 col-sm-12 col-lg-2"> <div class="col-12 col-sm-12 col-lg-2">
@ -101,17 +101,17 @@ export class Post extends Component<any, PostState> {
sortRadios() { sortRadios() {
return ( return (
<div class="btn-group btn-group-toggle mb-3"> <div class="btn-group btn-group-toggle mb-3">
<label className={`btn btn-sm btn-secondary ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>Hot <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>Hot
<input type="radio" value={CommentSortType.Hot} <input type="radio" value={CommentSortType.Hot}
checked={this.state.commentSort === CommentSortType.Hot} checked={this.state.commentSort === CommentSortType.Hot}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)} />
</label> </label>
<label className={`btn btn-sm btn-secondary ${this.state.commentSort === CommentSortType.Top && 'active'}`}>Top <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>Top
<input type="radio" value={CommentSortType.Top} <input type="radio" value={CommentSortType.Top}
checked={this.state.commentSort === CommentSortType.Top} checked={this.state.commentSort === CommentSortType.Top}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)} />
</label> </label>
<label className={`btn btn-sm btn-secondary ${this.state.commentSort === CommentSortType.New && 'active'}`}>New <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>New
<input type="radio" value={CommentSortType.New} <input type="radio" value={CommentSortType.New}
checked={this.state.commentSort === CommentSortType.New} checked={this.state.commentSort === CommentSortType.New}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)} />
@ -125,7 +125,7 @@ export class Post extends Component<any, PostState> {
<div class="sticky-top"> <div class="sticky-top">
<h4>New Comments</h4> <h4>New Comments</h4>
{this.state.comments.map(comment => {this.state.comments.map(comment =>
<CommentNodes nodes={[{comment: comment}]} noIndent /> <CommentNodes nodes={[{comment: comment}]} noIndent locked={this.state.post.locked} moderators={this.state.moderators} />
)} )}
</div> </div>
) )
@ -188,7 +188,7 @@ export class Post extends Component<any, PostState> {
let nodes = this.buildCommentsTree(); let nodes = this.buildCommentsTree();
return ( return (
<div className=""> <div className="">
<CommentNodes nodes={nodes} /> <CommentNodes nodes={nodes} locked={this.state.post.locked} moderators={this.state.moderators} />
</div> </div>
); );
} }
@ -216,6 +216,11 @@ export class Post extends Component<any, PostState> {
let found = this.state.comments.find(c => c.id == res.comment.id); let found = this.state.comments.find(c => c.id == res.comment.id);
found.content = res.comment.content; found.content = res.comment.content;
found.updated = res.comment.updated; found.updated = res.comment.updated;
found.removed = res.comment.removed;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
found.score = res.comment.score;
this.setState(this.state); this.setState(this.state);
} }
else if (op == UserOperation.CreateCommentLike) { else if (op == UserOperation.CreateCommentLike) {
@ -249,6 +254,15 @@ export class Post extends Component<any, PostState> {
this.state.community.subscribed = res.community.subscribed; this.state.community.subscribed = res.community.subscribed;
this.state.community.number_of_subscribers = res.community.number_of_subscribers; this.state.community.number_of_subscribers = res.community.number_of_subscribers;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanFromCommunity) {
let res: BanFromCommunityResponse = msg;
this.state.comments.filter(c => c.creator_id == res.user.id)
.forEach(c => c.banned = res.banned);
this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) {
let res: AddModToCommunityResponse = msg;
this.state.moderators = res.moderators;
this.setState(this.state);
} }
} }

147
ui/src/components/setup.tsx Normal file
View file

@ -0,0 +1,147 @@
import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
import { SiteForm } from './site-form';
interface State {
userForm: RegisterForm;
doneRegisteringUser: boolean;
userLoading: boolean;
}
export class Setup extends Component<any, State> {
private subscription: Subscription;
private emptyState: State = {
userForm: {
username: undefined,
password: undefined,
password_verify: undefined,
admin: true,
},
doneRegisteringUser: false,
userLoading: false,
}
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")
);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<div class="container">
<div class="row">
<div class="col-12 offset-lg-3 col-lg-6">
<h3>Lemmy Instance Setup</h3>
{!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />}
</div>
</div>
</div>
)
}
registerUser() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h4>Set up Site Administrator</h4>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" value={this.state.userForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} pattern="[a-zA-Z0-9_]+" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="email" class="form-control" placeholder="Optional" value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Verify Password</label>
<div class="col-sm-10">
<input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.userLoading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button>
</div>
</div>
</form>
);
}
handleRegisterSubmit(i: Setup, event: any) {
event.preventDefault();
i.state.userLoading = true;
i.setState(i.state);
event.preventDefault();
WebSocketService.Instance.register(i.state.userForm);
}
handleRegisterUsernameChange(i: Setup, event: any) {
i.state.userForm.username = event.target.value;
i.setState(i.state);
}
handleRegisterEmailChange(i: Setup, event: any) {
i.state.userForm.email = event.target.value;
i.setState(i.state);
}
handleRegisterPasswordChange(i: Setup, event: any) {
i.state.userForm.password = event.target.value;
i.setState(i.state);
}
handleRegisterPasswordVerifyChange(i: Setup, event: any) {
i.state.userForm.password_verify = event.target.value;
i.setState(i.state);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(msg.error);
this.state.userLoading = false;
this.setState(this.state);
return;
} else if (op == UserOperation.Register) {
this.state.userLoading = false;
this.state.doneRegisteringUser = true;
let res: LoginResponse = msg;
UserService.Instance.login(res);
console.log(res);
this.setState(this.state);
} else if (op == UserOperation.CreateSite) {
this.props.history.push('/');
}
}
}

View file

@ -1,8 +1,8 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Community, CommunityUser, FollowCommunityForm } from '../interfaces'; import { Community, CommunityUser, FollowCommunityForm, CommunityForm as CommunityFormI } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { mdToHtml } from '../utils'; import { mdToHtml, getUnixTime } from '../utils';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
interface SidebarProps { interface SidebarProps {
@ -12,12 +12,18 @@ interface SidebarProps {
interface SidebarState { interface SidebarState {
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean;
removeReason: string;
removeExpires: string;
} }
export class Sidebar extends Component<SidebarProps, SidebarState> { export class Sidebar extends Component<SidebarProps, SidebarState> {
private emptyState: SidebarState = { private emptyState: SidebarState = {
showEdit: false showEdit: false,
showRemoveDialog: false,
removeReason: null,
removeExpires: null
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -42,9 +48,13 @@ 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}</h4> <h4 className="mb-0">{community.title}
{community.removed &&
<small className="ml-2 text-muted font-italic">removed</small>
}
</h4>
<Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link> <Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
{this.amMod && {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">
<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>
@ -54,13 +64,41 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */} {/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */}
</li> </li>
} }
<li className="list-inline-item">
{!this.props.community.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
}
</li>
</ul> </ul>
} }
<ul class="list-inline"> {this.state.showRemoveDialog &&
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<div class="form-group row">
<label class="col-form-label">Reason</label>
<input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
</div>
<div class="form-group row">
<label class="col-form-label">Expires</label>
<input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} />
</div>
<div class="form-group row">
<button type="submit" class="btn btn-secondary">Remove Community</button>
</div>
</form>
}
<ul class="my-1 list-inline">
<li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li> <li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li>
<li className="list-inline-item badge badge-light">{community.number_of_subscribers} Subscribers</li> <li className="list-inline-item badge badge-light">{community.number_of_subscribers} Subscribers</li>
<li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li> <li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li>
<li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li> <li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li>
<li className="list-inline-item"><Link className="badge badge-light" to={`/modlog/community/${this.props.community.id}`}>Modlog</Link></li>
</ul>
<ul class="list-inline small">
<li class="list-inline-item">mods: </li>
{this.props.moderators.map(mod =>
<li class="list-inline-item"><Link class="text-info" to={`/user/${mod.user_id}`}>{mod.user_name}</Link></li>
)}
</ul> </ul>
<div> <div>
{community.subscribed {community.subscribed
@ -72,13 +110,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div> <div>
<hr /> <hr />
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} /> <div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
<hr />
</div> </div>
} }
<hr />
<h4>Moderators</h4>
{this.props.moderators.map(mod =>
<Link to={`/user/${mod.user_id}`}>{mod.user_name}</Link>
)}
</div> </div>
); );
} }
@ -119,13 +153,51 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
private get amCreator(): boolean { private get amCreator(): boolean {
return UserService.Instance.loggedIn && this.props.community.creator_id == UserService.Instance.user.id; return this.props.community.creator_id == UserService.Instance.user.id;
} }
private get amMod(): boolean { // private get amMod(): boolean {
console.log(this.props.moderators); // return UserService.Instance.loggedIn &&
console.log(this.props); // this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id);
return UserService.Instance.loggedIn && // }
this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id);
handleDeleteClick() {
} }
handleModRemoveShow(i: Sidebar) {
i.state.showRemoveDialog = true;
i.setState(i.state);
}
handleModRemoveReasonChange(i: Sidebar, event: any) {
i.state.removeReason = event.target.value;
i.setState(i.state);
}
handleModRemoveExpiresChange(i: Sidebar, event: any) {
console.log(event.target.value);
i.state.removeExpires = event.target.value;
i.setState(i.state);
}
handleModRemoveSubmit(i: Sidebar) {
event.preventDefault();
let deleteForm: CommunityFormI = {
name: i.props.community.name,
title: i.props.community.title,
category_id: i.props.community.category_id,
edit_id: i.props.community.id,
removed: !i.props.community.removed,
reason: i.state.removeReason,
expires: getUnixTime(i.state.removeExpires),
auth: null,
};
WebSocketService.Instance.editCommunity(deleteForm);
i.state.showRemoveDialog = false;
i.setState(i.state);
}
} }

View file

@ -0,0 +1,86 @@
import { Component, linkEvent } from 'inferno';
import { Site, SiteForm as SiteFormI } from '../interfaces';
import { WebSocketService } from '../services';
import * as autosize from 'autosize';
interface SiteFormProps {
site?: Site; // If a site is given, that means this is an edit
onCancel?(): any;
}
interface SiteFormState {
siteForm: SiteFormI;
loading: boolean;
}
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private emptyState: SiteFormState ={
siteForm: {
name: null
},
loading: false
}
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
}
componentDidMount() {
autosize(document.querySelectorAll('textarea'));
}
render() {
return (
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
<h4>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h4>
<div class="form-group row">
<label class="col-12 col-form-label">Name</label>
<div class="col-12">
<input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} />
</div>
</div>
<div class="form-group row">
<label class="col-12 col-form-label">Sidebar</label>
<div class="col-12">
<textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} />
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
this.props.site ? 'Save' : 'Create'}</button>
{this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
</div>
</div>
</form>
);
}
handleCreateSiteSubmit(i: SiteForm, event: any) {
event.preventDefault();
i.state.loading = true;
if (i.props.site) {
WebSocketService.Instance.editSite(i.state.siteForm);
} else {
WebSocketService.Instance.createSite(i.state.siteForm);
}
i.setState(i.state);
}
handleSiteNameChange(i: SiteForm, event: any) {
i.state.siteForm.name = event.target.value;
i.setState(i.state);
}
handleSiteDescriptionChange(i: SiteForm, event: any) {
i.state.siteForm.description = event.target.value;
i.setState(i.state);
}
handleCancel(i: SiteForm) {
i.props.onCancel();
}
}

View file

@ -125,24 +125,27 @@ export class User extends Component<any, UserState> {
} }
overview() { overview() {
let combined: Array<any> = []; let combined: Array<{type_: string, data: Comment | Post}> = [];
combined.push(...this.state.comments); let comments = this.state.comments.map(e => {return {type_: "comments", data: e}});
combined.push(...this.state.posts); let posts = this.state.posts.map(e => {return {type_: "posts", data: e}});
combined.push(...comments);
combined.push(...posts);
// Sort it // Sort it
if (this.state.sort == SortType.New) { if (this.state.sort == SortType.New) {
combined.sort((a, b) => b.published.localeCompare(a.published)); combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
} else { } else {
combined.sort((a, b) => b.score - a.score); combined.sort((a, b) => b.data.score - a.data.score);
} }
return ( return (
<div> <div>
{combined.map(i => {combined.map(i =>
<div> <div>
{i.community_id {i.type_ == "posts"
? <PostListing post={i} showCommunity viewOnly /> ? <PostListing post={i.data as Post} showCommunity viewOnly />
: <CommentNodes nodes={[{comment: i}]} noIndent viewOnly /> : <CommentNodes nodes={[{comment: i.data as Comment}]} noIndent viewOnly />
} }
</div> </div>
) )

View file

@ -1,2 +1,4 @@
export const endpoint = `${window.location.hostname}:8536`; let host = `${window.location.hostname}`;
export let wsUri = (window.location.protocol=='https:') ? 'wss://' : 'ws://' + endpoint + '/service/ws'; let port = `${window.location.port == "4444" ? '8536' : window.location.port}`;
let endpoint = `${host}:${port}`;
export let wsUri = `${(window.location.protocol=='https:') ? 'wss://' : 'ws://'}${endpoint}/service/ws`;

View file

@ -2,6 +2,7 @@ import { render, Component } from 'inferno';
import { HashRouter, Route, Switch } from 'inferno-router'; import { HashRouter, Route, Switch } from 'inferno-router';
import { Navbar } from './components/navbar'; import { Navbar } from './components/navbar';
import { Footer } from './components/footer';
import { Home } from './components/home'; import { Home } from './components/home';
import { Login } from './components/login'; import { Login } from './components/login';
import { CreatePost } from './components/create-post'; import { CreatePost } from './components/create-post';
@ -10,6 +11,8 @@ import { Post } from './components/post';
import { Community } from './components/community'; import { Community } from './components/community';
import { Communities } from './components/communities'; import { Communities } from './components/communities';
import { User } from './components/user'; import { User } from './components/user';
import { Modlog } from './components/modlog';
import { Setup } from './components/setup';
import { Symbols } from './components/symbols'; import { Symbols } from './components/symbols';
import './main.css'; import './main.css';
@ -42,9 +45,13 @@ 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={`/modlog/community/:community_id`} component={Modlog} />
<Route path={`/modlog`} component={Modlog} />
<Route path={`/setup`} component={Setup} />
</Switch> </Switch>
<Symbols /> <Symbols />
</div> </div>
<Footer />
</HashRouter> </HashRouter>
); );
} }

View file

@ -1,5 +1,17 @@
export enum UserOperation { export enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails 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
}
export enum CommentSortType {
Hot, Top, New
}
export enum ListingType {
All, Subscribed, Community
}
export enum SortType {
Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
} }
export interface User { export interface User {
@ -31,6 +43,8 @@ export interface CommunityUser {
export interface Community { export interface Community {
user_id?: number; user_id?: number;
subscribed?: boolean; subscribed?: boolean;
am_mod?: boolean;
removed?: boolean;
id: number; id: number;
name: string; name: string;
title: string; title: string;
@ -46,40 +60,12 @@ export interface Community {
updated?: string; updated?: string;
} }
export interface CommunityForm {
name: string;
title: string;
description?: string,
category_id: number,
edit_id?: number;
auth?: string;
}
export interface GetCommunityResponse {
op: string;
community: Community;
moderators: Array<CommunityUser>;
}
export interface CommunityResponse {
op: string;
community: Community;
}
export interface ListCommunitiesResponse {
op: string;
communities: Array<Community>;
}
export interface ListCategoriesResponse {
op: string;
categories: Array<Category>;
}
export interface Post { export interface Post {
user_id?: number; user_id?: number;
my_vote?: number; my_vote?: number;
am_mod?: boolean;
removed?: boolean;
locked?: boolean;
id: number; id: number;
name: string; name: string;
url?: string; url?: string;
@ -97,6 +83,279 @@ export interface Post {
updated?: string; updated?: string;
} }
export interface Comment {
id: number;
content: string;
creator_id: number;
creator_name: string;
post_id: number,
community_id: number,
parent_id?: number;
published: string;
updated?: string;
score: number;
upvotes: number;
downvotes: number;
my_vote?: number;
am_mod?: boolean;
removed?: boolean;
banned?: boolean;
}
export interface Category {
id: number;
name: string;
}
export interface Site {
id: number;
name: string;
description?: string;
creator_id: number;
published: string;
updated?: string;
creator_name: string;
number_of_users: number;
number_of_posts: number;
number_of_comments: number;
}
export interface FollowCommunityForm {
community_id: number;
follow: boolean;
auth?: string;
}
export interface GetFollowedCommunitiesResponse {
op: string;
communities: Array<CommunityUser>;
}
export interface GetUserDetailsForm {
user_id: number;
sort: string; // TODO figure this one out
limit: number;
community_id?: number;
auth?: string;
}
export interface UserDetailsResponse {
op: string;
user: UserView;
follows: Array<CommunityUser>;
moderates: Array<CommunityUser>;
comments: Array<Comment>;
posts: Array<Post>;
saved?: Array<Post>;
}
export interface BanFromCommunityForm {
community_id: number;
user_id: number;
ban: boolean;
reason?: string,
expires?: number,
auth?: string;
}
export interface BanFromCommunityResponse {
op: string;
user: UserView,
banned: boolean,
}
export interface AddModToCommunityForm {
community_id: number;
user_id: number;
added: boolean;
auth?: string;
}
export interface AddModToCommunityResponse {
op: string;
moderators: Array<CommunityUser>;
}
export interface GetModlogForm {
mod_user_id?: number;
community_id?: number;
limit?: number;
page?: number;
}
export interface GetModlogResponse {
op: string;
removed_posts: Array<ModRemovePost>,
locked_posts: Array<ModLockPost>,
removed_comments: Array<ModRemoveComment>,
removed_communities: Array<ModRemoveCommunity>,
banned_from_community: Array<ModBanFromCommunity>,
banned: Array<ModBan>,
added_to_community: Array<ModAddCommunity>,
added: Array<ModAdd>,
}
export interface ModRemovePost {
id: number;
mod_user_id: number;
post_id: number;
reason?: string;
removed?: boolean;
when_: string
mod_user_name: string;
post_name: string;
community_id: number;
community_name: string;
}
export interface ModLockPost {
id: number,
mod_user_id: number,
post_id: number,
locked?: boolean,
when_: string,
mod_user_name: string,
post_name: string,
community_id: number,
community_name: string,
}
export interface ModRemoveComment {
id: number,
mod_user_id: number,
comment_id: number,
reason?: string,
removed?: boolean,
when_: string,
mod_user_name: string,
comment_user_id: number,
comment_user_name: string,
comment_content: string,
post_id: number,
post_name: string,
community_id: number,
community_name: string,
}
export interface ModRemoveCommunity {
id: number,
mod_user_id: number,
community_id: number,
reason?: string,
removed?: boolean,
expires?: number,
when_: string,
mod_user_name: string,
community_name: string,
}
export interface ModBanFromCommunity {
id: number,
mod_user_id: number,
other_user_id: number,
community_id: number,
reason?: string,
banned?: boolean,
expires?: number,
when_: string,
mod_user_name: string,
other_user_name: string,
community_name: string,
}
export interface ModBan {
id: number,
mod_user_id: number,
other_user_id: number,
reason?: string,
banned?: boolean,
expires?: number,
when_: string,
mod_user_name: string,
other_user_name: string,
}
export interface ModAddCommunity {
id: number,
mod_user_id: number,
other_user_id: number,
community_id: number,
removed?: boolean,
when_: string,
mod_user_name: string,
other_user_name: string,
community_name: string,
}
export interface ModAdd {
id: number,
mod_user_id: number,
other_user_id: number,
removed?: boolean,
when_: string,
mod_user_name: string,
other_user_name: string,
}
export interface LoginForm {
username_or_email: string;
password: string;
}
export interface RegisterForm {
username: string;
email?: string;
password: string;
password_verify: string;
admin: boolean;
}
export interface LoginResponse {
op: string;
jwt: string;
}
export interface CommunityForm {
name: string;
title: string;
description?: string,
category_id: number,
edit_id?: number;
removed?: boolean;
reason?: string;
expires?: number;
auth?: string;
}
export interface GetCommunityResponse {
op: string;
community: Community;
moderators: Array<CommunityUser>;
}
export interface CommunityResponse {
op: string;
community: Community;
}
export interface ListCommunitiesForm {
sort: string;
limit?: number;
auth?: string;
}
export interface ListCommunitiesResponse {
op: string;
communities: Array<Community>;
}
export interface ListCategoriesResponse {
op: string;
categories: Array<Category>;
}
export interface PostForm { export interface PostForm {
name: string; name: string;
url?: string; url?: string;
@ -104,6 +363,10 @@ export interface PostForm {
community_id: number; community_id: number;
updated?: number; updated?: number;
edit_id?: number; edit_id?: number;
creator_id: number;
removed?: boolean;
reason?: string;
locked?: boolean;
auth: string; auth: string;
} }
@ -120,26 +383,14 @@ export interface PostResponse {
post: Post; post: Post;
} }
export interface Comment {
id: number;
content: string;
creator_id: number;
creator_name: string;
post_id: number,
parent_id?: number;
published: string;
updated?: string;
score: number;
upvotes: number;
downvotes: number;
my_vote?: number;
}
export interface CommentForm { export interface CommentForm {
content: string; content: string;
post_id: number; post_id: number;
parent_id?: number; parent_id?: number;
edit_id?: number; edit_id?: number;
creator_id: number;
removed?: boolean;
reason?: string;
auth: string; auth: string;
} }
@ -184,70 +435,49 @@ export interface CreatePostLikeResponse {
post: Post; post: Post;
} }
export interface Category { export interface SiteForm {
id: number;
name: string; name: string;
} description?: string,
removed?: boolean;
export interface FollowCommunityForm { reason?: string;
community_id: number; expires?: number;
follow: boolean;
auth?: string; auth?: string;
} }
export interface GetFollowedCommunitiesResponse { export interface GetSiteResponse {
op: string; op: string;
communities: Array<CommunityUser>; site: Site;
admins: Array<UserView>;
banned: Array<UserView>;
} }
export interface GetUserDetailsForm {
export interface SiteResponse {
op: string;
site: Site;
}
export interface BanUserForm {
user_id: number; user_id: number;
sort: string; // TODO figure this one out ban: boolean;
limit: number; reason?: string,
community_id?: number; expires?: number,
auth?: string; auth?: string;
} }
export interface BanUserResponse {
export interface UserDetailsResponse {
op: string; op: string;
user: UserView; user: UserView,
follows: Array<CommunityUser>; banned: boolean,
moderates: Array<CommunityUser>;
comments: Array<Comment>;
posts: Array<Post>;
saved?: Array<Post>;
} }
export interface AddAdminForm {
export interface LoginForm { user_id: number;
username_or_email: string; added: boolean;
password: string; auth?: string;
} }
export interface RegisterForm { export interface AddAdminResponse {
username: string;
email?: string;
password: string;
password_verify: string;
}
export interface LoginResponse {
op: string; op: string;
jwt: string; admins: Array<UserView>;
} }
export enum CommentSortType {
Hot, Top, New
}
export enum ListingType {
All, Subscribed, Community
}
export enum SortType {
Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
}

View file

@ -24,6 +24,11 @@ body {
color: #fff; color: #fff;
} }
.form-control:disabled {
background-color: var(--secondary);
opacity: .5;
}
.custom-select { .custom-select {
color: #fff; color: #fff;
background-color: var(--secondary); background-color: var(--secondary);
@ -66,3 +71,12 @@ body {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); } 100% { transform: rotate(359deg); }
} }
.dropdown-menu {
z-index: 2000;
}
.navbar-bg {
background-color: #222;
}

View file

@ -25,14 +25,10 @@ export class UserService {
} }
public logout() { public logout() {
this.user = null; this.user = undefined;
Cookies.remove("jwt"); Cookies.remove("jwt");
console.log("Logged out."); console.log("Logged out.");
this.sub.next(null); this.sub.next(undefined);
}
public get loggedIn(): boolean {
return this.user !== undefined;
} }
public get auth(): string { public get auth(): string {

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 } from '../interfaces'; import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, SiteForm, Site, UserView } 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';
@ -8,11 +8,16 @@ import { UserService } from './';
export class WebSocketService { export class WebSocketService {
private static _instance: WebSocketService; private static _instance: WebSocketService;
public subject: Subject<any>; public subject: Subject<any>;
public instanceName: string;
public site: Site;
public admins: Array<UserView>;
public banned: Array<UserView>;
private constructor() { private constructor() {
this.subject = webSocket(wsUri); this.subject = webSocket(wsUri);
// Even tho this isn't used, its necessary to not keep reconnecting // Necessary to not keep reconnecting
this.subject this.subject
.pipe(retryWhen(errors => errors.pipe(delay(60000), take(999)))) .pipe(retryWhen(errors => errors.pipe(delay(60000), take(999))))
.subscribe(); .subscribe();
@ -47,9 +52,9 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm)); this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm));
} }
public listCommunities() { public listCommunities(form: ListCommunitiesForm) {
let data = {auth: UserService.Instance.auth }; this.setAuth(form, false);
this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, data)); this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, form));
} }
public getFollowedCommunities() { public getFollowedCommunities() {
@ -106,11 +111,38 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm)); this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm));
} }
public banFromCommunity(form: BanFromCommunityForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.BanFromCommunity, form));
}
public addModToCommunity(form: AddModToCommunityForm) {
this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
}
public getUserDetails(form: GetUserDetailsForm) { public getUserDetails(form: GetUserDetailsForm) {
this.setAuth(form, false); this.setAuth(form, false);
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form)); this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
} }
public getModlog(form: GetModlogForm) {
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
}
public createSite(siteForm: SiteForm) {
this.setAuth(siteForm);
this.subject.next(this.wsSendWrapper(UserOperation.CreateSite, siteForm));
}
public editSite(siteForm: SiteForm) {
this.setAuth(siteForm);
this.subject.next(this.wsSendWrapper(UserOperation.EditSite, siteForm));
}
public getSite() {
this.subject.next(this.wsSendWrapper(UserOperation.GetSite, {}));
}
private wsSendWrapper(op: UserOperation, data: any) { private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data }; let send = { op: UserOperation[op], data: data };
console.log(send); console.log(send);
@ -124,7 +156,6 @@ export class WebSocketService {
throw "Not logged in"; throw "Not logged in";
} }
} }
} }
window.onbeforeunload = (() => { window.onbeforeunload = (() => {

View file

@ -31,3 +31,11 @@ export function hotRank(comment: Comment): number {
export function mdToHtml(text: string) { export function mdToHtml(text: string) {
return {__html: md.render(text)}; return {__html: md.render(text)};
} }
export function getUnixTime(text: string): number {
return text ? new Date(text).getTime()/1000 : undefined;
}
export function addTypeInfo<T>(arr: Array<T>, name: string): Array<{type_: string, data: T}> {
return arr.map(e => {return {type_: name, data: e}});
}

1
ui/src/version.ts Normal file
View file

@ -0,0 +1 @@
export let version: string = "v0.0.2-13-g1bf0dfd";