Merge branch 'master' into subscriptions

This commit is contained in:
Wyatt Johnson
2017-04-10 14:33:08 -06:00
63 changed files with 1924 additions and 1117 deletions
+2 -1
View File
@@ -3,4 +3,5 @@ client/lib
**/*.html
plugins/*
!plugins/coral-plugin-facebook-auth
node_modules
!plugins/coral-plugin-respect
node_modules
+2 -1
View File
@@ -16,4 +16,5 @@ coverage/
plugins.json
plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-respect
+1 -1
View File
@@ -1,5 +1,5 @@
{
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*"],
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql"
}
+3 -2
View File
@@ -10,10 +10,11 @@ By contributing to this project you agree to the [Code of Conduct](https://coral
You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625.
You can view product ideas and our longer term roadmap here https://trello.com/b/ILND751a/talk.
If you'd like to see our longer term roadmap, [get in touch.](https://coralproject.net/contact.html)
## Contribute to the documentation
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
We are looking for _documentarians_ to:
@@ -55,7 +56,7 @@ Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early
To get an idea of where the Coral Team is going, see:
* our [product/design Trello board](https://trello.com/b/ILND751a/talk),
* our [Slack channel](https://coralprojectslackin.herokuapp.com/), where you can talk directly with the Team and other community members,
* our [current stories](https://www.pivotaltracker.com/n/projects/1863625), and
* our [issues](https://github.com/coralproject/talk/issues).
+2 -2
View File
@@ -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(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
+5 -1
View File
@@ -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 {
? <TagLabel><BestIndicator /></TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<Slot fill="commentInfoBar" commentId={comment.id} />
<Content body={comment.body} />
<div className="commentActionsLeft comment__action-container">
@@ -187,6 +189,7 @@ class Comment extends React.Component {
removeBest={removeBestTag} />
</IfUserCanModifyBest>
</ActionButton>
<Slot fill="commentDetail" commentId={comment.id} />
</div>
<div className="commentActionsRight comment__action-container">
<ActionButton>
@@ -241,7 +244,8 @@ class Comment extends React.Component {
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply} />;
comment={reply}
/>;
})
}
{
+1 -1
View File
@@ -313,5 +313,5 @@ export default compose(
addCommentTag,
removeCommentTag,
deleteAction,
queryStream
queryStream,
)(Embed);
+5 -2
View File
@@ -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}
/>
)
}
</div>
+3 -3
View File
@@ -11,7 +11,7 @@ html, body {
}
body {
font-family: 'Lato', sans-serif;
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
width: 100%;
font-size: 14px;
margin: 0px;
@@ -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 {
+1 -1
View File
@@ -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);
+22 -1
View File
@@ -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)));
};
+9
View File
@@ -0,0 +1,9 @@
import * as authActions from './auth';
import * as assetActions from './asset';
import * as notificationActions from './notification';
export default {
authActions,
assetActions,
notificationActions,
};
+19
View File
@@ -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 (
<div>
{getSlotElements(fill, rest)}
</div>
);
}
}
Slot.propTypes = {
fill: React.PropTypes.string
};
export default Slot;
+21
View File
@@ -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}));
}
+8 -7
View File
@@ -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
};
@@ -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(',')}
];
`;
};
+2
View File
@@ -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
};
+3 -1
View File
@@ -16,9 +16,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: networkInterfaceWithSubscriptions,
});
export default client;
+7
View File
@@ -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];
+31
View File
@@ -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;
+75 -3
View File
@@ -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.
+1 -28
View File
@@ -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}));
+6 -3
View File
@@ -7,7 +7,7 @@
"postinstall": "./bin/cli plugins reconcile --skip-remote",
"start": "./bin/cli serve -j -w",
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"./bin/cli -c .env serve -j -w\"",
"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",
@@ -93,7 +93,7 @@
"parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"react-apollo": "^1.0.0-rc.3",
"react-apollo": "^1.0.0",
"react-recaptcha": "^2.2.6",
"redis": "^2.7.1",
"uuid": "^3.0.1",
@@ -102,8 +102,9 @@
"semver": "^5.3.0"
},
"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",
@@ -117,10 +118,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",
+4
View File
@@ -1,5 +1,9 @@
{
"server": [
"coral-plugin-respect",
"coral-plugin-facebook-auth"
],
"client": [
"coral-plugin-respect"
]
}
+1
View File
@@ -0,0 +1 @@
module.exports = JSON.parse(process.env.TALK_PLUGINS_JSON);
+25 -9
View File
@@ -3,9 +3,10 @@ const path = require('path');
const resolve = require('resolve');
const debug = require('debug')('talk:plugins');
const Joi = require('joi');
const amp = require('app-module-path');
// Add support for require rewriting.
require('app-module-path').addPath(__dirname);
// Add the current path to the module root.
amp.addPath(__dirname);
let plugins = {};
@@ -13,13 +14,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'));
@@ -35,17 +36,20 @@ try {
}
}
/**
* All the hooks from plugins must match the schema defined here.
*/
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()
};
@@ -82,6 +86,12 @@ function pluginPath(name) {
}
}
/**
* Itterates over the plugins and gets the plugin path's, version, and name.
*
* @param {Array<Object|String>} plugins
* @returns {Array<Object>}
*/
function itteratePlugins(plugins) {
return plugins.map((p) => {
let plugin = {};
@@ -111,6 +121,12 @@ function itteratePlugins(plugins) {
});
}
// Add each plugin folder to the allowed import path so that they can import our
// internal dependancies.
Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((plugin) => {
amp.enableForDir(path.dirname(plugin.path));
}));
/**
* Stores a reference to a section for a section of Plugins.
*/
@@ -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"
]
}
@@ -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"] }]
}
}
@@ -0,0 +1,6 @@
import React from 'react';
import cn from 'classnames';
export default ({className}) => (
<i className={cn('fa', 'fa-handshake-o', className)} aria-hidden="true"/>
);
@@ -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 (
<div className={styles.respect}>
<button
className={cn(styles.button, {[styles.respected]: respected})}
onClick={this.handleClick} >
<span>{lang.t(respected ? 'respected' : 'respect')}</span>
<Icon className={cn(styles.icon, {[styles.respected]: respected})} />
{count > 0 && count}
</button>
</div>
);
}
}
RespectButton.propTypes = {
data: React.PropTypes.object.isRequired
};
export default RespectButton;
@@ -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;
}
@@ -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);
@@ -0,0 +1,6 @@
import RespectButton from './containers/RespectButton';
export default {
slots: {
commentDetail: [RespectButton],
}
};
@@ -0,0 +1,10 @@
{
"en": {
"respect": "Respect",
"respected": "Respected"
},
"es": {
"respect": "Respeto",
"respected": "Respetado"
}
}
+36
View File
@@ -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';
}
}
}
}
}
};
@@ -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
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"extends": "../../client/.babelrc"
"extends": "../../.babelrc"
}
-1
View File
@@ -6,7 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coral - (Beta)</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<style>
/**
+3 -1
View File
@@ -1,5 +1,7 @@
test/helpers/*.js
test
test/e2e
test/server
--compilers js:babel-core/register
--require ignore-styles
--recursive
@@ -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', () => {
@@ -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());
@@ -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;
@@ -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());
@@ -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;
+1 -1
View File
@@ -1,4 +1,4 @@
const kue = require('../services/kue');
const kue = require('../../services/kue');
beforeEach(() => {
+1 -1
View File
@@ -1,4 +1,4 @@
const mongoose = require('./helpers/mongoose');
const mongoose = require('../helpers/mongoose');
before(function(done) {
this.timeout(30000);
+1 -1
View File
@@ -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) => {
@@ -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;
@@ -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', () => {
@@ -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', () => {
@@ -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', () => {
@@ -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', () => {
@@ -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;
@@ -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;
@@ -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']}};
@@ -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', () => {
@@ -1,4 +1,4 @@
const SettingsService = require('../../services/settings');
const SettingsService = require('../../../services/settings');
const expect = require('chai').expect;
describe('services.SettingsService', () => {
@@ -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;
@@ -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', () => {
+1 -1
View File
@@ -3,8 +3,8 @@
<head>
<meta property="csrf" content="<%= csrfToken %>">
<link rel="stylesheet" type="text/css" href="/client/embed/stream/default.css">
<link href="https://fonts.googleapis.com/css?family=Lato:400,700" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<% if (locals.customCssUrl) { %>
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
<% } %>
+37 -1
View File
@@ -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 = [
@@ -54,6 +74,11 @@ module.exports = {
},
module: {
rules: [
{
loader: 'plugins-loader',
test: /\.(json|js)$/,
include: pluginsConfigPath
},
{
loader: 'babel-loader',
exclude: /node_modules/,
@@ -116,12 +141,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')),
+1140 -981
View File
File diff suppressed because it is too large Load Diff