diff options
author | Dessalines <tyhou13@gmx.com> | 2019-04-04 15:29:14 -0700 |
---|---|---|
committer | Dessalines <tyhou13@gmx.com> | 2019-04-04 15:29:14 -0700 |
commit | ed688f9292f079e04fa5ad22b2505d8a45cb3d46 (patch) | |
tree | f0e99679eab81f0e67d51f5cb43fefc0bb1f3e9f | |
parent | f3cbe9e6cee4a03d8677414ae29b81a59d31b71c (diff) |
Community editing
- Community editing mostly working. Fixes #26
-rw-r--r-- | server/src/websocket_server/server.rs | 74 | ||||
-rw-r--r-- | ui/src/components/community-form.tsx | 155 | ||||
-rw-r--r-- | ui/src/components/community.tsx | 8 | ||||
-rw-r--r-- | ui/src/components/create-community.tsx | 131 | ||||
-rw-r--r-- | ui/src/components/post-form.tsx | 1 | ||||
-rw-r--r-- | ui/src/components/post-listing.tsx | 21 | ||||
-rw-r--r-- | ui/src/components/post.tsx | 8 | ||||
-rw-r--r-- | ui/src/components/sidebar.tsx | 58 | ||||
-rw-r--r-- | ui/src/interfaces.ts | 9 | ||||
-rw-r--r-- | ui/src/services/WebSocketService.ts | 9 | ||||
-rw-r--r-- | ui/tsconfig.json | 2 |
11 files changed, 330 insertions, 146 deletions
diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index 4c13aade..28b5832a 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -103,7 +103,7 @@ pub struct CreateCommunity { } #[derive(Serialize, Deserialize)] -pub struct CreateCommunityResponse { +pub struct CommunityResponse { op: String, community: CommunityView } @@ -244,6 +244,7 @@ pub struct EditPost { #[derive(Serialize, Deserialize)] pub struct EditCommunity { edit_id: i32, + name: String, title: String, description: Option<String>, category_id: i32, @@ -431,6 +432,10 @@ impl Handler<StandardMessage> for ChatServer { let edit_post: EditPost = serde_json::from_str(&data.to_string()).unwrap(); 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 { op: "Unknown".to_string(), @@ -597,7 +602,7 @@ impl Perform for CreateCommunity { let community_view = CommunityView::read(&conn, inserted_community.id).unwrap(); serde_json::to_string( - &CreateCommunityResponse { + &CommunityResponse { op: self.op_type().to_string(), community: community_view } @@ -796,7 +801,6 @@ impl Perform for GetCommunity { } }; - let moderators = match CommunityModeratorView::for_community(&conn, self.id) { Ok(moderators) => moderators, Err(_e) => { @@ -1187,6 +1191,68 @@ impl Perform for EditPost { 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 { // type Result = MessageResult<Login>; @@ -1315,7 +1381,7 @@ impl Perform for EditPost { // MessageResult( // Ok( -// CreateCommunityResponse { +// CommunityResponse { // op: UserOperation::CreateCommunity.to_string(), // community: community // } diff --git a/ui/src/components/community-form.tsx b/ui/src/components/community-form.tsx new file mode 100644 index 00000000..a8ea7b11 --- /dev/null +++ b/ui/src/components/community-form.tsx @@ -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); + } + } + +} diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 5505e01d..ed2cd98e 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -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 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 { MomentTime } from './moment-time'; import { PostListing } from './post-listing'; @@ -127,7 +127,7 @@ export class Community extends Component<any, State> { alert(msg.error); return; } else if (op == UserOperation.GetCommunity) { - let res: CommunityResponse = msg; + let res: GetCommunityResponse = msg; this.state.community = res.community; this.state.moderators = res.moderators; this.setState(this.state); @@ -143,6 +143,10 @@ export class Community extends Component<any, State> { found.upvotes = res.post.upvotes; found.downvotes = res.post.downvotes; this.setState(this.state); + } else if (op == UserOperation.EditCommunity) { + let res: CommunityResponse = msg; + this.state.community = res.community; + this.setState(this.state); } } } diff --git a/ui/src/components/create-community.tsx b/ui/src/components/create-community.tsx index e21db8ac..cd43c8fa 100644 --- a/ui/src/components/create-community.tsx +++ b/ui/src/components/create-community.tsx @@ -1,47 +1,11 @@ import { Component, linkEvent } from 'inferno'; -import { Subscription } from "rxjs"; -import { retryWhen, delay, take } from 'rxjs/operators'; -import { CommunityForm, UserOperation, Category, ListCategoriesResponse } from '../interfaces'; -import { WebSocketService, UserService } from '../services'; -import { msgOp } from '../utils'; +import { CommunityForm } from './community-form'; -import { Community } from '../interfaces'; - -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: [] - } +export class CreateCommunity extends Component<any, any> { constructor(props, context) { 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") - ); - - WebSocketService.Instance.listCategories(); - } - - componentWillUnmount() { - this.subscription.unsubscribe(); + this.handleCommunityCreate = this.handleCommunityCreate.bind(this); } render() { @@ -49,96 +13,17 @@ export class CreateCommunity extends Component<any, State> { <div class="container"> <div class="row"> <div class="col-12 col-lg-6 mb-4"> - {this.communityForm()} + <h3>Create Forum</h3> + <CommunityForm onCreate={this.handleCommunityCreate}/> </div> </div> </div> ) } - communityForm() { - return ( - <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); + handleCommunityCreate(id: number) { + this.props.history.push(`/community/${id}`); } +} - 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}`); - } - } -} diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index b64ad66d..6967bf0d 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -105,7 +105,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> { handlePostSubmit(i: PostForm, event) { event.preventDefault(); - console.log(i.state.postForm); if (i.props.post) { WebSocketService.Instance.editPost(i.state.postForm); } else { diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index c6c07d8b..cdb2fed3 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -34,15 +34,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> { this.handleEditPost = this.handleEditPost.bind(this); } - render() { - return ( - <div> - {!this.state.showEdit - ? this.listing() - : <PostForm post={this.props.post} onEdit={this.handleEditPost} /> - } - </div> - ) + render() { + return ( + <div> + {!this.state.showEdit + ? this.listing() + : <PostForm post={this.props.post} onEdit={this.handleEditPost} /> + } + </div> + ) } listing() { @@ -90,7 +90,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> { <Link to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link> </li> </ul> - {this.myPost && + {this.myPost && <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> @@ -132,6 +132,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> { i.setState(i.state); } + // The actual editing is done in the recieve for post handleEditPost(post: Post) { this.state.showEdit = false; this.setState(this.state); diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index f36ad979..0efa254a 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -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, 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 { msgOp, hotRank,mdToHtml } from '../utils'; import { MomentTime } from './moment-time'; @@ -223,6 +223,12 @@ export class Post extends Component<any, PostState> { let res: PostResponse = msg; this.state.post = res.post; 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); } } diff --git a/ui/src/components/sidebar.tsx b/ui/src/components/sidebar.tsx index e8a2f410..90c35924 100644 --- a/ui/src/components/sidebar.tsx +++ b/ui/src/components/sidebar.tsx @@ -1,7 +1,9 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { Community, CommunityUser } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; import { mdToHtml } from '../utils'; +import { CommunityForm } from './community-form'; interface SidebarProps { community: Community; @@ -9,20 +11,49 @@ interface SidebarProps { } interface SidebarState { + showEdit: boolean; } export class Sidebar extends Component<SidebarProps, SidebarState> { + private emptyState: SidebarState = { + showEdit: false + } + constructor(props, context) { super(props, context); + this.state = this.emptyState; + this.handleEditCommunity = this.handleEditCommunity.bind(this); } - render() { + return ( + <div> + {!this.state.showEdit + ? this.sidebar() + : <CommunityForm community={this.props.community} onEdit={this.handleEditCommunity} /> + } + </div> + ) + } + + sidebar() { let community = this.props.community; return ( <div> <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"> <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> @@ -44,4 +75,29 @@ export class Sidebar extends Component<SidebarProps, SidebarState> { </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); + } } diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index f202f7ac..0505a398 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -38,15 +38,22 @@ export interface CommunityForm { title: string; description?: string, category_id: number, + edit_id?: number; auth?: string; } -export interface CommunityResponse { +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>; diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index c99cfcfa..d89d0128 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -1,5 +1,5 @@ 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 { Subject } from 'rxjs'; import { retryWhen, delay, take } from 'rxjs/operators'; @@ -37,6 +37,11 @@ export class WebSocketService { 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() { this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, undefined)); } @@ -74,7 +79,7 @@ export class WebSocketService { this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form)); } - public getPosts(form: GetListingsForm) { + public getPosts(form: GetPostsForm) { this.setAuth(form, false); this.subject.next(this.wsSendWrapper(UserOperation.GetPosts, form)); } diff --git a/ui/tsconfig.json b/ui/tsconfig.json index f58da758..78723b5f 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2016", "sourceMap": true, "inlineSources": true, "jsx": "preserve", |