36 changed files with 969 additions and 419 deletions
@ -0,0 +1 @@ |
|||
drop view user_view; |
@ -0,0 +1,11 @@ |
|||
create view user_view as |
|||
select id, |
|||
name, |
|||
fedi_name, |
|||
published, |
|||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts, |
|||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score, |
|||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments, |
|||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score |
|||
from user_ u; |
|||
|
@ -0,0 +1,40 @@ |
|||
extern crate diesel; |
|||
use diesel::*; |
|||
use diesel::result::Error; |
|||
use serde::{Deserialize, Serialize}; |
|||
|
|||
table! { |
|||
user_view (id) { |
|||
id -> Int4, |
|||
name -> Varchar, |
|||
fedi_name -> Varchar, |
|||
published -> Timestamp, |
|||
number_of_posts -> BigInt, |
|||
post_score -> BigInt, |
|||
number_of_comments -> BigInt, |
|||
comment_score -> BigInt, |
|||
} |
|||
} |
|||
|
|||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)] |
|||
#[table_name="user_view"] |
|||
pub struct UserView { |
|||
pub id: i32, |
|||
pub name: String, |
|||
pub fedi_name: String, |
|||
pub published: chrono::NaiveDateTime, |
|||
pub number_of_posts: i64, |
|||
pub post_score: i64, |
|||
pub number_of_comments: i64, |
|||
pub comment_score: i64, |
|||
} |
|||
|
|||
impl UserView { |
|||
pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> { |
|||
use actions::user_view::user_view::dsl::*; |
|||
|
|||
user_view.find(from_user_id) |
|||
.first::<Self>(conn) |
|||
} |
|||
} |
|||
|
@ -0,0 +1,93 @@ |
|||
import { Component, linkEvent } from 'inferno'; |
|||
import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces'; |
|||
import { WebSocketService } from '../services'; |
|||
import * as autosize from 'autosize'; |
|||
|
|||
interface CommentFormProps { |
|||
postId?: number; |
|||
node?: CommentNodeI; |
|||
onReplyCancel?(): any; |
|||
edit?: boolean; |
|||
} |
|||
|
|||
interface CommentFormState { |
|||
commentForm: CommentFormI; |
|||
buttonTitle: string; |
|||
} |
|||
|
|||
export class CommentForm extends Component<CommentFormProps, CommentFormState> { |
|||
|
|||
private emptyState: CommentFormState = { |
|||
commentForm: { |
|||
auth: null, |
|||
content: null, |
|||
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId |
|||
}, |
|||
buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply" |
|||
} |
|||
|
|||
constructor(props: any, context: any) { |
|||
super(props, context); |
|||
|
|||
this.state = this.emptyState; |
|||
|
|||
if (this.props.node) { |
|||
if (this.props.edit) { |
|||
this.state.commentForm.edit_id = this.props.node.comment.id; |
|||
this.state.commentForm.parent_id = this.props.node.comment.parent_id; |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
|
|||
componentDidMount() { |
|||
autosize(document.querySelectorAll('textarea')); |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<div> |
|||
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}> |
|||
<div class="form-group row"> |
|||
<div class="col-sm-12"> |
|||
<textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required /> |
|||
</div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-sm-12"> |
|||
<button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button> |
|||
{this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>} |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
handleCommentSubmit(i: CommentForm, event: any) { |
|||
if (i.props.edit) { |
|||
WebSocketService.Instance.editComment(i.state.commentForm); |
|||
} else { |
|||
WebSocketService.Instance.createComment(i.state.commentForm); |
|||
} |
|||
|
|||
i.state.commentForm.content = undefined; |
|||
i.setState(i.state); |
|||
event.target.reset(); |
|||
if (i.props.node) { |
|||
i.props.onReplyCancel(); |
|||
} |
|||
} |
|||
|
|||
handleCommentContentChange(i: CommentForm, event: any) { |
|||
i.state.commentForm.content = event.target.value; |
|||
i.setState(i.state); |
|||
} |
|||
|
|||
handleReplyCancel(i: CommentForm) { |
|||
i.props.onReplyCancel(); |
|||
} |
|||
} |
@ -0,0 +1,148 @@ |
|||
import { Component, linkEvent } from 'inferno'; |
|||
import { Link } from 'inferno-router'; |
|||
import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI } from '../interfaces'; |
|||
import { WebSocketService, UserService } from '../services'; |
|||
import { mdToHtml } from '../utils'; |
|||
import { MomentTime } from './moment-time'; |
|||
import { CommentForm } from './comment-form'; |
|||
import { CommentNodes } from './comment-nodes'; |
|||
|
|||
interface CommentNodeState { |
|||
showReply: boolean; |
|||
showEdit: boolean; |
|||
} |
|||
|
|||
interface CommentNodeProps { |
|||
node: CommentNodeI; |
|||
noIndent?: boolean; |
|||
viewOnly?: boolean; |
|||
} |
|||
|
|||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { |
|||
|
|||
private emptyState: CommentNodeState = { |
|||
showReply: false, |
|||
showEdit: false |
|||
} |
|||
|
|||
constructor(props: any, context: any) { |
|||
super(props, context); |
|||
|
|||
this.state = this.emptyState; |
|||
this.handleReplyCancel = this.handleReplyCancel.bind(this); |
|||
this.handleCommentLike = this.handleCommentLike.bind(this); |
|||
this.handleCommentDisLike = this.handleCommentDisLike.bind(this); |
|||
} |
|||
|
|||
render() { |
|||
let node = this.props.node; |
|||
return ( |
|||
<div id={`comment-${node.comment.id}`} className={`comment ${node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''}`}> |
|||
<div className={`float-left small text-center ${this.props.viewOnly && 'no-click'}`}> |
|||
<div className={`pointer upvote ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>โฒ</div> |
|||
<div>{node.comment.score}</div> |
|||
<div className={`pointer downvote ${node.comment.my_vote == -1 && 'text-danger'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>โผ</div> |
|||
</div> |
|||
<div className="details ml-4"> |
|||
<ul class="list-inline mb-0 text-muted small"> |
|||
<li className="list-inline-item"> |
|||
<Link to={`/user/${node.comment.creator_id}`}>{node.comment.creator_name}</Link> |
|||
</li> |
|||
<li className="list-inline-item"> |
|||
<span>( |
|||
<span className="text-info">+{node.comment.upvotes}</span> |
|||
<span> | </span> |
|||
<span className="text-danger">-{node.comment.downvotes}</span> |
|||
<span>) </span> |
|||
</span> |
|||
</li> |
|||
<li className="list-inline-item"> |
|||
<span><MomentTime data={node.comment} /></span> |
|||
</li> |
|||
</ul> |
|||
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />} |
|||
{!this.state.showEdit && |
|||
<div> |
|||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.content)} /> |
|||
<ul class="list-inline mb-1 text-muted small font-weight-bold"> |
|||
{!this.props.viewOnly && |
|||
<span class="mr-2"> |
|||
<li className="list-inline-item"> |
|||
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span> |
|||
</li> |
|||
{this.myComment && |
|||
<li className="list-inline-item"> |
|||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span> |
|||
</li> |
|||
} |
|||
{this.myComment && |
|||
<li className="list-inline-item"> |
|||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> |
|||
</li> |
|||
} |
|||
</span> |
|||
} |
|||
<li className="list-inline-item"> |
|||
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
} |
|||
</div> |
|||
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />} |
|||
{this.props.node.children && <CommentNodes nodes={this.props.node.children} />} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
private get myComment(): boolean { |
|||
return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id; |
|||
} |
|||
|
|||
handleReplyClick(i: CommentNode) { |
|||
i.state.showReply = true; |
|||
i.setState(i.state); |
|||
} |
|||
|
|||
handleEditClick(i: CommentNode) { |
|||
i.state.showEdit = true; |
|||
i.setState(i.state); |
|||
} |
|||
|
|||
handleDeleteClick(i: CommentNode) { |
|||
let deleteForm: CommentFormI = { |
|||
content: "*deleted*", |
|||
edit_id: i.props.node.comment.id, |
|||
post_id: i.props.node.comment.post_id, |
|||
parent_id: i.props.node.comment.parent_id, |
|||
auth: null |
|||
}; |
|||
WebSocketService.Instance.editComment(deleteForm); |
|||
} |
|||
|
|||
handleReplyCancel() { |
|||
this.state.showReply = false; |
|||
this.state.showEdit = false; |
|||
this.setState(this.state); |
|||
} |
|||
|
|||
|
|||
handleCommentLike(i: CommentNodeI) { |
|||
|
|||
let form: CommentLikeForm = { |
|||
comment_id: i.comment.id, |
|||
post_id: i.comment.post_id, |
|||
score: (i.comment.my_vote == 1) ? 0 : 1 |
|||
}; |
|||
WebSocketService.Instance.likeComment(form); |
|||
} |
|||
|
|||
handleCommentDisLike(i: CommentNodeI) { |
|||
let form: CommentLikeForm = { |
|||
comment_id: i.comment.id, |
|||
post_id: i.comment.post_id, |
|||
score: (i.comment.my_vote == -1) ? 0 : -1 |
|||
}; |
|||
WebSocketService.Instance.likeComment(form); |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
import { Component } from 'inferno'; |
|||
import { CommentNode as CommentNodeI } from '../interfaces'; |
|||
import { CommentNode } from './comment-node'; |
|||
|
|||
interface CommentNodesState { |
|||
} |
|||
|
|||
interface CommentNodesProps { |
|||
nodes: Array<CommentNodeI>; |
|||
noIndent?: boolean; |
|||
viewOnly?: boolean; |
|||
} |
|||
|
|||
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> { |
|||
|
|||
constructor(props: any, context: any) { |
|||
super(props, context); |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<div className="comments"> |
|||
{this.props.nodes.map(node => |
|||
<CommentNode node={node} noIndent={this.props.noIndent} viewOnly={this.props.viewOnly}/> |
|||
)} |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|