Adding instant voting / vote animations. Fixes #526

This commit is contained in:
Dessalines 2020-02-09 15:04:41 -05:00
parent be539e3353
commit 9c8fe0379f
6 changed files with 130 additions and 102 deletions

View File

@ -280,6 +280,9 @@ impl ChatServer {
self.send_community_room_message(0, &post_sent_str, id); self.send_community_room_message(0, &post_sent_str, id);
self.send_community_room_message(community_id, &post_sent_str, id); self.send_community_room_message(community_id, &post_sent_str, id);
// Send it to the post room
self.send_post_room_message(post_sent.post.id, &post_sent_str, id);
to_json_string(&user_operation, post) to_json_string(&user_operation, post)
} }

View File

@ -175,3 +175,9 @@ hr {
.img-expanded { .img-expanded {
max-height: 90vh; max-height: 90vh;
} }
.vote-animate:active {
transform: scale(1.2);
-webkit-transform: scale(1.2);
-ms-transform: scale(1.2);
}

View File

@ -48,8 +48,10 @@ interface CommentNodeState {
showConfirmAppointAsAdmin: boolean; showConfirmAppointAsAdmin: boolean;
collapsed: boolean; collapsed: boolean;
viewSource: boolean; viewSource: boolean;
upvoteLoading: boolean; my_vote: number;
downvoteLoading: boolean; score: number;
upvotes: number;
downvotes: number;
} }
interface CommentNodeProps { interface CommentNodeProps {
@ -83,8 +85,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
showConfirmTransferCommunity: false, showConfirmTransferCommunity: false,
showConfirmAppointAsMod: false, showConfirmAppointAsMod: false,
showConfirmAppointAsAdmin: false, showConfirmAppointAsAdmin: false,
upvoteLoading: this.props.node.comment.upvoteLoading, my_vote: this.props.node.comment.my_vote,
downvoteLoading: this.props.node.comment.downvoteLoading, score: this.props.node.comment.score,
upvotes: this.props.node.comment.upvotes,
downvotes: this.props.node.comment.downvotes,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -97,15 +101,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
componentWillReceiveProps(nextProps: CommentNodeProps) { componentWillReceiveProps(nextProps: CommentNodeProps) {
if ( this.state.my_vote = nextProps.node.comment.my_vote;
nextProps.node.comment.upvoteLoading !== this.state.upvoteLoading || this.state.upvotes = nextProps.node.comment.upvotes;
nextProps.node.comment.downvoteLoading !== this.state.downvoteLoading this.state.downvotes = nextProps.node.comment.downvotes;
) { this.state.score = nextProps.node.comment.score;
this.setState({ this.setState(this.state);
upvoteLoading: false,
downvoteLoading: false,
});
}
} }
render() { render() {
@ -122,40 +122,26 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
.viewOnly && 'no-click'}`} .viewOnly && 'no-click'}`}
> >
<button <button
className={`btn btn-link p-0 ${ className={`vote-animate btn btn-link p-0 ${
node.comment.my_vote == 1 ? 'text-info' : 'text-muted' this.state.my_vote == 1 ? 'text-info' : 'text-muted'
}`} }`}
onClick={linkEvent(node, this.handleCommentUpvote)} onClick={linkEvent(node, this.handleCommentUpvote)}
> >
{this.state.upvoteLoading ? ( <svg class="icon upvote">
<svg class="icon icon-spinner spin upvote"> <use xlinkHref="#icon-arrow-up"></use>
<use xlinkHref="#icon-spinner"></use> </svg>
</svg>
) : (
<svg class="icon upvote">
<use xlinkHref="#icon-arrow-up"></use>
</svg>
)}
</button> </button>
<div class={`font-weight-bold text-muted`}> <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
{node.comment.score}
</div>
{WebSocketService.Instance.site.enable_downvotes && ( {WebSocketService.Instance.site.enable_downvotes && (
<button <button
className={`btn btn-link p-0 ${ className={`vote-animate btn btn-link p-0 ${
node.comment.my_vote == -1 ? 'text-danger' : 'text-muted' this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
}`} }`}
onClick={linkEvent(node, this.handleCommentDownvote)} onClick={linkEvent(node, this.handleCommentDownvote)}
> >
{this.state.downvoteLoading ? ( <svg class="icon downvote">
<svg class="icon icon-spinner spin downvote"> <use xlinkHref="#icon-arrow-down"></use>
<use xlinkHref="#icon-spinner"></use> </svg>
</svg>
) : (
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
)}
</button> </button>
)} )}
</div> </div>
@ -205,9 +191,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
<li className="list-inline-item"> <li className="list-inline-item">
<span> <span>
(<span className="text-info">+{node.comment.upvotes}</span> (<span className="text-info">+{this.state.upvotes}</span>
<span> | </span> <span> | </span>
<span className="text-danger">-{node.comment.downvotes}</span> <span className="text-danger">-{this.state.downvotes}</span>
<span>) </span> <span>) </span>
</span> </span>
</li> </li>
@ -772,31 +758,57 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleCommentUpvote(i: CommentNodeI) { handleCommentUpvote(i: CommentNodeI) {
if (UserService.Instance.user) { let new_vote = this.state.my_vote == 1 ? 0 : 1;
this.setState({
upvoteLoading: true, if (this.state.my_vote == 1) {
}); this.state.score--;
this.state.upvotes--;
} else if (this.state.my_vote == -1) {
this.state.downvotes--;
this.state.upvotes++;
this.state.score += 2;
} else {
this.state.upvotes++;
this.state.score++;
} }
this.state.my_vote = new_vote;
let form: CommentLikeForm = { let form: CommentLikeForm = {
comment_id: i.comment.id, comment_id: i.comment.id,
post_id: i.comment.post_id, post_id: i.comment.post_id,
score: i.comment.my_vote == 1 ? 0 : 1, score: this.state.my_vote,
}; };
WebSocketService.Instance.likeComment(form); WebSocketService.Instance.likeComment(form);
this.setState(this.state);
} }
handleCommentDownvote(i: CommentNodeI) { handleCommentDownvote(i: CommentNodeI) {
if (UserService.Instance.user) { let new_vote = this.state.my_vote == -1 ? 0 : -1;
this.setState({
downvoteLoading: true, if (this.state.my_vote == 1) {
}); this.state.score -= 2;
this.state.upvotes--;
this.state.downvotes++;
} else if (this.state.my_vote == -1) {
this.state.downvotes--;
this.state.score++;
} else {
this.state.downvotes++;
this.state.score--;
} }
this.state.my_vote = new_vote;
let form: CommentLikeForm = { let form: CommentLikeForm = {
comment_id: i.comment.id, comment_id: i.comment.id,
post_id: i.comment.post_id, post_id: i.comment.post_id,
score: i.comment.my_vote == -1 ? 0 : -1, score: this.state.my_vote,
}; };
WebSocketService.Instance.likeComment(form); WebSocketService.Instance.likeComment(form);
this.setState(this.state);
} }
handleModRemoveShow(i: CommentNode) { handleModRemoveShow(i: CommentNode) {

View File

@ -43,8 +43,10 @@ interface PostListingState {
showConfirmTransferCommunity: boolean; showConfirmTransferCommunity: boolean;
imageExpanded: boolean; imageExpanded: boolean;
viewSource: boolean; viewSource: boolean;
upvoteLoading: boolean; my_vote: number;
downvoteLoading: boolean; score: number;
upvotes: number;
downvotes: number;
} }
interface PostListingProps { interface PostListingProps {
@ -68,8 +70,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
showConfirmTransferCommunity: false, showConfirmTransferCommunity: false,
imageExpanded: false, imageExpanded: false,
viewSource: false, viewSource: false,
upvoteLoading: this.props.post.upvoteLoading, my_vote: this.props.post.my_vote,
downvoteLoading: this.props.post.downvoteLoading, score: this.props.post.score,
upvotes: this.props.post.upvotes,
downvotes: this.props.post.downvotes,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -83,15 +87,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
componentWillReceiveProps(nextProps: PostListingProps) { componentWillReceiveProps(nextProps: PostListingProps) {
if ( this.state.my_vote = nextProps.post.my_vote;
nextProps.post.upvoteLoading !== this.state.upvoteLoading || this.state.upvotes = nextProps.post.upvotes;
nextProps.post.downvoteLoading !== this.state.downvoteLoading this.state.downvotes = nextProps.post.downvotes;
) { this.state.score = nextProps.post.score;
this.setState({ this.setState(this.state);
upvoteLoading: false,
downvoteLoading: false,
});
}
} }
render() { render() {
@ -118,38 +118,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div class="listing col-12"> <div class="listing col-12">
<div className={`vote-bar mr-2 float-left small text-center`}> <div className={`vote-bar mr-2 float-left small text-center`}>
<button <button
className={`btn btn-link p-0 ${ className={`vote-animate btn btn-link p-0 ${
post.my_vote == 1 ? 'text-info' : 'text-muted' this.state.my_vote == 1 ? 'text-info' : 'text-muted'
}`} }`}
onClick={linkEvent(this, this.handlePostLike)} onClick={linkEvent(this, this.handlePostLike)}
> >
{this.state.upvoteLoading ? ( <svg class="icon upvote">
<svg class="icon icon-spinner spin upvote"> <use xlinkHref="#icon-arrow-up"></use>
<use xlinkHref="#icon-spinner"></use> </svg>
</svg>
) : (
<svg class="icon upvote">
<use xlinkHref="#icon-arrow-up"></use>
</svg>
)}
</button> </button>
<div class={`font-weight-bold text-muted`}>{post.score}</div> <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
{WebSocketService.Instance.site.enable_downvotes && ( {WebSocketService.Instance.site.enable_downvotes && (
<button <button
className={`btn btn-link p-0 ${ className={`vote-animate btn btn-link p-0 ${
post.my_vote == -1 ? 'text-danger' : 'text-muted' this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
}`} }`}
onClick={linkEvent(this, this.handlePostDisLike)} onClick={linkEvent(this, this.handlePostDisLike)}
> >
{this.state.downvoteLoading ? ( <svg class="icon downvote">
<svg class="icon icon-spinner spin downvote"> <use xlinkHref="#icon-arrow-down"></use>
<use xlinkHref="#icon-spinner"></use> </svg>
</svg>
) : (
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
)}
</button> </button>
)} )}
</div> </div>
@ -315,9 +303,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span> <span>
(<span className="text-info">+{post.upvotes}</span> (<span className="text-info">+{this.state.upvotes}</span>
<span> | </span> <span> | </span>
<span className="text-danger">-{post.downvotes}</span> <span className="text-danger">-{this.state.downvotes}</span>
<span>) </span> <span>) </span>
</span> </span>
</li> </li>
@ -747,28 +735,55 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handlePostLike(i: PostListing) { handlePostLike(i: PostListing) {
if (UserService.Instance.user) { let new_vote = i.state.my_vote == 1 ? 0 : 1;
i.setState({ upvoteLoading: true });
if (i.state.my_vote == 1) {
i.state.score--;
i.state.upvotes--;
} else if (i.state.my_vote == -1) {
i.state.downvotes--;
i.state.upvotes++;
i.state.score += 2;
} else {
i.state.upvotes++;
i.state.score++;
} }
i.state.my_vote = new_vote;
let form: CreatePostLikeForm = { let form: CreatePostLikeForm = {
post_id: i.props.post.id, post_id: i.props.post.id,
score: i.props.post.my_vote == 1 ? 0 : 1, score: i.state.my_vote,
}; };
WebSocketService.Instance.likePost(form); WebSocketService.Instance.likePost(form);
i.setState(i.state);
} }
handlePostDisLike(i: PostListing) { handlePostDisLike(i: PostListing) {
if (UserService.Instance.user) { let new_vote = i.state.my_vote == -1 ? 0 : -1;
i.setState({ downvoteLoading: true });
if (i.state.my_vote == 1) {
i.state.score -= 2;
i.state.upvotes--;
i.state.downvotes++;
} else if (i.state.my_vote == -1) {
i.state.downvotes--;
i.state.score++;
} else {
i.state.downvotes++;
i.state.score--;
} }
i.state.my_vote = new_vote;
let form: CreatePostLikeForm = { let form: CreatePostLikeForm = {
post_id: i.props.post.id, post_id: i.props.post.id,
score: i.props.post.my_vote == -1 ? 0 : -1, score: i.state.my_vote,
}; };
WebSocketService.Instance.likePost(form); WebSocketService.Instance.likePost(form);
i.setState(i.state);
} }
handleEditClick(i: PostListing) { handleEditClick(i: PostListing) {

View File

@ -177,8 +177,6 @@ export interface Post {
subscribed?: boolean; subscribed?: boolean;
read?: boolean; read?: boolean;
saved?: boolean; saved?: boolean;
upvoteLoading?: boolean;
downvoteLoading?: boolean;
duplicates?: Array<Post>; duplicates?: Array<Post>;
} }
@ -209,8 +207,6 @@ export interface Comment {
saved?: boolean; saved?: boolean;
user_mention_id?: number; // For mention type user_mention_id?: number; // For mention type
recipient_id?: number; recipient_id?: number;
upvoteLoading?: boolean;
downvoteLoading?: boolean;
} }
export interface Category { export interface Category {

4
ui/src/utils.ts vendored
View File

@ -601,8 +601,6 @@ export function createCommentLikeRes(
found.downvotes = data.comment.downvotes; found.downvotes = data.comment.downvotes;
if (data.comment.my_vote !== null) { if (data.comment.my_vote !== null) {
found.my_vote = data.comment.my_vote; found.my_vote = data.comment.my_vote;
found.upvoteLoading = false;
found.downvoteLoading = false;
} }
} }
} }
@ -620,8 +618,6 @@ export function createPostLikeRes(data: PostResponse, post: Post) {
post.downvotes = data.post.downvotes; post.downvotes = data.post.downvotes;
if (data.post.my_vote !== null) { if (data.post.my_vote !== null) {
post.my_vote = data.post.my_vote; post.my_vote = data.post.my_vote;
post.upvoteLoading = false;
post.downvoteLoading = false;
} }
} }