Adding instant voting / vote animations. Fixes #526
This commit is contained in:
parent
f494835e8a
commit
036d6260bb
6 changed files with 130 additions and 102 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
6
ui/assets/css/main.css
vendored
6
ui/assets/css/main.css
vendored
|
@ -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);
|
||||||
|
}
|
||||||
|
|
112
ui/src/components/comment-node.tsx
vendored
112
ui/src/components/comment-node.tsx
vendored
|
@ -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) {
|
||||||
|
|
103
ui/src/components/post-listing.tsx
vendored
103
ui/src/components/post-listing.tsx
vendored
|
@ -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) {
|
||||||
|
|
4
ui/src/interfaces.ts
vendored
4
ui/src/interfaces.ts
vendored
|
@ -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
4
ui/src/utils.ts
vendored
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue