diff --git a/.eslintignore b/.eslintignore
index d506a20c2..fc0d50a25 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,4 +3,5 @@ client/lib
**/*.html
plugins/*
!plugins/coral-plugin-facebook-auth
-node_modules
\ No newline at end of file
+!plugins/coral-plugin-respect
+node_modules
diff --git a/.gitignore b/.gitignore
index 6b7b187d9..8982613ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,5 @@ coverage/
plugins.json
plugins/*
-!plugins/coral-plugin-facebook-auth
\ No newline at end of file
+!plugins/coral-plugin-facebook-auth
+!plugins/coral-plugin-respect
diff --git a/.nodemon.json b/.nodemon.json
index 4c48c707e..7f7fd3d59 100644
--- a/.nodemon.json
+++ b/.nodemon.json
@@ -1,5 +1,5 @@
{
"verbose": true,
- "ignore": ["test/*", "client/*", "dist/*"],
+ "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql"
}
diff --git a/client/coral-docs/src/index.js b/client/coral-docs/src/index.js
index 5d8c49d9d..e706203a8 100644
--- a/client/coral-docs/src/index.js
+++ b/client/coral-docs/src/index.js
@@ -1,8 +1,8 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import {render} from 'react-dom';
import {GraphQLDocs} from 'graphql-docs';
import fetcher from './services/fetcher';
// Render the application into the DOM
-ReactDOM.render(, document.querySelector('#root'));
+render(, document.querySelector('#root'));
diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js
index d970a5f35..45f2ef084 100644
--- a/client/coral-embed-stream/src/Comment.js
+++ b/client/coral-embed-stream/src/Comment.js
@@ -19,6 +19,7 @@ import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
+import {Slot} from 'coral-framework';
import styles from './Comment.css';
@@ -157,6 +158,7 @@ class Comment extends React.Component {
?
: null }
+
@@ -187,6 +189,7 @@ class Comment extends React.Component {
removeBest={removeBestTag} />
+
@@ -241,7 +244,8 @@ class Comment extends React.Component {
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
- comment={reply} />;
+ comment={reply}
+ />;
})
}
{
diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js
index 016536708..d9b5c8310 100644
--- a/client/coral-embed-stream/src/Embed.js
+++ b/client/coral-embed-stream/src/Embed.js
@@ -313,5 +313,5 @@ export default compose(
addCommentTag,
removeCommentTag,
deleteAction,
- queryStream
+ queryStream,
)(Embed);
diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js
index 34145ed4d..66c19e0ab 100644
--- a/client/coral-embed-stream/src/Stream.js
+++ b/client/coral-embed-stream/src/Stream.js
@@ -41,7 +41,8 @@ class Stream extends React.Component {
deleteAction,
showSignInDialog,
addCommentTag,
- removeCommentTag
+ removeCommentTag,
+ pluginProps
} = this.props;
return (
@@ -67,7 +68,9 @@ class Stream extends React.Component {
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
- comment={comment} />
+ comment={comment}
+ pluginProps={pluginProps}
+ />
)
}
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css
index c712d1696..6a6a2214d 100644
--- a/client/coral-embed-stream/style/default.css
+++ b/client/coral-embed-stream/style/default.css
@@ -255,13 +255,13 @@ hr {
.commentActionsRight, .replyActionsRight {
display: flex;
justify-content: flex-end;
- width: 50%;
+ width: 30%;
}
.commentActionsLeft, .replyActionsLeft {
display: flex;
justify-content: flex-start;
float: left;
- width: 50%;
+ width: 70%;
}
.comment__action-container .material-icons {
diff --git a/client/coral-framework/actions/asset.js b/client/coral-framework/actions/asset.js
index fa927e58a..50efa3dcb 100644
--- a/client/coral-framework/actions/asset.js
+++ b/client/coral-framework/actions/asset.js
@@ -3,7 +3,7 @@ import coralApi from '../helpers/response';
import {addNotification} from '../actions/notification';
import {pym} from 'coral-framework';
-import I18n from '../../coral-framework/modules/i18n/i18n';
+import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js
index 01174cf05..11e7bd996 100644
--- a/client/coral-framework/actions/auth.js
+++ b/client/coral-framework/actions/auth.js
@@ -1,3 +1,5 @@
+import {gql} from 'react-apollo';
+import client from 'coral-framework/services/client';
import I18n from '../../coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
@@ -5,6 +7,20 @@ import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {pym} from 'coral-framework';
+const ME_QUERY = gql`
+ query Me {
+ me {
+ status
+ }
+ }
+`;
+
+function fetchMe() {
+ return client.query({
+ fetchPolicy: 'network-only',
+ query: ME_QUERY});
+}
+
// Dialog Actions
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
@@ -52,6 +68,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
+ fetchMe();
})
.catch(error => {
if (error.metadata) {
@@ -104,6 +121,7 @@ export const facebookCallback = (err, data) => dispatch => {
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(showCreateUsernameDialog());
+ fetchMe();
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -151,7 +169,10 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
- .then(() => dispatch(logOutSuccess()))
+ .then(() => {
+ dispatch(logOutSuccess());
+ fetchMe();
+ })
.catch(error => dispatch(logOutFailure(error)));
};
diff --git a/client/coral-framework/actions/index.js b/client/coral-framework/actions/index.js
new file mode 100644
index 000000000..65f7b87d3
--- /dev/null
+++ b/client/coral-framework/actions/index.js
@@ -0,0 +1,9 @@
+import * as authActions from './auth';
+import * as assetActions from './asset';
+import * as notificationActions from './notification';
+
+export default {
+ authActions,
+ assetActions,
+ notificationActions,
+};
diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js
new file mode 100644
index 000000000..3d1d61328
--- /dev/null
+++ b/client/coral-framework/components/Slot.js
@@ -0,0 +1,19 @@
+import React, {Component} from 'react';
+import {getSlotElements} from 'coral-framework/helpers/plugins';
+
+class Slot extends Component {
+ render() {
+ const {fill, ...rest} = this.props;
+ return (
+
+ {getSlotElements(fill, rest)}
+
+ );
+ }
+}
+
+Slot.propTypes = {
+ fill: React.PropTypes.string
+};
+
+export default Slot;
diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js
new file mode 100644
index 000000000..e0d345db4
--- /dev/null
+++ b/client/coral-framework/helpers/plugins.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import merge from 'lodash/merge';
+import flatten from 'lodash/flatten';
+import plugins from 'pluginsConfig';
+
+export const pluginReducers = merge(
+ ...plugins
+ .filter(o => o.module.reducer)
+ .map(o => ({...o.module.reducer}))
+);
+
+/**
+ * Returns React Elements for given slot.
+ */
+export function getSlotElements(slot, props = {}) {
+ const components = flatten(plugins
+ .filter(o => o.module.slots[slot])
+ .map(o => o.module.slots[slot]));
+ return components
+ .map((component, i) => React.createElement(component, {...props, key: i}));
+}
diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js
index 2900466ee..96750bffd 100644
--- a/client/coral-framework/index.js
+++ b/client/coral-framework/index.js
@@ -1,15 +1,16 @@
import store from './services/store';
import pym from './services/PymConnection';
import I18n from './modules/i18n/i18n';
-import * as authActions from './actions/auth';
-import * as assetActions from './actions/asset';
-import * as notificationActions from './actions/notification';
+import actions from './actions';
+import Slot from './components/Slot';
-export {
+// TODO (bc): Deprecate old actions. Spreading actions is now needed.
+
+export default {
pym,
+ Slot,
I18n,
store,
- authActions,
- assetActions,
- notificationActions
+ actions,
+ ...actions
};
diff --git a/client/coral-framework/loaders/plugins-loader.js b/client/coral-framework/loaders/plugins-loader.js
new file mode 100644
index 000000000..c3534df32
--- /dev/null
+++ b/client/coral-framework/loaders/plugins-loader.js
@@ -0,0 +1,33 @@
+/**
+ * Executes `source` to retrieve plugins configuration
+ * and loads the `index.js` of specified plugins.
+ *
+ * Outputs a module that looks like the following:
+ *
+ * module.exports = [{plugin: string, module: object}, ...]
+ *
+ */
+const {stripIndent} = require('common-tags');
+
+function getPluginList(config) {
+ if (config && config.client) {
+ return config.client.map(x => typeof x === 'string' ? x : Object.keys(x)[0]);
+ }
+
+ return [];
+}
+
+module.exports = function(source) {
+ this.cacheable();
+ const config = this.exec(source, this.resourcePath);
+ const plugins = getPluginList(config).map((plugin) => `{
+ module: require('${plugin}/client'),
+ plugin: '${plugin}'
+ }`);
+
+ return stripIndent`
+ module.exports = [
+ ${plugins.join(',')}
+ ];
+ `;
+};
diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js
index 9e98c901a..fae7e60d5 100644
--- a/client/coral-framework/reducers/index.js
+++ b/client/coral-framework/reducers/index.js
@@ -1,9 +1,11 @@
import auth from './auth';
import user from './user';
import asset from './asset';
+import {pluginReducers} from '../helpers/plugins';
export default {
auth,
user,
asset,
+ ...pluginReducers
};
diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js
index b4a7a38df..949d37fee 100644
--- a/client/coral-framework/services/client.js
+++ b/client/coral-framework/services/client.js
@@ -6,9 +6,11 @@ export const client = new ApolloClient({
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
- return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
+ return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
}
return null;
},
networkInterface: getNetworkInterface()
});
+
+export default client;
diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js
new file mode 100644
index 000000000..6d589cf23
--- /dev/null
+++ b/client/coral-framework/utils/index.js
@@ -0,0 +1,7 @@
+/**
+* getActionSummary
+* retrieves the action summary based on the type and the comment
+*/
+
+export const getActionSummary = (type, comment) =>
+ comment.action_summaries.filter(a => a.__typename === type)[0];
diff --git a/graph/helpers/response.js b/graph/helpers/response.js
new file mode 100644
index 000000000..ebdbc02ec
--- /dev/null
+++ b/graph/helpers/response.js
@@ -0,0 +1,31 @@
+const errors = require('../../errors');
+const {Error: {ValidationError}} = require('mongoose');
+
+/**
+ * Wraps up a promise to return an object with the resolution of the promise
+ * keyed at `key` or an error caught at `errors`.
+ */
+
+const wrapResponse = (key) => (promise) => {
+ return promise.then((value) => {
+ let res = {};
+ if (key) {
+ res[key] = value;
+ }
+ return res;
+ }).catch((err) => {
+ if (err instanceof errors.APIError) {
+ return {
+ errors: [err]
+ };
+ } else if (err instanceof ValidationError) {
+
+ // TODO: wrap this with one of our internal errors.
+ throw err;
+ }
+
+ throw err;
+ });
+};
+
+module.exports = wrapResponse;
diff --git a/graph/hooks.js b/graph/hooks.js
index 3fe1306cf..499eb60b1 100644
--- a/graph/hooks.js
+++ b/graph/hooks.js
@@ -1,4 +1,7 @@
-const {forEachField} = require('graphql-tools');
+const {
+ GraphQLObjectType,
+ GraphQLInterfaceType
+} = require('graphql');
const debug = require('debug')('talk:graph:schema');
/**
@@ -22,6 +25,33 @@ const defaultResolveFn = (source, args, context, {fieldName}) => {
}
};
+// This function is pretty much copied verbatim from the graphql-tools repo:
+// https://github.com/apollographql/graphql-tools/blob/b12973c86e00be209d04af0184780998056051c4/src/schemaGenerator.ts#L180-L194
+// With the small alteration that we look for the `resolveType` function on the
+// schema so we can wrap post hooks around it to provide additional resolve
+// points.
+const forEachField = (schema, fn) => {
+ const typeMap = schema.getTypeMap();
+ Object.keys(typeMap).forEach((typeName) => {
+ const type = typeMap[typeName];
+
+ if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) {
+
+ // Here we capture the change to extract the resolve type. We pass this
+ // with the `isResolveType = true` to introduce the specific beheviour.
+ if ('resolveType' in type) {
+ fn(type, typeName, '__resolveType', true);
+ }
+
+ const fields = type.getFields();
+ Object.keys(fields).forEach((fieldName) => {
+ const field = fields[fieldName];
+ fn(field, typeName, fieldName);
+ });
+ }
+ });
+};
+
/**
* Decorates the schema with pre and post hooks as provided by the Plugin
* Manager.
@@ -29,7 +59,7 @@ const defaultResolveFn = (source, args, context, {fieldName}) => {
* @param {Array} hooks hooks to apply to the schema
* @return {void}
*/
-const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName) => {
+const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName, isResolveType = false) => {
// Pull out the pre/post hooks from the available hooks.
const {
@@ -85,6 +115,48 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
return;
}
+ // If this is a resolve type, we need to do some specific things to handle
+ // this type of field.
+ if (isResolveType) {
+
+ // Warn if we have any pre hooks.
+ if (pre.length !== 0) {
+ throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`);
+ }
+
+ // This only needs to do something if post hooks are defined.
+ if (post.length === 0) {
+ return;
+ }
+
+ // Cache the original resolverType function.
+ let resolveType = field.resolveType;
+
+ // Return the function to handle the resolveType hooks.
+ field.resolveType = (obj, context, info) => {
+ let type = resolveType(obj, context, info);
+
+ // Only if a previous resolver was unable to resolve the field type do we
+ // progress to the hooks (in order!) to resolve the field name until we
+ // have resolved it.
+ if (typeof type !== 'undefined' && type != null) {
+ return type;
+ }
+
+ // We will walk through the post hooks until we find the right one. This
+ // follows what redux does to combine existing reducers.
+ for (let i = 0; i < post.length; i++) {
+ let resolveType = post[i];
+ type = resolveType(obj, context, info);
+ if (typeof type !== 'undefined' && type != null) {
+ return type;
+ }
+ }
+ };
+
+ return;
+ }
+
// Cache the original resolve function, this emulates the beheviour found in
// graphql-tools: https://github.com/apollographql/graphql-tools/blob/6e9cc124b10d673448386041e6c3d058bc205a02/src/schemaGenerator.ts#L423-L425
let resolve = field.resolve;
@@ -102,7 +174,7 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
await Promise.all(pre.map((pre) => pre(obj, args, context, info)));
// Resolve the field.
- let result = resolve(obj, args, context, info);
+ let result = await resolve(obj, args, context, info);
// Insure all post hooks after we've resolved the field with the result
// passed in as the fifth argument.
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index a474407d2..3d5ea9ad9 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -1,33 +1,6 @@
-const {Error: {ValidationError}} = require('mongoose');
-const errors = require('../../errors');
+const wrapResponse = require('../helpers/response');
const CommentsService = require('../../services/comments');
-/**
- * Wraps up a promise to return an object with the resolution of the promise
- * keyed at `key` or an error caught at `errors`.
- */
-const wrapResponse = (key) => (promise) => {
- return promise.then((value) => {
- let res = {};
- if (key) {
- res[key] = value;
- }
- return res;
- }).catch((err) => {
- if (err instanceof errors.APIError) {
- return {
- errors: [err]
- };
- } else if (err instanceof ValidationError) {
-
- // TODO: wrap this with one of our internal errors.
- throw err;
- }
-
- throw err;
- });
-};
-
const RootMutation = {
createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.create({asset_id, parent_id, body}));
diff --git a/package.json b/package.json
index adff68924..f4e2eb725 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"postinstall": "./bin/cli plugins reconcile --skip-remote",
"start": "./bin/cli serve --jobs",
"dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"",
- "build": "NODE_ENV=production webpack --progress -p --config webpack.config.js --bail",
+ "build": "NODE_ENV=production webpack -p --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint bin/* . --fix",
@@ -91,7 +91,7 @@
"parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
- "react-apollo": "^0.10.0",
+ "react-apollo": "^1.0.0",
"react-recaptcha": "^2.2.6",
"redis": "^2.7.1",
"resolve": "^1.3.2",
@@ -100,8 +100,9 @@
"uuid": "^2.0.3"
},
"devDependencies": {
- "apollo-client": "^0.8.3",
+ "apollo-client": "^1.0.0",
"autoprefixer": "^6.5.2",
+ "babel-cli": "^6.24.0",
"babel-core": "^6.24.0",
"babel-eslint": "^7.2.1",
"babel-jest": "^19.0.0",
@@ -115,10 +116,12 @@
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.0",
+ "babel-preset-react": "^6.23.0",
"babel-preset-stage-0": "^6.16.0",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"chai-http": "^3.0.0",
+ "common-tags": "^1.4.0",
"copy-webpack-plugin": "^4.0.0",
"css-loader": "^0.27.3",
"dialog-polyfill": "^0.4.4",
diff --git a/plugins.default.json b/plugins.default.json
index 40702cde9..b3443af48 100644
--- a/plugins.default.json
+++ b/plugins.default.json
@@ -1,5 +1,9 @@
{
"server": [
+ "coral-plugin-respect",
"coral-plugin-facebook-auth"
+ ],
+ "client": [
+ "coral-plugin-respect"
]
}
diff --git a/plugins.env.js b/plugins.env.js
new file mode 100644
index 000000000..7be520cc0
--- /dev/null
+++ b/plugins.env.js
@@ -0,0 +1 @@
+module.exports = JSON.parse(process.env.TALK_PLUGINS_JSON);
diff --git a/plugins.js b/plugins.js
index 949a5b9ab..2455e430f 100644
--- a/plugins.js
+++ b/plugins.js
@@ -13,13 +13,13 @@ let plugins = {};
// file isn't loaded, but continuing. Else, like a parsing error, throw it and
// crash the program.
try {
- let defaultPlugins = path.join(__dirname, 'plugins.default.json');
+ let envPlugins = path.join(__dirname, 'plugins.env.js');
let customPlugins = path.join(__dirname, 'plugins.json');
- let envPluginJSON = process.env.TALK_PLUGINS_JSON;
+ let defaultPlugins = path.join(__dirname, 'plugins.default.json');
- if (envPluginJSON && envPluginJSON.length > 0) {
+ if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) {
debug('Now using TALK_PLUGINS_JSON environment variable for plugins');
- plugins = JSON.parse(envPluginJSON);
+ plugins = require(envPlugins);
} else if (fs.existsSync(customPlugins)) {
debug(`Now using ${customPlugins} for plugins`);
plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8'));
@@ -39,13 +39,13 @@ const hookSchemas = {
passport: Joi.func().arity(1),
router: Joi.func().arity(1),
context: Joi.object().pattern(/\w/, Joi.func().maxArity(1)),
- hooks: Joi.object({
+ hooks: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.object({
pre: Joi.func(),
post: Joi.func()
- }),
+ }))),
loaders: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
mutators: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
- resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
+ resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.func())),
typeDefs: Joi.string()
};
diff --git a/plugins/coral-plugin-respect/client/.babelrc b/plugins/coral-plugin-respect/client/.babelrc
new file mode 100644
index 000000000..60be246eb
--- /dev/null
+++ b/plugins/coral-plugin-respect/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-respect/client/.eslintrc.json b/plugins/coral-plugin-respect/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/coral-plugin-respect/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-respect/client/components/Icon.js b/plugins/coral-plugin-respect/client/components/Icon.js
new file mode 100644
index 000000000..c24841e97
--- /dev/null
+++ b/plugins/coral-plugin-respect/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-respect/client/components/RespectButton.js b/plugins/coral-plugin-respect/client/components/RespectButton.js
new file mode 100644
index 000000000..9e851460e
--- /dev/null
+++ b/plugins/coral-plugin-respect/client/components/RespectButton.js
@@ -0,0 +1,67 @@
+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';
+
+const lang = new I18n(translations);
+
+class RespectButton extends Component {
+
+ handleClick = () => {
+ const {postRespect, showSignInDialog, deleteAction, commentId} = this.props;
+ const {me, comment} = this.props.data;
+
+ const respect = comment.action_summaries[0];
+ const respected = (respect && respect.current_user);
+
+ // If the current user does not exist, trigger sign in dialog.
+ if (!me) {
+ const offset = document.getElementById(`c_${commentId}`).getBoundingClientRect().top - 75;
+ showSignInDialog(offset);
+ return;
+ }
+
+ // If the current user is banned, do nothing.
+ if (me.status === 'BANNED') {
+ return;
+ }
+
+ if (!respected) {
+ postRespect({
+ item_id: commentId,
+ item_type: 'COMMENTS'
+ });
+ } else {
+ deleteAction(respect.current_user.id);
+ }
+ }
+
+ render() {
+ const {comment} = this.props.data;
+ const respect = comment && comment.action_summaries && comment.action_summaries[0];
+ const respected = respect && respect.current_user;
+ let count = respect ? respect.count : 0;
+
+ return (
+
+
+
+ );
+ }
+}
+
+RespectButton.propTypes = {
+ data: React.PropTypes.object.isRequired
+};
+
+export default RespectButton;
+
diff --git a/plugins/coral-plugin-respect/client/components/style.css b/plugins/coral-plugin-respect/client/components/style.css
new file mode 100644
index 000000000..32f9a8959
--- /dev/null
+++ b/plugins/coral-plugin-respect/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-respect/client/containers/RespectButton.js b/plugins/coral-plugin-respect/client/containers/RespectButton.js
new file mode 100644
index 000000000..f102a00b5
--- /dev/null
+++ b/plugins/coral-plugin-respect/client/containers/RespectButton.js
@@ -0,0 +1,140 @@
+import {compose, gql, graphql} from 'react-apollo';
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import get from 'lodash/get';
+
+import {showSignInDialog} from 'coral-framework/actions/auth';
+import RespectButton from '../components/RespectButton';
+
+// TODO: use `update` instead of `updateQueries` for optimistic mutations.
+// See https://dev-blog.apollodata.com/apollo-clients-new-imperative-store-api-6cb69318a1e3
+// and https://github.com/apollographql/apollo-client/issues/1224
+
+export const RESPECT_QUERY = gql`
+ query RespectQuery($commentId: ID!) {
+ comment(id: $commentId) {
+ id
+ action_summaries {
+ ... on RespectActionSummary {
+ count
+ current_user {
+ id
+ }
+ }
+ }
+ }
+ me {
+ status
+ }
+ }
+`;
+
+const withQuery = graphql(RESPECT_QUERY);
+
+const withDeleteAction = graphql(gql`
+ mutation deleteAction($id: ID!) {
+ deleteAction(id:$id) {
+ errors {
+ translation_key
+ }
+ }
+ }
+`, {
+ props: ({mutate}) => ({
+ deleteAction: (id) => {
+ return mutate({
+ variables: {id},
+ optimisticResponse: {
+ deleteAction: {
+ __typename: 'DeleteActionResponse',
+ errors: null,
+ }
+ },
+ updateQueries: {
+ RespectQuery: (prev) => {
+ if (get(prev, 'comment.action_summaries.0.current_user.id') !== id) {
+ return prev;
+ }
+ const next = {
+ ...prev,
+ comment: {
+ ...prev.comment,
+ action_summaries: [{
+ __typename: 'RespectActionSummary',
+ count: prev.comment.action_summaries[0].count - 1,
+ current_user: null,
+ }],
+ }
+ };
+ return next;
+ },
+ },
+ });
+ },
+ }),
+});
+
+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',
+ },
+ }
+ },
+ updateQueries: {
+ RespectQuery: (prev, {mutationResult, queryVariables}) => {
+ if (queryVariables.commentId !== respect.item_id) {
+ return prev;
+ }
+ const respectAction = mutationResult.data.createRespect.respect;
+ const count = prev.action_summaries ? prev.action_summaries.count : 0;
+ const next = {
+ ...prev,
+ comment: {
+ ...prev.comment,
+ action_summaries: [{
+ __typename: 'RespectActionSummary',
+ count: count + 1,
+ current_user: respectAction,
+ }],
+ }
+ };
+ return next;
+ },
+ },
+ });
+ },
+ }),
+});
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators({showSignInDialog}, dispatch);
+
+const enhance = compose(
+ connect(null, mapDispatchToProps),
+ withDeleteAction,
+ withPostRespect,
+ withQuery,
+);
+
+export default enhance(RespectButton);
+
diff --git a/plugins/coral-plugin-respect/client/index.js b/plugins/coral-plugin-respect/client/index.js
new file mode 100644
index 000000000..2caa6611b
--- /dev/null
+++ b/plugins/coral-plugin-respect/client/index.js
@@ -0,0 +1,6 @@
+import RespectButton from './containers/RespectButton';
+export default {
+ slots: {
+ commentDetail: [RespectButton],
+ }
+};
diff --git a/plugins/coral-plugin-respect/client/translations.json b/plugins/coral-plugin-respect/client/translations.json
new file mode 100644
index 000000000..643f32ccf
--- /dev/null
+++ b/plugins/coral-plugin-respect/client/translations.json
@@ -0,0 +1,10 @@
+{
+ "en": {
+ "respect": "Respect",
+ "respected": "Respected"
+ },
+ "es": {
+ "respect": "Respeto",
+ "respected": "Respetado"
+ }
+}
diff --git a/plugins/coral-plugin-respect/index.js b/plugins/coral-plugin-respect/index.js
new file mode 100644
index 000000000..5e960976d
--- /dev/null
+++ b/plugins/coral-plugin-respect/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(_, {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';
+ }
+ }
+ }
+ }
+ }
+};
diff --git a/plugins/coral-plugin-respect/server/typeDefs.graphql b/plugins/coral-plugin-respect/server/typeDefs.graphql
new file mode 100644
index 000000000..6639aa454
--- /dev/null
+++ b/plugins/coral-plugin-respect/server/typeDefs.graphql
@@ -0,0 +1,54 @@
+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/test/client/.babelrc b/test/client/.babelrc
index 9bd819c32..633f93f42 100644
--- a/test/client/.babelrc
+++ b/test/client/.babelrc
@@ -1,3 +1,3 @@
{
- "extends": "../../client/.babelrc"
+ "extends": "../../.babelrc"
}
diff --git a/test/mocha.opts b/test/mocha.opts
index a159ae7aa..36be84461 100644
--- a/test/mocha.opts
+++ b/test/mocha.opts
@@ -1,5 +1,7 @@
test/helpers/*.js
-test
+
+test/e2e
+test/server
--compilers js:babel-core/register
--require ignore-styles
--recursive
diff --git a/test/graph/context.js b/test/server/graph/context.js
similarity index 91%
rename from test/graph/context.js
rename to test/server/graph/context.js
index 413622678..b01917289 100644
--- a/test/graph/context.js
+++ b/test/server/graph/context.js
@@ -1,8 +1,8 @@
const expect = require('chai').expect;
-const User = require('../../models/user');
-const Context = require('../../graph/context');
-const errors = require('../../errors');
+const User = require('../../../models/user');
+const Context = require('../../../graph/context');
+const errors = require('../../../errors');
describe('graph.Context', () => {
diff --git a/test/graph/loaders/metrics.js b/test/server/graph/loaders/metrics.js
similarity index 92%
rename from test/graph/loaders/metrics.js
rename to test/server/graph/loaders/metrics.js
index 29f2eeff1..f095e201f 100644
--- a/test/graph/loaders/metrics.js
+++ b/test/server/graph/loaders/metrics.js
@@ -1,13 +1,13 @@
const {expect} = require('chai');
const {graphql} = require('graphql');
-const schema = require('../../../graph/schema');
-const Context = require('../../../graph/context');
-const UserModel = require('../../../models/user');
-const AssetModel = require('../../../models/asset');
-const SettingsService = require('../../../services/settings');
-const ActionModel = require('../../../models/action');
-const CommentModel = require('../../../models/comment');
+const schema = require('../../../../graph/schema');
+const Context = require('../../../../graph/context');
+const UserModel = require('../../../../models/user');
+const AssetModel = require('../../../../models/asset');
+const SettingsService = require('../../../../services/settings');
+const ActionModel = require('../../../../models/action');
+const CommentModel = require('../../../../models/comment');
describe('graph.loaders.Metrics', () => {
beforeEach(() => SettingsService.init());
diff --git a/test/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js
similarity index 86%
rename from test/graph/mutations/addCommentTag.js
rename to test/server/graph/mutations/addCommentTag.js
index 0835e81bc..018e96631 100644
--- a/test/graph/mutations/addCommentTag.js
+++ b/test/server/graph/mutations/addCommentTag.js
@@ -1,11 +1,11 @@
const expect = require('chai').expect;
const {graphql} = require('graphql');
-const schema = require('../../../graph/schema');
-const Context = require('../../../graph/context');
-const UserModel = require('../../../models/user');
-const SettingsService = require('../../../services/settings');
-const CommentsService = require('../../../services/comments');
+const schema = require('../../../../graph/schema');
+const Context = require('../../../../graph/context');
+const UserModel = require('../../../../models/user');
+const SettingsService = require('../../../../services/settings');
+const CommentsService = require('../../../../services/comments');
describe('graph.mutations.addCommentTag', () => {
let comment;
diff --git a/test/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js
similarity index 95%
rename from test/graph/mutations/createComment.js
rename to test/server/graph/mutations/createComment.js
index 26775933c..b3bdd459a 100644
--- a/test/graph/mutations/createComment.js
+++ b/test/server/graph/mutations/createComment.js
@@ -1,12 +1,12 @@
const expect = require('chai').expect;
const {graphql} = require('graphql');
-const schema = require('../../../graph/schema');
-const Context = require('../../../graph/context');
-const UserModel = require('../../../models/user');
-const AssetModel = require('../../../models/asset');
-const SettingsService = require('../../../services/settings');
-const ActionModel = require('../../../models/action');
+const schema = require('../../../../graph/schema');
+const Context = require('../../../../graph/context');
+const UserModel = require('../../../../models/user');
+const AssetModel = require('../../../../models/asset');
+const SettingsService = require('../../../../services/settings');
+const ActionModel = require('../../../../models/action');
describe('graph.mutations.createComment', () => {
beforeEach(() => SettingsService.init());
diff --git a/test/graph/mutations/removeCommentTag.js b/test/server/graph/mutations/removeCommentTag.js
similarity index 87%
rename from test/graph/mutations/removeCommentTag.js
rename to test/server/graph/mutations/removeCommentTag.js
index 99def095d..c0d452f37 100644
--- a/test/graph/mutations/removeCommentTag.js
+++ b/test/server/graph/mutations/removeCommentTag.js
@@ -1,11 +1,11 @@
const expect = require('chai').expect;
const {graphql} = require('graphql');
-const schema = require('../../../graph/schema');
-const Context = require('../../../graph/context');
-const UserModel = require('../../../models/user');
-const SettingsService = require('../../../services/settings');
-const CommentsService = require('../../../services/comments');
+const schema = require('../../../../graph/schema');
+const Context = require('../../../../graph/context');
+const UserModel = require('../../../../models/user');
+const SettingsService = require('../../../../services/settings');
+const CommentsService = require('../../../../services/comments');
describe('graph.mutations.removeCommentTag', () => {
let comment;
diff --git a/test/kue.js b/test/server/kue.js
similarity index 73%
rename from test/kue.js
rename to test/server/kue.js
index 504760b82..8bdc9b398 100644
--- a/test/kue.js
+++ b/test/server/kue.js
@@ -1,4 +1,4 @@
-const kue = require('../services/kue');
+const kue = require('../../services/kue');
beforeEach(() => {
diff --git a/test/mongoose.js b/test/server/mongoose.js
similarity index 80%
rename from test/mongoose.js
rename to test/server/mongoose.js
index 6fac193fa..a1bac6150 100644
--- a/test/mongoose.js
+++ b/test/server/mongoose.js
@@ -1,4 +1,4 @@
-const mongoose = require('./helpers/mongoose');
+const mongoose = require('../helpers/mongoose');
before(function(done) {
this.timeout(30000);
diff --git a/test/passport.js b/test/server/passport.js
similarity index 90%
rename from test/passport.js
rename to test/server/passport.js
index 2d1c53ab9..401e50b77 100644
--- a/test/passport.js
+++ b/test/server/passport.js
@@ -1,4 +1,4 @@
-const authorization = require('../middleware/authorization');
+const authorization = require('../../middleware/authorization');
// Add the passport middleware here before it's setup.
authorization.middleware.push((req, res, next) => {
diff --git a/test/routes/api/account/index.js b/test/server/routes/api/account/index.js
similarity index 92%
rename from test/routes/api/account/index.js
rename to test/server/routes/api/account/index.js
index 3a4493279..67744f9bc 100644
--- a/test/routes/api/account/index.js
+++ b/test/server/routes/api/account/index.js
@@ -1,17 +1,17 @@
const passport = require('../../../passport');
-const app = require('../../../../app');
+const app = require('../../../../../app');
const chai = require('chai');
const expect = chai.expect;
-const SettingsService = require('../../../../services/settings');
+const SettingsService = require('../../../../../services/settings');
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
// Setup chai.
chai.should();
chai.use(require('chai-http'));
-const UsersService = require('../../../../services/users');
+const UsersService = require('../../../../../services/users');
describe('/api/v1/account/username', () => {
let mockUser;
diff --git a/test/routes/api/assets/index.js b/test/server/routes/api/assets/index.js
similarity index 94%
rename from test/routes/api/assets/index.js
rename to test/server/routes/api/assets/index.js
index 9f1e4ad66..80708688b 100644
--- a/test/routes/api/assets/index.js
+++ b/test/server/routes/api/assets/index.js
@@ -1,6 +1,6 @@
const passport = require('../../../passport');
-const app = require('../../../../app');
+const app = require('../../../../../app');
const chai = require('chai');
const expect = chai.expect;
@@ -8,9 +8,9 @@ const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
-const AssetModel = require('../../../../models/asset');
-const AssetsService = require('../../../../services/assets');
-const SettingsService = require('../../../../services/settings');
+const AssetModel = require('../../../../../models/asset');
+const AssetsService = require('../../../../../services/assets');
+const SettingsService = require('../../../../../services/settings');
describe('/api/v1/assets', () => {
diff --git a/test/routes/api/auth/index.js b/test/server/routes/api/auth/index.js
similarity index 94%
rename from test/routes/api/auth/index.js
rename to test/server/routes/api/auth/index.js
index 38b867b7d..1d0ed6b3a 100644
--- a/test/routes/api/auth/index.js
+++ b/test/server/routes/api/auth/index.js
@@ -1,10 +1,10 @@
-const app = require('../../../../app');
+const app = require('../../../../../app');
const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-http'));
-const UsersService = require('../../../../services/users');
+const UsersService = require('../../../../../services/users');
describe('/api/v1/auth', () => {
describe('#get', () => {
@@ -19,7 +19,7 @@ describe('/api/v1/auth', () => {
});
});
-const SettingsService = require('../../../../services/settings');
+const SettingsService = require('../../../../../services/settings');
describe('/api/v1/auth/local', () => {
diff --git a/test/routes/api/settings/index.js b/test/server/routes/api/settings/index.js
similarity index 91%
rename from test/routes/api/settings/index.js
rename to test/server/routes/api/settings/index.js
index dfe846edf..54a8f357f 100644
--- a/test/routes/api/settings/index.js
+++ b/test/server/routes/api/settings/index.js
@@ -1,13 +1,13 @@
const passport = require('../../../passport');
-const app = require('../../../../app');
+const app = require('../../../../../app');
const chai = require('chai');
const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
-const SettingsService = require('../../../../services/settings');
+const SettingsService = require('../../../../../services/settings');
const defaults = {id: '1', moderation: 'PRE'};
describe('/api/v1/settings', () => {
diff --git a/test/routes/api/user/index.js b/test/server/routes/api/user/index.js
similarity index 92%
rename from test/routes/api/user/index.js
rename to test/server/routes/api/user/index.js
index f6e202243..fc1feb66e 100644
--- a/test/routes/api/user/index.js
+++ b/test/server/routes/api/user/index.js
@@ -1,18 +1,18 @@
const passport = require('../../../passport');
-const app = require('../../../../app');
-const mailer = require('../../../../services/mailer');
+const app = require('../../../../../app');
+const mailer = require('../../../../../services/mailer');
const chai = require('chai');
const expect = chai.expect;
-const SettingsService = require('../../../../services/settings');
+const SettingsService = require('../../../../../services/settings');
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
// Setup chai.
chai.should();
chai.use(require('chai-http'));
-const UsersService = require('../../../../services/users');
+const UsersService = require('../../../../../services/users');
describe('/api/v1/users/:user_id/email/confirm', () => {
diff --git a/test/services/actions.js b/test/server/services/actions.js
similarity index 96%
rename from test/services/actions.js
rename to test/server/services/actions.js
index 7fdec0a12..4d22dff6f 100644
--- a/test/services/actions.js
+++ b/test/server/services/actions.js
@@ -1,5 +1,5 @@
-const ActionModel = require('../../models/action');
-const ActionsService = require('../../services/actions');
+const ActionModel = require('../../../models/action');
+const ActionsService = require('../../../services/actions');
const expect = require('chai').expect;
diff --git a/test/services/assets.js b/test/server/services/assets.js
similarity index 95%
rename from test/services/assets.js
rename to test/server/services/assets.js
index 3bbe8ee6e..e46e75576 100644
--- a/test/services/assets.js
+++ b/test/server/services/assets.js
@@ -1,6 +1,6 @@
-const AssetModel = require('../../models/asset');
-const AssetsService = require('../../services/assets');
-const SettingsService = require('../../services/settings');
+const AssetModel = require('../../../models/asset');
+const AssetsService = require('../../../services/assets');
+const SettingsService = require('../../../services/settings');
const chai = require('chai');
const expect = chai.expect;
diff --git a/test/services/comments.js b/test/server/services/comments.js
similarity index 96%
rename from test/services/comments.js
rename to test/server/services/comments.js
index 782c00560..5b9cbf4d1 100644
--- a/test/services/comments.js
+++ b/test/server/services/comments.js
@@ -1,10 +1,10 @@
-const CommentModel = require('../../models/comment');
-const ActionModel = require('../../models/action');
+const CommentModel = require('../../../models/comment');
+const ActionModel = require('../../../models/action');
-const ActionsService = require('../../services/actions');
-const UsersService = require('../../services/users');
-const SettingsService = require('../../services/settings');
-const CommentsService = require('../../services/comments');
+const ActionsService = require('../../../services/actions');
+const UsersService = require('../../../services/users');
+const SettingsService = require('../../../services/settings');
+const CommentsService = require('../../../services/comments');
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
diff --git a/test/services/domainlist.js b/test/server/services/domainlist.js
similarity index 90%
rename from test/services/domainlist.js
rename to test/server/services/domainlist.js
index db53b77b4..9dafa438a 100644
--- a/test/services/domainlist.js
+++ b/test/server/services/domainlist.js
@@ -1,6 +1,6 @@
const expect = require('chai').expect;
-const Domainlist = require('../../services/domainlist');
-const SettingsService = require('../../services/settings');
+const Domainlist = require('../../../services/domainlist');
+const SettingsService = require('../../../services/settings');
describe('services.Domainlist', () => {
diff --git a/test/services/scraper.js b/test/server/services/scraper.js
similarity index 100%
rename from test/services/scraper.js
rename to test/server/services/scraper.js
diff --git a/test/services/settings.js b/test/server/services/settings.js
similarity index 96%
rename from test/services/settings.js
rename to test/server/services/settings.js
index 3620ae633..e23548a83 100644
--- a/test/services/settings.js
+++ b/test/server/services/settings.js
@@ -1,4 +1,4 @@
-const SettingsService = require('../../services/settings');
+const SettingsService = require('../../../services/settings');
const expect = require('chai').expect;
describe('services.SettingsService', () => {
diff --git a/test/services/users.js b/test/server/services/users.js
similarity index 98%
rename from test/services/users.js
rename to test/server/services/users.js
index 744906c5b..22fe7ddd5 100644
--- a/test/services/users.js
+++ b/test/server/services/users.js
@@ -1,5 +1,5 @@
-const UsersService = require('../../services/users');
-const SettingsService = require('../../services/settings');
+const UsersService = require('../../../services/users');
+const SettingsService = require('../../../services/settings');
const expect = require('chai').expect;
diff --git a/test/services/wordlist.js b/test/server/services/wordlist.js
similarity index 94%
rename from test/services/wordlist.js
rename to test/server/services/wordlist.js
index 26baf6405..417844da4 100644
--- a/test/services/wordlist.js
+++ b/test/server/services/wordlist.js
@@ -1,7 +1,7 @@
const expect = require('chai').expect;
-const Errors = require('../../errors');
-const Wordlist = require('../../services/wordlist');
-const SettingsService = require('../../services/settings');
+const Errors = require('../../../errors');
+const Wordlist = require('../../../services/wordlist');
+const SettingsService = require('../../../services/settings');
describe('services.Wordlist', () => {
diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs
index 129895fc4..97cd0c796 100644
--- a/views/embed/stream.ejs
+++ b/views/embed/stream.ejs
@@ -5,6 +5,7 @@
+
<% if (locals.customCssUrl) { %>
<% } %>
diff --git a/webpack.config.js b/webpack.config.js
index 3914dee2f..3a54e85c0 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,10 +1,30 @@
const path = require('path');
+const fs = require('fs');
const autoprefixer = require('autoprefixer');
const precss = require('precss');
const Copy = require('copy-webpack-plugin');
const LicenseWebpackPlugin = require('license-webpack-plugin');
const webpack = require('webpack');
+// Possibly load the config from the .env file (if there is one).
+require('dotenv').config();
+
+let pluginsConfigPath;
+
+let envPlugins = path.join(__dirname, 'plugins.env.js');
+let customPlugins = path.join(__dirname, 'plugins.json');
+let defaultPlugins = path.join(__dirname, 'plugins.default.json');
+
+if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) {
+ pluginsConfigPath = envPlugins;
+} else if (fs.existsSync(customPlugins)) {
+ pluginsConfigPath = customPlugins;
+} else {
+ pluginsConfigPath = defaultPlugins;
+}
+
+console.log(`Using ${pluginsConfigPath} as the plugin configuration path`);
+
// Edit the build targets and embeds below.
const buildTargets = [
@@ -53,6 +73,11 @@ module.exports = {
},
module: {
rules: [
+ {
+ loader: 'plugins-loader',
+ test: /\.(json|js)$/,
+ include: pluginsConfigPath
+ },
{
loader: 'babel-loader',
exclude: /node_modules/,
@@ -115,12 +140,23 @@ module.exports = {
}),
new webpack.DefinePlugin({
'process.env': {
- 'VERSION': `"${require('./package.json').version}"`
+ 'VERSION': `"${require('./package.json').version}"`,
}
+ }),
+ new webpack.EnvironmentPlugin({
+ 'TALK_PLUGINS_JSON': '{}'
})
],
+ resolveLoader: {
+ modules: ['node_modules', path.resolve(__dirname, 'client/coral-framework/loaders')],
+ },
resolve: {
+ alias: {
+ plugins: path.resolve(__dirname, 'plugins/'),
+ pluginsConfig: pluginsConfigPath
+ },
modules: [
+ path.resolve(__dirname, 'plugins'),
path.resolve(__dirname, 'client'),
...buildTargets.map(target => path.join(__dirname, 'client', target, 'src')),
...buildEmbeds.map(embed => path.join(__dirname, 'client', `coral-embed-${embed}`, 'src')),
diff --git a/yarn.lock b/yarn.lock
index 4d1a4ed9c..378e5a6c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -26,13 +26,17 @@
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
-"@types/graphql@^0.8.0", "@types/graphql@^0.8.5", "@types/graphql@^0.8.6":
+"@types/graphql@^0.8.5", "@types/graphql@^0.8.6":
version "0.8.6"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.8.6.tgz#b34fb880493ba835b0c067024ee70130d6f9bb68"
-"@types/isomorphic-fetch@0.0.30":
- version "0.0.30"
- resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.30.tgz#a21717624cde9a48c2db53a4e500fc5c32a99bbc"
+"@types/graphql@^0.9.0":
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.0.tgz#fccf859f0d2817687f210737dc3be48a18b1d754"
+
+"@types/isomorphic-fetch@0.0.33":
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.33.tgz#3ea1b86f8b73e6a7430d01d4dbd5b1f63fd72718"
"@types/mime@*":
version "0.0.29"
@@ -175,19 +179,20 @@ anymatch@^1.3.0:
arrify "^1.0.0"
micromatch "^2.1.5"
-apollo-client@^0.8.3:
- version "0.8.6"
- resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-0.8.6.tgz#9aa18b03ec338f0a3804122df7f77493a10b72a0"
+apollo-client@^1.0.0, apollo-client@^1.0.0-rc.9:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-1.0.2.tgz#4355bd49d53a1489bc91d9f56d5b3d0ffe33fb3c"
dependencies:
- graphql-anywhere "^2.1.0"
- graphql-tag "^1.1.1"
+ graphql "^0.9.1"
+ graphql-anywhere "^3.0.1"
+ graphql-tag "^2.0.0"
redux "^3.4.0"
symbol-observable "^1.0.2"
whatwg-fetch "^2.0.0"
optionalDependencies:
"@types/async" "^2.0.31"
- "@types/graphql" "^0.8.0"
- "@types/isomorphic-fetch" "0.0.30"
+ "@types/graphql" "^0.9.0"
+ "@types/isomorphic-fetch" "0.0.33"
app-module-path@^2.2.0:
version "2.2.0"
@@ -360,6 +365,27 @@ aws4@^1.2.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755"
+babel-cli@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.0.tgz#a05ffd210dca0c288a26d5319c5ac8669a265ad0"
+ dependencies:
+ babel-core "^6.24.0"
+ babel-polyfill "^6.23.0"
+ babel-register "^6.24.0"
+ babel-runtime "^6.22.0"
+ commander "^2.8.1"
+ convert-source-map "^1.1.0"
+ fs-readdir-recursive "^1.0.0"
+ glob "^7.0.0"
+ lodash "^4.2.0"
+ output-file-sync "^1.1.0"
+ path-is-absolute "^1.0.0"
+ slash "^1.0.0"
+ source-map "^0.5.0"
+ v8flags "^2.0.10"
+ optionalDependencies:
+ chokidar "^1.6.1"
+
babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
@@ -622,11 +648,15 @@ babel-plugin-syntax-export-extensions@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
+babel-plugin-syntax-flow@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+
babel-plugin-syntax-function-bind@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46"
-babel-plugin-syntax-jsx@^6.8.0:
+babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
@@ -879,6 +909,13 @@ babel-plugin-transform-export-extensions@^6.22.0:
babel-plugin-syntax-export-extensions "^6.8.0"
babel-runtime "^6.22.0"
+babel-plugin-transform-flow-strip-types@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
+ dependencies:
+ babel-plugin-syntax-flow "^6.18.0"
+ babel-runtime "^6.22.0"
+
babel-plugin-transform-function-bind@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz#c6fb8e96ac296a310b8cf8ea401462407ddf6a97"
@@ -899,6 +936,26 @@ babel-plugin-transform-object-rest-spread@^6.22.0, babel-plugin-transform-object
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.22.0"
+babel-plugin-transform-react-display-name@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.23.0.tgz#4398910c358441dc4cef18787264d0412ed36b37"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-self@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-source@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
babel-plugin-transform-react-jsx@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.23.0.tgz#23e892f7f2e759678eb5e4446a8f8e94e81b3470"
@@ -957,12 +1014,29 @@ babel-preset-es2015@^6.24.0:
babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0"
+babel-preset-flow@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d"
+ dependencies:
+ babel-plugin-transform-flow-strip-types "^6.22.0"
+
babel-preset-jest@^19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-19.0.0.tgz#22d67201d02324a195811288eb38294bb3cac396"
dependencies:
babel-plugin-jest-hoist "^19.0.0"
+babel-preset-react@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.23.0.tgz#eb7cee4de98a3f94502c28565332da9819455195"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.3.13"
+ babel-plugin-transform-react-display-name "^6.23.0"
+ babel-plugin-transform-react-jsx "^6.23.0"
+ babel-plugin-transform-react-jsx-self "^6.22.0"
+ babel-plugin-transform-react-jsx-source "^6.22.0"
+ babel-preset-flow "^6.23.0"
+
babel-preset-stage-0@^6.16.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.22.0.tgz#707eeb5b415da769eff9c42f4547f644f9296ef9"
@@ -1483,7 +1557,7 @@ cheerio@^0.22.0:
lodash.reject "^4.4.0"
lodash.some "^4.4.0"
-chokidar@^1.4.3:
+chokidar@^1.4.3, chokidar@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
dependencies:
@@ -1683,7 +1757,7 @@ commander@2.8.x:
dependencies:
graceful-readlink ">= 1.0.0"
-commander@2.9.0, commander@^2.5.0, commander@^2.9.0:
+commander@2.9.0, commander@^2.5.0, commander@^2.8.1, commander@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
dependencies:
@@ -1693,6 +1767,12 @@ commander@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781"
+common-tags@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.4.0.tgz#1187be4f3d4cf0c0427d43f74eef1f73501614c0"
+ dependencies:
+ babel-runtime "^6.18.0"
+
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -3079,6 +3159,10 @@ fs-promise@^0.3.1:
dependencies:
any-promise "~0.1.0"
+fs-readdir-recursive@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -3372,7 +3456,7 @@ gql-utils@^0.0.2:
bluebird "^3.4.6"
glob "^7.1.1"
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -3384,9 +3468,9 @@ graceful-fs@~2.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
-graphql-anywhere@^2.0.0, graphql-anywhere@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-2.1.0.tgz#888c0a1718db3ff866b313070747777380560f69"
+graphql-anywhere@^3.0.0, graphql-anywhere@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-3.0.1.tgz#73531db861174c8f212eafb9f8e84944b38b4e5a"
graphql-docs@^0.2.0:
version "0.2.0"
@@ -3425,10 +3509,18 @@ graphql-server-module-graphiql@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.5.2.tgz#7e2a0c78b0267e784f8483ce5633810baf558dee"
-graphql-tag@^1.1.1, graphql-tag@^1.2.3:
+graphql-tag@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.2.3.tgz#74c62443fbf3e693647426d7359f7e3e6ce7dace"
+graphql-tag@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.3.2.tgz#7abb3a8fd9f3415d07163314ed237061c785b759"
+
+graphql-tag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.0.0.tgz#f3efe3b4d64f33bfe8479ae06a461c9d72f2a6fe"
+
graphql-tools@^0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-0.9.2.tgz#c3f42d0b78d2d6c57cea5ef2894863de34af9a11"
@@ -3451,6 +3543,12 @@ graphql@^0.8.2:
dependencies:
iterall "1.0.2"
+graphql@^0.9.1:
+ version "0.9.2"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.9.2.tgz#2cb5c635de13f790a77c5879649cb401b1589386"
+ dependencies:
+ iterall "1.0.3"
+
growl@1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.8.1.tgz#4b2dec8d907e93db336624dcec0183502f8c9428"
@@ -4211,6 +4309,10 @@ iterall@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.2.tgz#41a2e96ce9eda5e61c767ee5dc312373bb046e91"
+iterall@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.3.tgz#e0b31958f835013c323ff0b10943829ac69aa4b7"
+
jade@0.26.3:
version "0.26.3"
resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c"
@@ -5517,6 +5619,14 @@ osenv@^0.1.0:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
+output-file-sync@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76"
+ dependencies:
+ graceful-fs "^4.1.4"
+ mkdirp "^0.5.1"
+ object-assign "^4.1.0"
+
p-limit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
@@ -6503,11 +6613,13 @@ react-addons-test-utils@^15.4.2:
fbjs "^0.8.4"
object-assign "^4.1.0"
-react-apollo@^0.10.0:
- version "0.10.1"
- resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-0.10.1.tgz#97fd50855f8575672aa68330b9c64a201cd13343"
+react-apollo@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.0.0.tgz#7fcc14adcc7aa4ca4d9e04ddedf50b8fb74daa91"
dependencies:
- graphql-anywhere "^2.0.0"
+ apollo-client "^1.0.0-rc.9"
+ graphql-anywhere "^3.0.0"
+ graphql-tag "^1.3.1"
hoist-non-react-statics "^1.2.0"
invariant "^2.2.1"
lodash.flatten "^4.2.0"
@@ -7843,6 +7955,10 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
+user-home@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
+
user-home@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
@@ -7871,6 +7987,12 @@ uuid@^3.0.0, uuid@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
+v8flags@^2.0.10:
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.0.11.tgz#bca8f30f0d6d60612cc2c00641e6962d42ae6881"
+ dependencies:
+ user-home "^1.1.1"
+
validate-commit-msg@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/validate-commit-msg/-/validate-commit-msg-2.5.0.tgz#a7d0a68aa3917171b560664689a2ac59c25908b6"