From 2a283a9a5a9754a8a7040e8488353d3011377a79 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 12 Jun 2017 23:01:38 +0700 Subject: [PATCH] Support live updates --- .../src/containers/Stream.js | 2 +- client/coral-framework/actions/auth.js | 4 +- client/coral-framework/hocs/index.js | 1 - client/coral-framework/hocs/withReaction.js | 240 ------------- client/coral-framework/services/client.js | 24 +- graph/mutators/action.js | 2 +- graph/resolvers/action.js | 1 + graph/subscriptions.js | 8 +- package.json | 4 +- plugin-api/beta/client/hocs/index.js | 2 +- plugin-api/beta/client/hocs/withReaction.js | 337 ++++++++++++++++++ plugin-api/beta/server/getReactionConfig.js | 143 ++++++-- .../coral-plugin-like/client/LikeButton.js | 6 +- .../coral-plugin-love/client/LoveButton.js | 6 +- .../client/RespectButton.js | 6 +- yarn.lock | 81 +++-- 16 files changed, 528 insertions(+), 339 deletions(-) delete mode 100644 client/coral-framework/hocs/withReaction.js create mode 100644 plugin-api/beta/client/hocs/withReaction.js diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index f6e1fd9ec..32b9c33c7 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -83,7 +83,7 @@ class StreamContainer extends React.Component { } }); - this.subscriptions.push(sub1, sub2); + this.subscriptions = [sub1, sub2]; } unsubscribe() { 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/resolvers/action.js b/graph/resolvers/action.js index 8ca05daf8..ce89b9d5c 100644 --- a/graph/resolvers/action.js +++ b/graph/resolvers/action.js @@ -13,6 +13,7 @@ const 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}) { + console.log(user); if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) { return Users.getByID.load(user_id); } diff --git a/graph/subscriptions.js b/graph/subscriptions.js index 0cc37e841..3a61b6b39 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,15 +58,15 @@ const createSubscriptionManager = (server) => new SubscriptionServer({ // Attach the context per request. baseParams.context = async () => { let req; - + try { req = await deserializeUser(upgradeReq); } catch (e) { - console.error(e); + console.error(connection); return new Context({}, pubsub); } - + return new Context(req, pubsub); }; diff --git a/package.json b/package.json index d78ba8713..f623b94ed 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.1", "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..67a042081 --- /dev/null +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -0,0 +1,337 @@ +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, 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'; +import * as PropTypes from 'prop-types'; + +export default (reaction) => (WrappedComponent) => { + if (typeof reaction !== 'string') { + console.error('Reaction must be a valid string'); + return null; + } + + let instances = 0; + 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); + 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 = ({[`${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 = ({[`${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--; + 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 = graphql( + 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 = graphql( + 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 index 101c86782..b157742ad 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -1,4 +1,5 @@ const wrapResponse = require('../../../graph/helpers/response'); +const {SEARCH_OTHER_USERS} = require('../../../perms/constants'); function getReactionConfig(reaction) { const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1); @@ -6,82 +7,142 @@ function getReactionConfig(reaction) { const typeDefs = ` enum ACTION_TYPE { - # Represents a ${Reaction}. - ${REACTION} + # Represents a ${Reaction}. + ${REACTION} } enum ASSET_METRICS_SORT { - # Represents a ${Reaction}Action. - ${REACTION} + # Represents a ${Reaction}Action. + ${REACTION} } - input Create${Reaction}Input { + input Create${Reaction}ActionInput { - # The item's id for which we are to create a ${reaction}. - item_id: ID! + # The item's id for which we are to create a ${reaction}. + item_id: ID! + } - # The type of the item for which we are to create the ${reaction}. - item_type: ACTION_ITEM_TYPE! + 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 ID of the action. + id: ID! - # The author of the action. - user: User + # The author of the action. + user: User - # The time when the Action was updated. - updated_at: Date + # The time when the Action was updated. + updated_at: Date - # The time when the Action was created. - created_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 count of actions with this group. + count: Int - # The current user's action. - current_user: ${Reaction}Action + # 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 ${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 + # Number of unique actionable types that are referenced by the ${reaction}s. + actionableItemCount: Int } - type Create${Reaction}Response implements Response { + type Create${Reaction}ActionResponse implements Response { - # The ${reaction} that was created. - ${reaction}: ${Reaction}Action + # The ${reaction} that was created. + ${reaction}: ${Reaction}Action - # An array of errors relating to the mutation that occurred. - errors: [UserError!] + # 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}(${reaction}: Create${Reaction}Input!): Create${Reaction}Response + # 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}`]: (_, {[reaction]: {item_id, item_type}}, {mutators: {Action}}) => { - return wrapResponse(reaction)(Action.create({item_id, item_type, action_type: REACTION})); + [`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) => { + 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) => { + pubsub.publish(`${reaction}ActionDeleted`, {action, comment}); + return Promise.resolve(action); + }); + }); + return wrapResponse(reaction)(response); } }, }, @@ -106,7 +167,19 @@ function getReactionConfig(reaction) { } } } - } + }, + 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, + }, + }), + }, }; } diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js index fac4fdfca..5b99f7df4 100644 --- a/plugins/coral-plugin-like/client/LikeButton.js +++ b/plugins/coral-plugin-like/client/LikeButton.js @@ -28,7 +28,7 @@ class LikeButton extends React.Component { return; } - if (alreadyReacted()) { + if (alreadyReacted) { deleteReaction(); } else { postReaction(); @@ -40,10 +40,10 @@ class LikeButton extends React.Component { return (
diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js index 8dbf51a6e..0e2b9ff01 100644 --- a/plugins/coral-plugin-love/client/LoveButton.js +++ b/plugins/coral-plugin-love/client/LoveButton.js @@ -28,7 +28,7 @@ class LoveButton extends React.Component { return; } - if (alreadyReacted()) { + if (alreadyReacted) { deleteReaction(); } else { postReaction(); @@ -40,10 +40,10 @@ class LoveButton extends React.Component { return (
diff --git a/plugins/coral-plugin-respect/client/RespectButton.js b/plugins/coral-plugin-respect/client/RespectButton.js index 14b0ec057..bde8c9669 100644 --- a/plugins/coral-plugin-respect/client/RespectButton.js +++ b/plugins/coral-plugin-respect/client/RespectButton.js @@ -28,7 +28,7 @@ class RespectButton extends React.Component { return; } - if (alreadyReacted()) { + if (alreadyReacted) { deleteReaction(); } else { postReaction(); @@ -40,11 +40,11 @@ class RespectButton extends React.Component { return (