diff --git a/plugins/coral-plugin-like/client/.babelrc b/plugins/coral-plugin-like/client/.babelrc
new file mode 100644
index 000000000..60be246eb
--- /dev/null
+++ b/plugins/coral-plugin-like/client/.babelrc
@@ -0,0 +1,14 @@
+{
+ "presets": [
+ "es2015"
+ ],
+ "plugins": [
+ "add-module-exports",
+ "transform-class-properties",
+ "transform-decorators-legacy",
+ "transform-object-assign",
+ "transform-object-rest-spread",
+ "transform-async-to-generator",
+ "transform-react-jsx"
+ ]
+}
\ No newline at end of file
diff --git a/plugins/coral-plugin-like/client/.eslintrc.json b/plugins/coral-plugin-like/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/coral-plugin-like/client/.eslintrc.json
@@ -0,0 +1,23 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true,
+ "mocha": true
+ },
+ "parserOptions": {
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "experimentalObjectRestSpread": true,
+ "jsx": true
+ }
+ },
+ "parser": "babel-eslint",
+ "plugins": [
+ "react"
+ ],
+ "rules": {
+ "react/jsx-uses-react": "error",
+ "react/jsx-uses-vars": "error",
+ "no-console": ["warn", { "allow": ["warn", "error"] }]
+ }
+}
diff --git a/plugins/coral-plugin-like/client/components/Icon.js b/plugins/coral-plugin-like/client/components/Icon.js
new file mode 100644
index 000000000..c24841e97
--- /dev/null
+++ b/plugins/coral-plugin-like/client/components/Icon.js
@@ -0,0 +1,6 @@
+import React from 'react';
+import cn from 'classnames';
+
+export default ({className}) => (
+
+);
diff --git a/plugins/coral-plugin-like/client/components/LikeButton.js b/plugins/coral-plugin-like/client/components/LikeButton.js
new file mode 100644
index 000000000..faeda1991
--- /dev/null
+++ b/plugins/coral-plugin-like/client/components/LikeButton.js
@@ -0,0 +1,69 @@
+import React, {Component} from 'react';
+import styles from './style.css';
+import Icon from './Icon';
+
+import {I18n} from 'coral-framework';
+import cn from 'classnames';
+import translations from '../translations.json';
+import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
+
+const lang = new I18n(translations);
+
+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-like/client/components/style.css b/plugins/coral-plugin-like/client/components/style.css
new file mode 100644
index 000000000..32f9a8959
--- /dev/null
+++ b/plugins/coral-plugin-like/client/components/style.css
@@ -0,0 +1,30 @@
+.respect {
+ 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;
+ }
+
+ &.respected {
+ color: #c98211;
+
+ &:hover {
+ color: #e59614;
+ cursor: pointer;
+ }
+ }
+}
+
+.icon {
+ padding: 0 5px;
+}
diff --git a/plugins/coral-plugin-like/client/containers/LikeButton.js b/plugins/coral-plugin-like/client/containers/LikeButton.js
new file mode 100644
index 000000000..ccd33182b
--- /dev/null
+++ b/plugins/coral-plugin-like/client/containers/LikeButton.js
@@ -0,0 +1,163 @@
+import {compose, gql, graphql} from 'react-apollo';
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import get from 'lodash/get';
+import withFragments from 'coral-framework/hocs/withFragments';
+import {showSignInDialog} from 'coral-framework/actions/auth';
+import RespectButton from '../components/LikeButton';
+
+const isRespectAction = (a) => a.__typename === 'RespectActionSummary';
+
+const COMMENT_FRAGMENT = gql`
+ fragment RespectButton_updateFragment on Comment {
+ action_summaries {
+ ... on RespectActionSummary {
+ 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 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 = graphql(gql`
+ mutation 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 RespectButton_root on RootQuery {
+ me {
+ status
+ }
+ }
+ `,
+ comment: gql`
+ fragment 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-like/client/index.js b/plugins/coral-plugin-like/client/index.js
new file mode 100644
index 000000000..e80b03c17
--- /dev/null
+++ b/plugins/coral-plugin-like/client/index.js
@@ -0,0 +1,7 @@
+import LikeButton from './containers/LikeButton';
+
+export default {
+ slots: {
+ commentDetail: [LikeButton],
+ }
+};
diff --git a/plugins/coral-plugin-like/client/translations.json b/plugins/coral-plugin-like/client/translations.json
new file mode 100644
index 000000000..643f32ccf
--- /dev/null
+++ b/plugins/coral-plugin-like/client/translations.json
@@ -0,0 +1,10 @@
+{
+ "en": {
+ "respect": "Respect",
+ "respected": "Respected"
+ },
+ "es": {
+ "respect": "Respeto",
+ "respected": "Respetado"
+ }
+}
diff --git a/plugins/coral-plugin-like/index.js b/plugins/coral-plugin-like/index.js
new file mode 100644
index 000000000..5182cd258
--- /dev/null
+++ b/plugins/coral-plugin-like/index.js
@@ -0,0 +1,36 @@
+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(_, {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';
+ }
+ }
+ }
+ }
+ }
+};
diff --git a/plugins/coral-plugin-like/server/typeDefs.graphql b/plugins/coral-plugin-like/server/typeDefs.graphql
new file mode 100644
index 000000000..1f94caf07
--- /dev/null
+++ b/plugins/coral-plugin-like/server/typeDefs.graphql
@@ -0,0 +1,54 @@
+enum ACTION_TYPE {
+
+ # Represents a Like.
+ 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
+}
+
+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
+}