diff --git a/.gitignore b/.gitignore
index a619f0988..f0c3b2a4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,5 +20,7 @@ plugins/*
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
+!plugins/coral-plugin-mod
+!plugins/coral-plugin-love
**/node_modules/*
diff --git a/LICENSE b/LICENSE
index 41cbbd5aa..597d8fa73 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2016 Mozilla Foundation
+Copyright 2017 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
index ffd4db2a6..55a00096d 100644
--- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
+++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js
@@ -1,16 +1,17 @@
import React, {PropTypes} from 'react';
import timeago from 'timeago.js';
-import Linkify from 'react-linkify';
-import Highlighter from 'react-highlight-words';
import {Link} from 'react-router';
+import Linkify from 'react-linkify';
-import styles from './styles.css';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
+import styles from './styles.css';
import CommentType from './CommentType';
+import Highlighter from 'react-highlight-words';
+import Slot from 'coral-framework/components/Slot';
+import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import BanUserButton from 'coral-admin/src/components/BanUserButton';
-import {getActionSummary} from 'coral-framework/utils';
const linkify = new Linkify();
@@ -18,11 +19,19 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations.json';
const lang = new I18n(translations);
-const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) => {
+const Comment = ({
+ actions = [],
+ comment,
+ suspectWords,
+ bannedWords,
+ ...props
+}) => {
const links = linkify.getMatches(comment.body);
const linkText = links ? links.map(link => link.raw) : [];
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
- const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction');
+ const flagActions =
+ comment.actions &&
+ comment.actions.filter(a => a.__typename === 'FlagAction');
let commentType = '';
if (comment.status === 'PREMOD') {
commentType = 'premod';
@@ -33,12 +42,17 @@ const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) =
// since words are checked against word boundaries on the backend,
// this should be the behavior on the front end as well.
// currently the highlighter plugin does not support this out of the box.
- const searchWords = [...suspectWords, ...bannedWords].filter(w => {
- return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
- }).concat(linkText);
+ const searchWords = [...suspectWords, ...bannedWords]
+ .filter(w => {
+ return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
+ })
+ .concat(linkText);
return (
-
+
@@ -46,53 +60,95 @@ const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) =
{comment.user.name}
- {timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
+ {timeago().format(
+ comment.created_at || Date.now() - props.index * 60 * 1000,
+ lang.getLocale().replace('-', '_')
+ )}
- props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')} />
+
+ props.showBanUserDialog(
+ comment.user,
+ comment.id,
+ comment.status,
+ comment.status !== 'REJECTED'
+ )}
+ />
- {comment.user.status === 'banned' ?
-
-
- {lang.t('comment.banned_user')}
-
+ {comment.user.status === 'banned'
+ ?
+
+ {lang.t('comment.banned_user')}
+
: null}
+
Story: {comment.asset.title}
- {!props.currentAsset && (
- Moderate →
- )}
+ {!props.currentAsset &&
+ Moderate →}
{lang.t('comment.view_context')}
+ textToHighlight={comment.body}
+ />
+ {' '}
+
+ {lang.t('comment.view_context')}
+
+
- {links ?
Contains Link : null}
+ {links
+ ?
+ Contains Link
+
+ : null}
{actions.map((action, i) => {
- const active = (action === 'REJECT' && comment.status === 'REJECTED') ||
- (action === 'APPROVE' && comment.status === 'ACCEPTED');
- return
comment.status === 'ACCEPTED' ? null : props.acceptComment({commentId: comment.id})}
- rejectComment={() => comment.status === 'REJECTED' ? null : props.rejectComment({commentId: comment.id})} />;
+ const active =
+ (action === 'REJECT' && comment.status === 'REJECTED') ||
+ (action === 'APPROVE' && comment.status === 'ACCEPTED');
+ return (
+
+ (comment.status === 'ACCEPTED'
+ ? null
+ : props.acceptComment({commentId: comment.id}))}
+ rejectComment={() =>
+ (comment.status === 'REJECTED'
+ ? null
+ : props.rejectComment({commentId: comment.id}))}
+ />
+ );
})}
+
- {
- flagActions && flagActions.length
- ?
- : null
- }
+
+
+
+ {flagActions && flagActions.length
+ ?
+ : null}
);
};
diff --git a/client/coral-framework/helpers/hoc.js b/client/coral-framework/helpers/hoc.js
new file mode 100644
index 000000000..bb034b179
--- /dev/null
+++ b/client/coral-framework/helpers/hoc.js
@@ -0,0 +1,3 @@
+export function getDisplayName(WrappedComponent) {
+ return WrappedComponent.displayName || WrappedComponent.name || 'Component';
+}
diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js
index 20aee7abb..d701b933f 100644
--- a/client/coral-framework/helpers/plugins.js
+++ b/client/coral-framework/helpers/plugins.js
@@ -21,7 +21,7 @@ export function getSlotElements(slot, props = {}) {
.filter(o => o.module.slots[slot])
.map(o => o.module.slots[slot]));
return components
- .map((component, i) => React.createElement(component, {...props, key: i}));
+ .map((component, i) => React.createElement(component, {key: i, ...props}));
}
function getComponentFragments(components) {
diff --git a/client/coral-framework/helpers/strings.js b/client/coral-framework/helpers/strings.js
new file mode 100644
index 000000000..0a267da59
--- /dev/null
+++ b/client/coral-framework/helpers/strings.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-framework/hocs/index.js b/client/coral-framework/hocs/index.js
new file mode 100644
index 000000000..831c356d2
--- /dev/null
+++ b/client/coral-framework/hocs/index.js
@@ -0,0 +1,7 @@
+import withFragments from './withFragments';
+import withReaction from './withReaction';
+
+export default {
+ withFragments,
+ withReaction
+};
diff --git a/client/coral-framework/hocs/withFragments.js b/client/coral-framework/hocs/withFragments.js
index a2686d94b..a5060f27a 100644
--- a/client/coral-framework/hocs/withFragments.js
+++ b/client/coral-framework/hocs/withFragments.js
@@ -1,11 +1,7 @@
import React from 'react';
+import {getDisplayName} from '../helpers/hoc';
// TODO: revisit `filtering` after https://github.com/apollographql/graphql-anywhere/issues/38.
-
-function getDisplayName(WrappedComponent) {
- return WrappedComponent.displayName || WrappedComponent.name || 'Component';
-}
-
export default fragments => WrappedComponent => {
class WithFragments extends React.Component {
render() {
diff --git a/client/coral-framework/hocs/withReaction.js b/client/coral-framework/hocs/withReaction.js
new file mode 100644
index 000000000..c383dbed8
--- /dev/null
+++ b/client/coral-framework/hocs/withReaction.js
@@ -0,0 +1,243 @@
+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 mapDispatchToProps = dispatch =>
+ bindActionCreators({showSignInDialog}, dispatch);
+
+ const enhance = compose(
+ withFragments({
+ root: gql`
+ fragment ${capitalize(reaction)}Button_root on RootQuery {
+ me {
+ status
+ }
+ }
+ `,
+ comment: gql`
+ fragment ${capitalize(reaction)}Button_comment on Comment {
+ action_summaries {
+ ... on ${capitalize(reaction)}ActionSummary {
+ count
+ current_user {
+ id
+ }
+ }
+ }
+ }`
+ }),
+ connect(null, mapDispatchToProps),
+ withDeleteReaction,
+ withPostReaction
+ );
+
+ WithReactions.displayName = `WithReactions(${getDisplayName(WrappedComponent)})`;
+
+ return enhance(WithReactions);
+};
diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js
index b85b69bcd..46ee704f4 100644
--- a/client/coral-framework/index.js
+++ b/client/coral-framework/index.js
@@ -1,6 +1,7 @@
import pym from './services/PymConnection';
import I18n from './modules/i18n/i18n';
import actions from './actions';
+import hocs from './hocs';
// TODO (bc): Deprecate old actions. Spreading actions is now needed.
@@ -8,5 +9,6 @@ export default {
pym,
I18n,
actions,
+ ...hocs,
...actions
};
diff --git a/client/coral-plugin-api/index.js b/client/coral-plugin-api/index.js
new file mode 100644
index 000000000..f0f5ad9cc
--- /dev/null
+++ b/client/coral-plugin-api/index.js
@@ -0,0 +1 @@
+export {withReaction} from '../coral-framework/hocs';
diff --git a/plugins/coral-plugin-love/client/.babelrc b/plugins/coral-plugin-love/client/.babelrc
new file mode 100644
index 000000000..60be246eb
--- /dev/null
+++ b/plugins/coral-plugin-love/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-love/client/.eslintrc.json b/plugins/coral-plugin-love/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/coral-plugin-love/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-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js
new file mode 100644
index 000000000..4364c4818
--- /dev/null
+++ b/plugins/coral-plugin-love/client/LoveButton.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import {Icon} from 'coral-ui';
+import styles from './styles.css';
+import {I18n} from 'coral-framework';
+import translations from './translations.json';
+import {withReaction} from 'coral-plugin-api';
+const lang = new I18n(translations);
+
+class LoveButton extends React.Component {
+ handleClick = () => {
+ const {
+ postReaction,
+ deleteReaction,
+ showSignInDialog,
+ alreadyReacted
+ } = this.props;
+ const {root: {me}, comment} = this.props;
+
+ // 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 (alreadyReacted()) {
+ deleteReaction();
+ } else {
+ postReaction();
+ }
+ };
+
+ render() {
+ const {count, alreadyReacted} = this.props;
+ return (
+
+ {lang.t(alreadyReacted() ? 'loved' : 'love')}
+
+ {count > 0 && count}
+
+ );
+ }
+}
+
+export default withReaction('love')(LoveButton);
diff --git a/plugins/coral-plugin-love/client/index.js b/plugins/coral-plugin-love/client/index.js
new file mode 100644
index 000000000..f048963b4
--- /dev/null
+++ b/plugins/coral-plugin-love/client/index.js
@@ -0,0 +1,7 @@
+import LoveButton from './LoveButton';
+
+export default {
+ slots: {
+ commentReactions: [LoveButton]
+ }
+};
diff --git a/plugins/coral-plugin-love/client/styles.css b/plugins/coral-plugin-love/client/styles.css
new file mode 100644
index 000000000..d48e7e28c
--- /dev/null
+++ b/plugins/coral-plugin-love/client/styles.css
@@ -0,0 +1,26 @@
+.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;
+ }
+
+ &.loved {
+ color: #e52338;
+
+ &:hover {
+ color: #e52839;
+ cursor: pointer;
+ }
+ }
+}
diff --git a/plugins/coral-plugin-love/client/translations.json b/plugins/coral-plugin-love/client/translations.json
new file mode 100644
index 000000000..a015efa77
--- /dev/null
+++ b/plugins/coral-plugin-love/client/translations.json
@@ -0,0 +1,10 @@
+{
+ "en": {
+ "love": "Love",
+ "loved": "Loved"
+ },
+ "es": {
+ "love": "Amo",
+ "loved": "Amé"
+ }
+}
diff --git a/plugins/coral-plugin-love/index.js b/plugins/coral-plugin-love/index.js
new file mode 100644
index 000000000..48b4d16c0
--- /dev/null
+++ b/plugins/coral-plugin-love/index.js
@@ -0,0 +1,37 @@
+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';
+ }
+ }
+ }
+ }
+ }
+};
+
diff --git a/plugins/coral-plugin-love/server/typeDefs.graphql b/plugins/coral-plugin-love/server/typeDefs.graphql
new file mode 100644
index 000000000..e22b91a2d
--- /dev/null
+++ b/plugins/coral-plugin-love/server/typeDefs.graphql
@@ -0,0 +1,70 @@
+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-mod/client/.babelrc b/plugins/coral-plugin-mod/client/.babelrc
new file mode 100644
index 000000000..60be246eb
--- /dev/null
+++ b/plugins/coral-plugin-mod/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-mod/client/.eslintrc.json b/plugins/coral-plugin-mod/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/coral-plugin-mod/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-mod/client/components/Box.js b/plugins/coral-plugin-mod/client/components/Box.js
new file mode 100644
index 000000000..15f5320b1
--- /dev/null
+++ b/plugins/coral-plugin-mod/client/components/Box.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import styles from './styles.css'
+
+export default (props) => (
+
+ Comment Status: {props.comment.status}
+
+)
\ No newline at end of file
diff --git a/plugins/coral-plugin-mod/client/components/Container.js b/plugins/coral-plugin-mod/client/components/Container.js
new file mode 100644
index 000000000..712031fdf
--- /dev/null
+++ b/plugins/coral-plugin-mod/client/components/Container.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import Box from './Box';
+import {Button} from 'coral-ui'
+import styles from './styles.css';
+
+export default class Footer extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ show: false
+ };
+ }
+
+ handleClick = () => {
+ this.setState(state => ({
+ show: !state.show
+ }))
+ }
+
+ render() {
+ const {show} = this.state;
+ return (
+
+
+ Show Comment Status
+
+ {show ? : null}
+
+ );
+ }
+}
diff --git a/plugins/coral-plugin-mod/client/components/styles.css b/plugins/coral-plugin-mod/client/components/styles.css
new file mode 100644
index 000000000..59bb63c3d
--- /dev/null
+++ b/plugins/coral-plugin-mod/client/components/styles.css
@@ -0,0 +1,8 @@
+.container {
+ padding: 0 14px 10px;
+}
+
+.box {
+ font-size: 12px;
+ padding: 10px 14px;
+}
\ No newline at end of file
diff --git a/plugins/coral-plugin-mod/client/index.js b/plugins/coral-plugin-mod/client/index.js
new file mode 100644
index 000000000..b0ea304f4
--- /dev/null
+++ b/plugins/coral-plugin-mod/client/index.js
@@ -0,0 +1,7 @@
+import Container from './components/Container';
+
+export default {
+ slots: {
+ adminCommentDetailArea: [Container],
+ }
+};
diff --git a/plugins/coral-plugin-mod/index.js b/plugins/coral-plugin-mod/index.js
new file mode 100644
index 000000000..42d90e144
--- /dev/null
+++ b/plugins/coral-plugin-mod/index.js
@@ -0,0 +1,4 @@
+const {readFileSync} = require('fs');
+const path = require('path');
+
+module.exports = {};
diff --git a/services/comments.js b/services/comments.js
index 0ef15c552..e23e4e486 100644
--- a/services/comments.js
+++ b/services/comments.js
@@ -205,7 +205,7 @@ module.exports = class CommentsService {
* @param {String} action the new action to the comment
* @return {Promise}
*/
- static addAction(item_id, user_id, action_type, metadata) {
+ static addAction(item_id, user_id, action_type, metadata = {}) {
return ActionsService.insertUserAction({
item_id,
item_type: 'COMMENTS',