Merge branch 'dev' into moderation
This commit is contained in:
commit
1bf0dfde20
22 changed files with 251 additions and 86 deletions
37
Dockerfile
37
Dockerfile
|
@ -1,12 +1,39 @@
|
|||
FROM node:10-jessie as node
|
||||
#If encounter Invalid cross-device error -run on host 'echo N | sudo tee /sys/module/overlay/parameters/metacopy'
|
||||
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
|
||||
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
|
||||
RUN cd /app/server && cargo build --release
|
||||
RUN mv /app/server/target/release/lemmy /app/
|
||||
WORKDIR /app/
|
||||
EXPOSE 8536
|
||||
|
|
|
@ -10,9 +10,9 @@ services:
|
|||
POSTGRES_DB: rrr
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U rrr"]
|
||||
interval: 30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
lemmy:
|
||||
build:
|
||||
context: .
|
||||
|
@ -22,6 +22,7 @@ services:
|
|||
environment:
|
||||
LEMMY_FRONT_END_DIR: /app/dist
|
||||
DATABASE_URL: postgres://rrr:rrr@db:5432/rrr
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
1
server/Cargo.lock
generated
1
server/Cargo.lock
generated
|
@ -1359,6 +1359,7 @@ dependencies = [
|
|||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
|
|
|
@ -24,4 +24,5 @@ rand = "0.6.5"
|
|||
strum = "0.14.0"
|
||||
strum_macros = "0.14.0"
|
||||
jsonwebtoken = "*"
|
||||
regex = "1"
|
||||
regex = "*"
|
||||
lazy_static = "*"
|
||||
|
|
|
@ -2,6 +2,7 @@ extern crate diesel;
|
|||
use diesel::*;
|
||||
use diesel::result::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use {SortType};
|
||||
|
||||
table! {
|
||||
community_view (id) {
|
||||
|
@ -83,17 +84,27 @@ impl CommunityView {
|
|||
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::*;
|
||||
let mut query = community_view.into_boxed();
|
||||
|
||||
|
||||
|
||||
// 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))
|
||||
.order_by((subscribed.desc(), number_of_subscribers.desc()));
|
||||
} else {
|
||||
query = query.filter(user_id.is_null())
|
||||
.order_by(number_of_subscribers.desc());
|
||||
|
||||
match sort {
|
||||
SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
|
||||
SortType::TopAll => {
|
||||
match from_user_id {
|
||||
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)
|
||||
|
|
|
@ -12,7 +12,7 @@ pub extern crate jsonwebtoken;
|
|||
pub extern crate bcrypt;
|
||||
pub extern crate regex;
|
||||
#[macro_use] pub extern crate strum_macros;
|
||||
|
||||
#[macro_use] pub extern crate lazy_static;
|
||||
pub mod schema;
|
||||
pub mod apub;
|
||||
pub mod actions;
|
||||
|
@ -89,21 +89,42 @@ pub fn naive_now() -> NaiveDateTime {
|
|||
}
|
||||
|
||||
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();
|
||||
re.is_match(test)
|
||||
EMAIL_REGEX.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)]
|
||||
mod tests {
|
||||
use {Settings, is_email_regex};
|
||||
use {Settings, is_email_regex, remove_slurs, has_slurs};
|
||||
#[test]
|
||||
fn test_api() {
|
||||
assert_eq!(Settings::get().api_endpoint(), "http://0.0.0.0/api/v1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email() {
|
||||
#[test] fn test_email() {
|
||||
assert!(is_email_regex("gush@gmail.com"));
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ use serde_json::{Value};
|
|||
use bcrypt::{verify};
|
||||
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::user::*;
|
||||
use actions::post::*;
|
||||
|
@ -111,6 +111,8 @@ pub struct CommunityResponse {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ListCommunities {
|
||||
sort: String,
|
||||
limit: Option<i64>,
|
||||
auth: Option<String>
|
||||
}
|
||||
|
||||
|
@ -391,25 +393,25 @@ impl Handler<StandardMessage> for ChatServer {
|
|||
let json: Value = serde_json::from_str(&msg.msg)
|
||||
.expect("Couldn't parse message");
|
||||
|
||||
let data: &Value = &json["data"];
|
||||
let data = &json["data"].to_string();
|
||||
let op = &json["op"].as_str().unwrap();
|
||||
let user_operation: UserOperation = UserOperation::from_str(&op).unwrap();
|
||||
|
||||
let res: String = match user_operation {
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
UserOperation::ListCategories => {
|
||||
|
@ -417,55 +419,55 @@ impl Handler<StandardMessage> for ChatServer {
|
|||
list_categories.perform(self, msg.id)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
// _ => {
|
||||
|
@ -541,6 +543,10 @@ impl Perform for Register {
|
|||
return self.error("Passwords do not match.");
|
||||
}
|
||||
|
||||
if has_slurs(&self.username) {
|
||||
return self.error("No slurs");
|
||||
}
|
||||
|
||||
// Register the new user
|
||||
let user_form = UserForm {
|
||||
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;
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
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
|
||||
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 post_form = PostForm {
|
||||
|
@ -894,8 +913,10 @@ impl Perform for CreateComment {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let content_slurs_removed = remove_slurs(&self.content.to_owned());
|
||||
|
||||
let comment_form = CommentForm {
|
||||
content: self.content.to_owned(),
|
||||
content: content_slurs_removed,
|
||||
parent_id: self.parent_id.to_owned(),
|
||||
post_id: self.post_id,
|
||||
creator_id: user_id,
|
||||
|
@ -976,8 +997,10 @@ impl Perform for EditComment {
|
|||
return self.error("Incorrect creator.");
|
||||
}
|
||||
|
||||
let content_slurs_removed = remove_slurs(&self.content.to_owned());
|
||||
|
||||
let comment_form = CommentForm {
|
||||
content: self.content.to_owned(),
|
||||
content: content_slurs_removed,
|
||||
parent_id: self.parent_id,
|
||||
post_id: self.post_id,
|
||||
creator_id: user_id,
|
||||
|
@ -1197,6 +1220,11 @@ impl Perform for EditPost {
|
|||
|
||||
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 claims = match Claims::decode(&self.auth) {
|
||||
|
@ -1264,6 +1292,10 @@ impl Perform for EditCommunity {
|
|||
|
||||
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 claims = match Claims::decode(&self.auth) {
|
||||
|
|
|
@ -11,7 +11,7 @@ const transformInferno = require('ts-transform-inferno').default;
|
|||
const transformClasscat = require('ts-transform-classcat').default;
|
||||
let fuse, app;
|
||||
let isProduction = false;
|
||||
var setVersion = require('./set_version.js').setVersion;
|
||||
// var setVersion = require('./set_version.js').setVersion;
|
||||
|
||||
Sparky.task('config', _ => {
|
||||
fuse = new FuseBox({
|
||||
|
@ -42,7 +42,7 @@ Sparky.task('config', _ => {
|
|||
});
|
||||
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('env', _ => (isProduction = true));
|
||||
Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/'));
|
||||
|
|
2
ui/set_version.js
Normal file → Executable file
2
ui/set_version.js
Normal file → Executable file
|
@ -7,3 +7,5 @@ exports.setVersion = function() {
|
|||
let line = `export let version: string = "${revision}";`;
|
||||
fs.writeFileSync("./src/version.ts", line);
|
||||
}
|
||||
|
||||
this.setVersion()
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Component, linkEvent } from 'inferno';
|
|||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
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 { msgOp } from '../utils';
|
||||
|
||||
|
@ -30,7 +30,12 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
(err) => console.error(err),
|
||||
() => 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() {
|
||||
return (
|
||||
<div class="container-fluid">
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div>
|
||||
|
|
|
@ -155,6 +155,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
|||
if (msg.error) {
|
||||
alert(msg.error);
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
return;
|
||||
} else if (op == UserOperation.ListCategories){
|
||||
let res: ListCategoriesResponse = msg;
|
||||
|
|
|
@ -97,13 +97,13 @@ export class Login extends Component<any, State> {
|
|||
<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.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 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" 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 class="form-group row">
|
||||
|
|
|
@ -2,13 +2,14 @@ import { Component } from 'inferno';
|
|||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
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 { PostListings } from './post-listings';
|
||||
import { msgOp } from '../utils';
|
||||
import { msgOp, repoUrl } from '../utils';
|
||||
|
||||
interface State {
|
||||
subscribedCommunities: Array<CommunityUser>;
|
||||
trendingCommunities: Array<Community>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
|
@ -17,6 +18,7 @@ export class Main extends Component<any, State> {
|
|||
private subscription: Subscription;
|
||||
private emptyState: State = {
|
||||
subscribedCommunities: [],
|
||||
trendingCommunities: [],
|
||||
loading: true
|
||||
}
|
||||
|
||||
|
@ -36,6 +38,13 @@ export class Main extends Component<any, State> {
|
|||
if (UserService.Instance.loggedIn) {
|
||||
WebSocketService.Instance.getFollowedCommunities();
|
||||
}
|
||||
|
||||
let listCommunitiesForm: ListCommunitiesForm = {
|
||||
sort: SortType[SortType.New],
|
||||
limit: 8
|
||||
}
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -46,26 +55,26 @@ export class Main extends Component<any, State> {
|
|||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-9">
|
||||
<div class="col-12 col-md-8">
|
||||
<PostListings />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<h4>A Landing message</h4>
|
||||
{UserService.Instance.loggedIn &&
|
||||
<div class="col-12 col-md-4">
|
||||
{this.state.loading ?
|
||||
<h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div>
|
||||
{this.trendingCommunities()}
|
||||
{UserService.Instance.loggedIn ?
|
||||
<div>
|
||||
{this.state.loading ?
|
||||
<h4 class="mt-3"><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> :
|
||||
<div>
|
||||
<hr />
|
||||
<h4>Subscribed forums</h4>
|
||||
<ul class="list-unstyled">
|
||||
{this.state.subscribedCommunities.map(community =>
|
||||
<li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<h4>Subscribed forums</h4>
|
||||
<ul class="list-inline">
|
||||
{this.state.subscribedCommunities.map(community =>
|
||||
<li class="list-inline-item"><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
</ul>
|
||||
</div> :
|
||||
this.landing()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</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) {
|
||||
console.log(msg);
|
||||
|
@ -85,6 +122,11 @@ export class Main extends Component<any, State> {
|
|||
this.state.subscribedCommunities = res.communities;
|
||||
this.state.loading = false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
|
||||
// Subscribe to user changes
|
||||
UserService.Instance.sub.subscribe(user => {
|
||||
let loggedIn: boolean = user !== null;
|
||||
let loggedIn: boolean = user !== undefined;
|
||||
this.setState({isLoggedIn: loggedIn});
|
||||
});
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
// TODO toggle css collapse
|
||||
navbar() {
|
||||
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="#">
|
||||
<svg class="icon mr-2"><use xlinkHref="#icon-mouse"></use></svg>
|
||||
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>
|
||||
</div>
|
||||
</li> :
|
||||
<Link class="nav-link" to="/login">Login</Link>
|
||||
<Link class="nav-link" to="/login">Login / Sign up</Link>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from "rxjs";
|
||||
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 { msgOp } from '../utils';
|
||||
import * as autosize from 'autosize';
|
||||
|
@ -56,7 +56,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
() => console.log('complete')
|
||||
);
|
||||
|
||||
WebSocketService.Instance.listCommunities();
|
||||
let listCommunitiesForm: ListCommunitiesForm = {
|
||||
sort: SortType[SortType.TopAll]
|
||||
}
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -151,7 +155,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
parseMessage(msg: any) {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (msg.error) {
|
||||
alert(msg.error);
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
return;
|
||||
} else if (op == UserOperation.ListCommunities) {
|
||||
let res: ListCommunitiesResponse = msg;
|
||||
|
|
|
@ -101,17 +101,17 @@ export class Post extends Component<any, PostState> {
|
|||
sortRadios() {
|
||||
return (
|
||||
<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}
|
||||
checked={this.state.commentSort === CommentSortType.Hot}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
||||
</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}
|
||||
checked={this.state.commentSort === CommentSortType.Top}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
||||
</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}
|
||||
checked={this.state.commentSort === CommentSortType.New}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
||||
|
|
|
@ -56,7 +56,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
}
|
||||
</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 badge badge-light">{community.number_of_subscribers} Subscribers</li>
|
||||
<li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li>
|
||||
|
|
|
@ -67,6 +67,12 @@ export interface CommunityResponse {
|
|||
community: Community;
|
||||
}
|
||||
|
||||
export interface ListCommunitiesForm {
|
||||
sort: string;
|
||||
limit?: number;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface ListCommunitiesResponse {
|
||||
op: string;
|
||||
communities: Array<Community>;
|
||||
|
|
|
@ -66,3 +66,12 @@ body {
|
|||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(359deg); }
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.navbar-bg {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,10 +25,10 @@ export class UserService {
|
|||
}
|
||||
|
||||
public logout() {
|
||||
this.user = null;
|
||||
this.user = undefined;
|
||||
Cookies.remove("jwt");
|
||||
console.log("Logged out.");
|
||||
this.sub.next(null);
|
||||
this.sub.next(undefined);
|
||||
}
|
||||
|
||||
public get loggedIn(): boolean {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { Subject } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
|
@ -47,9 +47,9 @@ export class WebSocketService {
|
|||
this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm));
|
||||
}
|
||||
|
||||
public listCommunities() {
|
||||
let data = {auth: UserService.Instance.auth };
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, data));
|
||||
public listCommunities(form: ListCommunitiesForm) {
|
||||
this.setAuth(form, false);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, form));
|
||||
}
|
||||
|
||||
public getFollowedCommunities() {
|
||||
|
|
|
@ -1 +1 @@
|
|||
export let version: string = "v0.0.2-0-gdae6651";
|
||||
export let version: string = "v0.0.2-9-g8e5a5d1";
|
Loading…
Reference in a new issue