diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js
index e212af99b..eaaee3034 100644
--- a/client/coral-framework/actions/auth.js
+++ b/client/coral-framework/actions/auth.js
@@ -311,11 +311,11 @@ export const checkLogin = () => (dispatch) => {
throw new Error('Not logged in');
}
- dispatch(checkLoginSuccess(result.user));
-
// Reset the websocket.
resetWebsocket();
+ dispatch(checkLoginSuccess(result.user));
+
// Display create username dialog if necessary.
if (result.user.canEditName && result.user.status !== 'BANNED') {
dispatch(showCreateUsernameDialog());
diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js
index ef4c2ac74..01b3daff4 100644
--- a/client/coral-framework/hocs/index.js
+++ b/client/coral-framework/hocs/index.js
@@ -1,5 +1,4 @@
export {default as withFragments} from './withFragments';
export {default as withMutation} from './withMutation';
export {default as withQuery} from './withQuery';
-export {default as withReaction} from './withReaction';
diff --git a/client/coral-framework/hocs/withReaction.js b/client/coral-framework/hocs/withReaction.js
deleted file mode 100644
index ca373b6bd..000000000
--- a/client/coral-framework/hocs/withReaction.js
+++ /dev/null
@@ -1,240 +0,0 @@
-import React from 'react';
-import get from 'lodash/get';
-import uuid from 'uuid/v4';
-import {connect} from 'react-redux';
-import {bindActionCreators} from 'redux';
-import {getDisplayName} from '../helpers/hoc';
-import {compose, gql, graphql} from 'react-apollo';
-import withFragments from 'coral-framework/hocs/withFragments';
-import {showSignInDialog} from 'coral-framework/actions/auth';
-import {capitalize} from 'coral-framework/helpers/strings';
-import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
-
-export default (reaction) => (WrappedComponent) => {
- if (typeof reaction !== 'string') {
- console.error('Reaction must be a valid string');
- return null;
- }
-
- reaction = reaction.toLowerCase();
-
- class WithReactions extends React.Component {
- render() {
- const {comment} = this.props;
-
- const reactionSummary = getMyActionSummary(
- `${capitalize(reaction)}ActionSummary`,
- comment
- );
-
- const count = getTotalActionCount(
- `${capitalize(reaction)}ActionSummary`,
- comment
- );
-
- const alreadyReacted = () => !!reactionSummary;
-
- const withReactionProps = {reactionSummary, count, alreadyReacted};
-
- return ;
- }
- }
-
- const isReaction = (a) =>
- a.__typename === `${capitalize(reaction)}ActionSummary`;
-
- const COMMENT_FRAGMENT = gql`
- fragment ${capitalize(reaction)}Button_updateFragment on Comment {
- action_summaries {
- ... on ${capitalize(reaction)}ActionSummary {
- count
- current_user {
- id
- }
- }
- }
- }
- `;
-
- const withDeleteReaction = graphql(
- gql`
- mutation deleteReaction($id: ID!) {
- deleteAction(id:$id) {
- errors {
- translation_key
- }
- }
- }
- `,
- {
- props: ({mutate, ownProps}) => ({
- deleteReaction: () => {
-
- const reactionSummary = getMyActionSummary(
- `${capitalize(reaction)}ActionSummary`,
- ownProps.comment
- );
-
- const reactionData = {
- id: reactionSummary.current_user.id,
- commentId: ownProps.comment.id
- };
-
- return mutate({
- variables: {id: reactionData.id},
- optimisticResponse: {
- deleteAction: {
- __typename: 'DeleteActionResponse',
- errors: null
- }
- },
- update: (proxy) => {
- const fragmentId = `Comment_${reactionData.commentId}`;
-
- // Read the data from our cache for this query.
- const data = proxy.readFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId
- });
-
- // Check whether we liked this comment.
- const idx = data.action_summaries.findIndex(isReaction);
- if (
- idx < 0 ||
- get(data.action_summaries[idx], 'current_user.id') !== reactionData.id
- ) {
- return;
- }
-
- data.action_summaries[idx] = {
- ...data.action_summaries[idx],
- count: data.action_summaries[idx].count - 1,
- current_user: null
- };
-
- // Write our data back to the cache.
- proxy.writeFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId,
- data
- });
- }
- });
- }
- })
- }
- );
-
- const withPostReaction = graphql(
- gql`
- mutation create${capitalize(reaction)}($${reaction}: Create${capitalize(reaction)}Input!) {
- create${capitalize(reaction)}(${reaction}: $${reaction}) {
- ${reaction} {
- id
- }
- errors {
- translation_key
- }
- }
- }
- `,
- {
- props: ({mutate, ownProps}) => ({
- postReaction: () => {
-
- const reactionData = {
- item_id: ownProps.comment.id,
- item_type: 'COMMENTS'
- };
-
- return mutate({
- variables: {[reaction]: reactionData},
- optimisticResponse: {
- [`create${capitalize(reaction)}`]: {
- __typename: `Create${capitalize(reaction)}Response`,
- errors: null,
- [reaction]: {
- __typename: `${capitalize(reaction)}Action`,
- id: uuid()
- }
- }
- },
- update: (proxy, mutationResult) => {
- const fragmentId = `Comment_${reactionData.item_id}`;
-
- // Read the data from our cache for this query.
- const data = proxy.readFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId
- });
-
- // Add our comment from the mutation to the end.
- let idx = data.action_summaries.findIndex(isReaction);
-
- // Check whether we already reactioned this comment.
- if (idx >= 0 && data.action_summaries[idx].current_user) {
- return;
- }
-
- if (idx < 0) {
-
- // Add initial action when it doesn't exist.
- data.action_summaries.push({
- __typename: `${capitalize(reaction)}ActionSummary`,
- count: 0,
- current_user: null
- });
- idx = data.action_summaries.length - 1;
- }
-
- data.action_summaries[idx] = {
- ...data.action_summaries[idx],
- count: data.action_summaries[idx].count + 1,
- current_user: mutationResult.data[
- `create${capitalize(reaction)}`
- ][reaction]
- };
-
- // Write our data back to the cache.
- proxy.writeFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId,
- data
- });
- }
- });
- }
- })
- }
- );
-
- const mapStateToProps = (state) => ({
- user: state.auth.toJS().user,
- });
-
- const mapDispatchToProps = (dispatch) =>
- bindActionCreators({showSignInDialog}, dispatch);
-
- const enhance = compose(
- withFragments({
- comment: gql`
- fragment ${capitalize(reaction)}Button_comment on Comment {
- action_summaries {
- ... on ${capitalize(reaction)}ActionSummary {
- count
- current_user {
- id
- }
- }
- }
- }`
- }),
- connect(mapStateToProps, mapDispatchToProps),
- withDeleteReaction,
- withPostReaction
- );
-
- WithReactions.displayName = `WithReactions(${getDisplayName(WrappedComponent)})`;
-
- return enhance(WithReactions);
-};
diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js
index 70408248c..26d8e391f 100644
--- a/client/coral-framework/services/client.js
+++ b/client/coral-framework/services/client.js
@@ -1,7 +1,7 @@
import ApolloClient, {addTypename} from 'apollo-client';
import {networkInterface} from './transport';
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
-import {SUBSCRIPTION_END} from 'subscriptions-transport-ws/dist/messageTypes';
+import MessageTypes from 'subscriptions-transport-ws/dist/message-types';
import {getAuthToken} from '../helpers/request';
let client, wsClient = null, wsClientToken = null;
@@ -13,18 +13,17 @@ export function resetWebsocket() {
return;
}
- // Unsubscribe from all the active subscriptions.
- Object.keys(wsClient.subscriptions).forEach((id) => {
-
- // Create the message.
- let message = {id: parseInt(id), type: SUBSCRIPTION_END};
-
- // Send the unsubscribe message.
- wsClient.client.send(JSON.stringify(message));
- });
-
- // Close the client, this will trigger a reconnect.
+ // Close socket connection which will also unregister subscriptions on the server-side.
wsClient.close();
+
+ // Reconnect to the server.
+ wsClient.connect();
+
+ // Reregister all subscriptions (uses non public api).
+ // See: https://github.com/apollographql/subscriptions-transport-ws/issues/171
+ Object.keys(wsClient.operations).forEach((id) => {
+ wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options);
+ });
}
export function getClient() {
@@ -35,6 +34,7 @@ export function getClient() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
wsClient = new SubscriptionClient(`${protocol}://${location.host}/api/v1/live`, {
reconnect: true,
+ lazy: true,
connectionParams: {
get token() {
diff --git a/graph/mutators/action.js b/graph/mutators/action.js
index 0d1cd49f8..de9efea1a 100644
--- a/graph/mutators/action.js
+++ b/graph/mutators/action.js
@@ -39,7 +39,7 @@ const createAction = async ({user = {}}, {item_id, item_type, action_type, group
* @return {Promise} resolves when the action is deleted
*/
const deleteAction = ({user}, {id}) => {
- return ActionModel.remove({
+ return ActionModel.findOneAndRemove({
id,
user_id: user.id
});
diff --git a/graph/subscriptions.js b/graph/subscriptions.js
index 0cc37e841..7d97d3521 100644
--- a/graph/subscriptions.js
+++ b/graph/subscriptions.js
@@ -50,7 +50,7 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
connection.upgradeReq.headers['authorization'] = `Bearer ${token}`;
}
},
- onSubscribe: (parsedMessage, baseParams, connection) => {
+ onOperation: (parsedMessage, baseParams, connection) => {
// Cache the upgrade request.
let upgradeReq = connection.upgradeReq;
@@ -58,7 +58,7 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
// Attach the context per request.
baseParams.context = async () => {
let req;
-
+
try {
req = await deserializeUser(upgradeReq);
} catch (e) {
@@ -66,7 +66,7 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
return new Context({}, pubsub);
}
-
+
return new Context(req, pubsub);
};
diff --git a/package.json b/package.json
index d78ba8713..f6a969de9 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,7 @@
"graphql-errors": "^2.1.0",
"graphql-redis-subscriptions": "^1.1.5",
"graphql-server-express": "^0.6.0",
- "graphql-subscriptions": "^0.3.1",
+ "graphql-subscriptions": "^0.4.3",
"graphql-tools": "^0.10.1",
"helmet": "^3.5.0",
"immutability-helper": "^2.2.0",
@@ -116,7 +116,7 @@
"semver": "^5.3.0",
"simplemde": "^1.11.2",
"snake-case": "^2.1.0",
- "subscriptions-transport-ws": "^0.5.5-alpha.0",
+ "subscriptions-transport-ws": "^0.7.2",
"timekeeper": "^1.0.0",
"uuid": "^3.0.1",
"yaml-loader": "^0.4.0",
diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js
index 7ec875656..7be682670 100644
--- a/plugin-api/beta/client/hocs/index.js
+++ b/plugin-api/beta/client/hocs/index.js
@@ -1 +1 @@
-export {withReaction} from 'coral-framework/hocs';
+export {default as withReaction} from './withReaction';
diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js
new file mode 100644
index 000000000..42ac79ec5
--- /dev/null
+++ b/plugin-api/beta/client/hocs/withReaction.js
@@ -0,0 +1,347 @@
+import React from 'react';
+import get from 'lodash/get';
+import uuid from 'uuid/v4';
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {getDisplayName} from 'coral-framework/helpers/hoc';
+import {compose, gql} from 'react-apollo';
+import withFragments from 'coral-framework/hocs/withFragments';
+import withMutation from 'coral-framework/hocs/withMutation';
+import {showSignInDialog} from 'coral-framework/actions/auth';
+import {capitalize} from 'coral-framework/helpers/strings';
+import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
+import * as PropTypes from 'prop-types';
+
+export default (reaction) => (WrappedComponent) => {
+ if (typeof reaction !== 'string') {
+ console.error('Reaction must be a valid string');
+ return null;
+ }
+
+ // Global instance counter for each `reaction` type.
+ let instances = 0;
+
+ // Track current subscriptions.
+ let createdSubscription = null;
+ let deletedSubscription = null;
+
+ reaction = reaction.toLowerCase();
+ const Reaction = capitalize(reaction);
+
+ const COMMENT_FRAGMENT = gql`
+ fragment ${Reaction}Button_updateFragment on Comment {
+ action_summaries {
+ ... on ${Reaction}ActionSummary {
+ count
+ current_user {
+ id
+ }
+ }
+ }
+ }
+ `;
+
+ const isReaction = (a) =>
+ a.__typename === `${Reaction}ActionSummary`;
+
+ const addReactionToStore = (proxy, {action, self}) => {
+ const fragmentId = `Comment_${action.item_id}`;
+
+ // Read the data from our cache for this query.
+ const data = proxy.readFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId
+ });
+
+ // Add our comment from the mutation to the end.
+ let idx = data.action_summaries.findIndex(isReaction);
+
+ // Check whether we already reactioned this comment.
+ if (self && idx >= 0 && data.action_summaries[idx].current_user) {
+ return;
+ }
+
+ if (idx < 0) {
+
+ // Add initial action when it doesn't exist.
+ data.action_summaries.push({
+ __typename: `${Reaction}ActionSummary`,
+ count: 0,
+ current_user: null
+ });
+ idx = data.action_summaries.length - 1;
+ }
+
+ data.action_summaries[idx] = {
+ ...data.action_summaries[idx],
+ count: data.action_summaries[idx].count + 1,
+ current_user: self ? action : data.action_summaries[idx].current_user
+ };
+
+ // Write our data back to the cache.
+ proxy.writeFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId,
+ data
+ });
+ };
+
+ const deleteReactionFromStore = (proxy, {action, self}) => {
+ const fragmentId = `Comment_${action.item_id}`;
+
+ // Read the data from our cache for this query.
+ const data = proxy.readFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId
+ });
+
+ // Check whether we liked this comment.
+ const idx = data.action_summaries.findIndex(isReaction);
+
+ if (
+ self &&
+ (idx < 0 || get(data.action_summaries[idx], 'current_user.id') !== action.id)
+ ) {
+ return;
+ }
+
+ data.action_summaries[idx] = {
+ ...data.action_summaries[idx],
+ count: data.action_summaries[idx].count - 1,
+ current_user: self ? null : data.action_summaries[idx].current_user,
+ };
+
+ // Write our data back to the cache.
+ proxy.writeFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId,
+ data
+ });
+ };
+
+ const REACTION_CREATED_SUBSCRIPTION = gql`
+ subscription ${Reaction}ActionCreated($assetId: ID!) {
+ ${reaction}ActionCreated(asset_id: $assetId) {
+ id
+ user {
+ id
+ }
+ item_id
+ }
+ }
+ `;
+
+ const REACTION_DELETED_SUBSCRIPTION = gql`
+ subscription ${Reaction}ActionDeleted($assetId: ID!) {
+ ${reaction}ActionDeleted(asset_id: $assetId) {
+ id
+ user {
+ id
+ }
+ item_id
+ }
+ }
+ `;
+
+ class WithReactions extends React.Component {
+
+ static contextTypes = {
+ client: PropTypes.object.isRequired,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ // Start subscriptions when it is first needed.
+ if (instances === 0) {
+ createdSubscription = context.client.subscribe({
+ query: REACTION_CREATED_SUBSCRIPTION,
+ variables: {
+ assetId: this.props.root.asset.id,
+ },
+ }).subscribe({
+ next: this.onReactionCreated,
+ error(err) { console.error('err', err); },
+ });
+
+ deletedSubscription = context.client.subscribe({
+ query: REACTION_DELETED_SUBSCRIPTION,
+ variables: {
+ assetId: this.props.root.asset.id,
+ },
+ }).subscribe({
+ next: this.onReactionDeleted,
+ error(err) { console.error('err', err); },
+ });
+ }
+ instances++;
+ }
+
+ // onReactionCreated handles live updates through the subscriptions.
+ onReactionCreated = ({[`${reaction}ActionCreated`]: action}) => {
+ if (this.props.user && action.user && this.props.user.id === action.user.id) {
+ return;
+ }
+ addReactionToStore(this.context.client, {action, self: false});
+ };
+
+ // onReactionDeleted handles live updates through the subscriptions.
+ onReactionDeleted = ({[`${reaction}ActionDeleted`]: action}) => {
+ if (this.props.user && action.user && this.props.user.id === action.user.id) {
+ return;
+ }
+ deleteReactionFromStore(this.context.client, {action, self: false});
+ };
+
+ componentWillUnmount() {
+ instances--;
+
+ // End subscriptions when last component will be unmounted.
+ if (instances === 0) {
+ try {
+ createdSubscription.unsubscribe();
+ deletedSubscription.unsubscribe();
+ }
+ catch(e) {
+ console.warn(e);
+ }
+ }
+ }
+
+ render() {
+ const {comment} = this.props;
+
+ const reactionSummary = getMyActionSummary(
+ `${Reaction}ActionSummary`,
+ comment
+ );
+
+ const count = getTotalActionCount(
+ `${Reaction}ActionSummary`,
+ comment
+ );
+
+ const alreadyReacted = !!reactionSummary;
+
+ const withReactionProps = {reactionSummary, count, alreadyReacted};
+
+ return ;
+ }
+ }
+
+ const withDeleteReaction = withMutation(
+ gql`
+ mutation Delete${Reaction}Action($input: Delete${Reaction}ActionInput!) {
+ delete${Reaction}Action(input: $input) {
+ errors {
+ translation_key
+ }
+ }
+ }
+ `,
+ {
+ props: ({mutate, ownProps}) => ({
+ deleteReaction: () => {
+
+ const reactionSummary = getMyActionSummary(
+ `${Reaction}ActionSummary`,
+ ownProps.comment
+ );
+
+ const id = reactionSummary.current_user.id;
+ const item_id = ownProps.comment.id;
+
+ const input = {id};
+ return mutate({
+ variables: {input},
+ optimisticResponse: {
+ [`delete${Reaction}Action`]: {
+ __typename: `Delete${Reaction}ActionResponse`,
+ errors: null
+ }
+ },
+ update: (proxy) => {
+ deleteReactionFromStore(proxy, {action: {item_id, id}, self: true});
+ }
+ });
+ }
+ })
+ }
+ );
+
+ const withPostReaction = withMutation(
+ gql`
+ mutation Create${Reaction}Action($input: Create${Reaction}ActionInput!) {
+ create${Reaction}Action(input: $input) {
+ ${reaction} {
+ id
+ }
+ errors {
+ translation_key
+ }
+ }
+ }
+ `,
+ {
+ props: ({mutate, ownProps}) => ({
+ postReaction: () => {
+
+ const input = {
+ item_id: ownProps.comment.id,
+ };
+
+ return mutate({
+ variables: {input},
+ optimisticResponse: {
+ [`create${Reaction}Action`]: {
+ __typename: `Create${Reaction}ActionResponse`,
+ errors: null,
+ [reaction]: {
+ __typename: `${Reaction}Action`,
+ id: uuid()
+ }
+ }
+ },
+ update: (proxy, {data: {[`create${Reaction}Action`]: {[reaction]: action}}}) => {
+ const a = {
+ ...action,
+ item_id: input.item_id,
+ };
+ addReactionToStore(proxy, {action: a, self: true});
+ }
+ });
+ }
+ })
+ }
+ );
+
+ const mapStateToProps = (state) => ({
+ user: state.auth.toJS().user,
+ });
+
+ const mapDispatchToProps = (dispatch) =>
+ bindActionCreators({showSignInDialog}, dispatch);
+
+ const enhance = compose(
+ withFragments({
+ comment: gql`
+ fragment ${Reaction}Button_comment on Comment {
+ action_summaries {
+ ... on ${Reaction}ActionSummary {
+ count
+ current_user {
+ id
+ }
+ }
+ }
+ }`
+ }),
+ connect(mapStateToProps, mapDispatchToProps),
+ withDeleteReaction,
+ withPostReaction
+ );
+
+ WithReactions.displayName = `WithReactions(${getDisplayName(WrappedComponent)})`;
+
+ return enhance(WithReactions);
+};
diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js
new file mode 100644
index 000000000..83c45dcb5
--- /dev/null
+++ b/plugin-api/beta/server/getReactionConfig.js
@@ -0,0 +1,192 @@
+const wrapResponse = require('../../../graph/helpers/response');
+const {SEARCH_OTHER_USERS} = require('../../../perms/constants');
+
+function getReactionConfig(reaction) {
+ reaction = reaction.toLowerCase();
+
+ const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
+ const REACTION = reaction.toUpperCase();
+ const typeDefs = `
+ enum ACTION_TYPE {
+
+ # Represents a ${Reaction}.
+ ${REACTION}
+ }
+
+ enum ASSET_METRICS_SORT {
+
+ # Represents a ${Reaction}Action.
+ ${REACTION}
+ }
+
+ input Create${Reaction}ActionInput {
+
+ # The item's id for which we are to create a ${reaction}.
+ item_id: ID!
+ }
+
+ input Delete${Reaction}ActionInput {
+
+ # The item's id for which we are deleting a ${reaction}.
+ id: ID!
+ }
+
+ # ${Reaction}Action is used by users who "${reaction}" a specific entity.
+ type ${Reaction}Action implements Action {
+
+ # The ID of the action.
+ id: ID!
+
+ # The author of the action.
+ user: User
+
+ # The time when the Action was updated.
+ updated_at: Date
+
+ # The time when the Action was created.
+ created_at: Date
+
+ # The item's id for which the Action was created.
+ item_id: ID!
+ }
+
+ type ${Reaction}ActionSummary implements ActionSummary {
+
+ # The count of actions with this group.
+ count: Int
+
+ # The current user's action.
+ current_user: ${Reaction}Action
+ }
+
+ # A summary of counts related to all the ${Reaction}s on an Asset.
+ type ${Reaction}AssetActionSummary implements AssetActionSummary {
+
+ # Number of ${reaction}s associated with actionable types on this this Asset.
+ actionCount: Int
+
+ # Number of unique actionable types that are referenced by the ${reaction}s.
+ actionableItemCount: Int
+ }
+
+ type Create${Reaction}ActionResponse implements Response {
+
+ # The ${reaction} that was created.
+ ${reaction}: ${Reaction}Action
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError!]
+ }
+
+ type Delete${Reaction}ActionResponse implements Response {
+
+ # The ${reaction} that was created.
+ ${reaction}: ${Reaction}Action
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError!]
+ }
+
+ type RootMutation {
+
+ # Creates a ${reaction} on an entity.
+ create${Reaction}Action(input: Create${Reaction}ActionInput!): Create${Reaction}ActionResponse
+ delete${Reaction}Action(input: Delete${Reaction}ActionInput!): Delete${Reaction}ActionResponse
+ }
+
+ type Subscription {
+
+ # Subscribe to ${reaction}s.
+ ${reaction}ActionCreated(asset_id: ID!): ${Reaction}Action
+
+ # Subscribe to ${reaction} removals.
+ ${reaction}ActionDeleted(asset_id: ID!): ${Reaction}Action
+ }
+ `;
+
+ return {
+ typeDefs,
+ resolvers: {
+ Subscription: {
+ [`${reaction}ActionCreated`]: ({action}) => {
+ return action;
+ },
+ [`${reaction}ActionDeleted`]: ({action}) => {
+ return action;
+ },
+ },
+ [`${Reaction}Action`]: {
+
+ // This will load the user for the specific action. We'll limit this to the
+ // admin users only or the current logged in user.
+ user({user_id}, _, {loaders: {Users}, user}) {
+ if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) {
+ return Users.getByID.load(user_id);
+ }
+ }
+ },
+ RootMutation: {
+ [`create${Reaction}Action`]: (_, {input: {item_id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
+ const response = Comments.get.load(item_id).then((comment) => {
+ return Action.create({item_id, item_type: 'COMMENTS', action_type: REACTION})
+ .then((action) => {
+
+ // The comment is needed to allow better filtering e.g. by asset_id.
+ pubsub.publish(`${reaction}ActionCreated`, {action, comment});
+ return Promise.resolve(action);
+ });
+ });
+ return wrapResponse(reaction)(response);
+ },
+ [`delete${Reaction}Action`]: (_, {input: {id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
+ const response = Action.delete({id})
+ .then((action) => {
+ return Comments.get.load(action.item_id).then((comment) => {
+
+ // The comment is needed to allow better filtering e.g. by asset_id.
+ pubsub.publish(`${reaction}ActionDeleted`, {action, comment});
+ return Promise.resolve(action);
+ });
+ });
+ return wrapResponse(reaction)(response);
+ }
+ },
+ },
+ hooks: {
+ Action: {
+ __resolveType: {
+ post({action_type}) {
+ switch (action_type) {
+ case REACTION:
+ return `${Reaction}Action`;
+ }
+ }
+ }
+ },
+ ActionSummary: {
+ __resolveType: {
+ post({action_type}) {
+ switch (action_type) {
+ case REACTION:
+ return `${Reaction}ActionSummary`;
+ }
+ }
+ }
+ }
+ },
+ setupFunctions: {
+ [`${reaction}ActionCreated`]: (options, args) => ({
+ [`${reaction}ActionCreated`]: {
+ filter: ({comment}) => comment.asset_id === args.asset_id,
+ },
+ }),
+ [`${reaction}ActionDeleted`]: (options, args) => ({
+ [`${reaction}ActionDeleted`]: {
+ filter: ({comment}) => comment.asset_id === args.asset_id,
+ },
+ }),
+ },
+ };
+}
+
+module.exports = getReactionConfig;
diff --git a/plugin-api/beta/server/index.js b/plugin-api/beta/server/index.js
new file mode 100644
index 000000000..b32d78a45
--- /dev/null
+++ b/plugin-api/beta/server/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ getReactionConfig: require('./getReactionConfig'),
+};
diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js
new file mode 100644
index 000000000..5b99f7df4
--- /dev/null
+++ b/plugins/coral-plugin-like/client/LikeButton.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import styles from './styles.css';
+import {withReaction} from 'plugin-api/beta/client/hocs';
+import {t, can} from 'plugin-api/beta/client/services';
+import {Icon} from 'plugin-api/beta/client/components';
+import cn from 'classnames';
+
+const plugin = 'coral-plugin-like';
+
+class LikeButton extends React.Component {
+ handleClick = () => {
+ const {
+ postReaction,
+ deleteReaction,
+ showSignInDialog,
+ alreadyReacted,
+ user,
+ } = this.props;
+
+ // If the current user does not exist, trigger sign in dialog.
+ if (!user) {
+ showSignInDialog();
+ return;
+ }
+
+ // If the current user is suspended, do nothing.
+ if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
+ return;
+ }
+
+ if (alreadyReacted) {
+ deleteReaction();
+ } else {
+ postReaction();
+ }
+ };
+
+ render() {
+ const {count, alreadyReacted} = this.props;
+ return (
+
+
+
+ );
+ }
+}
+
+export default withReaction('like')(LikeButton);
diff --git a/plugins/coral-plugin-like/client/components/LikeButton.js b/plugins/coral-plugin-like/client/components/LikeButton.js
deleted file mode 100644
index 7b66a11d4..000000000
--- a/plugins/coral-plugin-like/client/components/LikeButton.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import React, {Component} from 'react';
-import styles from './style.css';
-
-import cn from 'classnames';
-import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
-import t from 'coral-framework/services/i18n';
-
-const name = 'coral-plugin-like';
-
-class LikeButton extends Component {
- handleClick = () => {
- const {postLike, showSignInDialog, deleteAction} = this.props;
- const {root: {me}, comment} = this.props;
-
- const myLikeActionSummary = getMyActionSummary(
- 'LikeActionSummary',
- comment
- );
-
- // If the current user does not exist, trigger sign in dialog.
- if (!me) {
- showSignInDialog();
- return;
- }
-
- // If the current user is banned, do nothing.
- if (me.status === 'BANNED') {
- return;
- }
-
- if (myLikeActionSummary) {
- deleteAction(myLikeActionSummary.current_user.id, comment.id);
- } else {
- postLike({
- item_id: comment.id,
- item_type: 'COMMENTS'
- });
- }
- };
-
- render() {
- const {comment} = this.props;
-
- if (!comment) {
- return null;
- }
-
- const myLike = getMyActionSummary('LikeActionSummary', comment);
- let count = getTotalActionCount('LikeActionSummary', comment);
-
- return (
-
-
-
- );
- }
-}
-
-LikeButton.propTypes = {
- data: React.PropTypes.object.isRequired
-};
-
-export default LikeButton;
diff --git a/plugins/coral-plugin-like/client/containers/LikeButton.js b/plugins/coral-plugin-like/client/containers/LikeButton.js
deleted file mode 100644
index 51bee1b40..000000000
--- a/plugins/coral-plugin-like/client/containers/LikeButton.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import get from 'lodash/get';
-import {connect} from 'react-redux';
-import {bindActionCreators} from 'redux';
-import {compose, gql, graphql} from 'react-apollo';
-import LikeButton from '../components/LikeButton';
-import withFragments from 'coral-framework/hocs/withFragments';
-import{showSignInDialog} from 'coral-framework/actions/auth';
-
-const isLikeAction = (a) => a.__typename === 'LikeActionSummary';
-
-const COMMENT_FRAGMENT = gql`
- fragment LikeButton_updateFragment on Comment {
- action_summaries {
- ... on LikeActionSummary {
- count
- current_user {
- id
- }
- }
- }
- }
-`;
-
-const withDeleteAction = graphql(
- gql`
- mutation deleteAction($id: ID!) {
- deleteAction(id:$id) {
- errors {
- translation_key
- }
- }
- }
-`,
- {
- props: ({mutate}) => ({
- deleteAction: (id, commentId) => {
- return mutate({
- variables: {id},
- optimisticResponse: {
- deleteAction: {
- __typename: 'DeleteActionResponse',
- errors: null
- }
- },
- update: (proxy) => {
- const fragmentId = `Comment_${commentId}`;
-
- // Read the data from our cache for this query.
- const data = proxy.readFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId
- });
-
- // Check whether we liked this comment.
- const idx = data.action_summaries.findIndex(isLikeAction);
- if (
- idx < 0 ||
- get(data.action_summaries[idx], 'current_user.id') !== id
- ) {
- return;
- }
-
- data.action_summaries[idx] = {
- ...data.action_summaries[idx],
- count: data.action_summaries[idx].count - 1,
- current_user: null
- };
-
- // Write our data back to the cache.
- proxy.writeFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId,
- data
- });
- }
- });
- }
- })
- }
-);
-
-const withPostLike = graphql(
- gql`
- mutation createLike($like: CreateLikeInput!) {
- createLike(like: $like) {
- like {
- id
- }
- errors {
- translation_key
- }
- }
- }
-`,
- {
- props: ({mutate}) => ({
- postLike: (like) => {
- return mutate({
- variables: {like},
- optimisticResponse: {
- createLike: {
- __typename: 'CreateLikeResponse',
- errors: null,
- like: {
- __typename: 'LikeAction',
- id: 'pending'
- }
- }
- },
- update: (proxy, mutationResult) => {
- const fragmentId = `Comment_${like.item_id}`;
-
- // Read the data from our cache for this query.
- const data = proxy.readFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId
- });
-
- // Add our comment from the mutation to the end.
- let idx = data.action_summaries.findIndex(isLikeAction);
-
- // Check whether we already liked this comment.
- if (idx >= 0 && data.action_summaries[idx].current_user) {
- return;
- }
-
- if (idx < 0) {
-
- // Add initial action when it doesn't exist.
- data.action_summaries.push({
- __typename: 'LikeActionSummary',
- count: 0,
- current_user: null
- });
- idx = data.action_summaries.length - 1;
- }
-
- data.action_summaries[idx] = {
- ...data.action_summaries[idx],
- count: data.action_summaries[idx].count + 1,
- current_user: mutationResult.data.createLike.like
- };
-
- // Write our data back to the cache.
- proxy.writeFragment({
- fragment: COMMENT_FRAGMENT,
- id: fragmentId,
- data
- });
- }
- });
- }
- })
- }
-);
-
-const mapDispatchToProps = (dispatch) =>
- bindActionCreators({showSignInDialog}, dispatch);
-
-const enhance = compose(
- withFragments({
- root: gql`
- fragment LikeButton_root on RootQuery {
- me {
- status
- }
- }
- `,
- comment: gql`
- fragment LikeButton_comment on Comment {
- action_summaries {
- ... on LikeActionSummary {
- count
- current_user {
- id
- }
- }
- }
- }`
- }),
- connect(null, mapDispatchToProps),
- withDeleteAction,
- withPostLike
-);
-
-export default enhance(LikeButton);
diff --git a/plugins/coral-plugin-like/client/index.js b/plugins/coral-plugin-like/client/index.js
index 86d20863a..68b7a2c46 100644
--- a/plugins/coral-plugin-like/client/index.js
+++ b/plugins/coral-plugin-like/client/index.js
@@ -1,5 +1,5 @@
-import LikeButton from './containers/LikeButton';
-import translations from './translations.json';
+import LikeButton from './LikeButton';
+import translations from './translations.yml';
export default {
translations,
diff --git a/plugins/coral-plugin-like/client/components/style.css b/plugins/coral-plugin-like/client/styles.css
similarity index 89%
rename from plugins/coral-plugin-like/client/components/style.css
rename to plugins/coral-plugin-like/client/styles.css
index f45df86ef..cb372fa47 100644
--- a/plugins/coral-plugin-like/client/components/style.css
+++ b/plugins/coral-plugin-like/client/styles.css
@@ -1,6 +1,6 @@
-.like {
+.container {
display: inline-block;
- }
+}
.button {
color: #2a2a2a;
@@ -17,14 +17,9 @@
&.liked {
color: rgb(0,134,227);
-
&:hover {
color: rgb(0,134,227);
cursor: pointer;
}
}
}
-
-.icon {
- padding: 0 5px;
-}
diff --git a/plugins/coral-plugin-like/client/translations.json b/plugins/coral-plugin-like/client/translations.json
deleted file mode 100644
index 93d73d3a2..000000000
--- a/plugins/coral-plugin-like/client/translations.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "en": {
- "like": "Like",
- "liked": "Liked"
- },
- "es": {
- "like": "Me Gusta",
- "liked": "Me Gustó"
- }
-}
diff --git a/plugins/coral-plugin-like/client/translations.yml b/plugins/coral-plugin-like/client/translations.yml
new file mode 100644
index 000000000..c534663af
--- /dev/null
+++ b/plugins/coral-plugin-like/client/translations.yml
@@ -0,0 +1,9 @@
+en:
+ coral-plugin-like:
+ like: Like
+ liked: Liked
+es:
+ coral-plugin-like:
+ like: Me Gusta
+ liked: Me Gustó
+
diff --git a/plugins/coral-plugin-like/index.js b/plugins/coral-plugin-like/index.js
index 4fc0285f4..690577bed 100644
--- a/plugins/coral-plugin-like/index.js
+++ b/plugins/coral-plugin-like/index.js
@@ -1,36 +1,2 @@
-const {readFileSync} = require('fs');
-const path = require('path');
-const wrapResponse = require('../../graph/helpers/response');
-
-module.exports = {
- typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
- resolvers: {
- RootMutation: {
- createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) {
- return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'}));
- }
- }
- },
- hooks: {
- Action: {
- __resolveType: {
- post({action_type}) {
- switch (action_type) {
- case 'LIKE':
- return 'LikeAction';
- }
- }
- }
- },
- ActionSummary: {
- __resolveType: {
- post({action_type}) {
- switch (action_type) {
- case 'LIKE':
- return 'LikeActionSummary';
- }
- }
- }
- }
- }
-};
+const {getReactionConfig} = require('../../plugin-api/beta/server');
+module.exports = getReactionConfig('like');
diff --git a/plugins/coral-plugin-like/server/typeDefs.graphql b/plugins/coral-plugin-like/server/typeDefs.graphql
deleted file mode 100644
index 40c600f2f..000000000
--- a/plugins/coral-plugin-like/server/typeDefs.graphql
+++ /dev/null
@@ -1,70 +0,0 @@
-enum ACTION_TYPE {
-
- # Represents a Like.
- LIKE
-}
-
-enum ASSET_METRICS_SORT {
-
- # Represents a LikeAction.
- LIKE
-}
-
-input CreateLikeInput {
-
- # The item's id for which we are to create a like.
- item_id: ID!
-
- # The type of the item for which we are to create the like.
- item_type: ACTION_ITEM_TYPE!
-}
-
-# LikeAction is used by users who "like" a specific entity.
-type LikeAction implements Action {
-
- # The ID of the action.
- id: ID!
-
- # The author of the action.
- user: User
-
- # The time when the Action was updated.
- updated_at: Date
-
- # The time when the Action was created.
- created_at: Date
-}
-
-type LikeActionSummary implements ActionSummary {
-
- # The count of actions with this group.
- count: Int
-
- # The current user's action.
- current_user: LikeAction
-}
-
-# A summary of counts related to all the Likes on an Asset.
-type LikeAssetActionSummary implements AssetActionSummary {
-
- # Number of likes associated with actionable types on this this Asset.
- actionCount: Int
-
- # Number of unique actionable types that are referenced by the likes.
- actionableItemCount: Int
-}
-
-type CreateLikeResponse implements Response {
-
- # The like that was created.
- like: LikeAction
-
- # An array of errors relating to the mutation that occurred.
- errors: [UserError!]
-}
-
-type RootMutation {
-
- # Creates a like on an entity.
- createLike(like: CreateLikeInput!): CreateLikeResponse
-}
diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js
index de4c4c9e7..0e2b9ff01 100644
--- a/plugins/coral-plugin-love/client/LoveButton.js
+++ b/plugins/coral-plugin-love/client/LoveButton.js
@@ -1,8 +1,11 @@
import React from 'react';
-import {Icon} from 'coral-ui';
import styles from './styles.css';
import {withReaction} from 'plugin-api/beta/client/hocs';
import {t, can} from 'plugin-api/beta/client/services';
+import {Icon} from 'plugin-api/beta/client/components';
+import cn from 'classnames';
+
+const plugin = 'coral-plugin-love';
class LoveButton extends React.Component {
handleClick = () => {
@@ -25,7 +28,7 @@ class LoveButton extends React.Component {
return;
}
- if (alreadyReacted()) {
+ if (alreadyReacted) {
deleteReaction();
} else {
postReaction();
@@ -35,14 +38,16 @@ class LoveButton extends React.Component {
render() {
const {count, alreadyReacted} = this.props;
return (
-
+
+
+
);
}
}
diff --git a/plugins/coral-plugin-love/client/index.js b/plugins/coral-plugin-love/client/index.js
index fa2f71159..fd7174d81 100644
--- a/plugins/coral-plugin-love/client/index.js
+++ b/plugins/coral-plugin-love/client/index.js
@@ -1,5 +1,5 @@
import LoveButton from './LoveButton';
-import translations from './translations.json';
+import translations from './translations.yml';
export default {
translations,
diff --git a/plugins/coral-plugin-love/client/styles.css b/plugins/coral-plugin-love/client/styles.css
index d48e7e28c..e16e17ca4 100644
--- a/plugins/coral-plugin-love/client/styles.css
+++ b/plugins/coral-plugin-love/client/styles.css
@@ -1,26 +1,25 @@
-.respect {
- display: inline-block;
+.container {
+ display: inline-block;
}
.button {
- color: #2a2a2a;
- margin: 5px 10px 5px 0px;
- background: none;
- padding: 0px;
- border: none;
- font-size: inherit;
+ color: #2a2a2a;
+ margin: 5px 10px 5px 0px;
+ background: none;
+ padding: 0px;
+ border: none;
+ font-size: inherit;
+ &:hover {
+ color: #767676;
+ cursor: pointer;
+ }
+
+ &.loved {
+ color: #e52338;
&:hover {
- color: #767676;
- cursor: pointer;
- }
-
- &.loved {
- color: #e52338;
-
- &:hover {
- color: #e52839;
- cursor: pointer;
- }
+ color: #e52839;
+ cursor: pointer;
}
+ }
}
diff --git a/plugins/coral-plugin-love/client/translations.json b/plugins/coral-plugin-love/client/translations.json
deleted file mode 100644
index a015efa77..000000000
--- a/plugins/coral-plugin-love/client/translations.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "en": {
- "love": "Love",
- "loved": "Loved"
- },
- "es": {
- "love": "Amo",
- "loved": "Amé"
- }
-}
diff --git a/plugins/coral-plugin-love/client/translations.yml b/plugins/coral-plugin-love/client/translations.yml
new file mode 100644
index 000000000..06b6e5257
--- /dev/null
+++ b/plugins/coral-plugin-love/client/translations.yml
@@ -0,0 +1,9 @@
+en:
+ coral-plugin-love:
+ love: Love
+ loved: Loved
+es:
+ coral-plugin-love:
+ love: Amo
+ loved: Amé
+
diff --git a/plugins/coral-plugin-love/index.js b/plugins/coral-plugin-love/index.js
index 543c2a099..b57fdec39 100644
--- a/plugins/coral-plugin-love/index.js
+++ b/plugins/coral-plugin-love/index.js
@@ -1,36 +1,2 @@
-const {readFileSync} = require('fs');
-const path = require('path');
-const wrapResponse = require('../../graph/helpers/response');
-
-module.exports = {
- typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
- resolvers: {
- RootMutation: {
- createLove(_, {love: {item_id, item_type}}, {mutators: {Action}}) {
- return wrapResponse('love')(Action.create({item_id, item_type, action_type: 'LOVE'}));
- }
- }
- },
- hooks: {
- Action: {
- __resolveType: {
- post({action_type}) {
- switch (action_type) {
- case 'LOVE':
- return 'LoveAction';
- }
- }
- }
- },
- ActionSummary: {
- __resolveType: {
- post({action_type}) {
- switch (action_type) {
- case 'LOVE':
- return 'LoveActionSummary';
- }
- }
- }
- }
- }
-};
+const {getReactionConfig} = require('../../plugin-api/beta/server');
+module.exports = getReactionConfig('love');
diff --git a/plugins/coral-plugin-love/server/typeDefs.graphql b/plugins/coral-plugin-love/server/typeDefs.graphql
deleted file mode 100644
index edc45e20b..000000000
--- a/plugins/coral-plugin-love/server/typeDefs.graphql
+++ /dev/null
@@ -1,70 +0,0 @@
-enum ACTION_TYPE {
-
- # Represents a Love.
- LOVE
-}
-
-enum ASSET_METRICS_SORT {
-
- # Represents a LoveAction.
- LOVE
-}
-
-input CreateLoveInput {
-
- # The item's id for which we are to create a love.
- item_id: ID!
-
- # The type of the item for which we are to create the love.
- item_type: ACTION_ITEM_TYPE!
-}
-
-# LoveAction is used by users who "love" a specific entity.
-type LoveAction implements Action {
-
- # The ID of the action.
- id: ID!
-
- # The author of the action.
- user: User
-
- # The time when the Action was updated.
- updated_at: Date
-
- # The time when the Action was created.
- created_at: Date
-}
-
-type LoveActionSummary implements ActionSummary {
-
- # The count of actions with this group.
- count: Int
-
- # The current user's action.
- current_user: LoveAction
-}
-
-# A summary of counts related to all the Loves on an Asset.
-type LoveAssetActionSummary implements AssetActionSummary {
-
- # Number of loves associated with actionable types on this this Asset.
- actionCount: Int
-
- # Number of unique actionable types that are referenced by the loves.
- actionableItemCount: Int
-}
-
-type CreateLoveResponse implements Response {
-
- # The love that was created.
- love: LoveAction
-
- # An array of errors relating to the mutation that occurred.
- errors: [UserError!]
-}
-
-type RootMutation {
-
- # Creates a love on an entity.
- createLove(love: CreateLoveInput!): CreateLoveResponse
-}
diff --git a/plugins/coral-plugin-like/client/components/Icon.js b/plugins/coral-plugin-respect/client/Icon.js
similarity index 100%
rename from plugins/coral-plugin-like/client/components/Icon.js
rename to plugins/coral-plugin-respect/client/Icon.js
diff --git a/plugins/coral-plugin-respect/client/RespectButton.js b/plugins/coral-plugin-respect/client/RespectButton.js
new file mode 100644
index 000000000..bde8c9669
--- /dev/null
+++ b/plugins/coral-plugin-respect/client/RespectButton.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import Icon from './Icon';
+import styles from './styles.css';
+import {withReaction} from 'plugin-api/beta/client/hocs';
+import {t, can} from 'plugin-api/beta/client/services';
+import cn from 'classnames';
+
+const plugin = 'coral-plugin-respect';
+
+class RespectButton extends React.Component {
+ handleClick = () => {
+ const {
+ postReaction,
+ deleteReaction,
+ showSignInDialog,
+ alreadyReacted,
+ user,
+ } = this.props;
+
+ // If the current user does not exist, trigger sign in dialog.
+ if (!user) {
+ showSignInDialog();
+ return;
+ }
+
+ // If the current user is suspended, do nothing.
+ if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
+ return;
+ }
+
+ if (alreadyReacted) {
+ deleteReaction();
+ } else {
+ postReaction();
+ }
+ };
+
+ render() {
+ const {count, alreadyReacted} = this.props;
+ return (
+
+
+
+ );
+ }
+}
+
+export default withReaction('respect')(RespectButton);
diff --git a/plugins/coral-plugin-respect/client/components/Icon.js b/plugins/coral-plugin-respect/client/components/Icon.js
deleted file mode 100644
index c24841e97..000000000
--- a/plugins/coral-plugin-respect/client/components/Icon.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react';
-import cn from 'classnames';
-
-export default ({className}) => (
-
-);
diff --git a/plugins/coral-plugin-respect/client/components/RespectButton.js b/plugins/coral-plugin-respect/client/components/RespectButton.js
deleted file mode 100644
index 58bb35e9c..000000000
--- a/plugins/coral-plugin-respect/client/components/RespectButton.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, {Component} from 'react';
-import styles from './style.css';
-import Icon from './Icon';
-
-import t from 'coral-framework/services/i18n';
-import cn from 'classnames';
-
-import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
-
-const name = 'coral-plugin-respect';
-
-class RespectButton extends Component {
-
- handleClick = () => {
- const {postRespect, showSignInDialog, deleteAction} = this.props;
- const {root: {me}, comment} = this.props;
-
- const myRespectActionSummary = getMyActionSummary('RespectActionSummary', comment);
-
- // If the current user does not exist, trigger sign in dialog.
- if (!me) {
- showSignInDialog();
- return;
- }
-
- // If the current user is banned, do nothing.
- if (me.status === 'BANNED') {
- return;
- }
-
- if (myRespectActionSummary) {
- deleteAction(myRespectActionSummary.current_user.id, comment.id);
- } else {
- postRespect({
- item_id: comment.id,
- item_type: 'COMMENTS'
- });
- }
- }
-
- render() {
- const {comment} = this.props;
-
- if (!comment) {
- return null;
- }
-
- const myRespect = getMyActionSummary('RespectActionSummary', comment);
- let count = getTotalActionCount('RespectActionSummary', comment);
-
- return (
-
-
-
- );
- }
-}
-
-RespectButton.propTypes = {
- data: React.PropTypes.object.isRequired
-};
-
-export default RespectButton;
diff --git a/plugins/coral-plugin-respect/client/containers/RespectButton.js b/plugins/coral-plugin-respect/client/containers/RespectButton.js
deleted file mode 100644
index 296ca9a1f..000000000
--- a/plugins/coral-plugin-respect/client/containers/RespectButton.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import {compose, gql} from 'react-apollo';
-import {connect} from 'react-redux';
-import {bindActionCreators} from 'redux';
-import get from 'lodash/get';
-import {withFragments, withMutation} from 'coral-framework/hocs';
-import {showSignInDialog} from 'coral-framework/actions/auth';
-import RespectButton from '../components/RespectButton';
-
-const isRespectAction = (a) => a.__typename === 'RespectActionSummary';
-
-const COMMENT_FRAGMENT = gql`
- fragment CoralRespect_UpdateFragment on Comment {
- action_summaries {
- ... on RespectActionSummary {
- count
- current_user {
- id
- }
- }
- }
- }
-`;
-
-const withDeleteAction = withMutation(gql`
- mutation CoralRespect_DeleteAction($id: ID!) {
- deleteAction(id:$id) {
- errors {
- translation_key
- }
- }
- }
-`, {
- props: ({mutate}) => ({
- deleteAction: (id, commentId) => {
- return mutate({
- variables: {id},
- optimisticResponse: {
- deleteAction: {
- __typename: 'DeleteActionResponse',
- errors: null,
- }
- },
- update: (proxy) => {
- const fragmentId = `Comment_${commentId}`;
-
- // Read the data from our cache for this query.
- const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
-
- // Check whether we respected this comment.
- const idx = data.action_summaries.findIndex(isRespectAction);
- if (idx < 0 || get(data.action_summaries[idx], 'current_user.id') !== id) {
- return;
- }
-
- data.action_summaries[idx] = {
- ...data.action_summaries[idx],
- count: data.action_summaries[idx].count - 1,
- current_user: null,
- };
-
- // Write our data back to the cache.
- proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
- },
- });
- },
- }),
-});
-
-const withPostRespect = withMutation(gql`
- mutation CoralRespect_CreateRespect($respect: CreateRespectInput!) {
- createRespect(respect: $respect) {
- respect {
- id
- }
- errors {
- translation_key
- }
- }
- }
-`, {
- props: ({mutate}) => ({
- postRespect: (respect) => {
- return mutate({
- variables: {respect},
- optimisticResponse: {
- createRespect: {
- __typename: 'CreateRespectResponse',
- errors: null,
- respect: {
- __typename: 'RespectAction',
- id: 'pending',
- },
- }
- },
- update: (proxy, mutationResult) => {
- const fragmentId = `Comment_${respect.item_id}`;
-
- // Read the data from our cache for this query.
- const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
-
- // Add our comment from the mutation to the end.
- let idx = data.action_summaries.findIndex(isRespectAction);
-
- // Check whether we already respected this comment.
- if (idx >= 0 && data.action_summaries[idx].current_user) {
- return;
- }
-
- if (idx < 0) {
-
- // Add initial action when it doesn't exist.
- data.action_summaries.push({
- __typename: 'RespectActionSummary',
- count: 0,
- current_user: null,
- });
- idx = data.action_summaries.length - 1;
- }
-
- data.action_summaries[idx] = {
- ...data.action_summaries[idx],
- count: data.action_summaries[idx].count + 1,
- current_user: mutationResult.data.createRespect.respect,
- };
-
- // Write our data back to the cache.
- proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
- },
- });
- },
- }),
-});
-
-const mapDispatchToProps = (dispatch) =>
- bindActionCreators({showSignInDialog}, dispatch);
-
-const enhance = compose(
- withFragments({
- root: gql`
- fragment CoralRespect_RespectButton_root on RootQuery {
- me {
- status
- }
- }
- `,
- comment: gql`
- fragment CoralRespect_RespectButton_comment on Comment {
- action_summaries {
- ... on RespectActionSummary {
- count
- current_user {
- id
- }
- }
- }
- }`,
- }),
- connect(null, mapDispatchToProps),
- withDeleteAction,
- withPostRespect,
-);
-
-export default enhance(RespectButton);
diff --git a/plugins/coral-plugin-respect/client/index.js b/plugins/coral-plugin-respect/client/index.js
index 4df7c66ec..0a1b58a0a 100644
--- a/plugins/coral-plugin-respect/client/index.js
+++ b/plugins/coral-plugin-respect/client/index.js
@@ -1,9 +1,9 @@
-import RespectButton from './containers/RespectButton';
-import translations from './translations.json';
+import RespectButton from './RespectButton';
+import translations from './translations.yml';
export default {
translations,
slots: {
- commentActions: [RespectButton],
+ commentReactions: [RespectButton]
}
};
diff --git a/plugins/coral-plugin-respect/client/components/style.css b/plugins/coral-plugin-respect/client/styles.css
similarity index 95%
rename from plugins/coral-plugin-respect/client/components/style.css
rename to plugins/coral-plugin-respect/client/styles.css
index 32f9a8959..f0b780b86 100644
--- a/plugins/coral-plugin-respect/client/components/style.css
+++ b/plugins/coral-plugin-respect/client/styles.css
@@ -1,6 +1,6 @@
-.respect {
+.container {
display: inline-block;
- }
+}
.button {
color: #2a2a2a;
diff --git a/plugins/coral-plugin-respect/client/translations.json b/plugins/coral-plugin-respect/client/translations.json
deleted file mode 100644
index a74e04b3f..000000000
--- a/plugins/coral-plugin-respect/client/translations.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "en": {
- "respect": "Respect",
- "respected": "Respected"
- },
- "es": {
- "respect": "Respetar",
- "respected": "Respetado"
- }
-}
diff --git a/plugins/coral-plugin-respect/client/translations.yml b/plugins/coral-plugin-respect/client/translations.yml
new file mode 100644
index 000000000..f30ed5719
--- /dev/null
+++ b/plugins/coral-plugin-respect/client/translations.yml
@@ -0,0 +1,9 @@
+en:
+ coral-plugin-respect:
+ respect: Respect
+ respected: Respected
+es:
+ coral-plugin-respect:
+ respect: Respetar
+ respected: Respetado
+
diff --git a/plugins/coral-plugin-respect/index.js b/plugins/coral-plugin-respect/index.js
index 019f0ef11..97f0e5a7c 100644
--- a/plugins/coral-plugin-respect/index.js
+++ b/plugins/coral-plugin-respect/index.js
@@ -1,41 +1,2 @@
-const {readFileSync} = require('fs');
-const path = require('path');
-const wrapResponse = require('../../graph/helpers/response');
-
-module.exports = {
- typeDefs: readFileSync(
- path.join(__dirname, 'server/typeDefs.graphql'),
- 'utf8'
- ),
- resolvers: {
- RootMutation: {
- createRespect(_, {respect: {item_id, item_type}}, {mutators: {Action}}) {
- return wrapResponse('respect')(
- Action.create({item_id, item_type, action_type: 'RESPECT'})
- );
- }
- }
- },
- hooks: {
- Action: {
- __resolveType: {
- post({action_type}) {
- switch (action_type) {
- case 'RESPECT':
- return 'RespectAction';
- }
- }
- }
- },
- ActionSummary: {
- __resolveType: {
- post({action_type}) {
- switch (action_type) {
- case 'RESPECT':
- return 'RespectActionSummary';
- }
- }
- }
- }
- }
-};
+const {getReactionConfig} = require('../../plugin-api/beta/server');
+module.exports = getReactionConfig('respect');
diff --git a/plugins/coral-plugin-respect/server/typeDefs.graphql b/plugins/coral-plugin-respect/server/typeDefs.graphql
deleted file mode 100644
index 56734e543..000000000
--- a/plugins/coral-plugin-respect/server/typeDefs.graphql
+++ /dev/null
@@ -1,54 +0,0 @@
-enum ACTION_TYPE {
-
- # Represents a Respect.
- RESPECT
-}
-
-input CreateRespectInput {
-
- # The item's id for which we are to create a respect.
- item_id: ID!
-
- # The type of the item for which we are to create the respect.
- item_type: ACTION_ITEM_TYPE!
-}
-
-# RespectAction is used by users who "respect" a specific entity.
-type RespectAction implements Action {
-
- # The ID of the action.
- id: ID!
-
- # The author of the action.
- user: User
-
- # The time when the Action was updated.
- updated_at: Date
-
- # The time when the Action was created.
- created_at: Date
-}
-
-type RespectActionSummary implements ActionSummary {
-
- # The count of actions with this group.
- count: Int
-
- # The current user's action.
- current_user: RespectAction
-}
-
-type CreateRespectResponse implements Response {
-
- # The respect that was created.
- respect: RespectAction
-
- # An array of errors relating to the mutation that occurred.
- errors: [UserError!]
-}
-
-type RootMutation {
-
- # Creates a respect on an entity.
- createRespect(respect: CreateRespectInput!): CreateRespectResponse
-}
diff --git a/yarn.lock b/yarn.lock
index 7fcee99c5..6013a3e49 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -30,9 +30,9 @@
version "0.8.6"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.8.6.tgz#b34fb880493ba835b0c067024ee70130d6f9bb68"
-"@types/graphql@^0.9.0":
- version "0.9.0"
- resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.0.tgz#fccf859f0d2817687f210737dc3be48a18b1d754"
+"@types/graphql@^0.9.0", "@types/graphql@^0.9.1":
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.1.tgz#b04ebe84bc997cc60dbea2ed4d0d4342c737f99d"
"@types/isomorphic-fetch@0.0.33":
version "0.0.33"
@@ -53,9 +53,9 @@
"@types/express-serve-static-core" "*"
"@types/mime" "*"
-"@types/ws@0.0.38":
- version "0.0.38"
- resolved "https://registry.yarnpkg.com/@types/ws/-/ws-0.0.38.tgz#42106fff4b422ca956734e29f0d73a6d893194d3"
+"@types/ws@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.0.0.tgz#4682a04d385484e73f7d8275cabc8b672d66143c"
dependencies:
"@types/node" "*"
@@ -2704,6 +2704,10 @@ es6-promise@^3.0.2, es6-promise@^3.2.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
+es6-promise@^4.0.5:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.0.tgz#dda03ca8f9f89bc597e689842929de7ba8cebdf0"
+
es6-set@~0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
@@ -2941,7 +2945,7 @@ event-stream@~3.3.0:
stream-combiner "~0.0.4"
through "~2.3.1"
-eventemitter3@^2.0.2:
+eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
@@ -3631,13 +3635,15 @@ graphql-subscriptions@^0.2.0:
dependencies:
es6-promise "^3.2.1"
-graphql-subscriptions@^0.3.0, graphql-subscriptions@^0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.3.1.tgz#0cedc2d507420cf26cf414080b079f05402f0303"
+graphql-subscriptions@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.3.tgz#2aed6ba87551cc747742b793497ff24b22991867"
dependencies:
- es6-promise "^3.2.1"
+ "@types/graphql" "^0.9.1"
+ es6-promise "^4.0.5"
+ iterall "^1.1.1"
-graphql-tag@^1.2.3, graphql-tag@^1.2.4:
+graphql-tag@^1.2.3:
version "1.3.2"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.3.2.tgz#7abb3a8fd9f3415d07163314ed237061c785b759"
@@ -4456,6 +4462,10 @@ iterall@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.3.tgz#e0b31958f835013c323ff0b10943829ac69aa4b7"
+iterall@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214"
+
jju@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa"
@@ -5777,10 +5787,6 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
-options@>=0.0.5:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
-
os-browserify@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
@@ -7386,7 +7392,7 @@ rx@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
-safe-buffer@^5.0.1:
+safe-buffer@^5.0.1, safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
@@ -7834,19 +7840,20 @@ stylus@0.54.5, stylus@~0.54.5:
sax "0.5.x"
source-map "0.1.x"
-subscriptions-transport-ws@^0.5.5-alpha.0:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.5.5.tgz#7715ed4306f532a9dd53c02cbd789d2ee8871bb9"
+subscriptions-transport-ws@^0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.7.2.tgz#26917fd4ba23fa37fcd28accb6fe36a040b3bbe9"
dependencies:
- "@types/ws" "0.0.38"
+ "@types/ws" "^3.0.0"
backo2 "^1.0.2"
- eventemitter3 "^2.0.2"
- graphql "^0.9.1"
- graphql-subscriptions "^0.3.0"
- graphql-tag "^1.2.4"
+ eventemitter3 "^2.0.3"
+ graphql-subscriptions "^0.4.3"
+ graphql-tag "^2.0.0"
+ iterall "^1.1.1"
+ lodash.assign "^4.2.0"
lodash.isobject "^3.0.2"
lodash.isstring "^4.0.1"
- ws "^1.1.0"
+ ws "^3.0.0"
sugarss@^0.2.0:
version "0.2.0"
@@ -8190,9 +8197,9 @@ uid-safe@2.1.4, uid-safe@~2.1.4:
dependencies:
random-bytes "~1.0.0"
-ultron@1.0.x:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
+ultron@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
undefsafe@0.0.3:
version "0.0.3"
@@ -8510,12 +8517,12 @@ write@^0.2.1:
dependencies:
mkdirp "^0.5.1"
-ws@^1.1.0:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.4.tgz#57f40d036832e5f5055662a397c4de76ed66bf61"
+ws@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-3.0.0.tgz#98ddb00056c8390cb751e7788788497f99103b6c"
dependencies:
- options ">=0.0.5"
- ultron "1.0.x"
+ safe-buffer "~5.0.1"
+ ultron "~1.1.0"
x-xss-protection@1.0.0:
version "1.0.0"