Skip to content

Commit

Permalink
Add reaction mutation functionality
Browse files Browse the repository at this point in the history
Users can do meta-reviews directly on the gh-board.

Closes #146
  • Loading branch information
li-boxuan committed Aug 7, 2018
1 parent 38a94ce commit b062087
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 145 deletions.
10 changes: 10 additions & 0 deletions script/queries/github_reaction_add.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mutation($id: ID!, $content: ReactionContent!) {
addReaction(input:{subjectId:$id, content: $content}) {
reaction {
content
}
subject {
id
}
}
}
10 changes: 10 additions & 0 deletions script/queries/github_reaction_remove.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mutation($id: ID!, $content: ReactionContent!) {
removeReaction(input:{subjectId:$id, content: $content}) {
reaction {
content
}
subject {
id
}
}
}
6 changes: 5 additions & 1 deletion script/queries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import GITHUB_ISSUE_INFO_QUERY from './github_issue_info.graphql';
import GITHUB_PR_INFO_QUERY from './github_pr_info.graphql';
import GITHUB_LABEL_INFO_QUERY from './github_label_info.graphql';
import GITHUB_REACTION_INFO_QUERY from './github_reaction_info.graphql';
import GITHUB_REACTION_ADD_MUTATION from './github_reaction_add.graphql';
import GITHUB_REACTION_REMOVE_MUTATION from './github_reaction_remove.graphql';
export {
GITHUB_ISSUE_INFO_QUERY,
GITHUB_PR_INFO_QUERY,
GITHUB_LABEL_INFO_QUERY,
GITHUB_REACTION_INFO_QUERY
GITHUB_REACTION_INFO_QUERY,
GITHUB_REACTION_ADD_MUTATION,
GITHUB_REACTION_REMOVE_MUTATION,
};
34 changes: 34 additions & 0 deletions src/components/login-auth.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Component} from 'react';

import CurrentUserStore from '../user-store';
import Client from '../github-client';

function withAuth(WrappedComponent) {
return class extends Component {
state = {loginInfo: null};

componentDidMount() {
Client.on('changeToken', this.onChangeToken);
this.onChangeToken();
}

componentWillUnmount() {
Client.off('changeToken', this.onChangeToken);
}

onChangeToken = () => {
CurrentUserStore.fetchUser()
.then((loginInfo) => {
this.setState({loginInfo});
}).catch(() => {
this.setState({loginInfo: null});
});
};

render() {
return <WrappedComponent {...this.props} loginInfo={this.state.loginInfo} />;
}
};
}

export default withAuth;
219 changes: 180 additions & 39 deletions src/components/reactions.jsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,186 @@
import {Component} from 'react';
import * as BS from 'react-bootstrap';

function Reactions({stat}) {
// use null when count is zero because we don't want to display
// number zero on frontend
const reactions = [
{
emoji: 'πŸ‘',
count: stat.THUMBS_UP || null,
name: 'THUMBS_UP'
},
{
emoji: 'πŸ‘Ž',
count: stat.THUMBS_DOWN || null,
name: 'THUMBS_DOWN'
},
{
emoji: 'πŸ˜„',
count: stat.LAUGH || null,
name: 'LAUGH'
},
{
emoji: 'πŸŽ‰',
count: stat.HOORAY || null,
name: 'HOORAY'
},
{
emoji: 'πŸ˜•',
count: stat.CONFUSED || null,
name: 'CONFUSED'
},
{
emoji: '❀️',
count: stat.HEART || null,
name: 'HEART'
import Client from '../github-client';

class Reactions extends Component {
constructor(props) {
super(props);
this.state = {
canAdd: {},
// use cache to reflect reaction count on frontend
// if we fetch up-to-date reaction count after mutation,
// we have to refetch the whole pull request which wastes
// a lot of API hits (there is no way to fetch single review
// comment at the moment)
cacheCount: {
THUMBS_UP: 0,
THUMBS_DOWN: 0,
LAUGH: 0,
HOORAY: 0,
CONFUSED: 0,
HEART: 0
}
};
}

onClick = async (id, content) => {
const canAdd = this.state.canAdd[content];
const saveToDatabase = this.props.saveCallBack;
let result, msg;
if (canAdd) {
({ result, msg } = await Client.getGraphQLClient().addReaction(
{id, content}
));
} else {
({ result, msg } = await Client.getGraphQLClient().removeReaction(
{id, content}
));
}
if (result) {
if (canAdd) {
// reaction creation succeeds

// Note that if it is already meta-reviewed by the user but not via gh-board,
// action (add reaction) will fail, but GitHub won't return any error/warning.
// The good news is that user won't be annoyed because the frontend behavior
// is they add reactions successfully.

// A side note is that gh-board will not update accordingly if user does
// meta-review directly on GitHub web page instead of on gh-board. This is
// because the `updatedBy` attribute of the pull request won't get changed
// due to meta-review.

this.setState((prevState) => {
let newState = prevState;
newState.canAdd[content] = false;
// update cache
newState.cacheCount[content] += 1;
return newState;
});

saveToDatabase(content, true);
} else {
// reaction removal succeeds
this.setState((prevState) => {
let newState = prevState;
newState.canAdd[content] = true;
// update cache
newState.cacheCount[content] -= 1;
return newState;
});

saveToDatabase(content, false);
}
} else {
if (canAdd) {
// reaction creation fails
console.log('add', content, 'to comment id', id, 'failed.',
'message: ', msg);
} else {
console.log('remove', content, 'from comment id', id, 'failed',
'message:', msg);
// reaction removal fails
if (msg && msg.length && msg[0].type === 'FORBIDDEN') {
console.log('reaction removal failed due to permission error.',
'This is probably because user has done meta-review somewhere out',
'of gh-board.');
this.setState((prevState) => {
let newState = prevState;
newState.canAdd[content] = true;
// clean cache
newState.cacheCount[content] = 0;
return newState;
});
this.syncReview();
}
}
}
}

render() {
// id is the global identifier for the corresponding review comment
const {id, stat, hasLogin, noReactionByMe} = this.props;

if (noReactionByMe && !Object.keys(this.state.canAdd).length) {
// use deep copy for canAdd instead of reference so that we can
// we deliberately only copy them once
this.state.canAdd = {
THUMBS_UP: noReactionByMe.THUMBS_UP,
THUMBS_DOWN: noReactionByMe.THUMBS_DOWN,
LAUGH: noReactionByMe.LAUGH,
HOORAY: noReactionByMe.HOORAY,
CONFUSED: noReactionByMe.CONFUSED,
HEART: noReactionByMe.HEART
};
}
];
return reactions.map(reaction => (
<BS.Button bsClass="reaction-btn">
{reaction.emoji} {reaction.count}
</BS.Button>
));

// props reflect real status of reactions, but may be out of date
// we need to update cached information (this.state) accordingly
if (noReactionByMe && this.state.canAdd) {
const contents = ['THUMBS_UP', 'THUMBS_DOWN', 'LAUGH', 'HOORAY', 'CONFUSED', 'HEART'];
for (const content of contents) {
if (!noReactionByMe[content] && !this.state.canAdd[content]
&& this.state.cacheCount[content] === 1) {
// our action (reaction creation) is now correctly reflected by props
// need to flush cache, otherwise reaction count would be wrong
console.log('flush creation cache of content', content);
this.state.cacheCount[content] = 0;
}
if (noReactionByMe[content] && this.state.canAdd[content]
&& this.state.cacheCount[content] === -1) {
// our action (reaction removal) is now correctly reflected by props
// need to flush cache, otherwise reaction count would be wrong
console.log('flush removal cache of content', content);
this.state.cacheCount[content] = 0;
}
}
}

// use null when count is zero because we don't want to display
// number zero on frontend
const reactions = [
{
emoji: 'πŸ‘',
count: stat.THUMBS_UP + this.state.cacheCount.THUMBS_UP || null,
name: 'THUMBS_UP'
},
{
emoji: 'πŸ‘Ž',
count: stat.THUMBS_DOWN + this.state.cacheCount.THUMBS_DOWN || null,
name: 'THUMBS_DOWN'
},
{
emoji: 'πŸ˜„',
count: stat.LAUGH + this.state.cacheCount.LAUGH || null,
name: 'LAUGH'
},
{
emoji: 'πŸŽ‰',
count: stat.HOORAY + this.state.cacheCount.HOORAY || null,
name: 'HOORAY'
},
{
emoji: 'πŸ˜•',
count: stat.CONFUSED + this.state.cacheCount.CONFUSED || null,
name: 'CONFUSED'
},
{
emoji: '❀️',
count: stat.HEART + this.state.cacheCount.HEART || null,
name: 'HEART'
}
];
return reactions.map(reaction => (
<BS.Button
key={reaction.name}
bsClass="reaction-btn"
onClick={() => this.onClick(id, reaction.name)}
disabled={!hasLogin}>
{reaction.emoji} {reaction.count}
</BS.Button>
));
}
}

export default Reactions;
Loading

0 comments on commit b062087

Please sign in to comment.