From a5f6e1f343061cd0246052a479ed9f046c419253 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 8 May 2017 11:32:46 -0300 Subject: [PATCH] Adding coral-reaction --- client/coral-reaction/actions.js | 0 .../components/CoralReaction.js | 98 +++++++++ client/coral-reaction/components/style.css | 30 +++ client/coral-reaction/constants.js | 0 .../containers/CoralReaction.js | 188 ++++++++++++++++++ client/coral-reaction/helpers.js | 4 + client/coral-reaction/index.js | 3 + client/coral-reaction/reducer.js | 0 client/coral-reaction/typeDefGenerator.js | 80 ++++++++ 9 files changed, 403 insertions(+) create mode 100644 client/coral-reaction/actions.js create mode 100644 client/coral-reaction/components/CoralReaction.js create mode 100644 client/coral-reaction/components/style.css create mode 100644 client/coral-reaction/constants.js create mode 100644 client/coral-reaction/containers/CoralReaction.js create mode 100644 client/coral-reaction/helpers.js create mode 100644 client/coral-reaction/index.js create mode 100644 client/coral-reaction/reducer.js create mode 100644 client/coral-reaction/typeDefGenerator.js diff --git a/client/coral-reaction/actions.js b/client/coral-reaction/actions.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-reaction/components/CoralReaction.js b/client/coral-reaction/components/CoralReaction.js new file mode 100644 index 000000000..f691e2bb7 --- /dev/null +++ b/client/coral-reaction/components/CoralReaction.js @@ -0,0 +1,98 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import styles from './style.css'; + +import {capitalize} from '../helpers'; +import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils'; + +class CoralReaction extends React.Component { + handleClick = () => { + const name = this.props.children; + const {postReaction, deleteReaction, showSignInDialog} = this.props; + + const {root: {me}, comment} = this.props; + + const myActionSummary = getMyActionSummary( + `${capitalize(name)}ActionSummary`, + 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 (myActionSummary) { + deleteReaction(myActionSummary.current_user.id, comment.id); + } else { + postReaction({ + item_id: comment.id, + item_type: 'COMMENTS' + }); + } + }; + + render() { + const name = this.props.children; + const {comment} = this.props; + + console.log(this.props) + + if (!comment) { + return null; + } + + const myReaction = getMyActionSummary(`${capitalize(name)}ActionSummary`, comment); + let count = getTotalActionCount(`${capitalize(name)}ActionSummary`, comment); + + return ( +
+ +
+ ); + } +} + +CoralReaction.propTypes = { + children: PropTypes.string.isRequired, + icon: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + tag: PropTypes.string, + translations: PropTypes.object +}; + +export default CoralReaction; + +/** + * + * icon: Could be a string or a component https://material.io/icons/ + **/ diff --git a/client/coral-reaction/components/style.css b/client/coral-reaction/components/style.css new file mode 100644 index 000000000..a82f20ff6 --- /dev/null +++ b/client/coral-reaction/components/style.css @@ -0,0 +1,30 @@ +.like { + display: inline-block; +} + +.button { + color: #2a2a2a; + margin: 5px 10px 5px 0px; + background: none; + padding: 0px; + border: none; + font-size: inherit; + + &:hover { + color: #767676; + cursor: pointer; + } + + &.liked { + color: rgb(0,134,227); + + &:hover { + color: rgb(0,134,227); + cursor: pointer; + } + } +} + +.icon { + padding: 0 5px; +} diff --git a/client/coral-reaction/constants.js b/client/coral-reaction/constants.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-reaction/containers/CoralReaction.js b/client/coral-reaction/containers/CoralReaction.js new file mode 100644 index 000000000..f8880a935 --- /dev/null +++ b/client/coral-reaction/containers/CoralReaction.js @@ -0,0 +1,188 @@ +import get from 'lodash/get'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {compose, gql, graphql} from 'react-apollo'; +import CoralReaction from '../components/CoralReaction'; +import withFragments from 'coral-framework/hocs/withFragments'; +import {showSignInDialog} from 'coral-framework/actions/auth'; +import {capitalize} from '../helpers'; + +const name = 'love'; + +const isReaction = a => a.__typename === `${capitalize(name)}ActionSummary`; + +const COMMENT_FRAGMENT = gql` + fragment ${capitalize(name)}Button_updateFragment on Comment { + action_summaries { + ... on ${capitalize(name)}ActionSummary { + count + current_user { + id + } + } + } + } +`; + +const withDeleteReaction = graphql( + gql` + mutation deleteReaction($id: ID!) { + deleteAction(id:$id) { + errors { + translation_key + } + } + } + `, + { + props: ({mutate}) => ({ + deleteReaction: (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(isReaction); + 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 withPostReaction = graphql( + gql` + mutation create${capitalize(name)}($${name}: Create${capitalize(name)}Input!) { + create${capitalize(name)}(${name}: $${capitalize(name)}) { + ${name} { + id + } + errors { + translation_key + } + } + } + `, + { + props: ({mutate}) => ({ + postReaction: reaction => { + return mutate({ + variables: {reaction}, + optimisticResponse: { + [`create${capitalize(name)}`]: { + __typename: `Create${capitalize(name)}Response`, + errors: null, + [name]: { + __typename: `${capitalize(name)}Action`, + id: 'pending' + } + } + }, + update: (proxy, mutationResult) => { + const fragmentId = `Comment_${reaction.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(name)}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(name)}`][name] + }; + + // 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 ${capitalize(name)}Button_root on RootQuery { + me { + status + } + } + `, + comment: gql` + fragment ${capitalize(name)}Button_comment on Comment { + action_summaries { + ... on ${capitalize(name)}ActionSummary { + count + current_user { + id + } + } + } + }` + }), + connect(null, mapDispatchToProps), + withDeleteReaction, + withPostReaction +); + +export default enhance(CoralReaction); diff --git a/client/coral-reaction/helpers.js b/client/coral-reaction/helpers.js new file mode 100644 index 000000000..0a267da59 --- /dev/null +++ b/client/coral-reaction/helpers.js @@ -0,0 +1,4 @@ +export function capitalize(str) { + const newString = new String(str); + return newString.charAt(0).toUpperCase() + newString.slice(1); +} diff --git a/client/coral-reaction/index.js b/client/coral-reaction/index.js new file mode 100644 index 000000000..d8527b9fd --- /dev/null +++ b/client/coral-reaction/index.js @@ -0,0 +1,3 @@ +import CoralReaction from './containers/CoralReaction'; + +export default CoralReaction; \ No newline at end of file diff --git a/client/coral-reaction/reducer.js b/client/coral-reaction/reducer.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-reaction/typeDefGenerator.js b/client/coral-reaction/typeDefGenerator.js new file mode 100644 index 000000000..63c20684e --- /dev/null +++ b/client/coral-reaction/typeDefGenerator.js @@ -0,0 +1,80 @@ +export function typeDefGenerator(type = '') { + function capitalize(str) { + const newString = String.new(str); + return newString.charAt(0).toUpperCase() + newString.slice(1); + } + + return ` +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 +} + +`; +} \ No newline at end of file