Adding comment editing

- Fixes #8
This commit is contained in:
Dessalines 2019-03-28 21:56:23 -07:00
parent e3ef57c242
commit 6f1ba67e40
6 changed files with 187 additions and 45 deletions

View file

@ -174,7 +174,7 @@ impl CommentView {
post_id: comment.post_id, post_id: comment.post_id,
attributed_to: comment.attributed_to.to_owned(), attributed_to: comment.attributed_to.to_owned(),
published: comment.published, published: comment.published,
updated: None, updated: comment.updated,
upvotes: upvotes, upvotes: upvotes,
score: score, score: score,
downvotes: downvotes, downvotes: downvotes,

View file

@ -6,12 +6,11 @@ use actix::prelude::*;
use rand::{rngs::ThreadRng, Rng}; use rand::{rngs::ThreadRng, Rng};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Result, Value}; use serde_json::{Value};
use bcrypt::{verify}; use bcrypt::{verify};
use std::str::FromStr; use std::str::FromStr;
use std::{thread, time};
use {Crud, Joinable, Likeable, establish_connection}; use {Crud, Joinable, Likeable, establish_connection, naive_now};
use actions::community::*; use actions::community::*;
use actions::user::*; use actions::user::*;
use actions::post::*; use actions::post::*;
@ -20,7 +19,7 @@ use actions::comment::*;
#[derive(EnumString,ToString,Debug)] #[derive(EnumString,ToString,Debug)]
pub enum UserOperation { pub enum UserOperation {
Login, Register, Logout, CreateCommunity, ListCommunities, CreatePost, GetPost, GetCommunity, CreateComment, CreateCommentLike, Join, Edit, Reply, Vote, Delete, NextPage, Sticky Login, Register, Logout, CreateCommunity, ListCommunities, CreatePost, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, Join, Edit, Reply, Vote, Delete, NextPage, Sticky
} }
@ -178,6 +177,7 @@ pub struct GetCommunityResponse {
pub struct CreateComment { pub struct CreateComment {
content: String, content: String,
parent_id: Option<i32>, parent_id: Option<i32>,
edit_id: Option<i32>,
post_id: i32, post_id: i32,
auth: String auth: String
} }
@ -189,6 +189,21 @@ pub struct CreateCommentResponse {
} }
#[derive(Serialize, Deserialize)]
pub struct EditComment {
content: String,
parent_id: Option<i32>,
edit_id: i32,
post_id: i32,
auth: String
}
#[derive(Serialize, Deserialize)]
pub struct EditCommentResponse {
op: String,
comment: CommentView
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct CreateCommentLike { pub struct CreateCommentLike {
comment_id: i32, comment_id: i32,
@ -360,6 +375,10 @@ impl Handler<StandardMessage> for ChatServer {
let create_comment: CreateComment = serde_json::from_str(&data.to_string()).unwrap(); let create_comment: CreateComment = serde_json::from_str(&data.to_string()).unwrap();
create_comment.perform(self, msg.id) create_comment.perform(self, msg.id)
}, },
UserOperation::EditComment => {
let edit_comment: EditComment = serde_json::from_str(&data.to_string()).unwrap();
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.to_string()).unwrap();
create_comment_like.perform(self, msg.id) create_comment_like.perform(self, msg.id)
@ -483,7 +502,9 @@ impl Perform for CreateCommunity {
}; };
let user_id = claims.id; let user_id = claims.id;
let username = claims.username;
let iss = claims.iss; let iss = claims.iss;
let fedi_user_id = format!("{}/{}", iss, username);
let community_form = CommunityForm { let community_form = CommunityForm {
name: self.name.to_owned(), name: self.name.to_owned(),
@ -499,7 +520,7 @@ impl Perform for CreateCommunity {
let community_user_form = CommunityUserForm { let community_user_form = CommunityUserForm {
community_id: inserted_community.id, community_id: inserted_community.id,
fedi_user_id: format!("{}/{}", iss, user_id) fedi_user_id: fedi_user_id
}; };
let inserted_community_user = match CommunityUser::join(&conn, &community_user_form) { let inserted_community_user = match CommunityUser::join(&conn, &community_user_form) {
@ -558,15 +579,16 @@ impl Perform for CreatePost {
}; };
let user_id = claims.id; let user_id = claims.id;
let username = claims.username;
let iss = claims.iss; let iss = claims.iss;
let fedi_user_id = format!("{}/{}", iss, username);
let post_form = PostForm { let post_form = PostForm {
name: self.name.to_owned(), name: self.name.to_owned(),
url: self.url.to_owned(), url: self.url.to_owned(),
body: self.body.to_owned(), body: self.body.to_owned(),
community_id: self.community_id, community_id: self.community_id,
attributed_to: format!("{}/{}", iss, user_id), attributed_to: fedi_user_id,
updated: None updated: None
}; };
@ -603,9 +625,10 @@ impl Perform for GetPost {
Some(auth) => { Some(auth) => {
match Claims::decode(&auth) { match Claims::decode(&auth) {
Ok(claims) => { Ok(claims) => {
let user_id = claims.claims.id; let username = claims.claims.username;
let iss = claims.claims.iss; let iss = claims.claims.iss;
Some(format!("{}/{}", iss, user_id)) let fedi_user_id = format!("{}/{}", iss, username);
Some(fedi_user_id)
} }
Err(e) => None Err(e) => None
} }
@ -692,8 +715,9 @@ impl Perform for CreateComment {
}; };
let user_id = claims.id; let user_id = claims.id;
let username = claims.username;
let iss = claims.iss; let iss = claims.iss;
let fedi_user_id = format!("{}/{}", iss, user_id); let fedi_user_id = format!("{}/{}", iss, username);
let comment_form = CommentForm { let comment_form = CommentForm {
content: self.content.to_owned(), content: self.content.to_owned(),
@ -729,7 +753,6 @@ impl Perform for CreateComment {
let comment_view = CommentView::from_comment(&inserted_comment, &likes, &Some(fedi_user_id)); let comment_view = CommentView::from_comment(&inserted_comment, &likes, &Some(fedi_user_id));
let mut comment_sent = comment_view.clone(); let mut comment_sent = comment_view.clone();
comment_sent.my_vote = None; comment_sent.my_vote = None;
@ -741,7 +764,6 @@ impl Perform for CreateComment {
) )
.unwrap(); .unwrap();
let comment_sent_out = serde_json::to_string( let comment_sent_out = serde_json::to_string(
&CreateCommentLikeResponse { &CreateCommentLikeResponse {
op: self.op_type().to_string(), op: self.op_type().to_string(),
@ -756,6 +778,75 @@ impl Perform for CreateComment {
} }
} }
impl Perform for EditComment {
fn op_type(&self) -> UserOperation {
UserOperation::EditComment
}
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 username = claims.username;
let iss = claims.iss;
let fedi_user_id = format!("{}/{}", iss, username);
let comment_form = CommentForm {
content: self.content.to_owned(),
parent_id: self.parent_id,
post_id: self.post_id,
attributed_to: fedi_user_id.to_owned(),
updated: Some(naive_now())
};
let updated_comment = match Comment::update(&conn, self.edit_id, &comment_form) {
Ok(comment) => comment,
Err(e) => {
return self.error("Couldn't update Comment");
}
};
let likes = match CommentLike::read(&conn, self.edit_id) {
Ok(likes) => likes,
Err(e) => {
return self.error("Couldn't get likes");
}
};
let comment_view = CommentView::from_comment(&updated_comment, &likes, &Some(fedi_user_id));
let mut comment_sent = comment_view.clone();
comment_sent.my_vote = None;
let comment_out = serde_json::to_string(
&CreateCommentResponse {
op: self.op_type().to_string(),
comment: comment_view
}
)
.unwrap();
let comment_sent_out = serde_json::to_string(
&CreateCommentLikeResponse {
op: self.op_type().to_string(),
comment: comment_sent
}
)
.unwrap();
chat.send_room_message(self.post_id, &comment_sent_out, addr);
comment_out
}
}
impl Perform for CreateCommentLike { impl Perform for CreateCommentLike {
fn op_type(&self) -> UserOperation { fn op_type(&self) -> UserOperation {
@ -774,8 +865,9 @@ impl Perform for CreateCommentLike {
}; };
let user_id = claims.id; let user_id = claims.id;
let username = claims.username;
let iss = claims.iss; let iss = claims.iss;
let fedi_user_id = format!("{}/{}", iss, user_id); let fedi_user_id = format!("{}/{}", iss, username);
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
comment_id: self.comment_id, comment_id: self.comment_id,

View file

@ -91,7 +91,7 @@ export class Post extends Component<any, State> {
newComments() { newComments() {
return ( return (
<div class="sticky-top"> <div class="sticky-top">
<h4>New Comments</h4> <h5>New Comments</h5>
{this.state.comments.map(comment => {this.state.comments.map(comment =>
<CommentNodes nodes={[{comment: comment}]} noIndent /> <CommentNodes nodes={[{comment: comment}]} noIndent />
)} )}
@ -102,7 +102,7 @@ export class Post extends Component<any, State> {
sidebar() { sidebar() {
return ( return (
<div class="sticky-top"> <div class="sticky-top">
<h4>Sidebar</h4> <h5>Sidebar</h5>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div> </div>
); );
@ -155,7 +155,14 @@ export class Post extends Component<any, State> {
let res: CommentResponse = msg; let res: CommentResponse = msg;
this.state.comments.unshift(res.comment); this.state.comments.unshift(res.comment);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.EditComment) {
let res: CommentResponse = msg;
let found = this.state.comments.find(c => c.id == res.comment.id);
found.content = res.comment.content;
found.updated = res.comment.updated;
this.setState(this.state);
}
else if (op == UserOperation.CreateCommentLike) {
let res: CreateCommentLikeResponse = msg; let res: CreateCommentLikeResponse = msg;
let found: Comment = this.state.comments.find(c => c.id === res.comment.id); let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
found.score = res.comment.score; found.score = res.comment.score;
@ -163,7 +170,6 @@ export class Post extends Component<any, State> {
found.downvotes = res.comment.downvotes; found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) if (res.comment.my_vote !== null)
found.my_vote = res.comment.my_vote; found.my_vote = res.comment.my_vote;
console.log(res.comment.my_vote);
this.setState(this.state); this.setState(this.state);
} }
@ -198,6 +204,7 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
interface CommentNodeState { interface CommentNodeState {
showReply: boolean; showReply: boolean;
showEdit: boolean;
} }
interface CommentNodeProps { interface CommentNodeProps {
@ -208,7 +215,8 @@ interface CommentNodeProps {
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
private emptyState: CommentNodeState = { private emptyState: CommentNodeState = {
showReply: false showReply: false,
showEdit: false
} }
constructor(props, context) { constructor(props, context) {
@ -246,15 +254,25 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<span><MomentTime data={node.comment} /></span> <span><MomentTime data={node.comment} /></span>
</li> </li>
</ul> </ul>
<p className="mb-0">{node.comment.content}</p> {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />}
<ul class="list-inline mb-1 text-muted small font-weight-bold"> {!this.state.showEdit &&
<li className="list-inline-item"> <div>
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span> <p className='mb-0'>{node.comment.content}</p>
</li> <ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item"> <li className="list-inline-item">
<a className="text-muted" href="test">link</a> <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
</li> </li>
</ul> {this.myComment &&
<li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
</li>
}
<li className="list-inline-item">
<a className="text-muted" href="test">link</a>
</li>
</ul>
</div>
}
</div> </div>
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />} {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
{this.props.node.children && <CommentNodes nodes={this.props.node.children}/>} {this.props.node.children && <CommentNodes nodes={this.props.node.children}/>}
@ -262,8 +280,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
) )
} }
private getScore(): number { private get myComment(): boolean {
return (this.props.node.comment.upvotes - this.props.node.comment.downvotes) || 0; return this.props.node.comment.attributed_to == UserService.Instance.fediUserId;
} }
handleReplyClick(i: CommentNode, event) { handleReplyClick(i: CommentNode, event) {
@ -271,11 +289,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state); i.setState(i.state);
} }
handleEditClick(i: CommentNode, event) {
i.state.showEdit = true;
i.setState(i.state);
}
handleReplyCancel(): any { handleReplyCancel(): any {
this.state.showReply = false; this.state.showReply = false;
this.state.showEdit = false;
this.setState(this.state); this.setState(this.state);
} }
handleCommentLike(i: CommentNodeI, event) { handleCommentLike(i: CommentNodeI, event) {
let form: CommentLikeForm = { let form: CommentLikeForm = {
@ -300,10 +325,12 @@ interface CommentFormProps {
postId?: number; postId?: number;
node?: CommentNodeI; node?: CommentNodeI;
onReplyCancel?(); onReplyCancel?();
edit?: boolean;
} }
interface CommentFormState { interface CommentFormState {
commentForm: CommentFormI; commentForm: CommentFormI;
buttonTitle: string;
} }
export class CommentForm extends Component<CommentFormProps, CommentFormState> { export class CommentForm extends Component<CommentFormProps, CommentFormState> {
@ -312,27 +339,33 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
commentForm: { commentForm: {
auth: null, auth: null,
content: null, content: null,
post_id: null, post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId
parent_id: null },
} buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply"
} }
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = this.emptyState; this.state = this.emptyState;
if (this.props.node) { if (this.props.node) {
this.state.commentForm.post_id = this.props.node.comment.post_id; if (this.props.edit) {
this.state.commentForm.parent_id = this.props.node.comment.id; this.state.commentForm.edit_id = this.props.node.comment.id;
} else { this.state.commentForm.parent_id = this.props.node.comment.parent_id;
this.state.commentForm.post_id = this.props.postId; this.state.commentForm.content = this.props.node.comment.content;
} else {
// A reply gets a new parent id
this.state.commentForm.parent_id = this.props.node.comment.id;
}
} }
} }
render() { render() {
return ( return (
<div> <div>
<form onSubmit={linkEvent(this, this.handleCreateCommentSubmit)}> <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-12"> <div class="col-sm-12">
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required /> <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required />
@ -340,7 +373,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-secondary mr-2">Post</button> <button type="submit" class="btn btn-secondary mr-2">{this.state.buttonTitle}</button>
{this.props.node && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>} {this.props.node && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
</div> </div>
</div> </div>
@ -349,8 +382,13 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
); );
} }
handleCreateCommentSubmit(i: CommentForm, event) { handleCommentSubmit(i: CommentForm, event) {
WebSocketService.Instance.createComment(i.state.commentForm); if (i.props.edit) {
WebSocketService.Instance.editComment(i.state.commentForm);
} else {
WebSocketService.Instance.createComment(i.state.commentForm);
}
i.state.commentForm.content = undefined; i.state.commentForm.content = undefined;
i.setState(i.state); i.setState(i.state);
event.target.reset(); event.target.reset();
@ -360,8 +398,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
} }
handleCommentContentChange(i: CommentForm, event) { handleCommentContentChange(i: CommentForm, event) {
// TODO don't use setState, it triggers a re-render
i.state.commentForm.content = event.target.value; i.state.commentForm.content = event.target.value;
i.setState(i.state);
} }
handleReplyCancel(i: CommentForm, event) { handleReplyCancel(i: CommentForm, event) {

View file

@ -1,9 +1,10 @@
export enum UserOperation { export enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, GetPost, GetCommunity, CreateComment, CreateCommentLike Login, Register, CreateCommunity, CreatePost, ListCommunities, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike
} }
export interface User { export interface User {
id: number; id: number;
iss: string;
username: string; username: string;
} }
@ -73,6 +74,7 @@ export interface CommentForm {
content: string; content: string;
post_id: number; post_id: number;
parent_id?: number; parent_id?: number;
edit_id?: number;
auth: string; auth: string;
} }

View file

@ -42,6 +42,11 @@ export class UserService {
private setUser(jwt: string) { private setUser(jwt: string) {
this.user = jwt_decode(jwt); this.user = jwt_decode(jwt);
this.sub.next(this.user); this.sub.next(this.user);
console.log(this.user);
}
public get fediUserId(): string {
return `${this.user.iss}/${this.user.username}`;
} }
public static get Instance(){ public static get Instance(){

View file

@ -14,7 +14,7 @@ export class WebSocketService {
// Even tho this isn't used, its necessary to not keep reconnecting // Even tho this isn't used, its necessary to not keep reconnecting
this.subject this.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(retryWhen(errors => errors.pipe(delay(60000), take(999))))
.subscribe(); .subscribe();
console.log(`Connected to ${wsUri}`); console.log(`Connected to ${wsUri}`);
@ -60,6 +60,11 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.CreateComment, commentForm)); this.subject.next(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
} }
public editComment(commentForm: CommentForm) {
this.setAuth(commentForm);
this.subject.next(this.wsSendWrapper(UserOperation.EditComment, commentForm));
}
public likeComment(form: CommentLikeForm) { public likeComment(form: CommentLikeForm) {
this.setAuth(form); this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form)); this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form));