Merge branch 'dev' into moderation

This commit is contained in:
Dessalines 2019-04-11 07:15:22 -07:00
commit 1bf0dfde20
22 changed files with 251 additions and 86 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

@ -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

@ -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) {
@ -83,17 +84,27 @@ 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())
}
}
_ => ()
};
if let Some(limit) = limit {
query = query.limit(limit);
}; };
query.load::<Self>(conn) query.load::<Self>(conn)

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;
@ -89,21 +89,42 @@ pub fn naive_now() -> NaiveDateTime {
} }
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

@ -10,7 +10,7 @@ use serde_json::{Value};
use bcrypt::{verify}; use bcrypt::{verify};
use std::str::FromStr; use std::str::FromStr;
use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now, SortType}; use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now, SortType, has_slurs, remove_slurs};
use actions::community::*; use actions::community::*;
use actions::user::*; use actions::user::*;
use actions::post::*; use actions::post::*;
@ -111,6 +111,8 @@ pub struct CommunityResponse {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct ListCommunities { pub struct ListCommunities {
sort: String,
limit: Option<i64>,
auth: Option<String> auth: Option<String>
} }
@ -391,25 +393,25 @@ impl Handler<StandardMessage> for ChatServer {
let json: Value = serde_json::from_str(&msg.msg) let json: Value = serde_json::from_str(&msg.msg)
.expect("Couldn't parse message"); .expect("Couldn't parse message");
let data: &Value = &json["data"]; let data = &json["data"].to_string();
let op = &json["op"].as_str().unwrap(); let op = &json["op"].as_str().unwrap();
let user_operation: UserOperation = UserOperation::from_str(&op).unwrap(); let user_operation: UserOperation = UserOperation::from_str(&op).unwrap();
let res: String = match user_operation { let res: String = match user_operation {
UserOperation::Login => { UserOperation::Login => {
let login: Login = serde_json::from_str(&data.to_string()).unwrap(); let login: Login = serde_json::from_str(data).unwrap();
login.perform(self, msg.id) login.perform(self, msg.id)
}, },
UserOperation::Register => { UserOperation::Register => {
let register: Register = serde_json::from_str(&data.to_string()).unwrap(); let register: Register = serde_json::from_str(data).unwrap();
register.perform(self, msg.id) register.perform(self, msg.id)
}, },
UserOperation::CreateCommunity => { UserOperation::CreateCommunity => {
let create_community: CreateCommunity = serde_json::from_str(&data.to_string()).unwrap(); let create_community: CreateCommunity = serde_json::from_str(data).unwrap();
create_community.perform(self, msg.id) create_community.perform(self, msg.id)
}, },
UserOperation::ListCommunities => { UserOperation::ListCommunities => {
let list_communities: ListCommunities = serde_json::from_str(&data.to_string()).unwrap(); let list_communities: ListCommunities = serde_json::from_str(data).unwrap();
list_communities.perform(self, msg.id) list_communities.perform(self, msg.id)
}, },
UserOperation::ListCategories => { UserOperation::ListCategories => {
@ -417,55 +419,55 @@ impl Handler<StandardMessage> for ChatServer {
list_categories.perform(self, msg.id) list_categories.perform(self, msg.id)
}, },
UserOperation::CreatePost => { UserOperation::CreatePost => {
let create_post: CreatePost = serde_json::from_str(&data.to_string()).unwrap(); let create_post: CreatePost = serde_json::from_str(data).unwrap();
create_post.perform(self, msg.id) create_post.perform(self, msg.id)
}, },
UserOperation::GetPost => { UserOperation::GetPost => {
let get_post: GetPost = serde_json::from_str(&data.to_string()).unwrap(); let get_post: GetPost = serde_json::from_str(data).unwrap();
get_post.perform(self, msg.id) get_post.perform(self, msg.id)
}, },
UserOperation::GetCommunity => { UserOperation::GetCommunity => {
let get_community: GetCommunity = serde_json::from_str(&data.to_string()).unwrap(); let get_community: GetCommunity = serde_json::from_str(data).unwrap();
get_community.perform(self, msg.id) get_community.perform(self, msg.id)
}, },
UserOperation::CreateComment => { UserOperation::CreateComment => {
let create_comment: CreateComment = serde_json::from_str(&data.to_string()).unwrap(); let create_comment: CreateComment = serde_json::from_str(data).unwrap();
create_comment.perform(self, msg.id) create_comment.perform(self, msg.id)
}, },
UserOperation::EditComment => { UserOperation::EditComment => {
let edit_comment: EditComment = serde_json::from_str(&data.to_string()).unwrap(); let edit_comment: EditComment = serde_json::from_str(data).unwrap();
edit_comment.perform(self, msg.id) edit_comment.perform(self, msg.id)
}, },
UserOperation::CreateCommentLike => { UserOperation::CreateCommentLike => {
let create_comment_like: CreateCommentLike = serde_json::from_str(&data.to_string()).unwrap(); let create_comment_like: CreateCommentLike = serde_json::from_str(data).unwrap();
create_comment_like.perform(self, msg.id) create_comment_like.perform(self, msg.id)
}, },
UserOperation::GetPosts => { UserOperation::GetPosts => {
let get_posts: GetPosts = serde_json::from_str(&data.to_string()).unwrap(); let get_posts: GetPosts = serde_json::from_str(data).unwrap();
get_posts.perform(self, msg.id) get_posts.perform(self, msg.id)
}, },
UserOperation::CreatePostLike => { UserOperation::CreatePostLike => {
let create_post_like: CreatePostLike = serde_json::from_str(&data.to_string()).unwrap(); let create_post_like: CreatePostLike = serde_json::from_str(data).unwrap();
create_post_like.perform(self, msg.id) create_post_like.perform(self, msg.id)
}, },
UserOperation::EditPost => { UserOperation::EditPost => {
let edit_post: EditPost = serde_json::from_str(&data.to_string()).unwrap(); let edit_post: EditPost = serde_json::from_str(data).unwrap();
edit_post.perform(self, msg.id) edit_post.perform(self, msg.id)
}, },
UserOperation::EditCommunity => { UserOperation::EditCommunity => {
let edit_community: EditCommunity = serde_json::from_str(&data.to_string()).unwrap(); let edit_community: EditCommunity = serde_json::from_str(data).unwrap();
edit_community.perform(self, msg.id) edit_community.perform(self, msg.id)
}, },
UserOperation::FollowCommunity => { UserOperation::FollowCommunity => {
let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap(); let follow_community: FollowCommunity = serde_json::from_str(data).unwrap();
follow_community.perform(self, msg.id) follow_community.perform(self, msg.id)
}, },
UserOperation::GetFollowedCommunities => { UserOperation::GetFollowedCommunities => {
let followed_communities: GetFollowedCommunities = serde_json::from_str(&data.to_string()).unwrap(); let followed_communities: GetFollowedCommunities = serde_json::from_str(data).unwrap();
followed_communities.perform(self, msg.id) followed_communities.perform(self, msg.id)
}, },
UserOperation::GetUserDetails => { UserOperation::GetUserDetails => {
let get_user_details: GetUserDetails = serde_json::from_str(&data.to_string()).unwrap(); let get_user_details: GetUserDetails = serde_json::from_str(data).unwrap();
get_user_details.perform(self, msg.id) get_user_details.perform(self, msg.id)
}, },
// _ => { // _ => {
@ -541,6 +543,10 @@ impl Perform for Register {
return self.error("Passwords do not match."); return self.error("Passwords do not match.");
} }
if has_slurs(&self.username) {
return self.error("No slurs");
}
// Register the new user // Register the new user
let user_form = UserForm { let user_form = UserForm {
name: self.username.to_owned(), name: self.username.to_owned(),
@ -587,6 +593,12 @@ impl Perform for CreateCommunity {
} }
}; };
if has_slurs(&self.name) ||
has_slurs(&self.title) ||
(self.description.is_some() && has_slurs(&self.description.to_owned().unwrap())) {
return self.error("No slurs");
}
let user_id = claims.id; let user_id = claims.id;
// When you create a community, make sure the user becomes a moderator and a follower // When you create a community, make sure the user becomes a moderator and a follower
@ -665,7 +677,9 @@ impl Perform for ListCommunities {
None => None None => None
}; };
let communities: Vec<CommunityView> = CommunityView::list_all(&conn, user_id).unwrap(); let sort = SortType::from_str(&self.sort).expect("listing sort");
let communities: Vec<CommunityView> = CommunityView::list(&conn, user_id, sort, self.limit).unwrap();
// Return the jwt // Return the jwt
serde_json::to_string( serde_json::to_string(
@ -716,6 +730,11 @@ impl Perform for CreatePost {
} }
}; };
if has_slurs(&self.name) ||
(self.body.is_some() && has_slurs(&self.body.to_owned().unwrap())) {
return self.error("No slurs");
}
let user_id = claims.id; let user_id = claims.id;
let post_form = PostForm { let post_form = PostForm {
@ -894,8 +913,10 @@ impl Perform for CreateComment {
let user_id = claims.id; let user_id = claims.id;
let content_slurs_removed = remove_slurs(&self.content.to_owned());
let comment_form = CommentForm { let comment_form = CommentForm {
content: self.content.to_owned(), content: content_slurs_removed,
parent_id: self.parent_id.to_owned(), parent_id: self.parent_id.to_owned(),
post_id: self.post_id, post_id: self.post_id,
creator_id: user_id, creator_id: user_id,
@ -976,8 +997,10 @@ impl Perform for EditComment {
return self.error("Incorrect creator."); return self.error("Incorrect creator.");
} }
let content_slurs_removed = remove_slurs(&self.content.to_owned());
let comment_form = CommentForm { let comment_form = CommentForm {
content: self.content.to_owned(), content: content_slurs_removed,
parent_id: self.parent_id, parent_id: self.parent_id,
post_id: self.post_id, post_id: self.post_id,
creator_id: user_id, creator_id: user_id,
@ -1197,6 +1220,11 @@ impl Perform for EditPost {
fn perform(&self, chat: &mut ChatServer, addr: usize) -> String { fn perform(&self, chat: &mut ChatServer, addr: usize) -> String {
if has_slurs(&self.name) ||
(self.body.is_some() && has_slurs(&self.body.to_owned().unwrap())) {
return self.error("No slurs");
}
let conn = establish_connection(); let conn = establish_connection();
let claims = match Claims::decode(&self.auth) { let claims = match Claims::decode(&self.auth) {
@ -1264,6 +1292,10 @@ impl Perform for EditCommunity {
fn perform(&self, chat: &mut ChatServer, addr: usize) -> String { fn perform(&self, chat: &mut ChatServer, addr: usize) -> String {
if has_slurs(&self.name) || has_slurs(&self.title) {
return self.error("No slurs");
}
let conn = establish_connection(); let conn = establish_connection();
let claims = match Claims::decode(&self.auth) { let claims = match Claims::decode(&self.auth) {

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,7 +42,7 @@ 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/'));

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

@ -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>

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

@ -97,13 +97,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">

View file

@ -2,13 +2,14 @@ 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 } 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 } from '../utils';
interface State { interface State {
subscribedCommunities: Array<CommunityUser>; subscribedCommunities: Array<CommunityUser>;
trendingCommunities: Array<Community>;
loading: boolean; loading: boolean;
} }
@ -17,6 +18,7 @@ export class Main extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: State = { private emptyState: State = {
subscribedCommunities: [], subscribedCommunities: [],
trendingCommunities: [],
loading: true loading: true
} }
@ -36,6 +38,13 @@ export class Main extends Component<any, State> {
if (UserService.Instance.loggedIn) { if (UserService.Instance.loggedIn) {
WebSocketService.Instance.getFollowedCommunities(); WebSocketService.Instance.getFollowedCommunities();
} }
let listCommunitiesForm: ListCommunitiesForm = {
sort: SortType[SortType.New],
limit: 8
}
WebSocketService.Instance.listCommunities(listCommunitiesForm);
} }
componentWillUnmount() { componentWillUnmount() {
@ -46,24 +55,24 @@ 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.loggedIn ?
<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>
} }
@ -73,6 +82,34 @@ export class Main extends Component<any, State> {
) )
} }
trendingCommunities() {
return (
<div>
<h4>Trending forums</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>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 +122,11 @@ 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);
} }
} }
} }

View file

@ -25,7 +25,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 +40,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
@ -74,7 +74,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,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 { 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 } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
@ -56,7 +56,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() {
@ -151,7 +155,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

@ -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)} />

View file

@ -56,7 +56,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
</ul> </ul>
} }
<ul class="list-inline"> <ul class="mt-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>

View file

@ -67,6 +67,12 @@ export interface CommunityResponse {
community: Community; community: Community;
} }
export interface ListCommunitiesForm {
sort: string;
limit?: number;
auth?: string;
}
export interface ListCommunitiesResponse { export interface ListCommunitiesResponse {
op: string; op: string;
communities: Array<Community>; communities: Array<Community>;

View file

@ -66,3 +66,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,10 +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 { public get loggedIn(): boolean {

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 } 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';
@ -47,9 +47,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() {

View file

@ -1 +1 @@
export let version: string = "v0.0.2-0-gdae6651"; export let version: string = "v0.0.2-9-g8e5a5d1";