Community editing

- Community editing mostly working. Fixes #26
pull/722/head
Dessalines 2019-04-04 15:29:14 -07:00
parent f3cbe9e6ce
commit ed688f9292
11 changed files with 332 additions and 148 deletions

View File

@ -103,7 +103,7 @@ pub struct CreateCommunity {
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct CreateCommunityResponse { pub struct CommunityResponse {
op: String, op: String,
community: CommunityView community: CommunityView
} }
@ -244,6 +244,7 @@ pub struct EditPost {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct EditCommunity { pub struct EditCommunity {
edit_id: i32, edit_id: i32,
name: String,
title: String, title: String,
description: Option<String>, description: Option<String>,
category_id: i32, category_id: i32,
@ -431,6 +432,10 @@ impl Handler<StandardMessage> for ChatServer {
let edit_post: EditPost = serde_json::from_str(&data.to_string()).unwrap(); let edit_post: EditPost = serde_json::from_str(&data.to_string()).unwrap();
edit_post.perform(self, msg.id) edit_post.perform(self, msg.id)
}, },
UserOperation::EditCommunity => {
let edit_community: EditCommunity = serde_json::from_str(&data.to_string()).unwrap();
edit_community.perform(self, msg.id)
},
_ => { _ => {
let e = ErrorMessage { let e = ErrorMessage {
op: "Unknown".to_string(), op: "Unknown".to_string(),
@ -597,7 +602,7 @@ impl Perform for CreateCommunity {
let community_view = CommunityView::read(&conn, inserted_community.id).unwrap(); let community_view = CommunityView::read(&conn, inserted_community.id).unwrap();
serde_json::to_string( serde_json::to_string(
&CreateCommunityResponse { &CommunityResponse {
op: self.op_type().to_string(), op: self.op_type().to_string(),
community: community_view community: community_view
} }
@ -796,7 +801,6 @@ impl Perform for GetCommunity {
} }
}; };
let moderators = match CommunityModeratorView::for_community(&conn, self.id) { let moderators = match CommunityModeratorView::for_community(&conn, self.id) {
Ok(moderators) => moderators, Ok(moderators) => moderators,
Err(_e) => { Err(_e) => {
@ -1187,6 +1191,68 @@ impl Perform for EditPost {
post_out post_out
} }
} }
impl Perform for EditCommunity {
fn op_type(&self) -> UserOperation {
UserOperation::EditCommunity
}
fn perform(&self, chat: &mut ChatServer, addr: usize) -> String {
let conn = establish_connection();
let claims = match Claims::decode(&self.auth) {
Ok(claims) => claims.claims,
Err(_e) => {
return self.error("Not logged in.");
}
};
let user_id = claims.id;
let community_form = CommunityForm {
name: self.name.to_owned(),
title: self.title.to_owned(),
description: self.description.to_owned(),
category_id: self.category_id.to_owned(),
creator_id: user_id,
updated: Some(naive_now())
};
let _updated_community = match Community::update(&conn, self.edit_id, &community_form) {
Ok(community) => community,
Err(_e) => {
return self.error("Couldn't update Community");
}
};
let community_view = CommunityView::read(&conn, self.edit_id).unwrap();
// Do the subscriber stuff here
// let mut community_sent = post_view.clone();
// community_sent.my_vote = None;
let community_out = serde_json::to_string(
&CommunityResponse {
op: self.op_type().to_string(),
community: community_view
}
)
.unwrap();
// let post_sent_out = serde_json::to_string(
// &PostResponse {
// op: self.op_type().to_string(),
// post: post_sent
// }
// )
// .unwrap();
chat.send_room_message(self.edit_id, &community_out, addr);
community_out
}
}
// impl Handler<Login> for ChatServer { // impl Handler<Login> for ChatServer {
// type Result = MessageResult<Login>; // type Result = MessageResult<Login>;
@ -1315,7 +1381,7 @@ impl Perform for EditPost {
// MessageResult( // MessageResult(
// Ok( // Ok(
// CreateCommunityResponse { // CommunityResponse {
// op: UserOperation::CreateCommunity.to_string(), // op: UserOperation::CreateCommunity.to_string(),
// community: community // community: community
// } // }

View File

@ -0,0 +1,155 @@
import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
import { Community } from '../interfaces';
interface CommunityFormProps {
community?: Community; // If a community is given, that means this is an edit
onCancel?();
onCreate?(id: number);
onEdit?(community: Community);
}
interface CommunityFormState {
communityForm: CommunityFormI;
categories: Array<Category>;
}
export class CommunityForm extends Component<CommunityFormProps, CommunityFormState> {
private subscription: Subscription;
private emptyState: CommunityFormState = {
communityForm: {
name: null,
title: null,
category_id: null
},
categories: []
}
constructor(props, context) {
super(props, context);
this.state = this.emptyState;
if (this.props.community) {
this.state.communityForm = {
name: this.props.community.name,
title: this.props.community.title,
category_id: this.props.community.category_id,
description: this.props.community.description,
edit_id: this.props.community.id,
auth: null
}
}
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")
);
WebSocketService.Instance.listCategories();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} pattern="[a-z0-9_]+" title="lowercase, underscores, and no spaces."/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Title / Headline</label>
<div class="col-sm-10">
<input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Description / Sidebar</label>
<div class="col-sm-10">
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={6} />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Category</label>
<div class="col-sm-10">
<select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}>
{this.state.categories.map(category =>
<option value={category.id}>{category.name}</option>
)}
</select>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.props.community ? 'Edit' : 'Create'} Community</button>
</div>
</div>
</form>
);
}
handleCreateCommunitySubmit(i: CommunityForm, event) {
event.preventDefault();
if (i.props.community) {
WebSocketService.Instance.editCommunity(i.state.communityForm);
} else {
WebSocketService.Instance.createCommunity(i.state.communityForm);
}
}
handleCommunityNameChange(i: CommunityForm, event) {
i.state.communityForm.name = event.target.value;
i.setState(i.state);
}
handleCommunityTitleChange(i: CommunityForm, event) {
i.state.communityForm.title = event.target.value;
i.setState(i.state);
}
handleCommunityDescriptionChange(i: CommunityForm, event) {
i.state.communityForm.description = event.target.value;
i.setState(i.state);
}
handleCommunityCategoryChange(i: CommunityForm, event) {
i.state.communityForm.category_id = Number(event.target.value);
i.setState(i.state);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
console.log(msg);
if (msg.error) {
alert(msg.error);
return;
} else if (op == UserOperation.ListCategories){
let res: ListCategoriesResponse = msg;
this.state.categories = res.categories;
this.state.communityForm.category_id = res.categories[0].id;
this.setState(this.state);
} else if (op == UserOperation.CreateCommunity) {
let res: CommunityResponse = msg;
this.props.onCreate(res.community.id);
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.props.onEdit(res.community);
}
}
}

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 as CommunityI, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces'; import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
@ -127,7 +127,7 @@ export class Community extends Component<any, State> {
alert(msg.error); alert(msg.error);
return; return;
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let res: CommunityResponse = msg; let res: GetCommunityResponse = msg;
this.state.community = res.community; this.state.community = res.community;
this.state.moderators = res.moderators; this.state.moderators = res.moderators;
this.setState(this.state); this.setState(this.state);
@ -143,6 +143,10 @@ export class Community extends Component<any, State> {
found.upvotes = res.post.upvotes; found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes; found.downvotes = res.post.downvotes;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.state.community = res.community;
this.setState(this.state);
} }
} }
} }

View File

@ -1,47 +1,11 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { CommunityForm } from './community-form';
import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm, UserOperation, Category, ListCategoriesResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
import { Community } from '../interfaces'; export class CreateCommunity extends Component<any, any> {
interface State {
communityForm: CommunityForm;
categories: Array<Category>;
}
export class CreateCommunity extends Component<any, State> {
private subscription: Subscription;
private emptyState: State = {
communityForm: {
name: null,
title: null,
category_id: null
},
categories: []
}
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
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")
);
WebSocketService.Instance.listCategories();
}
componentWillUnmount() {
this.subscription.unsubscribe();
} }
render() { render() {
@ -49,96 +13,17 @@ export class CreateCommunity extends Component<any, State> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 mb-4"> <div class="col-12 col-lg-6 mb-4">
{this.communityForm()} <h3>Create Forum</h3>
<CommunityForm onCreate={this.handleCommunityCreate}/>
</div> </div>
</div> </div>
</div> </div>
) )
} }
communityForm() { handleCommunityCreate(id: number) {
return ( this.props.history.push(`/community/${id}`);
<div>
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
<h3>Create Forum</h3>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} pattern="[a-z0-9_]+" title="lowercase, underscores, and no spaces."/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Title / Headline</label>
<div class="col-sm-10">
<input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Description / Sidebar</label>
<div class="col-sm-10">
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={6} />
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Category</label>
<div class="col-sm-10">
<select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}>
{this.state.categories.map(category =>
<option value={category.id}>{category.name}</option>
)}
</select>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">Create</button>
</div>
</div>
</form>
</div>
);
} }
handleCreateCommunitySubmit(i: CreateCommunity, event) {
event.preventDefault();
WebSocketService.Instance.createCommunity(i.state.communityForm);
}
handleCommunityNameChange(i: CreateCommunity, event) {
i.state.communityForm.name = event.target.value;
i.setState(i.state);
}
handleCommunityTitleChange(i: CreateCommunity, event) {
i.state.communityForm.title = event.target.value;
i.setState(i.state);
}
handleCommunityDescriptionChange(i: CreateCommunity, event) {
i.state.communityForm.description = event.target.value;
i.setState(i.state);
}
handleCommunityCategoryChange(i: CreateCommunity, event) {
i.state.communityForm.category_id = Number(event.target.value);
i.setState(i.state);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
console.log(msg);
if (msg.error) {
alert(msg.error);
return;
} else if (op == UserOperation.ListCategories){
let res: ListCategoriesResponse = msg;
this.state.categories = res.categories;
this.state.communityForm.category_id = res.categories[0].id;
this.setState(this.state);
} else if (op == UserOperation.CreateCommunity) {
let community: Community = msg.community;
this.props.history.push(`/community/${community.id}`);
}
}
} }

View File

@ -105,7 +105,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
handlePostSubmit(i: PostForm, event) { handlePostSubmit(i: PostForm, event) {
event.preventDefault(); event.preventDefault();
console.log(i.state.postForm);
if (i.props.post) { if (i.props.post) {
WebSocketService.Instance.editPost(i.state.postForm); WebSocketService.Instance.editPost(i.state.postForm);
} else { } else {

View File

@ -132,6 +132,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
i.setState(i.state); i.setState(i.state);
} }
// The actual editing is done in the recieve for post
handleEditPost(post: Post) { handleEditPost(post: Post) {
this.state.showEdit = false; this.state.showEdit = false;
this.setState(this.state); this.setState(this.state);

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, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, CommunityUser } from '../interfaces'; import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp, hotRank,mdToHtml } from '../utils'; import { msgOp, hotRank,mdToHtml } from '../utils';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
@ -223,6 +223,12 @@ export class Post extends Component<any, PostState> {
let res: PostResponse = msg; let res: PostResponse = msg;
this.state.post = res.post; this.state.post = res.post;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.state.community = res.community;
this.state.post.community_id = res.community.id;
this.state.post.community_name = res.community.name;
this.setState(this.state);
} }
} }

View File

@ -1,7 +1,9 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Community, CommunityUser } from '../interfaces'; import { Community, CommunityUser } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { mdToHtml } from '../utils'; import { mdToHtml } from '../utils';
import { CommunityForm } from './community-form';
interface SidebarProps { interface SidebarProps {
community: Community; community: Community;
@ -9,20 +11,49 @@ interface SidebarProps {
} }
interface SidebarState { interface SidebarState {
showEdit: boolean;
} }
export class Sidebar extends Component<SidebarProps, SidebarState> { export class Sidebar extends Component<SidebarProps, SidebarState> {
constructor(props, context) { private emptyState: SidebarState = {
super(props, context); showEdit: false
} }
constructor(props, context) {
super(props, context);
this.state = this.emptyState;
this.handleEditCommunity = this.handleEditCommunity.bind(this);
}
render() { render() {
return (
<div>
{!this.state.showEdit
? this.sidebar()
: <CommunityForm community={this.props.community} onEdit={this.handleEditCommunity} />
}
</div>
)
}
sidebar() {
let community = this.props.community; let community = this.props.community;
return ( return (
<div> <div>
<h4>{community.title}</h4> <h4>{community.title}</h4>
{this.amMod &&
<ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
</li>
{this.amCreator &&
<li className="list-inline-item">
{/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */}
</li>
}
</ul>
}
<ul class="list-inline"> <ul class="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>
@ -44,4 +75,29 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</div> </div>
); );
} }
handleEditClick(i: Sidebar, event) {
i.state.showEdit = true;
i.setState(i.state);
}
handleEditCommunity(community: Community) {
this.state.showEdit = false;
this.setState(this.state);
}
// TODO no deleting communities yet
handleDeleteClick(i: Sidebar, event) {
}
private get amCreator(): boolean {
return UserService.Instance.loggedIn && this.props.community.creator_id == UserService.Instance.user.id;
}
private get amMod(): boolean {
console.log(this.props.moderators);
console.log(this.props);
return UserService.Instance.loggedIn &&
this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id);
}
} }

View File

@ -38,13 +38,20 @@ export interface CommunityForm {
title: string; title: string;
description?: string, description?: string,
category_id: number, category_id: number,
edit_id?: number;
auth?: string; auth?: string;
} }
export interface GetCommunityResponse {
op: string;
community: Community;
moderators: Array<CommunityUser>;
}
export interface CommunityResponse { export interface CommunityResponse {
op: string; op: string;
community: Community; community: Community;
moderators: Array<CommunityUser>;
} }
export interface ListCommunitiesResponse { export interface ListCommunitiesResponse {

View File

@ -1,5 +1,5 @@
import { wsUri } from '../env'; import { wsUri } from '../env';
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetListingsForm, CreatePostLikeForm } from '../interfaces'; import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm } 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';
@ -37,6 +37,11 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)); this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm));
} }
public editCommunity(communityForm: CommunityForm) {
this.setAuth(communityForm);
this.subject.next(this.wsSendWrapper(UserOperation.EditCommunity, communityForm));
}
public listCommunities() { public listCommunities() {
this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, undefined)); this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, undefined));
} }
@ -74,7 +79,7 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form)); this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form));
} }
public getPosts(form: GetListingsForm) { public getPosts(form: GetPostsForm) {
this.setAuth(form, false); this.setAuth(form, false);
this.subject.next(this.wsSendWrapper(UserOperation.GetPosts, form)); this.subject.next(this.wsSendWrapper(UserOperation.GetPosts, form));
} }

View File

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es2015", "target": "es2016",
"sourceMap": true, "sourceMap": true,
"inlineSources": true, "inlineSources": true,
"jsx": "preserve", "jsx": "preserve",